Example: Static inverse free-boundary equilibrium calculations (in SPARC)


Here we will generate an equilibrium (find coil currents with the inverse solver) in a SPARC-like tokamak.

The machine description comes from files located here.

The equilbirium\profile parameters are completely made up - please experiment on your own and change them to more realistic values as you please!

Import packages

import matplotlib.pyplot as plt
import numpy as np

Create the machine object

# build machine
from freegsnke import build_machine
tokamak = build_machine.tokamak(
    active_coils_path=f"../machine_configs/SPARC/SPARC_active_coils.pickle",
    passive_coils_path=f"../machine_configs/SPARC/SPARC_passive_coils.pickle",
    limiter_path=f"../machine_configs/SPARC/SPARC_limiter.pickle",
    wall_path=f"../machine_configs/SPARC/SPARC_wall.pickle",
)
Active coils --> built from pickle file.
Passive structures --> built from pickle file.
Limiter --> built from pickle file.
Wall --> built from pickle file.
Magnetic probes --> none provided.
Resistance (R) and inductance (M) matrices --> built using actives (and passives if present).
Tokamak built.
fig1, ax1 = plt.subplots(1, 1, figsize=(7, 15), dpi=80)

ax1.grid(zorder=0, alpha=0.75)
ax1.set_aspect('equal')
tokamak.plot(axis=ax1,show=False)                                                          # plots the active coils and passive structures
ax1.fill(tokamak.wall.R, tokamak.wall.Z, color='k', linewidth=1.2, facecolor='w', zorder=0)   # plots the limiter
[<matplotlib.patches.Polygon at 0x1528a50c0>]
../_images/986e8a05a204361090ec0be68835ebd82d1f0b9da9dc408cd10d203bdeb426f2.png

Instantiate an equilibrium

from freegsnke import equilibrium_update

eq = equilibrium_update.Equilibrium(
    tokamak=tokamak,      # provide tokamak object
    Rmin=1.1, Rmax=2.7,   # radial range
    Zmin=-1.8, Zmax=1.8,  # vertical range
    nx=129,                # number of grid points in the radial direction (needs to be of the form (2**n + 1) with n being an integer)
    ny=129,                # number of grid points in the vertical direction (needs to be of the form (2**n + 1) with n being an integer)
    # psi=plasma_psi
)

Instantiate a profile object

# initialise the profiles
from freegsnke.jtor_update import ConstrainPaxisIp
profiles = ConstrainPaxisIp(
    eq=eq,        # equilibrium object
    paxis=5e4,    # pressure on axis
    Ip=8.7e6,       # plasma current
    fvac=0.5,     # fvac = rB_{tor}
    alpha_m=1.8,  # profile function parameter
    alpha_n=1.2   # profile function parameter
)

Load the static nonlinear solver

from freegsnke import GSstaticsolver
GSStaticSolver = GSstaticsolver.NKGSsolver(eq)    

Constraints

Rx = 1.55      # X-point radius
Zx = 1.15      # X-point height

# set desired null_points locations
# this can include X-point and O-point locations
null_points = [[Rx, Rx], [Zx, -Zx]]

Rout = 2.4    # outboard midplane radius
Rin = 1.3    # inboard midplane radius

# set desired isoflux constraints with format 
# isoflux_set = [isoflux_0, isoflux_1 ... ] 
# with each isoflux_i = [R_coords, Z_coords]
isoflux_set = np.array([[[Rx, Rx, Rout, Rin, 1.7, 1.7], [Zx, -Zx, 0.0, 0.0, 1.5, -1.5]]])
           
# instantiate the freegsnke constrain object
from freegsnke.inverse import Inverse_optimizer
constrain = Inverse_optimizer(null_points=null_points,
                              isoflux_set=isoflux_set)

The inverse solve

GSStaticSolver.inverse_solve(eq=eq, 
                     profiles=profiles, 
                     constrain=constrain, 
                     target_relative_tolerance=1e-6,
                     target_relative_psit_update=1e-3,
                     verbose=False, # print output
                     l2_reg=np.array([1e-16]*10 + [1e-5]), 
                     )
                     
Inverse static solve SUCCESS. Tolerance 3.65e-07 (vs. requested 1e-06) reached in 11/100 iterations.
# refine with a forward solve (now the currents are known)
GSStaticSolver.solve(eq=eq, 
                     profiles=profiles, 
                     constrain=None, 
                     target_relative_tolerance=1e-9,
                     verbose=False, # print output
                     )
Forward static solve SUCCESS. Tolerance 2.14e-10 (vs. requested 1.00e-09) reached in 4/100 iterations.
fig1, ax1 = plt.subplots(1, 1, figsize=(7, 15), dpi=80)

ax1.grid(zorder=0, alpha=0.75)
ax1.set_aspect('equal')
eq.tokamak.plot(axis=ax1,show=False)                                                          # plots the active coils and passive structures
ax1.fill(tokamak.wall.R, tokamak.wall.Z, color='k', linewidth=1.2, facecolor='w', zorder=0)   # plots the limiter
eq.plot(axis=ax1,show=False)                                                                  # plots the equilibrium
constrain.plot(axis=ax1, show=False)                                                          # plots the contraints
ax1.set_xlim(1.0, 3.0)
ax1.set_ylim(-2.0, 2.0)
(-2.0, 2.0)
../_images/0a37f00e74841f6e23b011fc74b399008da681ae944dbe38d368411491ebe46e.png
eq.tokamak.getCurrents()

# # save coil currents to file
# import pickle
# with open('simple_diverted_currents_PaxisIp.pk', 'wb') as f:
#     pickle.dump(obj=inverse_current_values, file=f)
{'CS1I': -67750.4895278049,
 'CS1O': -61587.99877444454,
 'CS2': -162906.56986075832,
 'CS3': 58001.720719223755,
 'PF1': 100420.28963501936,
 'PF2': 88283.60063791212,
 'PF3': -97548.58916319988,
 'PF4': -140162.231729873,
 'DV1': 19990.499914512042,
 'DV2': -8231.20219673651,
 'VS1': 0.0057785679935671,
 'vacuum_vessel_0': 0.0,
 'vacuum_vessel_1': 0.0,
 'vacuum_vessel_2': 0.0,
 'vacuum_vessel_3': 0.0,
 'vacuum_vessel_4': 0.0,
 'vacuum_vessel_5': 0.0,
 'vacuum_vessel_6': 0.0,
 'vacuum_vessel_7': 0.0,
 'vacuum_vessel_8': 0.0,
 'vacuum_vessel_9': 0.0,
 'vacuum_vessel_10': 0.0,
 'vacuum_vessel_11': 0.0,
 'vacuum_vessel_12': 0.0,
 'vacuum_vessel_13': 0.0,
 'vacuum_vessel_14': 0.0,
 'vacuum_vessel_15': 0.0,
 'VSC_coil_cover0': 0.0,
 'VSC_coil_cover1': 0.0}