Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update VMECIO to specify Nyquist spectrum and fix issues with asymmetric wouts #1211

Merged
merged 24 commits into from
Sep 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
ac16d6d
fix plot_vmec_cmparison for asym wout files
dpanici Aug 20, 2024
66c4955
fix small typo
dpanici Aug 20, 2024
9d91529
allow the N_nyq and M_nyq parameters to be set manually when saving t…
dpanici Aug 20, 2024
08c314b
Merge branch 'master' into dp/vmecio-asym
dpanici Aug 23, 2024
ed3f793
fix typo in vmec wout loading for asym
dpanici Aug 23, 2024
65a64fd
Merge branch 'master' into dp/vmecio-asym
dpanici Aug 23, 2024
fef383a
fix bug in nyquist freq
dpanici Aug 23, 2024
5d183df
Merge branch 'master' into dp/vmecio-asym
dpanici Aug 26, 2024
dd12e8e
Merge branch 'master' into dp/vmecio-asym
dpanici Aug 27, 2024
7436bce
Merge branch 'master' into dp/vmecio-asym
dpanici Aug 27, 2024
e45ee24
add comment and make basis sym explicit
dpanici Aug 27, 2024
6aa3667
fix error in wout asym saving where the transform fit was called dire…
dpanici Aug 27, 2024
a8916b5
add asymmetric wout test (with very low poloidal resolution, will upd…
dpanici Aug 27, 2024
1404304
update path
dpanici Aug 28, 2024
8efaf20
Merge branch 'master' into dp/vmecio-asym
dpanici Aug 28, 2024
7d0e31d
Merge branch 'master' into dp/vmecio-asym
dpanici Aug 28, 2024
862eba0
Merge branch 'master' into dp/vmecio-asym
dpanici Aug 28, 2024
7ab4056
Merge branch 'master' into dp/vmecio-asym
dpanici Aug 29, 2024
cc47940
rename wout
dpanici Aug 29, 2024
a1e4834
Merge branch 'dp/vmecio-asym' of github.com:PlasmaControl/DESC into d…
dpanici Aug 29, 2024
49cb844
update changelog
dpanici Aug 29, 2024
6507b78
update again
dpanici Aug 29, 2024
5aa099d
Merge branch 'master' into dp/vmecio-asym
dpanici Aug 30, 2024
95a3292
add warning
dpanici Aug 31, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ Changelog
New Features

- Add ``use_signed_distance`` flag to ``PlasmaVesselDistance`` which will use a signed distance as the target, which is positive when the plasma is inside of the vessel surface and negative if the plasma is outside of the vessel surface, to allow optimizer to distinguish if the equilbrium surface exits the vessel surface and guard against it by targeting a positive signed distance.
- Allow specification of Nyquist spectrum maximum modenumbers when using ``VMECIO.save`` to save a DESC .h5 file as a VMEC-format wout file

Bug Fixes

- Fixes bugs that occur when saving asymmetric equilibria as wout files
- Fixes bug that occurs when using ``VMECIO.plot_vmec_comparison`` to compare to an asymmetric wout file



v0.12.1
-------
Expand Down
143 changes: 111 additions & 32 deletions desc/vmec.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
from desc.objectives.utils import factorize_linear_constraints
from desc.profiles import PowerSeriesProfile, SplineProfile
from desc.transform import Transform
from desc.utils import Timer
from desc.utils import Timer, warnif
from desc.vmec_utils import (
fourier_to_zernike,
ptolemy_identity_fwd,
Expand Down Expand Up @@ -158,7 +158,7 @@
zax_cs = file.variables["zaxis_cs"][:].filled()
try:
rax_cs = file.variables["raxis_cs"][:].filled()
rax_cc = file.variables["zaxis_cc"][:].filled()
zax_cc = file.variables["zaxis_cc"][:].filled()

Check warning on line 161 in desc/vmec.py

View check run for this annotation

Codecov / codecov/patch

desc/vmec.py#L161

Added line #L161 was not covered by tests
except KeyError:
rax_cs = np.zeros_like(rax_cc)
zax_cc = np.zeros_like(zax_cs)
Expand Down Expand Up @@ -208,7 +208,9 @@
return eq

@classmethod
def save(cls, eq, path, surfs=128, verbose=1): # noqa: C901 - FIXME - simplify
def save( # noqa: C901 - FIXME - simplify
cls, eq, path, surfs=128, verbose=1, M_nyq=None, N_nyq=None
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I won't force you to change this, but I would prefer to consistently have verbose always be the last kwarg

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I won't just because changing it means changing positional args that others might be using and that would break code. I know we have not committed to an API but for this I will keep as is

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is cool! In a future PR I would be in favor of requiring stuff like verbose to be keyword arguments. That would help with backward-compatibility of the code

):
"""Save an Equilibrium as a netCDF file in the VMEC format.

Parameters
Expand All @@ -224,6 +226,10 @@
* 0: no output
* 1: status of quantities computed
* 2: as above plus timing information
M_nyq, N_nyq: int
The max poloidal and toroidal modenumber to use in the
Nyquist spectrum that the derived quantities are Fourier
fit with. Defaults to M+4 and N+2.

Returns
-------
Expand All @@ -242,8 +248,14 @@
NFP = eq.NFP
M = eq.M
N = eq.N
M_nyq = M + 4
N_nyq = N + 2 if N > 0 else 0
M_nyq = M + 4 if M_nyq is None else M_nyq
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are these still the correct defaults? It sounded like N_nyq = NZETA*2?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also you can use setdefault for a cleaner version of this

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are still correct, I think. At least VMEC by default gives M nyq of Mpol-1+4, and Mpol-1 = our Mpol

warnif(
N_nyq is not None and int(N) == 0,
UserWarning,
"Passed in N_nyq but equilibrium is axisymmetric, setting N_nyq to zero",
)
N_nyq = N + 2 if N_nyq is None else N_nyq
N_nyq = 0 if int(N) == 0 else N_nyq
dpanici marked this conversation as resolved.
Show resolved Hide resolved

# VMEC radial coordinate: s = rho^2 = Psi / Psi(LCFS)
s_full = np.linspace(0, 1, surfs)
Expand Down Expand Up @@ -807,6 +819,14 @@
lmnc.long_name = "cos(m*t-n*p) component of lambda, on half mesh"
lmnc.units = "rad"
l1 = np.ones_like(eq.L_lmn)
# should negate lambda coefs bc theta_DESC + lambda = theta_PEST,
# since we are reversing the theta direction (and the theta_PEST direction),
# so -theta_PEST = -theta_DESC - lambda, so the negative of lambda is what
# should be saved, so that would be negating all of eq.L_lmn
# BUT since we are also reversing the poloidal angle direction, which
# would negate only the coeffs of L_lmn corresponding to m<0
# (sin theta modes in DESC), the effective result is to only
# negate the cos(theta) (m>0) lambda modes
l1[eq.L_basis.modes[:, 1] >= 0] *= -1
m, n, x_mn = zernike_to_fourier(l1 * eq.L_lmn, basis=eq.L_basis, rho=r_half)
xm, xn, s, c = ptolemy_identity_rev(m, n, x_mn)
Expand All @@ -823,7 +843,7 @@

sin_basis = DoubleFourierSeries(M=M_nyq, N=N_nyq, NFP=NFP, sym="sin")
cos_basis = DoubleFourierSeries(M=M_nyq, N=N_nyq, NFP=NFP, sym="cos")
full_basis = DoubleFourierSeries(M=M_nyq, N=N_nyq, NFP=NFP, sym=None)
full_basis = DoubleFourierSeries(M=M_nyq, N=N_nyq, NFP=NFP, sym=False)
if eq.sym:
sin_transform = Transform(
grid=grid_lcfs, basis=sin_basis, build=False, build_pinv=True
Expand Down Expand Up @@ -932,7 +952,7 @@
if eq.sym:
x_mn[i, :] = cosfit(data[i, :])
else:
x_mn[i, :] = full_transform.fit(data[i, :])
x_mn[i, :] = fullfit(data[i, :])
xm, xn, s, c = ptolemy_identity_rev(m, n, x_mn)
bmnc[0, :] = 0
bmnc[1:, :] = c
Expand Down Expand Up @@ -975,7 +995,7 @@
if eq.sym:
x_mn[i, :] = cosfit(data[i, :])
else:
x_mn[i, :] = full_transform.fit(data[i, :])
x_mn[i, :] = fullfit(data[i, :])
xm, xn, s, c = ptolemy_identity_rev(m, n, x_mn)
bsupumnc[0, :] = 0
bsupumnc[1:, :] = -c # negative sign for negative Jacobian
Expand Down Expand Up @@ -1018,7 +1038,7 @@
if eq.sym:
x_mn[i, :] = cosfit(data[i, :])
else:
x_mn[i, :] = full_transform.fit(data[i, :])
x_mn[i, :] = fullfit(data[i, :])
xm, xn, s, c = ptolemy_identity_rev(m, n, x_mn)
bsupvmnc[0, :] = 0
bsupvmnc[1:, :] = c
Expand Down Expand Up @@ -1641,13 +1661,15 @@
return C + S

@classmethod
def compute_theta_coords(cls, lmns, xm, xn, s, theta_star, zeta, si=None):
def compute_theta_coords(
cls, lmns, xm, xn, s, theta_star, zeta, si=None, lmnc=None
):
"""Find theta such that theta + lambda(theta) == theta_star.

Parameters
----------
lmns : array-like
fourier coefficients for lambda
sin(mt-nz) Fourier coefficients for lambda
xm : array-like
poloidal mode numbers
xn : array-like
Expand All @@ -1662,6 +1684,8 @@
si : ndarray
values of radial coordinates where lmns are defined. Defaults to linearly
spaced on half grid between (0,1)
lmnc : array-like, optional
cos(mt-nz) Fourier coefficients for lambda

Returns
-------
Expand All @@ -1672,19 +1696,30 @@
if si is None:
si = np.linspace(0, 1, lmns.shape[0])
si[1:] = si[0:-1] + 0.5 / (lmns.shape[0] - 1)
lmbda_mn = interpolate.CubicSpline(si, lmns)
lmbda_mns = interpolate.CubicSpline(si, lmns)
if lmnc is None:
lmbda_mnc = lambda s: 0
else:
lmbda_mnc = interpolate.CubicSpline(si, lmnc)

Check warning on line 1703 in desc/vmec.py

View check run for this annotation

Codecov / codecov/patch

desc/vmec.py#L1703

Added line #L1703 was not covered by tests
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is cubic spline the best way to interpolate fourier coefficients in the radial direction?
A fourier coefficient may not always be smooth.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might not be, esp. at edge, but I don't feel the need to change as this is not really used anywhere important in the code

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think vmec itself implicitly assumes the fourier coeffs at at least c1 smooth


# Note: theta* (also known as vartheta) is the poloidal straight field line
# angle in PEST-like flux coordinates

def root_fun(theta):
lmbda = np.sum(
lmbda_mn(s)
lmbda_mns(s)
* np.sin(
xm[np.newaxis] * theta[:, np.newaxis]
- xn[np.newaxis] * zeta[:, np.newaxis]
),
axis=-1,
) + np.sum(
lmbda_mnc(s)
* np.cos(
xm[np.newaxis] * theta[:, np.newaxis]
- xn[np.newaxis] * zeta[:, np.newaxis]
),
axis=-1,
)
theta_star_k = theta + lmbda # theta* = theta + lambda
err = theta_star - theta_star_k # FIXME: mod by 2pi
Expand Down Expand Up @@ -1782,36 +1817,80 @@
t_nodes = t_grid.nodes
t_nodes[:, 0] = t_nodes[:, 0] ** 2

sym = "lmnc" not in vmec_data.keys()

v_nodes = cls.compute_theta_coords(
vmec_data["lmns"],
vmec_data["xm"],
vmec_data["xn"],
t_nodes[:, 0],
t_nodes[:, 1],
t_nodes[:, 2],
lmnc=vmec_data["lmnc"] if not sym else None,
)

t_nodes[:, 1] = v_nodes
if sym:
ddudt marked this conversation as resolved.
Show resolved Hide resolved
Rr_vmec, Zr_vmec = cls.vmec_interpolate(
vmec_data["rmnc"],
vmec_data["zmns"],
vmec_data["xm"],
vmec_data["xn"],
theta=r_nodes[:, 1],
phi=r_nodes[:, 2],
s=r_nodes[:, 0],
)

Rr_vmec, Zr_vmec = cls.vmec_interpolate(
vmec_data["rmnc"],
vmec_data["zmns"],
vmec_data["xm"],
vmec_data["xn"],
theta=r_nodes[:, 1],
phi=r_nodes[:, 2],
s=r_nodes[:, 0],
)

Rv_vmec, Zv_vmec = cls.vmec_interpolate(
vmec_data["rmnc"],
vmec_data["zmns"],
vmec_data["xm"],
vmec_data["xn"],
theta=t_nodes[:, 1],
phi=t_nodes[:, 2],
s=t_nodes[:, 0],
)
Rv_vmec, Zv_vmec = cls.vmec_interpolate(
vmec_data["rmnc"],
vmec_data["zmns"],
vmec_data["xm"],
vmec_data["xn"],
theta=t_nodes[:, 1],
phi=t_nodes[:, 2],
s=t_nodes[:, 0],
)
else:
Rr_vmec = cls.vmec_interpolate(

Check warning on line 1854 in desc/vmec.py

View check run for this annotation

Codecov / codecov/patch

desc/vmec.py#L1854

Added line #L1854 was not covered by tests
vmec_data["rmnc"],
vmec_data["rmns"],
vmec_data["xm"],
vmec_data["xn"],
theta=r_nodes[:, 1],
phi=r_nodes[:, 2],
s=r_nodes[:, 0],
sym=False,
)
Zr_vmec = cls.vmec_interpolate(

Check warning on line 1864 in desc/vmec.py

View check run for this annotation

Codecov / codecov/patch

desc/vmec.py#L1864

Added line #L1864 was not covered by tests
vmec_data["zmnc"],
vmec_data["zmns"],
vmec_data["xm"],
vmec_data["xn"],
theta=r_nodes[:, 1],
phi=r_nodes[:, 2],
s=r_nodes[:, 0],
sym=False,
)
Rv_vmec = cls.vmec_interpolate(

Check warning on line 1874 in desc/vmec.py

View check run for this annotation

Codecov / codecov/patch

desc/vmec.py#L1874

Added line #L1874 was not covered by tests
vmec_data["rmnc"],
vmec_data["rmns"],
vmec_data["xm"],
vmec_data["xn"],
theta=t_nodes[:, 1],
phi=t_nodes[:, 2],
s=t_nodes[:, 0],
sym=False,
)
Zv_vmec = cls.vmec_interpolate(

Check warning on line 1884 in desc/vmec.py

View check run for this annotation

Codecov / codecov/patch

desc/vmec.py#L1884

Added line #L1884 was not covered by tests
vmec_data["zmnc"],
vmec_data["zmns"],
vmec_data["xm"],
vmec_data["xn"],
theta=t_nodes[:, 1],
phi=t_nodes[:, 2],
s=t_nodes[:, 0],
sym=False,
)

coords = {
"Rr_desc": Rr_desc,
Expand Down
19 changes: 19 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -335,3 +335,22 @@ def VMEC_save(SOLOVEV, tmpdir_factory):
)
desc = Dataset(str(SOLOVEV["desc_nc_path"]), mode="r")
return vmec, desc


@pytest.fixture(scope="session")
def VMEC_save_asym(tmpdir_factory):
"""Save an asymmetric equilibrium in VMEC netcdf format for comparison."""
tmpdir = tmpdir_factory.mktemp("asym_wout")
filename = tmpdir.join("wout_HELIO_asym_desc.nc")
vmec = Dataset("./tests/inputs/wout_HELIOTRON_asym_NTHETA50_NZETA100.nc", mode="r")
eq = Equilibrium.load("./tests/inputs/HELIO_asym.h5")
VMECIO.save(
eq,
filename,
surfs=vmec.variables["ns"][:],
verbose=0,
M_nyq=round(np.max(vmec.variables["xm_nyq"][:])),
N_nyq=round(np.max(vmec.variables["xn_nyq"][:]) / eq.NFP),
)
desc = Dataset(filename, mode="r")
return vmec, desc, eq
Binary file added tests/inputs/HELIO_asym.h5
Binary file not shown.
Binary file not shown.
Loading
Loading