Skip to content

Commit 6de453b

Browse files
committed
Merge branch 'master' of https://github.com/PlasmaControl/DESC into rg/Mercier_rational
2 parents c5be646 + cc87f93 commit 6de453b

25 files changed

+15208
-338
lines changed

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,21 @@
11
Changelog
22
=========
33

4+
New Features
5+
- 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.
6+
Optionally can also contract the profiles of the original ``Equilibrium`` so that the new ``Equilibrium``'s profiles match the original's in real space.
7+
- Adds second-order NAE constraints, accessible by passing ``order=2`` to ``desc.objectives.get_NAE_constraints``.
8+
- Adds automatically generated header file showing date the input file was created with `desc.vmec.VMECIO.write_vmec_input`
9+
10+
11+
v0.14.1
12+
-------
13+
414
Bug Fixes
515
- Fixes bug in ``desc.vmec.VMECIO.write_vmec_input`` for current-constrained equilibria, where DESC was incorrectly writing the ``s**0`` mode, where VMEC actually assumes it is zero and starts at the ``s**1`` (which is different than the usual convention VMEC uses for its current profile when it uses the current derivative, where it starts with the ``s**0`` mode).
616
- Fixes error that occurs when using the default grid for ``SplineXYZCoil`` in an optimization.
717
- Fixes bug in ``desc.coils.CoilSet.save_in_makegrid_format`` for ``CoilSet`` objects with ``sym=True`` or ``NFP>1``
18+
- Adds missing `&END` to the input file created from `desc.vmec.VMECIO.write_vmec_input`
819

920
v0.14.0
1021
-------
@@ -37,6 +48,7 @@ for compatibility with other codes which expect such files from the Booz_Xform c
3748
- Adds a new objective ``desc.objectives.ExternalObjective`` for wrapping external codes with finite differences.
3849
- DESC/JAX version and device info is no longer printed by default, but can be accessed with the function `desc.backend.print_backend_info()`.
3950

51+
4052
Performance Improvements
4153

4254
- A number of minor improvements to basis function evaluation and spectral transforms to improve speed. These will also enable future improvements for larger gains.

desc/compat.py

Lines changed: 134 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
import numpy as np
66

77
from desc.grid import Grid, LinearGrid, QuadratureGrid
8-
from desc.utils import errorif
8+
from desc.profiles import FourierZernikeProfile, PowerSeriesProfile, SplineProfile
9+
from desc.utils import errorif, setdefault, warnif
910

1011

1112
def ensure_positive_jacobian(eq):
@@ -330,3 +331,135 @@ def _get_new_coeffs(fun):
330331
eq_rotated.axis = eq_rotated.get_axis()
331332

332333
return eq_rotated
334+
335+
336+
def contract_equilibrium(
337+
eq, inner_rho, contract_profiles=True, profile_num_points=None, copy=True
338+
):
339+
"""Contract an equilibrium so that an inner flux surface is the new boundary.
340+
341+
Parameters
342+
----------
343+
eq : Equilibrium
344+
Equilibrium to contract.
345+
inner_rho: float
346+
rho value (<1) to contract the Equilibrium to.
347+
contract_profiles : bool
348+
Whether or not to contract the profiles.
349+
If True, the new profile's value at ``rho=1.0`` will be the same as the old
350+
profile's value at ``rho=inner_rho``, i.e. in physical space, the new
351+
profile is the same as the old profile. If the profile is a
352+
``PowerSeriesProfile`` or ``SplineProfile``, the same profile type will be
353+
returned. If not one of these two classes, a ``SplineProfile`` will be returned
354+
as other profile classes cannot be safely contracted.
355+
If False, the new profile will have the same functional form
356+
as the old profile, with no rescaling performed. This means the new equilibrium
357+
has a physically different profile than the original equilibrium.
358+
profile_num_points : int
359+
Number of points to use when fitting or re-scaling the profiles. Defaults to
360+
``eq.L_grid``
361+
copy : bool
362+
Whether or not to return a copy or to modify the original equilibrium.
363+
364+
Returns
365+
-------
366+
eq_inner: New Equilibrium object, contracted from the old one such that
367+
eq.pressure(rho=inner_rho) = eq_inner.pressure(rho=1), and
368+
eq_inner LCFS = eq's rho=inner_rho surface.
369+
Note that this will not be in force balance, and so must be re-solved.
370+
371+
"""
372+
errorif(
373+
not (inner_rho < 1 and inner_rho > 0),
374+
ValueError,
375+
f"Surface must be in the range 0 < inner_rho < 1, instead got {inner_rho}.",
376+
)
377+
profile_num_points = setdefault(profile_num_points, eq.L_grid)
378+
379+
def scale_profile(profile, rho):
380+
errorif(
381+
isinstance(profile, FourierZernikeProfile),
382+
ValueError,
383+
"contract_equilibrium does not support FourierZernikeProfile",
384+
)
385+
if profile is None:
386+
return profile
387+
is_power_series = isinstance(profile, PowerSeriesProfile)
388+
if contract_profiles and is_power_series:
389+
# only PowerSeriesProfile both
390+
# a) has a from_values
391+
# b) can safely use that from_values to represent a
392+
# subset of itself.
393+
x = np.linspace(0, 1, profile_num_points)
394+
grid = LinearGrid(rho=x / rho)
395+
y = profile.compute(grid)
396+
return profile.from_values(x=x, y=y)
397+
elif contract_profiles:
398+
warnif(
399+
not isinstance(profile, SplineProfile),
400+
UserWarning,
401+
f"{profile} is not a PowerSeriesProfile or SplineProfile,"
402+
" so cannot safely contract using the same profile type."
403+
"Falling back to fitting the values with a SplineProfile",
404+
)
405+
x = np.linspace(0, 1, profile_num_points)
406+
grid = LinearGrid(rho=x / rho)
407+
y = profile.compute(grid)
408+
return SplineProfile(knots=x, values=y)
409+
else: # don't do any scaling of the profile
410+
return profile
411+
412+
# create new profiles for contracted equilibrium
413+
pressure = scale_profile(eq.pressure, inner_rho)
414+
iota = scale_profile(eq.iota, inner_rho)
415+
current = scale_profile(eq.current, inner_rho)
416+
electron_density = scale_profile(eq.electron_density, inner_rho)
417+
electron_temperature = scale_profile(eq.electron_temperature, inner_rho)
418+
ion_temperature = scale_profile(eq.ion_temperature, inner_rho)
419+
atomic_number = scale_profile(eq.atomic_number, inner_rho)
420+
anisotropy = scale_profile(eq.anisotropy, inner_rho)
421+
422+
surf_inner = eq.get_surface_at(rho=inner_rho)
423+
surf_inner.rho = 1.0
424+
from .equilibrium import Equilibrium
425+
426+
eq_inner = Equilibrium(
427+
surface=surf_inner,
428+
pressure=pressure,
429+
iota=iota,
430+
current=current,
431+
electron_density=electron_density,
432+
electron_temperature=electron_temperature,
433+
ion_temperature=ion_temperature,
434+
atomic_number=atomic_number,
435+
anisotropy=anisotropy,
436+
Psi=(
437+
eq.Psi * inner_rho**2
438+
), # flux (in Webers) within the new last closed flux surface
439+
NFP=eq.NFP,
440+
L=eq.L,
441+
M=eq.M,
442+
N=eq.N,
443+
L_grid=eq.L_grid,
444+
M_grid=eq.M_grid,
445+
N_grid=eq.N_grid,
446+
sym=eq.sym,
447+
ensure_nested=False, # we fit the surfaces later so don't check now
448+
)
449+
inner_grid = LinearGrid(
450+
rho=np.linspace(0, inner_rho, eq.L_grid * 2),
451+
M=eq.M_grid,
452+
N=eq.N_grid,
453+
NFP=eq.NFP,
454+
axis=True,
455+
)
456+
inner_data = eq.compute(["R", "Z", "lambda"], grid=inner_grid)
457+
nodes = inner_grid.nodes
458+
nodes[:, 0] = nodes[:, 0] / inner_rho
459+
eq_inner.set_initial_guess(
460+
nodes, inner_data["R"], inner_data["Z"], inner_data["lambda"]
461+
)
462+
if not copy: # overwrite the original eq
463+
eq.__dict__.update(eq_inner.__dict__)
464+
return eq
465+
return eq_inner

desc/equilibrium/equilibrium.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2047,16 +2047,15 @@ def from_near_axis(
20472047
A_Zw = A_Z * W[:, None]
20482048
A_Lw = A_L * W[:, None]
20492049

2050+
rho = grid.nodes[grid.unique_rho_idx, 0]
20502051
R_1D = np.zeros((grid.num_nodes,))
20512052
Z_1D = np.zeros((grid.num_nodes,))
20522053
L_1D = np.zeros((grid.num_nodes,))
2054+
phi_cyl_ax = np.linspace(0, 2 * np.pi / na_eq.nfp, na_eq.nphi, endpoint=False)
2055+
nu_B_ax = na_eq.nu_spline(phi_cyl_ax)
2056+
phi_B = phi_cyl_ax + nu_B_ax
20532057
for rho_i in rho:
20542058
R_2D, Z_2D, phi0_2D = na_eq.Frenet_to_cylindrical(r * rho_i, ntheta)
2055-
phi_cyl_ax = np.linspace(
2056-
0, 2 * np.pi / na_eq.nfp, na_eq.nphi, endpoint=False
2057-
)
2058-
nu_B_ax = na_eq.nu_spline(phi_cyl_ax)
2059-
phi_B = phi_cyl_ax + nu_B_ax
20602059
nu_B = phi_B - phi0_2D
20612060
idx = np.nonzero(grid.nodes[:, 0] == rho_i)[0]
20622061
R_1D[idx] = R_2D.flatten(order="F")

desc/objectives/getters.py

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Utilities for getting standard groups of objectives and constraints."""
22

3-
from desc.utils import flatten_list, get_all_instances, isposint, unique_list
3+
from desc.utils import errorif, flatten_list, get_all_instances, isposint, unique_list
44

55
from ._equilibrium import Energy, ForceBalance, HelicalForceBalance, RadialForceBalance
66
from .linear_objectives import (
@@ -29,7 +29,11 @@
2929
FixPsi,
3030
FixSheetCurrent,
3131
)
32-
from .nae_utils import calc_zeroth_order_lambda, make_RZ_cons_1st_order
32+
from .nae_utils import (
33+
calc_zeroth_order_lambda,
34+
make_RZ_cons_1st_order,
35+
make_RZ_cons_2nd_order,
36+
)
3337
from .objective_funs import ObjectiveFunction
3438

3539
_PROFILE_CONSTRAINTS = {
@@ -189,6 +193,15 @@ def get_NAE_constraints(
189193
A list of the linear constraints used in fixed-axis problems.
190194
191195
"""
196+
if qsc_eq is not None:
197+
errorif(
198+
qsc_eq.lasym and fix_lambda is not False,
199+
NotImplementedError,
200+
"NAE Constrained equilibria with lambda constrained "
201+
" do not yet work correctly with asymmetric equilibria, "
202+
" as the NAE-prescribed lambda may not have the correct "
203+
" gauge that DESC enforces (zero flux-surface average).",
204+
)
192205
kwargs = {"eq": desc_eq, "normalize": normalize, "normalize_target": normalize}
193206
if not isinstance(fix_lambda, bool):
194207
fix_lambda = int(fix_lambda)
@@ -266,6 +279,7 @@ def _get_NAE_constraints(
266279
Whether to constrain lambda to match that of the NAE near-axis
267280
if an `int`, fixes lambda up to that order in rho {0,1}
268281
if `True`, fixes lambda up to the specified order given by `order`
282+
(maximum of `order=1`)
269283
normalize : bool
270284
Whether to apply constraints in normalized units.
271285
@@ -298,10 +312,19 @@ def _get_NAE_constraints(
298312

299313
if order >= 1: # first order constraints
300314
constraints += make_RZ_cons_1st_order(
301-
qsc=qsc_eq, desc_eq=desc_eq, N=N, fix_lambda=fix_lambda and fix_lambda > 0
315+
qsc=qsc_eq,
316+
desc_eq=desc_eq,
317+
N=N,
318+
fix_lambda=fix_lambda and fix_lambda > 0,
319+
)
320+
if order == 2: # 2nd order constraints
321+
constraints += make_RZ_cons_2nd_order(
322+
qsc=qsc_eq,
323+
desc_eq=desc_eq,
324+
N=N,
302325
)
303-
if order >= 2: # 2nd order constraints
304-
raise NotImplementedError("NAE constraints only implemented up to O(rho) ")
326+
if order > 2:
327+
raise NotImplementedError("NAE constraints only implemented up to O(rho^2) ")
305328

306329
return constraints
307330

0 commit comments

Comments
 (0)