FreeGSNKE MAST-U comparison

import json
from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np

from bluemira.base.file import get_bluemira_path
from bluemira.display.auto_config import plot_defaults
from bluemira.equilibria.coils import (
    Circuit,
    Coil,
    CoilSet,
)
from bluemira.equilibria.diagnostics import PicardDiagnostic, PicardDiagnosticOptions
from bluemira.equilibria.equilibrium import Equilibrium
from bluemira.equilibria.grid import Grid
from bluemira.equilibria.optimisation.constraints import (
    FieldNullConstraint,
    IsofluxConstraint,
    MagneticConstraintSet,
)
from bluemira.equilibria.optimisation.problem import (
    UnconstrainedTikhonovCurrentGradientCOP,
)
from bluemira.equilibria.profiles import CustomProfile
from bluemira.equilibria.solve import (
    DudsonConvergence,
    PicardIterator,
)

plot_defaults()

Get MAST-U coils used by FreeGSNKE and convert to BM coilset of circuits. NOTE: Some coils appear to overlap in the FreeGSNKE example. We “fix” the solenoid here, but other overlaps are not addressed.

path = get_bluemira_path("equilibria", subfolder="examples")
name = "MAST-U_like_active_coils.json"
with open(Path(path, name)) as file:
    coil_dict = json.load(file)

circuits = []
count = []
for n, d in coil_dict.items():
    coils = []
    if n == "Solenoid":
        count.append(len(d["R"]))
        for i, (r, z) in enumerate(zip(d["R"], d["Z"], strict=False)):
            coil = Coil(
                r,
                z,
                current=5000,
                dx=d["dR"] / 2,
                dz=0.00489474,  # NOTE: Not quite sure how FreeGSNKE handles dZ
                # appears to create overlaps
                ctype="CS",
                name=n + "_" + str(i),
            )
            coils.append(coil)

    else:
        count.append(len(d["1"]["R"]))
        for i, (r1, z1, r2, z2) in enumerate(
            zip(
                d["1"]["R"],
                d["1"]["Z"],
                d["2"]["R"],
                d["2"]["Z"],
                strict=False,
            )
        ):
            coil_up = Coil(
                r1,
                z1,
                current=0,
                dx=d["1"]["dR"] / 2,
                dz=d["1"]["dZ"] / 2,
                ctype="PF",
                name=n + "U_" + str(i),
            )
            coil_low = Coil(
                r2,
                z2,
                current=0,
                dx=d["2"]["dR"] / 2,
                dz=d["2"]["dZ"] / 2,
                ctype="PF",
                name=n + "L_" + str(i),
            )
            coils.extend((coil_up, coil_low))

    circuit = Circuit(*coils)
    circuits.append(circuit)
full_coilset = CoilSet(*circuits)

full_coilset.control = [n for n in full_coilset.name if "Solenoid" not in n]

Load up the FreeGSNKE equilibrium for comparison purposes

path = get_bluemira_path("equilibria", subfolder="examples")
name = "MASTU-FREEGSNKE.eqdsk"

freegsnke_eq = Equilibrium.from_eqdsk(Path(path, name), from_cocos=7)

Match the profiles from the FreeGSNKE equilibrium with a CustomProfile

I_p = 6e5  # A
R_0 = 0.85  # m
B_0 = 0.588  # T, in FreeGSNKE fvac = R * B_0 = 0.5

pn = np.linspace(0, 1, 50)
profiles = CustomProfile(
    freegsnke_eq.profiles.pprime(pn), freegsnke_eq.ffprime(pn), R_0, B_0, I_p=I_p
)

Instantiate a new equilibrium

grid = Grid(0.1, 2.0, -2.2, 2.2, 65, 129)
eq = Equilibrium(full_coilset, grid, profiles, force_symmetry=True)

Match constraints (straight from FreeGSNKE example)

Rx = 0.6  # X-point radius
Zx = 1.1  # X-point height
Ra = 0.85
Rout = 1.4  # outboard midplane radius
Rin = 0.34  # inboard midplane radius
x_point_u = FieldNullConstraint(Rx, Zx, tolerance=1e-6)  # FreeGSNKE target tolerence
x_point_l = FieldNullConstraint(Rx, -Zx, tolerance=1e-6)
isoflux = IsofluxConstraint(
    [Rx, Rx, Rin, Rout, 1.3, 1.3, 0.8, 0.8],
    [Zx, -Zx, 0.0, 0.0, 2.1, -2.1, 1.62, -1.62],
    Rx,
    Zx,
    tolerance=1e-6,
)

constraints_list = [isoflux, x_point_u, x_point_l]
constraints_set = MagneticConstraintSet(constraints_list)

Set up an optimisation problem and converge an equilibrium. FreeGSNKE uses least squares problem with Tikhonov regularisation term (weights vary across coils - here we just use a floating point term)

current_opt_problem = UnconstrainedTikhonovCurrentGradientCOP(
    eq,
    targets=constraints_set,
    gamma=1e-8,
)

program = PicardIterator(
    eq,
    current_opt_problem,
    fixed_coils=True,
    diagnostic_plotting=PicardDiagnosticOptions(PicardDiagnostic.EQ),
    convergence=DudsonConvergence(
        1.0e-3,
    ),
    relaxation=0.0,  # FreeGSNKE blend = 0.0
    maxiter=50,
)
_ = program()

Compare the equilibria and profiles

f, ax = plt.subplots(1, 3)
freegsnke_eq.plot(ax=ax[0])
ax[0].set_title("FreeGSNKE")
eq.plot(ax=ax[1])
ax[1].set_title("BLUEMIRA")

sep = eq.get_separatrix()
fsep = freegsnke_eq.get_separatrix()

eq.coilset.plot(ax=ax[2])
ax[2].contour(eq.x, eq.z, eq.psi(), levels=20, cmap="viridis")
constraints_set.plot(ax=ax[2])
for fs in fsep:
    ax[2].plot(fs.x, fs.z, color="r")

for fs in sep:
    ax[2].plot(fs.x, fs.z, color="b", linestyle="--")

ax[2].set_title("Comparison")
ax[2].set_aspect("equal")
plt.show()


pn = np.linspace(0, 1, 50)
f, ax = plt.subplots(1, 2)
ax[0].plot(pn, eq.ffprime(pn), color="b", ls="--", label="BLUEMIRA")
ax[0].plot(pn, freegsnke_eq.ffprime(pn), color="r", label="FreeGSNKE")
ax[0].set_title("p'")
ax[0].set_xlabel(r"$\psi_n$")
ax[1].plot(pn, eq.pprime(pn), color="b", ls="--", label="BLUEMIRA")
ax[1].set_title("FF'")
ax[1].set_xlabel(r"$\psi_n$")
ax[1].plot(pn, freegsnke_eq.pprime(pn), color="r", label="FreeGSNKE")
ax[0].legend()
ax[1].legend()
plt.show()