Perform a N-D scan in parameters A,B,C etc.#

It is often common to perform parameters scan when running gyrokinetic analysis. The PyroScan object allows for easy generation of input files for an N-D parameter scan along with the ability to read in the outputs for all the runs.

Simple 1-D scan#

To start of you need a fully initialised Pyro object and a dictionary object that outlines the variable to scan through. Here we scan through \(k_y\rho_s\)

# Initialise a Pyro object
from pyrokinetics import Pyro, PyroScan, template_dir
pyro = Pyro(gk_file= template_dir / "input.cgyro")

# Write input files
param_dict = {"ky": [0.1, 0.2, 0.3]}

# Create PyroScan object
pyro_scan = PyroScan(
    pyro,
    param_dict,
    base_directory="run_directory"
)

# Write input files
pyro_scan.write()

This will write 3 different input files where the parameter pyrokinetics.numerics.Numerics.ky has been changed, with each input file in its own directory under a base directory of run_directory.

The variable here is always defined in pyrokinetics default units, so when using a code that has a different normalised be aware that the values defined here will be converted to the code own units. See Normalisation conventions.

This also created a pyroscan.json file that can be used to initialise a run

Note ky here is a default parameter key to scan through with others listed here pyrokinetics.pyroscan.PyroScan.load_default_parameter_keys(). To do a bespoke parameter see below

Running/Submitting simulations#

To actually run/submit these simulations to the cluster, it is necessary to go into each input file directory and run/submit from there. These directories are stored in pyro_scan.run_directories

from shutil import copy
from pyrokinetics.pyroscan import cd
import os

for run_dir in pyro_scan.run_directories:

    # Use pyroscan change directory method to go into each run directory
    with cd(run_dir):
        # Run a simulation
        os.system('cgyro -n 32 -nomp 3 ')

        # Copy (existing) batch script and submit
        copy("batch.src", run_dir)
        with cd(run_dir):

            # Submit job with run directory names
            os.system(f"sbatch -J pyroscan_{rel_path} batch.src")

Analysing output#

Once the simulations are complete the output data is stored as an xarray Dataset. The dimensions of the Dataset are the parameter values names specified in params_dict along with the original dimensions of the data

# Load output from
pyro_scan.load_gk_output()

data = pyro_scan.gk_output
growth_rate = data['growth_rate']
mode_frequency = data['mode_frequency']

growth_rate_tolerance = data['growth_rate_tolerance']
growth_rate = growth_rate.where(growth_rate_tolerance < 0.1)
mode_frequency = mode_frequency.where(growth_rate_tolerance < 0.1)

import matplotlib.pyplot as plt
fig, (ax1, ax2) = plt.subplots(2, 1, sharex=True, figsize=(11,9))

ax1.plot(growth_rate.ky, growth_rate.data)
ax2.plot(mode_frequency.ky, mode_frequency.data)

ax1.grid(True)
ax2.grid(True)
ax1.set_ylabel(r'$\gamma (c_{s}/a)$')
ax2.set_ylabel(r'$\omega (c_{s}/a)$')
ax2.set_xlabel(r"$k_y \rho_s$")

fig.tight_layout()
plt.show()

For linear runs the following data is stored but only for the final time slice

  • growth_rate

  • mode_frequency

  • eigenfunctions

  • growth_rate_tolerance

  • particle (flux)

  • heat (flux)

Nonlinear simulations are not currently supported

Higher dimensional scans#

To perform a higher dimensional scan, the only additional requirement is to extend param_dict with more key:value pairs

# Initialise a Pyro object
from pyrokinetics import Pyro, PyroScan, template_dir
pyro = Pyro(gk_file= template_dir / "input.cgyro")

# Define parameters
param_1 = "ky"
values_1 = [0.1, 0.2, 0.3]

param_2 = "kappa"
values_2 = [1.0, 1.5, 2.0]

param_dict = {
              param_1: values_1,
              param_2: values_2,
             }

# Create PyroScan object
pyro_scan = PyroScan(
    pyro,
    param_dict,
    value_fmt=".3f",
    value_separator="_",
    parameter_separator="_",
    base_directory="run_directory"
)

After which the process is the same. Note that an outer product is formed of all the specified parameters so the number of runs can become large very quickly.

Here we have specified:

  • How to separate each value from its parameter: value_separator

  • How to separate each different value: parameter_separator

  • How each value is formatted: value_fmt

If we wanted to have each run in its own directory then we could set parameter_separator="/"

Outputs are loaded in the same with, but now with the elongation (pyrokinetics.local_geometry.miller.LocalGeometryMiller.kappa) as a dimension too

Bespoke parameters/functions#

Given that the PyroScan class can’t contain all parameters it is possible to modify any parameter defined in a Pyro object via the pyrokinetics.pyroscan.PyroScan.add_parameter_key() method

# Use existing parameter
param_1 = "q"
values_1 = np.arange(1.0, 1.5, 0.1)

# Add new parameter to scan through
param_2 = "my_electron_density_gradient"
values_2 = np.arange(0.0, 1.5, 0.5)

# Dictionary of param and values
param_dict = {param_1: values_1, param_2: values_2}

# Create PyroScan object
pyro_scan = PyroScan(
    pyro,
    param_dict,
    value_fmt=".3f",
    value_separator="_",
    parameter_separator="_",
    base_directory="run_directory",
)

# Add in path to each defined parameter to scan through
pyro_scan.add_parameter_key(param_1, "local_geometry", ["q"])
pyro_scan.add_parameter_key(param_2, "local_species", ["electron", "inverse_ln"])

The scan as is would violate quasi-neutrality as the density gradient is changing for only one species. So it is possible to apply a function to the Pyro object after each variable is set.

def maintain_quasineutrality(pyro):
    for species in pyro.local_species.names:
        if species != "electron":
            pyro.local_species[species].inverse_ln = pyro.local_species.electron.inverse_ln

# If there are kwargs to function then define here
param_2_kwargs = {}

# Add function to pyro
pyro_scan.add_parameter_func(param_2, maintain_quasineutrality, param_2_kwargs)

This allows for multiple parameters to be changed in tandem and can be defined for each input parameter.