The Open FUSION Toolkit 1.0.0-6f445ef
An open-source framework for fusion and plasma science and engineering
Loading...
Searching...
No Matches
ThinCurr Python Example: Compute time-domain with mode driver in a torus

In this example we demonstrate how to perform a time-domain simulation for a model driven by the plasma mode computed in ThinCurr Python Example: Compute current potential from B-norm.

Note
Running this example requires the h5py and pyvista python packages, which are installable using pip or other standard methods.
import sys
import os
import h5py
import numpy as np
import matplotlib.pyplot as plt
import matplotlib as mpl
try:
import pyvista
pyvista.set_jupyter_backend('static') # Comment to enable interactive PyVista plots
have_pyvista = True
except ImportError:
have_pyvista = False
plt.rcParams['figure.figsize']=(6,6)
plt.rcParams['font.weight']='bold'
plt.rcParams['axes.labelweight']='bold'
plt.rcParams['lines.linewidth']=2
plt.rcParams['lines.markeredgewidth']=2
%matplotlib inline
%config InlineBackend.figure_format = "retina"

Load ThinCurr library

To load the ThinCurr python module we need to tell python where to the module is located. This can be done either through the PYTHONPATH environment variable or within a script using sys.path.append() as below, where we look for the environement variable OFT_ROOTPATH to provide the path to where the OpenFUSIONToolkit is installed (/Applications/OFT for binaries on macOS).

thincurr_python_path = os.getenv('OFT_ROOTPATH')
if thincurr_python_path is not None:
sys.path.append(os.path.join(thincurr_python_path,'python'))
from OpenFUSIONToolkit import OFT_env
from OpenFUSIONToolkit.ThinCurr import ThinCurr
from OpenFUSIONToolkit.ThinCurr.sensor import Mirnov, save_sensors
from OpenFUSIONToolkit.io import histfile
ThinCurr sensor definition and manipulation functionality.
Definition sensor.py:1
Python interface for ThinCurr thin-wall E-M functionality.
Definition __init__.py:1
General I/O functionality for Open FUSION Toolkit (OFT) Python interfaces.
Definition io.py:1

Compute frequency response

Setup ThinCurr model

We now create a OFT_env instance for execution using four threads and a ThinCurr instance that utilizes that execution environment. Once created, we setup the model from an existing HDF5 and XML mesh definition using setup_model().

We also initialize I/O for this model using setup_io() to enable output of plotting files for 3D visualization in VisIt, Paraview, or using pyvista below.

myOFT = OFT_env(nthreads=2)
tw_torus = ThinCurr(myOFT)
tw_torus.setup_model(mesh_file='thincurr_ex-torus.h5',xml_filename='oft_in.xml')
tw_torus.setup_io()
#----------------------------------------------
Open FUSION Toolkit Initialized
Development branch:    tMaker_eq_split
Revision id:           6b5d2f29
Parallelization Info:
  # of MPI tasks      =    1
  # of NUMA nodes     =    1
  # of OpenMP threads =    2
Integer Precisions    =    4   8
Float Precisions      =    4   8  16
Complex Precisions    =    4   8
LA backend            = native
#----------------------------------------------


Creating thin-wall model
  No V(t) driver coils found
  Loading I(t) driver coils
    Masked      0 coils from sensors
  Building holes

  Loading region resistivity:
     1  1.2570E-05

  Setup complete:
    # of points    =         2394
    # of edges     =         7182
    # of cells     =         4788
    # of holes     =            2
    # of closures  =            0
    # of Vcoils    =            0
    # of Icoils    =            1

Create sensor file

Before running the main calculations we will also define some sensors to measure the magnetic field. In ThinCurr all sensors measure the flux passing through a 3D path of points, but there are several helper classes to define common sensors (eg. Poloidal flux and Mirnovs). Here we define two Mirnov sensors to measure the Z-component of the magnetic field 5 cm on either side of the torus. save_sensors() is then used to save the resulting sensor for later use.

After defining the sensors we use compute_Msensor() to setup the sensors and compute mutual matrices between the sensors and the model (Msensor) and the sensors and Icoils (Msc).

sensors = [
Mirnov([1.45,0.0,0.0], [0.0,0.0,1.0], 'Bz_inner'),
Mirnov([1.55,0.0,0.0], [0.0,0.0,1.0], 'Bz_outer'),
Mirnov([1.45,0.0,0.0], [1.0,0.0,0.0], 'Br_inner'),
Mirnov([1.55,0.0,0.0], [1.0,0.0,0.0], 'Br_outer'),
]
save_sensors(sensors)
Msensor, Msc, sensor_obj = tw_torus.compute_Msensor('floops.loc')
Loading sensor information
  Loading flux loops from file: floops.loc
    # of floops =
␄   
Building element->sensor inductance matrix
  Time =  0s          
Building coil->sensor inductance matrix
  Time =  0s          

Compute self-inductance and resistivity matrices

With the model setup, we can now compute the self-inductance and resistivity matrices. A numpy version of the self-inductance matrix will be stored at tw_torus.Lmat. By default the resistivity matrix is not moved to python as it is sparse and converting to dense representation would require an increase in memory. These matrices correspond to the \(\textrm{L}\) and \(\textrm{R}\) matrices for the physical system

\(\textrm{L} \frac{\partial I}{\partial t} + \textrm{R} I = V\)

Note
For larger models calculating the self-inductance may take some time due to the \(N^2\) interaction of the elements (see ThinCurr Example: Using HODLR approximation for more information).
Mc = tw_torus.compute_Mcoil()
tw_torus.compute_Lmat()
tw_torus.compute_Rmat()
Building coil<->element inductance matrices
  Time =  0s          
Building element<->element self inductance matrix
  Time =  7s          
Building resistivity matrix

Setup plasma mode driver model

We now create a second ThinCurr instance for the plasma mode utilizes the same execution environment as above. For this case we also load the plasma mode current patterns created in ThinCurr Python Example: Compute current potential from B-norm.

tw_mode = ThinCurr(myOFT)
tw_mode.setup_model(mesh_file='thincurr_mode.h5')
with h5py.File('thincurr_mode.h5', 'r+') as h5_file:
mode_drive = np.asarray(h5_file['thincurr/driver'])
Creating thin-wall model
  No V(t) driver coils found
  No I(t) driver coils found
  Building holes

  Setup complete:
    # of points    =         6320
    # of edges     =        18960
    # of cells     =        12640
    # of holes     =            3
    # of closures  =            2
    # of Vcoils    =            0
    # of Icoils    =            0

Compute coupling from plasma mode to torus model

To use this current distribution we need the inductive coupling between the mode currents and the ThinCurr model of the wall (tw_wall). This can be done using cross_eval(), which in this case computes the flux through each element on tw_wall due to the currents specified by weights mode_drive corresponding to the model tw_mode. We also compute the coupling of the plasma mode to the sensors.

Msensor_plasma, _, _ = tw_mode.compute_Msensor('floops.loc')
mode_driver = tw_mode.cross_eval(tw_torus,mode_drive)
sensor_mode = np.dot(mode_drive,Msensor_plasma)
Loading sensor information
  Loading flux loops from file: floops.loc
    # of floops =
␄   
Building element->sensor inductance matrix
  Time =  0s          
Building coil->sensor inductance matrix
  No magnetic sensors or coils, skipping...
Applying MF element<->element inductance matrix
  Time = 32s          

Perform time-domain simulation of growing and rotating mode

With the model fully defined we can now use run_td() to perform a time-domain simulation. In this case we simulate the mode with a growth rate of 2,000 1/s and a rotation frequency of 1 kHz for four periods using a timestep of 1/50 of the rotation period (200 steps). We also specify using a direct solver for the time-advance (direct=True).

The driver waveform is set by defining appropriate Sin/Cos scale functions for the driver basis functions (mode_driver) and their derivatives, which in turn correspond to scale factors for the current and voltage. These are then used to define a voltage source for the time-domain and the sensor signal produced by these currents, which are not directly included in the simulation.

Note
The inductive voltage induced by a time-varying current is \(V = - M dI/dt\)
mode_freq = 1.E3
mode_growth = 2.E3
dt = (1.0/1.E3)/50.0
nsteps = 200
timebase_current = np.arange(0.0,dt*nsteps+1,dt/4.0); timebase_voltage = (timebase_current[1:]+timebase_current[:-1])/2.0
cos_current = timebase_current/mode_growth*np.cos(mode_freq*2.0*np.pi*timebase_current); cos_voltage = -np.diff(cos_current)/np.diff(timebase_current)
sin_current = timebase_current/mode_growth*np.sin(mode_freq*2.0*np.pi*timebase_current); sin_voltage = -np.diff(sin_current)/np.diff(timebase_current)
volt_full = np.zeros((nsteps+2,tw_torus.nelems+1))
sensor_signals = np.zeros((nsteps+2,sensor_mode.shape[1]+1))
for i in range(nsteps+2):
volt_full[i,0] = dt*i
sensor_signals[i,0] = dt*i
if i > 0:
volt_full[i,1:] = mode_driver[0,:]*np.interp(volt_full[i,0],timebase_voltage,cos_voltage) \
+ mode_driver[1,:]*np.interp(volt_full[i,0],timebase_voltage,sin_voltage)
sensor_signals[i,1:] = sensor_mode[0,:]*np.interp(volt_full[i,0],timebase_current,cos_current) \
+ sensor_mode[1,:]*np.interp(volt_full[i,0],timebase_current,sin_current)
tw_torus.run_td(dt,nsteps,status_freq=10,full_volts=volt_full,sensor_obj=sensor_obj,direct=True,sensor_values=sensor_signals)
Starting time-domain simulation
  timestep    time           sol_norm     nits    solver time
  Starting factorization
 Inverting real matrix
   Time =   0.31881199999999998     
      10    2.000000E-04    3.9983E-07       1        0.00
      20    4.000000E-04    5.5657E-07       1        0.00
      30    6.000000E-04    1.0040E-06       1        0.00
      40    8.000000E-04    1.5244E-06       1        0.00
      50    1.000000E-03    1.1763E-06       1        0.00
      60    1.200000E-03    2.4465E-06       1        0.00
      70    1.400000E-03    1.9497E-06       1        0.00
      80    1.600000E-03    2.6759E-06       1        0.00
      90    1.800000E-03    3.4373E-06       1        0.00
     100    2.000000E-03    2.3643E-06       1        0.00
     110    2.200000E-03    4.4932E-06       1        0.00
     120    2.400000E-03    3.3429E-06       1        0.00
     130    2.600000E-03    4.3478E-06       1        0.00
     140    2.800000E-03    5.3502E-06       1        0.00
     150    3.000000E-03    3.5522E-06       1        0.00
     160    3.200000E-03    6.5399E-06       1        0.00
     170    3.400000E-03    4.7362E-06       1        0.00
     180    3.600000E-03    6.0197E-06       1        0.00
     190    3.800000E-03    7.2631E-06       1        0.00
     200    4.000000E-03    4.7400E-06       1        0.00

Plot sensor evolution

We can now plot the signals from the two flux loops defined above as a function of time. During the time-domain run this information is stored in OFT's binary history file format, which can be read using the histfile class. This class stores the resulting signals in a Python dict-like representation.

In the below plots the vacuum field (no eddy currents) is shown in the dashed lines and the full field is shown in the solid lines, where the external fields are suppressed by eddy currents and the internal tangential field (Bz_inner) is enhanced.

hist_file = histfile('floops.hist')
print(hist_file)
fig, ax = plt.subplots(1,1)
ax.plot(hist_file['time']*1.E3,hist_file['Bz_inner'],color='tab:blue')
ax.plot(hist_file['time']*1.E3,hist_file['Bz_outer'],color='tab:red')
ax.plot(hist_file['time']*1.E3,sensor_signals[:-1,1],'--',color='tab:blue')
ax.plot(hist_file['time']*1.E3,sensor_signals[:-1,2],'--',color='tab:red')
_ = ax.set_xlim(left=0.0)
fig, ax = plt.subplots(1,1)
ax.plot(hist_file['time']*1.E3,hist_file['Br_inner'],color='tab:blue')
ax.plot(hist_file['time']*1.E3,hist_file['Br_outer'],color='tab:red')
ax.plot(hist_file['time']*1.E3,sensor_signals[:-1,3],'--',color='tab:blue')
ax.plot(hist_file['time']*1.E3,sensor_signals[:-1,4],'--',color='tab:red')
_ = ax.set_xlim(left=0.0)
OFT History file: floops.hist
  Number of fields = 5
  Number of entries = 201

  Fields:
    time: Simulation time [s] (d1)
    Bz_inner: No description (d1)
    Bz_outer: No description (d1)
    Br_inner: No description (d1)
    Br_outer: No description (d1)

Generate plot files

After completing the simulation we can generate plot files using tw_torus.plot_td(). Plot files are saved at a fixed timestep interval, specified by the nplot argument to run_td() with a default value of 10.

Once all fields have been saved for plotting tw_torus.build_XDMF() to generate the XDMF descriptor files for plotting with VisIt of Paraview. This method also returns a XDMF_plot_file object, which can be used to read and interact with plot data in Python (see below).

tw_torus.plot_td(nsteps,sensor_obj=sensor_obj)
plot_data = tw_torus.build_XDMF()
Post-processing time-domain simulation

Creating output files: oft_xdmf.XXXX.h5
  Removing old Xdmf files
    Removed 2 files
  Found Group: thincurr
    Found Mesh: icoils
      # of blocks: 1
    Found Mesh: smesh
      # of blocks: 1

Plot current fields

For demonstration purposes we now plot the the solution at the end of the driven phase using pyvista. We now use the plot_data object to generate a 3D plot of the current at t=2.E-3. For more information on the basic steps in this block see ThinCurr Python Example: Compute eigenstates in a plate

if have_pyvista:
grid = plot_data['ThinCurr']['smesh'].get_pyvista_grid()
J = plot_data['ThinCurr']['smesh'].get_field('J_v',2.E-3)
# Plot mesh and current density using PyVista
p = pyvista.Plotter()
grid["vectors"] = J
grid.set_active_vectors("vectors")
scale = 0.2/(np.linalg.norm(J,axis=1)).max()
arrows = grid.glyph(scale="vectors", orient="vectors", factor=scale)
p.add_mesh(arrows, cmap="turbo", scalar_bar_args={'title': "Imag(J)", "vertical": True, "position_y":0.25, "position_x": 0.0})
p.add_mesh(grid, color="white", opacity=1.0, show_edges=False)
p.show()