Example: Static forward free-boundary equilibrium calculations


This example notebook shows how to use FreeGSNKE to solve static forward free-boundary Grad-Shafranov (GS) problems.

In the forward solve mode we solve for the plasma equilibrium using user-defined active poloidal field coil currents, passive structure currents, and plasma current density profiles.

Below, we illustrate how to use the solver for both diverted and limited plasma configurations in a MAST-U-like tokamak using stored pickle files containing the machine description. These machine description files partially come from the FreeGS repository and are not an exact replica of MAST-U.

Note:

It is recommended to go through the inverse solver notebook before this one as we omit many of the commonly shared details!

We’ll now go through the steps required to solve the forward problem in FreeGSNKE.

Import some packages

import matplotlib.pyplot as plt
import numpy as np

Create the machine object

First, we build the machine object from previously created pickle files in the “machine_configs/MAST-U” directory.

FreeGSNKE requires the following paths in order to build the machine:

  • active_coils_path

  • passive_coils_path

  • limiter_path

  • wall_path

  • magnetic_probe_path (not required here)

# build machine
from freegsnke import build_machine
tokamak = build_machine.tokamak(
    active_coils_path=f"../machine_configs/MAST-U/MAST-U_like_active_coils.pickle",
    passive_coils_path=f"../machine_configs/MAST-U/MAST-U_like_passive_coils.pickle",
    limiter_path=f"../machine_configs/MAST-U/MAST-U_like_limiter.pickle",
    wall_path=f"../machine_configs/MAST-U/MAST-U_like_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.

Instantiate an equilibrium

from freegsnke import equilibrium_update

eq = equilibrium_update.Equilibrium(
    tokamak=tokamak,      # provide tokamak object
    Rmin=0.1, Rmax=2.0,   # radial range
    Zmin=-2.2, Zmax=2.2,  # vertical range
    nx=65,                # 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

See inverse solver notebook for a list of some of the different profile objects available in FreeGSNKE. Later on, we will try some of these out.

Here, we will use the ConstrainPaxisIp profile.

# initialise the profiles
from freegsnke.jtor_update import ConstrainPaxisIp
profiles = ConstrainPaxisIp(
    eq=eq,        # equilibrium object
    paxis=8e3,    # profile object
    Ip=6e5,       # 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

We can now load FreeGSNKE’s Grad-Shafranov static solver. The equilibrium is used to inform the solver of the computational domain and of the tokamak properties. The solver below can be used for both inverse and forward solve modes.

Note: It’s not necessary to instantiate a new solver when aiming to use it on new or different equilibria, as long as the integration domain, mesh grid, and tokamak are consistent across solves.

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

Define the coil currents

As mentioned before, during a forward solve, we use fixed coil currents (as well as given profile functions/parameters) as inputs to solve for the equilibrium. To do this, we can use the set of currents we identified within the inverse solve notebook. Note that the passive structure currents are zero.

# load the coil currents
import pickle
with open('simple_diverted_currents_PaxisIp.pk', 'rb') as f:
    currents_dict = pickle.load(f)
    
# assign currents to the eq object
for key in currents_dict.keys():
    eq.tokamak.set_coil_current(coil_label=key, current_value=currents_dict[key])
    
eq.tokamak.getCurrents()
{'Solenoid': 5000.0,
 'PX': 4664.804998209495,
 'D1': 6037.479557316644,
 'D2': 1901.7767659342976,
 'D3': 1674.6060187513935,
 'Dp': -403.2350010512724,
 'D5': 3575.277366947996,
 'D6': -1090.53244932471,
 'D7': -568.10426314966,
 'P4': -4561.424627984298,
 'P5': -4036.498820520552,
 'P6': 0.0005657921621672163,
 'vessel_1': 0.0,
 'vessel_2': 0.0,
 'vessel_3': 0.0,
 'vessel_4': 0.0,
 'vessel_5': 0.0,
 'vessel_6': 0.0,
 'vessel_7': 0.0,
 'vessel_8': 0.0,
 'vessel_9': 0.0,
 'vessel_10': 0.0,
 'vessel_11': 0.0,
 'vessel_12': 0.0,
 'vessel_13': 0.0,
 'vessel_14': 0.0,
 'vessel_15': 0.0,
 'vessel_16': 0.0,
 'vessel_17': 0.0,
 'vessel_18': 0.0,
 'vessel_19': 0.0,
 'vessel_20': 0.0,
 'vessel_21': 0.0,
 'vessel_22': 0.0,
 'centrecolumn_1': 0.0,
 'centrecolumn_2': 0.0,
 'centrecolumn_3': 0.0,
 'centrecolumn_4': 0.0,
 'centrecolumn_5': 0.0,
 'centrecolumn_6': 0.0,
 'centrecolumn_7': 0.0,
 'centrecolumn_8': 0.0,
 'centrecolumn_9': 0.0,
 'centrecolumn_10': 0.0,
 'colosseum_upper_1': 0.0,
 'colosseum_upper_2': 0.0,
 'colosseum_upper_3': 0.0,
 'colosseum_lower_1': 0.0,
 'colosseum_lower_2': 0.0,
 'colosseum_lower_3': 0.0,
 'colosseum_outer_upper_1': 0.0,
 'colosseum_outer_upper_2': 0.0,
 'colosseum_outer_lower_1': 0.0,
 'colosseum_outer_lower_2': 0.0,
 'ring_plate_minor_upper_1': 0.0,
 'ring_plate_minor_upper_2': 0.0,
 'ring_plate_minor_lower_1': 0.0,
 'ring_plate_minor_lower_2': 0.0,
 'ring_plate_major_upper_1': 0.0,
 'ring_plate_major_lower_1': 0.0,
 'psp_upper_1': 0.0,
 'psp_lower_1': 0.0,
 'gas_baffle_upper_1': 0.0,
 'gas_baffle_upper_2': 0.0,
 'gas_baffle_upper_3': 0.0,
 'gas_baffle_lower_1': 0.0,
 'gas_baffle_lower_2': 0.0,
 'gas_baffle_lower_3': 0.0,
 'd1_case_upper_0': 0.0,
 'd1_case_upper_1': 0.0,
 'd1_case_upper_2': 0.0,
 'd1_case_upper_3': 0.0,
 'd1_case_lower_0': 0.0,
 'd1_case_lower_1': 0.0,
 'd1_case_lower_2': 0.0,
 'd1_case_lower_3': 0.0,
 'd2_case_upper_0': 0.0,
 'd2_case_upper_1': 0.0,
 'd2_case_upper_2': 0.0,
 'd2_case_upper_3': 0.0,
 'd2_case_lower_0': 0.0,
 'd2_case_lower_1': 0.0,
 'd2_case_lower_2': 0.0,
 'd2_case_lower_3': 0.0,
 'd3_case_upper_0': 0.0,
 'd3_case_upper_1': 0.0,
 'd3_case_upper_2': 0.0,
 'd3_case_upper_3': 0.0,
 'd3_case_lower_0': 0.0,
 'd3_case_lower_1': 0.0,
 'd3_case_lower_2': 0.0,
 'd3_case_lower_3': 0.0,
 'd5_case_upper_0': 0.0,
 'd5_case_upper_1': 0.0,
 'd5_case_upper_2': 0.0,
 'd5_case_upper_3': 0.0,
 'd5_case_lower_0': 0.0,
 'd5_case_lower_1': 0.0,
 'd5_case_lower_2': 0.0,
 'd5_case_lower_3': 0.0,
 'd6_case_upper_0': 0.0,
 'd6_case_upper_1': 0.0,
 'd6_case_upper_2': 0.0,
 'd6_case_upper_3': 0.0,
 'd6_case_lower_0': 0.0,
 'd6_case_lower_1': 0.0,
 'd6_case_lower_2': 0.0,
 'd6_case_lower_3': 0.0,
 'd7_case_upper_0': 0.0,
 'd7_case_upper_1': 0.0,
 'd7_case_upper_2': 0.0,
 'd7_case_upper_3': 0.0,
 'd7_case_lower_0': 0.0,
 'd7_case_lower_1': 0.0,
 'd7_case_lower_2': 0.0,
 'd7_case_lower_3': 0.0,
 'dp_case_upper_0': 0.0,
 'dp_case_upper_1': 0.0,
 'dp_case_upper_2': 0.0,
 'dp_case_upper_3': 0.0,
 'dp_case_lower_0': 0.0,
 'dp_case_lower_1': 0.0,
 'dp_case_lower_2': 0.0,
 'dp_case_lower_3': 0.0,
 'p4_case_upper_0': 0.0,
 'p4_case_upper_1': 0.0,
 'p4_case_upper_2': 0.0,
 'p4_case_upper_3': 0.0,
 'p4_case_lower_0': 0.0,
 'p4_case_lower_1': 0.0,
 'p4_case_lower_2': 0.0,
 'p4_case_lower_3': 0.0,
 'p5_case_upper_0': 0.0,
 'p5_case_upper_1': 0.0,
 'p5_case_upper_2': 0.0,
 'p5_case_upper_3': 0.0,
 'p5_case_lower_0': 0.0,
 'p5_case_lower_1': 0.0,
 'p5_case_lower_2': 0.0,
 'p5_case_lower_3': 0.0,
 'p6_case_upper_0': 0.0,
 'p6_case_upper_1': 0.0,
 'p6_case_upper_2': 0.0,
 'p6_case_upper_3': 0.0,
 'p6_case_upper_4': 0.0,
 'p6_case_lower_0': 0.0,
 'p6_case_lower_1': 0.0,
 'p6_case_lower_2': 0.0,
 'p6_case_lower_3': 0.0,
 'p6_case_lower_4': 0.0}

The forward solve

The syntax of a forward solve is identical to that of an inverse call (i.e. calling GSStaticSolver.solve()), however the nonlinear solver is not provided with a constrain object (i.e. we set constrain=None).

Therefore, coil current values are not modified during the solve but instead the solver uses them as inputs to calculate the equilibrium.

FreeGSNKE uses a Newton-Krylov (NK) method to solve the static forward problem stated in the inverse solve notebook (which we re-write below as a root-problem, boundary condition omitted here):

\[ \Delta^* \psi + \mu_0 R J_{\phi}(\psi,R,Z) = 0, \qquad (R,Z) \in \Omega. \]

Given the coil flux \(\psi_c\) is known prior to solving, we only require an initial guess for the plasma flux \(\psi_p\). This is generated automatically in FreeGSNKE and scaled automatically according to the size of the coil currents and/or plasma current. If a good initial guess is known, it can be provided to the solver in the equilbirium object above via the psi option.

The NK method helps mitigate the numerical instability problems associated with Picard-based iterations and enables considerably more restrictive tolerance requests. The stopping criterion is defined as

\[ \frac{\text{max} | \psi^{(n+1)} - \psi^{(n)} |}{\text{max} \ \psi^{(n)} - \text{min} \ \psi^{(n)}} < \varepsilon, \]

where \(n\) is the iteration number.

# call the solver
GSStaticSolver.solve(eq=eq, 
                     profiles=profiles, 
                     constrain=None, 
                     target_relative_tolerance=1e-9,
                     verbose=True, # print output
                     )
-----
Forward static solve starting...
Initial guess for plasma_psi successful, residual found.
Initial relative error = 9.65e-01
-----
Picard iteration: 0
Update too large, resized.
...relative error =  9.46e-01
-----
Picard iteration: 1
Update too large, resized.
...relative error =  9.23e-01
-----
Picard iteration: 2
Update too large, resized.
...relative error =  8.97e-01
-----
Picard iteration: 3
Update too large, resized.
...relative error =  8.67e-01
-----
Picard iteration: 4
Update too large, resized.
...relative error =  8.31e-01
-----
Picard iteration: 5
Update too large, resized.
...relative error =  7.90e-01
-----
Picard iteration: 6
Update too large, resized.
...relative error =  7.43e-01
-----
Picard iteration: 7
Update too large, resized.
...relative error =  6.92e-01
-----
Picard iteration: 8
Update too large, resized.
...relative error =  6.32e-01
-----
Picard iteration: 9
Update too large, resized.
...relative error =  5.60e-01
-----
Picard iteration: 10
Update too large, resized.
...relative error =  4.75e-01
-----
Picard iteration: 11
Update too large, resized.
...relative error =  3.77e-01
-----
Picard iteration: 12
Update too large, resized.
...relative error =  2.66e-01
-----
Picard iteration: 13
Update too large, resized.
...relative error =  1.40e-01
-----
Picard iteration: 14
...relative error =  2.44e-02
-----
-----
Newton-Krylov iteration: 15
...relative error =  4.60e-03
-----
-----
Newton-Krylov iteration: 16
...relative error =  5.33e-04
-----
-----
Newton-Krylov iteration: 17
...relative error =  3.06e-05
-----
-----
Newton-Krylov iteration: 18
...relative error =  3.09e-06
-----
-----
Newton-Krylov iteration: 19
...relative error =  8.18e-07
-----
-----
Newton-Krylov iteration: 20
...relative error =  1.03e-07
-----
-----
Newton-Krylov iteration: 21
...relative error =  3.46e-08
-----
-----
Newton-Krylov iteration: 22
...relative error =  1.50e-08
-----
-----
Newton-Krylov iteration: 23
...relative error =  2.96e-09
-----
-----
Newton-Krylov iteration: 24
...relative error =  4.87e-10
-----
Forward static solve SUCCESS. Tolerance 4.87e-10 (vs. requested 1.00e-09) reached in 25/100 iterations.

What we have done here is improve on the equilibrium found during the inverse solve by taking the coil currents (found during the inverse solve) and the same profile parameters and feeding them into the forward solver.

We do this because it is often difficult to achieve low relative tolerances in inverse solve calls (for example, the above was set at a loose target_relative_tolerance=1e-6) and so the strategy of using a forward solve after an inverse one is useful to obtain better (more converged) equilibria at stricter tolerances.

As an additional example, below we manually vary some of the coil currents, perform new forward solves and compare the resulting equilibria. Note that the manual current changes cause the equilibria to transition from a diverted to a limiter configuration (this is handled through FreeGS4E).

from copy import deepcopy

# copy the original eq object (for the new forward solves with modified currents)
eq_forward_1 = deepcopy(eq)
eq_forward_2 = deepcopy(eq)

# modify the P4 current and solve
eq_forward_1.tokamak.set_coil_current('P4', 1.5*eq.tokamak['P4'].current)
GSStaticSolver.solve(eq=eq_forward_1, 
                     profiles=profiles, 
                     constrain=None, 
                     target_relative_tolerance=1e-9)

# modify the P4 current (even more) and solve
eq_forward_2.tokamak.set_coil_current('P4', 1.5**2 * eq.tokamak['P4'].current)
GSStaticSolver.solve(eq=eq_forward_2, 
                     profiles=profiles, 
                     constrain=None, 
                     target_relative_tolerance=1e-9)


# plot the resulting equilbria 
fig1, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(12, 8), dpi=80)

# original 
ax1.grid(True, which='both')
eq.plot(axis=ax1,show=False)
eq.tokamak.plot(axis=ax1,show=False)
ax1.set_xlim(0.1, 2.15)
ax1.set_ylim(-2.25, 2.25)

# modified 1
ax2.grid(True, which='both')
eq_forward_1.plot(axis=ax2,show=False)
eq_forward_1.tokamak.plot(axis=ax2,show=False)
ax2.set_xlim(0.1, 2.15)
ax2.set_ylim(-2.25, 2.25)

# modified 2
ax3.grid(True, which='both')
eq_forward_2.plot(axis=ax3,show=False)
eq_forward_2.tokamak.plot(axis=ax3,show=False)
ax3.set_xlim(0.1, 2.15)
ax3.set_ylim(-2.25, 2.25)

plt.tight_layout()
Forward static solve SUCCESS. Tolerance 1.14e-10 (vs. requested 1.00e-09) reached in 19/100 iterations.
Forward static solve DID NOT CONVERGE. Tolerance 9.59e-09 (vs. requested 1.00e-09) reached in 100/100 iterations.
../_images/98b337efd9c2691f9416081757a5b48689c187468633d36152cf8f6c94b69d87.png

Alternative profile functions

Here, we will illustrate how to use some of the other available profile objects.

We’ll start with the ConstrainBetapIp object.

from freegsnke.jtor_update import ConstrainBetapIp

profiles_beta = ConstrainBetapIp(
    eq=eq,
    betap=0.05,
    Ip=6e5,
    fvac=0.5,
    alpha_m=1.8,
    alpha_n=1.2
)

We can then use it directly in a new solve (with the coil currents found by the inverse solve).

# instatiate new equilibrium object
eq_beta = deepcopy(eq)

# call solver with new profile object
GSStaticSolver.solve(eq=eq_beta, 
                     profiles=profiles_beta, 
                     constrain=None, 
                     target_relative_tolerance=1e-9)


# plot the resulting equilbria 
fig1, ax1 = plt.subplots(1, 1, figsize=(4, 8), dpi=80)
ax1.grid(True, which='both')
eq_beta.plot(axis=ax1, show=False)
eq_beta.tokamak.plot(axis=ax1, show=False)
ax1.set_xlim(0.1, 2.15)
ax1.set_ylim(-2.25, 2.25)
plt.tight_layout()
Forward static solve SUCCESS. Tolerance 3.95e-10 (vs. requested 1.00e-09) reached in 7/100 iterations.
../_images/55be68e75671aa21c5c2600ec616a03839e968ae1ac732c800d20c582688ffa8.png

We can also use the Fiesta_Topeol profile (see equation 13 in L. L. Lao et al (1985) Nucl. Fusion 25 1611). This has the same parameterisation as the previous two profiles except that we can now specify the Beta0 parameter directly.

from freegsnke.jtor_update import Fiesta_Topeol

profiles_topeol = Fiesta_Topeol(
    eq=eq,        # equilibrium object
    Beta0=0.3665, # beta0 parameter
    Ip=6e5,       # plasma current
    fvac=0.5,     # fvac = rB_{tor}
    alpha_m=2,    # profile function parameter
    alpha_n=1     # profile function parameter
)
# instatiate new equilibrium object
eq_topeol = deepcopy(eq)

# call solver with new profile object
GSStaticSolver.solve(eq=eq_topeol, 
                     profiles=profiles_topeol, 
                     constrain=None, 
                     target_relative_tolerance=1e-9)


# plot the resulting equilbria 
fig1, ax1 = plt.subplots(1, 1, figsize=(4, 8), dpi=80)
ax1.grid(True, which='both')
eq_topeol.plot(axis=ax1, show=False)
eq_topeol.tokamak.plot(axis=ax1, show=False)
ax1.set_xlim(0.1, 2.15)
ax1.set_ylim(-2.25, 2.25)
plt.tight_layout()
Forward static solve SUCCESS. Tolerance 6.40e-10 (vs. requested 1.00e-09) reached in 10/100 iterations.
../_images/37c56e988e8b13fe5d2d82217e9c95a17a0bb7d00e1b283ad548841bf8c4f8b7.png

We now test the Lao85 profile (see equations 4 and 5 in L. L. Lao et al (1985) Nucl. Fusion 25 1611).

These profiles are parametrised as:

\[J_{p}(\psi, R, Z) = \lambda\big[ \frac{R}{R_{0}} p'(\tilde{\psi}) + \frac{R_0}{R} \frac{1}{\mu_0} F F'(\tilde{\psi}) \big] \quad (R,Z) \in \Omega_p, \]

where the pressure and toroidal current profiles are given by $\( p'(\tilde{\psi}) = \sum_{i=0}^{n_p} \alpha_i \tilde{\psi}^i - \hat{\alpha} \tilde{\psi}^{n_p + 1} \sum_{i=0}^{n_p} \alpha_i\)\( and \)\( F F'(\tilde{\psi}) = \sum_{i=0}^{n_F} \beta_i \tilde{\psi}^i - \hat{\beta} \tilde{\psi}^{n_F + 1} \sum_{i=0}^{n_F} \beta_i.\)$

The required parameters are:

  • Ip (total plasma current).

  • fvac (\(rB_{tor}\), vacuum toroidal field strength).

  • alpha (array of alpha coefficients).

  • beta (array of beta coefficients).

  • alpha_logic, and beta_logic (Booleans that correspond to \(\hat{\alpha}\) and \(\hat{\beta}\) above, sets boundary condition for plasma current at plasma boundary).

  • Ip_logic (if False, Ip is not used, if True, Ip is used to normalise \(J_p\) and find \(\lambda\)).

We can use the Lao85 profile to set up the same identical equilibrium as generated by the Topeol profile.

In the Topeol profiles, we have \(\alpha_m = 2\) and \(\alpha_n = 1\) which means both \(p'\) and \(FF'\) are proportional to \(1 - \psi_n^2\) (with \(\hat{\alpha} = \hat{\beta} = 1\). This corresponds to (the vectors) \(\alpha, \beta \propto (1, 0 , -1)\) - check this.

We also need to take into account the scalings: alpha over beta is mu0.

from freegsnke.jtor_update import Lao85
from freegs4e.gradshafranov import mu0   # permeability

alpha = np.array([1,0,-1])
beta = (1 - profiles_topeol.Beta0)/profiles_topeol.Beta0 * alpha * mu0

profiles_lao = Lao85(
    eq=eq,
    Ip=6e5,
    fvac=0.5,
    alpha=alpha,
    beta=beta,
    alpha_logic=False,
    beta_logic=False,
    Ip_logic=True,
)

Note that above we’re providing as input the full list of alpha and beta coefficients and therefore setting both logic inputs to False. The following is entirely equivalent to the above:

alpha = np.array([1,0])
beta = (1 - profiles_topeol.Beta0)/profiles_topeol.Beta0 * alpha * mu0

profiles_lao = Lao85(
    eq=eq_forward,
    Ip=6e5,
    fvac=0.5,
    alpha=alpha,
    beta=beta,
    alpha_logic=True,
    beta_logic=True,
    Ip_logic=True,
)
# instatiate new equilibrium object
eq_lao = deepcopy(eq)

# call solver with new profile object
GSStaticSolver.solve(eq=eq_lao, 
                     profiles=profiles_lao, 
                     constrain=None, 
                     target_relative_tolerance=1e-9)


# plot the resulting equilbria 
fig1, ax1 = plt.subplots(1, 1, figsize=(4, 8), dpi=80)
ax1.grid(True, which='both')
eq_lao.plot(axis=ax1, show=False)
eq_lao.tokamak.plot(axis=ax1, show=False)
ax1.set_xlim(0.1, 2.15)
ax1.set_ylim(-2.25, 2.25)
plt.tight_layout()
Forward static solve SUCCESS. Tolerance 6.37e-10 (vs. requested 1.00e-09) reached in 10/100 iterations.
../_images/a98a1bd5ce6a314ec593a2c330ff459610b0d1624dc3dfce5ac2941ab76c7efc.png

The following illustrates that the two profile functions indeed generate the same current distribution.

laoj = profiles_lao.Jtor(R=eq.R, Z=eq.Z, psi=eq.psi())
topj = profiles_topeol.Jtor(R=eq.R, Z=eq.Z, psi=eq.psi())


fig1, ax1 = plt.subplots(1, 1, figsize=(5, 8), dpi=80)
ax1.grid(True, which='both')
plt.contourf(eq.R, eq.Z, (laoj-topj))
eq.tokamak.plot(axis=ax1, show=False)
plt.plot(eq.tokamak.wall.R, eq.tokamak.wall.Z, 'k', 1.2)
ax1.set_xlim(0.1, 2.15)
ax1.set_ylim(-2.25, 2.25)
plt.tight_layout()
plt.colorbar()
<matplotlib.colorbar.Colorbar at 0x3147aa380>
../_images/5e2fa0f851196fb4b69d2a0832a188cfda8aeed2ce40c48ce4f6b41d7967046f.png

FreeGSNKE also enables one to specify a set of Topeol profile parameters that best fit a set of Lao85 parameters (using the Lao_parameters method).

alpha, beta = profiles_topeol.Lao_parameters(n_alpha=2, n_beta=2, alpha_logic=True, beta_logic=True)
print(f"Original alpha's = {profiles_lao.alpha[0:2]} vs. Fitted from Topeol = {alpha}.")
print(f"Original beta's = {profiles_lao.beta[0:2]} vs. Fitted from Topeol = {beta}.")
Original alpha's = [1 0] vs. Fitted from Topeol = [ 1.00000000e+00 -9.06219544e-15].
Original beta's = [2.17211345e-06 0.00000000e+00] vs. Fitted from Topeol = [ 2.17211345e-06 -1.97745490e-20].
profiles_lao_fit = Lao85(
    eq=eq,
    Ip=6e5,
    fvac=0.5,
    alpha=alpha,
    beta=beta,
    alpha_logic=True,
    beta_logic=True,
)
laoj = profiles_lao_fit.Jtor(R=eq.R, Z=eq.Z, psi=eq.psi())
topj = profiles_topeol.Jtor(R=eq.R, Z=eq.Z, psi=eq.psi())


fig1, ax1 = plt.subplots(1, 1, figsize=(5, 8), dpi=80)
ax1.grid(True, which='both')
plt.contourf(eq.R, eq.Z, (laoj-topj))
eq.tokamak.plot(axis=ax1, show=False)
plt.plot(eq.tokamak.wall.R, eq.tokamak.wall.Z, 'k', 1.2)
ax1.set_xlim(0.1, 2.15)
ax1.set_ylim(-2.25, 2.25)
plt.tight_layout()
plt.colorbar()
<matplotlib.colorbar.Colorbar at 0x31797fd60>
../_images/514bcd568357a88733baefa82752eb6ea4cf048b118b8c06bf87c0bd6ee534ed.png

The reverse is also possible, using the the Topeol_parameters method in the Lao85 profile object.

alpha_m, alpha_n, beta_0 = profiles_lao_fit.Topeol_parameters()

print(f"Original alpha_m = {profiles_topeol.alpha_m} vs. Fitted from Lao85 = {alpha_m}.")
print(f"Original alpha_n = {profiles_topeol.alpha_n} vs. Fitted from Lao85 = {alpha_n}.")
print(f"Original beta_0 = {profiles_topeol.Beta0} vs. Fitted from Lao85 = {beta_0}.")
Original alpha_m = 2 vs. Fitted from Lao85 = 2.000000083796057.
Original alpha_n = 1 vs. Fitted from Lao85 = 1.0000000244807388.
Original beta_0 = 0.3665 vs. Fitted from Lao85 = 0.3664999995587856.

Forward solve: limiter plasma

Here we use the saved limiter plasma currents from the prior notebook.

# initialise the equilibrium
eq_limiter = deepcopy(eq)


# initialise the profiles
profiles = ConstrainPaxisIp(
    eq=eq_limiter,        # equilibrium object
    paxis=6e3,    # profile object
    Ip=4e5,       # plasma current
    fvac=0.5,     # fvac = rB_{tor}
    alpha_m=1.8,  # profile function parameter
    alpha_n=1.2   # profile function parameter
)

# load the nonlinear solver
from freegsnke import GSstaticsolver
GSStaticSolver = GSstaticsolver.NKGSsolver(eq_limiter)    

# set the coil currents
import pickle
with open('simple_limited_currents_PaxisIp.pk', 'rb') as f:
    current_values = pickle.load(f)

for key in current_values.keys():
    eq_limiter.tokamak.set_coil_current(coil_label=key, current_value=current_values[key])

# carry out the foward solve to find the equilibrium
GSStaticSolver.solve(eq=eq_limiter, 
                     profiles=profiles, 
                     constrain=None, 
                     target_relative_tolerance=1e-9)


# plot the resulting equilbria 
fig1, ax1 = plt.subplots(1, 1, figsize=(4, 8), dpi=80)
ax1.grid(True, which='both')
eq_limiter.plot(axis=ax1, show=False)
eq_limiter.tokamak.plot(axis=ax1, show=False)
ax1.set_xlim(0.1, 2.15)
ax1.set_ylim(-2.25, 2.25)
plt.tight_layout()
Forward static solve SUCCESS. Tolerance 9.27e-10 (vs. requested 1.00e-09) reached in 16/100 iterations.
../_images/19549e3a9f00328a3c94bb42be7a0ee296c5df38f052ecd6695c12d9429d0cfb.png