Skip to content

Commit 3b26e21

Browse files
dpaniciYigitElma
andauthored
Fix Redl consistency with spline profiles (#1651)
- Adds error for incorrect grid with Redl, allow Redl to use SplineProfile, - Update Redl tutorial to include a spline profile optimization Resolves #1650 --------- Co-authored-by: Yigit Gunsur Elmacioglu <[email protected]>
1 parent d15ca25 commit 3b26e21

File tree

9 files changed

+668
-223
lines changed

9 files changed

+668
-223
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,13 @@ Changelog
22
=========
33

44
New Features
5+
56
- Adds a new utility function ``desc.compat.contract_equilibrium`` which takes in an ``Equilibrium`` object and an argument ``inner_rho``, and returns a new ``Equilibrium`` with original ``Equilibrium``'s ``inner_rho`` flux surface as its boundary.
67
Optionally can also contract the profiles of the original ``Equilibrium`` so that the new ``Equilibrium``'s profiles match the original's in real space.
78
- Adds second-order NAE constraints, accessible by passing ``order=2`` to ``desc.objectives.get_NAE_constraints``.
9+
- Adds error for incorrect grids in ``desc.objectives.BootstrapRedlConsistency`` and when computing ``current Redl`` and ``<J*B> Redl`` compute quantities
10+
- Allows Redl compute quantities to use SplineProfile
11+
- Updates Redl bootstrap current consistency tutorial to include a ``SplineProfile`` optimization
812
- Adds automatically generated header file showing date the input file was created with `desc.vmec.VMECIO.write_vmec_input`
913

1014

desc/compute/_bootstrap.py

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
from ..backend import fori_loop, jnp
1616
from ..integrals.surface_integral import surface_averages_map
17+
from ..profiles import PowerSeriesProfile
1718
from .data_index import register_compute_fun
1819

1920

@@ -421,20 +422,21 @@ def _current_Redl(params, transforms, profiles, data, **kwargs):
421422
* transforms["grid"].compress(data["<J*B> Redl"])
422423
/ transforms["grid"].compress(data["<|B|^2>"])
423424
)
424-
degree = kwargs.get(
425-
"degree",
426-
min(
427-
(
428-
profiles["current"].basis.L
429-
if profiles["current"] is not None
430-
else transforms["grid"].num_rho - 1
425+
if isinstance(profiles["current"], PowerSeriesProfile):
426+
degree = kwargs.get(
427+
"degree",
428+
min(
429+
profiles["current"].basis.L,
430+
transforms["grid"].num_rho - 1,
431431
),
432-
transforms["grid"].num_rho - 1,
433-
),
434-
)
432+
)
433+
else:
434+
degree = kwargs.get("degree", transforms["grid"].num_rho - 1)
435+
435436
XX = jnp.vander(rho, degree + 1)[:, :-1] # remove constant term
436437
c_l_r = jnp.pad(jnp.linalg.lstsq(XX, current_r)[0], (0, 1)) # manual polyfit
437438
c_l = jnp.polyint(c_l_r)
438439
current = jnp.polyval(c_l, rho)
440+
439441
data["current Redl"] = transforms["grid"].expand(current)
440442
return data

desc/equilibrium/equilibrium.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -858,6 +858,46 @@ def compute( # noqa: C901
858858
msg="must pass in a Grid object for argument grid!"
859859
f" instead got type {type(grid)}",
860860
)
861+
# a check for Redl to prevent computing on-axis or
862+
# at a rho=1.0 point where profiles vanish
863+
if (
864+
any(["Redl" in name for name in names])
865+
and self.electron_density is not None
866+
):
867+
warnif(
868+
grid.axis.size,
869+
UserWarning,
870+
"Redl formula is undefined at rho=0, "
871+
"but grid has grid points at rho=0, note that on-axis"
872+
"current will be NaN.",
873+
)
874+
rho = grid.compress(grid.nodes[:, 0], "rho")
875+
876+
# check if profiles may go to zero
877+
# if they are exactly zero this would cause NaNs since the profiles
878+
# vanish.
879+
warnif(
880+
np.any(np.isclose(self.electron_density(rho), 0.0, atol=1e-8)),
881+
UserWarning,
882+
"Redl formula is undefined where kinetic profiles vanish, "
883+
"but given electron density vanishes at at least one provided"
884+
"rho grid point.",
885+
)
886+
warnif(
887+
np.any(np.isclose(self.electron_temperature(rho), 0.0, atol=1e-8)),
888+
UserWarning,
889+
"Redl formula is undefined where kinetic profiles vanish, "
890+
"but given electron temperature vanishes at at least one provided"
891+
"rho grid point.",
892+
)
893+
warnif(
894+
np.any(np.isclose(self.ion_temperature(rho), 0.0, atol=1e-8)),
895+
UserWarning,
896+
"Redl formula is undefined where kinetic profiles vanish, "
897+
"but given ion temperature vanishes at at least one provided"
898+
"rho grid point.",
899+
)
900+
861901
if grid.coordinates != "rtz":
862902
inbasis = {
863903
"r": "rho",

desc/objectives/_bootstrap.py

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,20 @@ class BootstrapRedlConsistency(_Objective):
3232
eq : Equilibrium
3333
Equilibrium that will be optimized to satisfy the Objective.
3434
grid : Grid, optional
35-
Collocation grid containing the nodes to evaluate at.
36-
Defaults to ``LinearGrid(eq.L_grid, eq.M_grid, eq.N_grid)``
35+
Collocation grid containing the nodes to evaluate at. Requires poloidal and
36+
toroidal resolution as the objective must compute flux surface averages.
37+
Defaults to
38+
``grid = LinearGrid(M=eq.M_grid,N=eq.N_grid,NFP=eq.NFP,``
39+
``sym=eq.sym,rho=np.linspace(1 / eq.L, 1, eq.L) - 1 / (2 * eq.L),)``
3740
helicity : tuple, optional
3841
Type of quasi-symmetry (M, N). Default = quasi-axisymmetry (1, 0).
3942
First entry must be M=1. Second entry is the toroidal mode number N,
40-
used for evaluating the Redl bootstrap current formula. Set to 0 for axisymmetry
41-
or quasi-axisymmetry; set to +/-NFP for quasi-helical symmetry.
43+
used for evaluating the Redl bootstrap current formula. Set to 0 for
44+
axisymmetry or quasi-axisymmetry; set to +/-NFP for quasi-helical symmetry.
45+
degree : int, optional
46+
The `degree` kwarg to pass to the `<J*B>_Redl` compute call, which
47+
controls the degree of polynomial fit to the Redl current derivative
48+
before it is integrated.
4249
4350
"""
4451

@@ -62,6 +69,7 @@ def __init__(
6269
deriv_mode="auto",
6370
grid=None,
6471
helicity=(1, 0),
72+
degree=None,
6573
name="Bootstrap current self-consistency (Redl)",
6674
jac_chunk_size=None,
6775
):
@@ -71,6 +79,7 @@ def __init__(
7179
assert helicity[0] == 1, "Redl bootstrap current model assumes helicity[0] == 1"
7280
self._grid = grid
7381
self.helicity = helicity
82+
self._degree = degree
7483
super().__init__(
7584
things=eq,
7685
target=target,
@@ -125,6 +134,12 @@ def build(self, use_jit=True, verbose=1):
125134
ValueError,
126135
"Helicity toroidal mode number should be 0 (QA) or +/- NFP (QH)",
127136
)
137+
rho = grid.nodes[grid.unique_rho_idx, 0]
138+
errorif(
139+
grid.axis.size,
140+
ValueError,
141+
"Redl formula is undefined at rho=0, but grid has grid points at rho=0",
142+
)
128143

129144
self._dim_f = grid.num_rho
130145
self._data_keys = ["<J*B>", "<J*B> Redl"]
@@ -146,10 +161,34 @@ def build(self, use_jit=True, verbose=1):
146161
"Bootstrap current calculation requires an ion temperature profile.",
147162
)
148163

164+
rho = grid.compress(grid.nodes[:, 0], "rho")
165+
166+
# check if profiles may go to zero
167+
# if they are exactly zero this would cause NaNs since the profiles
168+
# vanish.
169+
errorif(
170+
np.any(np.isclose(eq.electron_density(rho), 0.0, atol=1e-8)),
171+
ValueError,
172+
"Redl formula is undefined where kinetic profiles vanish, "
173+
"but given electron density vanishes at at least one provided"
174+
"rho grid point.",
175+
)
176+
errorif(
177+
np.any(np.isclose(eq.electron_temperature(rho), 0.0, atol=1e-8)),
178+
ValueError,
179+
"Redl formula is undefined where kinetic profiles vanish, "
180+
"but given electron temperature vanishes at at least one provided"
181+
"rho grid point.",
182+
)
183+
errorif(
184+
np.any(np.isclose(eq.ion_temperature(rho), 0.0, atol=1e-8)),
185+
ValueError,
186+
"Redl formula is undefined where kinetic profiles vanish, "
187+
"but given ion temperature vanishes at at least one provided"
188+
"rho grid point.",
189+
)
149190
# Try to catch cases in which density or temperatures are specified in the
150191
# wrong units. Densities should be ~ 10^20, temperatures are ~ 10^3.
151-
rho = eq.compute("rho", grid=grid)["rho"]
152-
153192
warnif(
154193
jnp.any(eq.electron_density(rho) > 1e22),
155194
UserWarning,
@@ -166,7 +205,9 @@ def build(self, use_jit=True, verbose=1):
166205
UserWarning,
167206
"Ion temperature is surprisingly high. It should have units of eV",
168207
)
169-
# Profiles may go to 0 at rho=1, so exclude the last few grid points from lower
208+
209+
# Profiles may go to 0 at rho=1 (and we've already checked if our
210+
# grid has points there), so exclude the last few grid points from lower
170211
# bounds:
171212
rho = rho[rho < 0.85]
172213
warnif(
@@ -197,6 +238,7 @@ def build(self, use_jit=True, verbose=1):
197238
"transforms": transforms,
198239
"profiles": profiles,
199240
"helicity": self._helicity,
241+
"degree": self._degree,
200242
}
201243

202244
timer.stop("Precomputing transforms")
@@ -235,6 +277,7 @@ def compute(self, params, constants=None):
235277
transforms=constants["transforms"],
236278
profiles=constants["profiles"],
237279
helicity=constants["helicity"],
280+
degree=constants["degree"],
238281
)
239282
return constants["transforms"]["grid"].compress(
240283
data["<J*B>"] - data["<J*B> Redl"]

0 commit comments

Comments
 (0)