Optimizing for quasisymmetry

In this tutorial it is shown how to optimize the boundary shape of a VMEC configuration to achieve quasisymmetry on an interior flux surface. First we will show the standard approach of optimizing with fixed numerical resolution parameters. In the second section we show a more advanced example in which the numerical resolution and size of the parameter space are adjusted during the optimization.

For these optimization examples, you probably want to write a script that is submitted to the queue on a computing cluster, where they are likely to take minutes or tens of minutes on one node.

Fixed resolution

This example is also available as QH_fixed_resolution in the examples/2_Intermediate directory. As usual, a driver script begins with imports of the classes and functions we will need:

#!/usr/bin/env python

from simsopt.util.mpi import MpiPartition
from simsopt.mhd import Vmec, Boozer, Quasisymmetry
from simsopt.objectives.graph_least_squares import LeastSquaresProblem
from simsopt.solve.graph_mpi import least_squares_mpi_solve

For this problem we will want MPI for parallelized finite difference gradients. As explained below, this particular problem has 24 independent variables, so we can take advantage of 24 + 1 = 25 concurrent function evaluations for one-sided differences. It is therefore optimal to divide the MPI processes into 25 worker groups. For more information about MPI and worker groups, see MPI Partitions and worker groups or MpiPartition. In our script, we therefore use:

mpi = MpiPartition(25)

It is not necessary to use 25 groups - the code will function properly for any choice. If you select more than 25 groups, the groups beyond the 25th will sit idle. If you select fewer than 25 groups, the 25 function evaluations needed for a finite difference gradient will be distributed among the groups that are available. There is no need to make the number of groups a multiple of the number of available MPI processes, although there cannot be more groups than processes.

We next initialize a VMEC object from an input file:

vmec = Vmec("input.nfp4_QH_warm_start", mpi=mpi)

This file can be found in the examples/2_Intermediate/inputs directory. The file describes a configuration that has already been partially optimized for quasi-helical symmetry in a very small parameter space, keeping poloidal and toroidal mode numbers only up through 1:

_images/example_quasisymmetry_QH_before.png

In the present example we will refine the configuration by optimizing in a larger parameter space, with poloidal and toroidal mode numbers up through 2. To define this parameter space, we set the fixed property of the boundary’s Fourier modes as follows:

# Define parameter space:
surf = vmec.boundary
surf.fix_all()
max_mode = 2
surf.fixed_range(mmin=0, mmax=max_mode,
                 nmin=-max_mode, nmax=max_mode, fixed=False)
surf.fix("rc(0,0)") # Major radius

The above code first fixes all modes of the boundary, since we want the mode numbers greater than 2 to all be fixed. Then the desired range of modes is set to be not fixed. This range includes the m=n=0 mode which is essentially the mean major radius. We don’t need or want to vary the overall scale of the configuration, so it is convenient to remove this mode from the parameter space.

Next, we need to configure a term in the objective function to represent the departure from quasisymmetry. This can be done as follows:

# Configure quasisymmetry objective:
qs = Quasisymmetry(Boozer(vmec),
                   0.5, # Radius to target
                   1, 1) # (M, N) you want in |B|

There are several adjustable options, the details of which can be found in the API documentation for Boozer and Quasisymmetry. The numerical resolution of the Boozer-coordinate transformation can be adjusted by passing parameters to the Boozer constructor, as in Boozer(vmec, mpol=64, ntor=32). The second argument to Quasisymmetry above sets the quasisymmetry objective to be evaluated at normalized toroidal flux of 0.5, but you are free to provide different values. Or, a list of values can be provided to target quasisymmetry on multiple surfaces. The Quasisymmetry also has optional arguments to adjust the normalization and weighting of different Fourier modes.

We are now ready to define the total objective function. Here we will include quasisymmetry and aspect ratio. Aspect ratio must be included because otherwise quasisymmetry can be made arbitrarily good by increasing the aspect ratio to infinity. The simsopt objective function is defined as follows:

# Define objective function
prob = LeastSquaresProblem.from_tuples([(vmec.aspect, 7, 1),
                                        (qs, 0, 1)])

It can be seen that we are targeting an aspect ratio of 7. This objective function will be a sum of 2017 least-squares terms, 2016 of which correspond to symmetry-breaking Fourier modes of the Boozer spectrum, plus one additional term (vmec.aspect - 7) ** 2.

Finally, we solve the optimization problem:

least_squares_mpi_solve(prob, mpi, grad=True)

Suppose you have written the above commands in a file named simsopt_driver. Depending on your computing system, the script can be run using a command like srun python simsopt_driver (for SLURM systems) or mpirun -n 25 simsopt_driver.

Since this objective function has multiple local minima, the final result of the optimization can be sensitive to small changes in simsopt, VMEC, or the packages they depend on. Therefore you will not necessarily obtain exactly the result shown here. But one result produced by this optimization script is the following configuration:

_images/example_quasisymmetry_QH_after.png _images/example_quasisymmetry_QH_after_3D.png _images/example_quasisymmetry_QH_after_Boozer.png

This last figure shows that reasonably good quasisymmetry has been achieved on the desired magnetic surface. The quality of quasisymmetry can be improved by further refining the configuration using one or more rounds of optimization with more Fourier modes in the parameter space.

Dynamic resolution

Since simsopt optimization problems are defined using a python script, you are free to add other scripting. Here we show how this capability can be used to increase the numerical resolution of VMEC and booz_xform during the optimization. At the same time, we will increase the number of Fourier modes in the parameter space during the optimization. This example can also be found in the examples/2_Intermediate directory as resolution_increase.

As usual, we begin with the necessary imports:

#!/usr/bin/env python

from simsopt.util.mpi import MpiPartition
from simsopt.mhd import Vmec, Boozer, Quasisymmetry
from simsopt.objectives.graph_least_squares import LeastSquaresProblem
from simsopt.solve.graph_mpi import least_squares_mpi_solve

We again split the pool of MPI processes into worker groups. Here, for simplicity, we make each process its own worker group, by omitting the argument:

mpi = MpiPartition()

We initialize a VMEC configuration from an input file. This starting configuration is axisymmetric with a circular cross-section, so we are starting “from scratch”:

vmec = Vmec("input.nfp2_QA", mpi=mpi)

This input file can be found in the examples/2_Intermediate/inputs directory. We define the quasisymmetry objective as in the previous section, except that we specify a helicity of (1,0) instead of (1,1) to get quasi-axisymmetry instead of quasi-helical symmetry:

# Configure quasisymmetry objective:
boozer = Boozer(vmec)
qs = Quasisymmetry(boozer,
                   0.5, # Radius to target
                   1, 0) # (M, N) you want in |B|

We now define the total objective function. For this example, it is necessary to include a nonzero target value for the rotational transform in the objective, to prevent the optimum from being truly axisymmetric:

# Define objective function
prob = LeastSquaresProblem.from_tuples([(vmec.aspect, 6, 1),
                                        (vmec.iota_axis, 0.465, 1),
                                        (vmec.iota_edge, 0.495, 1),
                                        (qs, 0, 1)])

It can be seen here that we are seeking a configuration with aspect ratio 6, and iota slightly below 0.5.

Now, we set up a loop over several optimization steps. At each step, the resolution parameters mpol and ntor for VMEC increase, as do the the Fourier resolution parameters for booz_xform. At the same time, in each optimization step a larger range of poloidal and toroidal mode numbers are set to be varied in the optimization:

for step in range(4):
    max_mode = step + 1

    # VMEC's mpol & ntor will be 3, 4, 5, 6:
    vmec.indata.mpol = 3 + step
    vmec.indata.ntor = vmec.indata.mpol

    # booz_xform's mpol & ntor will be 16, 24, 32, 40:
    boozer.mpol = 16 + step * 8
    boozer.ntor = boozer.mpol

    if mpi.proc0_world:
        print("Beginning optimization with max_mode =", max_mode, \
              ", vmec mpol=ntor=", vmec.indata.mpol, \
              ", boozer mpol=ntor=", boozer.mpol, \
              ". Previous vmec iteration = ", vmec.iter)

    # Define parameter space:
    surf.fix_all()
    surf.fixed_range(mmin=0, mmax=max_mode,
                     nmin=-max_mode, nmax=max_mode, fixed=False)
    surf.fix("rc(0,0)") # Major radius

    # Carry out the optimization for this step:
    least_squares_mpi_solve(prob, mpi, grad=True)

    if mpi.proc0_world:
        print("Done optimization with max_mode =", max_mode, \
              ". Final vmec iteration = ", vmec.iter)

If you like, other parameters could be adjusted at each step too, such as the radial resolution or number of iterations in VMEC, the solver tolerances, or the maximum number of iteration of the optimization algorithm.

As in the previous section, the final result of this optimization can be sensitive to small changes in simsopt, VMEC, or the packages they depend on. Therefore you will not necessarily obtain exactly the result shown here. But one result produced by this optimization script is the following configuration:

_images/example_quasisymmetry_QA_after.png _images/example_quasisymmetry_QA_after_3D.png _images/example_quasisymmetry_QA_after_Boozer.png