Published: Jul 4, 2022 by Liam Pattinson
Project info
Gyrokinetics is a branch of theoretical plasma physics that aims to provide a detailed description of plasma behaviour at length scales down to the gyroradius. It is commonly used by tokamak scientists to describe the turbulent transport effects that tend to dominate at the temperatures and pressures necessary to achieve practical fusion energy.
The gyrokinetic equation is a gyro-averaged version of the Fokker-Planck equation, which describes the evolution of a plasma distribution function in phase space. Sub-gyroradius phenomena do not have much influence on macro-scale plasmas, so by averaging over the gyroradius we may reduce the dimensionality of the phase space distribution function from 7D (3 spatial, 3 velocity, 1 time) to 6D (3 spatial, 2 velocity, 1 time). Solving the resuling 6D equation is far from trivial, so for all but the simplest cases we must turn to computational methods.
A number of software packages have been developed for this purpose, such as GS2 and CGYRO, and while each of these use different approaches to solve the gyrokinetics equation, there is a large overlap in their respective domains. It is common for gyrokinetics researchers to make use of multiple codes to ensure their results are valid and replicable, but this requires learning the idiosyncracies of each code and duplicating input files and analysis scripts to match each case. This can be error prone, as each code makes use of different normalisations and defines geometrical parameters in subtly different ways. Pyrokinetics is a Python library that aims to standarise the researcher’s interface with these codes, and it provides a means to convert input files from one code to another. It also allows the user to gather gyrokinetics output data to a standardised format, and it allows ‘local’ geometries and species profiles (that is, those on a single tokamak flux surface) to be generated from global equilibrium and kinetics data.
Our work on Pyrokinetics was divided into two sub-projects: firstly to implement a
plug-in system for each code type, and secondly to refactor a large portion of the
codebase. The plug-in system was implemented using a factory pattern, which
may be conveniently implemented in Python using the built-in UserDict
abstract base class to achieve a dict
-like interface. New classes may be ‘registered’
to the factory by assigning the class to a given key (achieved by providing a custom
__setitem__
method to the factory), and new instances of that class are constructed by
indexing the factory with that key (uses the factory’s custom __getitem__
method):
# Register to factory
my_factory["my_class"] = MyClass
# Build a new instance of MyClass [implied call to MyClass.__init__(self)]
my_class = my_factory["my_class"]
By subclassing UserDict
and implementing custom __setitem__
and __getitem__
methods, the factory automatically includes most dict
-like features, such as the
items()
or get()
methods. The factory class allows Pyrokinetics developers to avoid
the following code pattern:
if code_type == "CodeTypeA":
my_class = CodeTypeAClass()
elif code_type == "CodeTypeB":
my_class = CodeTypeBClass()
elif ...
This pattern can be error prone, especially if it is repeated many times throughout a large project, as the developers must remember to update every instance of the pattern whenever a new subclass is added or an old one is updated. Using a factory, this pattern reduces to:
my_class = my_factory[code_type]
This solution is much more robust to future code changes. The implementation in Pyrokinetics also features automatic file type inference, which is achieved by passing a file name in place of a code type:
import pyrokinetics as pk
# Get a GKInputGS2 class from the gk_inputs factory
gs2_input = pk.gk_code.gk_inputs["GS2"]
# Get the same by passing a GS2 input file
gs2_input = pk.gk.code.gk_inputs["gs2_input_file.in"]
# Get a GKInputCGYRO class by instead passing a CGYRO input file
cgyro_input = pk.gk_Code.gk_inputs["input.cgyro"]
This is achieved by implementing a short verify()
function in each ‘reader’ class,
which attempts to read a file and checks that it contains some expected information. If
it does not, the verify()
function raises an exception. The factory determines the
correct class type by iterating over each ‘reader’ class and calling their verify()
functions until one returns without having raised an exception. This adds a significant
overhead when reading files, as Pyrokinetics must iterate through each reader class,
attempt to read and verify the file for each of them, and finally read and process the
file in full after having determined the correct reader class. To circumvent this, the
user may optionally pass the code type alongside the file name to most file reading
functions in Pyrokinetics.
It is hoped that the new plug-in architecture will aid in the expansion of Pyrokinetics to include more code types. It makes it easier for users to implement their own Pyrokinetics plug-ins, as the requirements for a new plug-in are simply to implement a new class and to register it with the relevant factory. For instance, the external developer of “MyGyroCode” might implement a module for reading “MyGyroCode” input files, and integrate it with Pyrokinetics as so:
#===========================
# MyGyroCodePlugin.py
from pyrokinetics.gk_code import GKInput, gk_inputs
# Define gyrokinetics input file reading/writing class
class GKInputMyGyroCode(GKInput):
# class definition goes here...
# Register with Pyrokinetics factory
gk_inputs["MyGyroCode"] = GKInputMyGyroCode
#===========================
# Some other script...
import pyrokinetics as pk
import MyGyroCodePlugin # this import will load the plug-in
pyro = pk.Pyro(gk_file = "my_gyro_code.in")
The second stage to this project was a refactor to decouple the classes Pyro
and
GKCode
. The Pyro
class is the central concept of Pyrokinetics, and it facilitates
the reading and writing of various files and allows the user to combine data in various
ways. GKCode
was the base class for a collection of objects for reading/writing
gyrokinetics input/output files. Previously, these classes were closely linked, as
Pyro
required a GKCode
in order to process gyrokinetics files, but GKCode
required
a Pyro
object in order to perform most operations:
pyro = pk.Pyro(gk_file="gs2_input.in", gk_code="GS2")
# internally, Pyro calls...
# Uses properties, sets self.gk_code to an empty GKCodeGS2
self.gk_code = gk_code
# Pass self to GKCodeGS2.read. Modifies self.
self.gk_code.read(self, gk_file)
The GKCodeGS2
instance created by the Pyro
object would set the attributes of the
Pyro
object rather than storing its own data. This implementation broke
encapsulation, requiring developers to have a detailed understanding of
how both Pyro
and GKCode
objects worked in order to develop either one of them.
Furthermore, any changes to the internal workings of one of these classes would often
require corresponding updates to the others. The GKCode
classes were similarly
responsible for setting up GKOutput
instances, adding a further layer of coupling.
The refactor of this section of code included the following steps:
- Reimplement
GKCode
classes asGKInput
classes, which own and manage gyrokinetics input data independent ofPyro
objects. - Implement
GKOutputReader
classes which process gyrokinetics output data independent ofGKInput
orPyro
objects. These objects were not designed to be stateful, but instead were tasked with receiving an input file and returning an xarrayDataset
of gyrokinetics data. - Refactor
Pyro
to make use of these new classes instead ofGKCode
. - At each step, implement unit tests and write documentation.
Following this refactor, some further work was carried out to allow Pyro
objects to
maintain separate ‘contexts’ for each gyrokinetics code. There are cases where it may be
useful to convert a Pyro
object from one gyrokinetics code to another, and then to
switch back to the original. Switching back and forth between gyrokinetics codes should
not destroy any information, so the capabilities of Pyro
were expanded to permit
multiple independent contexts per Pyro
object, and the rules of when data will be
overwritten or converted were formalised.
There are already ongoing discussions about future updates to the Pyrokinetics API, so
it is likely that further refactoring will be required (in particular, the
context-switching behaviour of Pyro
may need a rethink). However, by decoupling the
classes that make up the Pyrokinetics project, any further API changes will be much
easier to implement.