from desc import set_device set_device("gpu") import nvtx import numpy as np from desc.continuation import solve_continuation_automatic from desc.equilibrium import EquilibriaFamily, Equilibrium from desc.geometry import FourierRZToroidalSurface from desc.grid import LinearGrid from desc.objectives import ( AspectRatio, FixBoundaryR, FixBoundaryZ, FixCurrent, FixPressure, FixPsi, ForceBalance, ObjectiveFunction, QuasisymmetryTwoTerm, Elongation, RotationalTransform, Volume, ) from desc.optimize import Optimizer surf = FourierRZToroidalSurface( R_lmn=[1, 0.125, 0.1], Z_lmn=[-0.125, -0.1], modes_R=[[0, 0], [1, 0], [0, 1]], modes_Z=[[-1, 0], [0, -1]], NFP=4, ) # create initial equilibrium. Psi chosen to give B ~ 1 T. Could also give profiles here, # default is zero pressure and zero current eq = Equilibrium(M=12, N=12, Psi=0.04, surface=surf) with nvtx.annotate("solve_continuation_automatic"): # this is usually all you need to solve a fixed boundary equilibrium eq0 = solve_continuation_automatic(eq, verbose=0)[-1] # it will be helpful to store intermediate results eqfam = EquilibriaFamily(eq0) def run_qh_step(k, eq): """Run a step of the precise QH optimization example from Landreman & Paul.""" # this step will only optimize boundary modes with |m|,|n| <= k # create grid where we want to minimize QS error. Here we do it on 3 surfaces grid = LinearGrid( M=eq.M_grid, N=eq.N_grid, NFP=eq.NFP, rho=np.array([0.6, 0.8, 1.0]), sym=True ) # we create an ObjectiveFunction, in this case made up of multiple objectives # which will be combined in a least squares sense objective = ObjectiveFunction( ( # pass in the grid we defined, and don't forget the target helicity! QuasisymmetryTwoTerm(eq=eq, helicity=(1, eq.NFP), grid=grid), # try to keep the aspect ratio about the same AspectRatio(eq=eq, target=8, weight=100), ), ) # as opposed to SIMSOPT and STELLOPT where variables are assumed fixed, in DESC # we assume variables are free. Here we decide which ones to fix, starting with # the major radius (R mode = [0,0,0]) and all modes with m,n > k R_modes = np.vstack( ( [0, 0, 0], eq.surface.R_basis.modes[ np.max(np.abs(eq.surface.R_basis.modes), 1) > k, : ], ) ) Z_modes = eq.surface.Z_basis.modes[ np.max(np.abs(eq.surface.Z_basis.modes), 1) > k, : ] # next we create the constraints, using the mode number arrays just created # if we didn't pass those in, it would fix all the modes (like for the profiles) constraints = ( ForceBalance(eq=eq), FixBoundaryR(eq=eq, modes=R_modes), FixBoundaryZ(eq=eq, modes=Z_modes), FixPressure(eq=eq), FixCurrent(eq=eq), FixPsi(eq=eq), ) # this is the default optimizer, which re-solves the equilibrium at each step optimizer = Optimizer("proximal-lsq-exact") eq_new, history = eq.optimize( objective=objective, constraints=constraints, optimizer=optimizer, maxiter=20, # we don't need to solve to optimality at each multigrid step verbose=3, copy=True, # don't modify original, return a new optimized copy options={ # Sometimes the default initial trust radius is too big, allowing the # optimizer to take too large a step in a bad direction. If this happens, # we can manually specify a smaller starting radius. Each optimizer has a # number of different options that can be used to tune the performance. # See the documentation for more info. "initial_trust_ratio": 1.0, }, ) return eq_new with nvtx.annotate("optimize_1"): eq1 = run_qh_step(1, eq0) eqfam.append(eq1)