3.8. Advanced Setup: Python Scripting

In addition to the many settings which can be typed into the main JSON file, users can create Python scripts and/or modules which can either add settings to what’s in the JSON file or change settings depending on the run matrix. For example, a complex run configuration with 100 or more databook components could define them in a Python loop rather than with many lines of ASCII text in the JSON file.

Use of Python can be done in two main ways: linking to modules via certain options within the JSON file that point to Python modules and functions or executing scripts which modify the settings via the command-line option -x. The JSON options that result in the use of Python functions are PythonPath, Modules, InitFunction, and CaseFunction.

The PythonPath key is an option simply appends folder names to the environment variable $PYTHONPATH so that .py files in that folder can be directly imported. Next, Modules simply gives a list of Python files to import as modules whenever the JSON file is read. Once these modules have been imported, the InitFunction specifies one or more functions that is called as soon as pyCart is done interpreting the JSON file. Thus InitFunction is usually most useful to determine settings that are universal to the whole run matrix but for whatever reason are easier to program in Python than write out in JSON text. The last option, CaseFunction, specifies one or more functions to call before submitting a new case (or running the --apply command). These functions take the case number as an argument and can be used to change any setting depending on what the conditions for that case are.

3.8.1. Using Scripts to Modify or Add Settings

Probably the simplest but least reliable method to move some of the work of creating the settings from JSON to Python is the -x option. When a command-line call sees -x SCRIPT, it first reads the relevant JSON file and then uses execfile() to execute that script.

Consider the following example.

$ pyfun -f poweroff.json -x poweroff.py -x report.py

When the executable script pyfun sees this command, it first loads the settings from poweroff.json and then runs the scripts poweroff.py and report.py (in that order) using execfile(). To do the same thing with a manually-created script, the following syntax would work.

# Load relevant module
import cape.pyfun
# Read JSON file
cntl = cape.pyfun.Cntl('poweroff.json')
# Apply scripts
execfile('poweroff.py')
execfile('report.py')

As the example somewhat indirectly shows (at least to those familiar with execfile()), the global variable fun3d is accessible within the two .py scripts, and modifications to the settings must reference the variable fun3d. For example, if the goal of poweroff.py is to set some extra data book components, the following example might work.

# List of components to add
comps = ["nozzle1', "nozzle2", "nozzle3", "attachment"]
# Add them as default force and moment components
cntl.opts["DataBook"]["Components"] += comps

# Loop through four boosters
for k in range(4):
    # Name of booster
    name = "booster%s" % (k+1)
    # Name of line load component
    comp = "%s_ll" % name
    # Clocking angle so that *CN* points away from core rocket
    clock = k*90.0
    # Add the component
    cntl.opts["DataBook"]["Components"].append(comp)
    # Add the definition
    cntl.opts["DataBook"][comp] = {
        "Type": "LineLoad",
        "CompID": name,
        "Transformations": [{"Type": "Euler321", "phi":clock}]
    }

As this example might make clear, these scripting capabilities have a tendency to require or at least benefit from knowledge of the pyCart API. However, while API functions may be useful for generating these scripts, all settings can be accessed as if they were in a standard Python dict. The JSON file is basically read in as a dictionary that is saved in fun3d.opts. For example, the "Report" section of the JSON file becomes fun3d.opts["Report"], and the Report Subfigures subsection is fun3d.opts["Report"]["Subfigures"], etc.

Finally, because this -x command uses global variables, the scripts must use the correct variable name for the global settings handle, which is cntl.

3.8.2. Global Settings from Python Modules

A more reliable method for altering settings from Python instead of JSON (reliable in the sense that users might forget to add all the correct -x options while InitFunction contains the proper information right within the JSON file) is to use initialization functions.

In a way, using modules instead of scripts is slightly more work than using scripts with the -x option because it requires creating a module (which is similar to creating a script) and also setting a few options within the JSON file.

These functions are controlled in the JSON file using the global "InitFunction" setting. Consider the following example from a master JSON input file.

"PythonPath": ["tools"],
"Modules": ["tfm"],
"InitFunction": ["tfm.InitArchive"],

These options import a module called tfm, which may be defined in the file tools/tfm.py or tools/tfm/__init__.py. The function tfm.InitArchive() is called immediately after the JSON file is interpreted, and it can be used to change the settings otherwise specified in the JSON file.

Example contents of the tfm module defined in tools/tfm.py are below:

# System modules
import os

# Define an initialization function
def InitArchive(cntl):
    """Change archive folder to appropriate value for calling user"""
    # Set the *ArchiveFolder* option using an environment variable
    cntl.opts.set_ArchiveFolder(os.path.join('/u/',
        os.environ['USER'], 'sls', '10008', 'f3_tfm'))

This function is not terribly complex but accomplishes a task that cannot be performed directly in the JSON file. The example is useful under the assumption that multiple users are running cases from the same input file, and this allows each user to set the archive location to their own home folder even while using identical input files.

The input to the InitFunction is the cape.cntl.Cntl, cape.pycart.cntl.Cntl, cape.pyfun.cntl.Cntl, etc. global object that contains all of the settings from the JSON file and an interface to act on them. The JSON settings are stored within cntl.opts.

The InitFunction can be used to accomplish many more complex tasks. For example you may have many different databook components that have their own definitions, which may or may not be simple. It is usually possible to define these within the JSON file, but it may require many lines of repetitive text. The InitFunction allows the user define these components within a loop using Python code and the Cape API.

3.8.3. Special Settings for Individual Runs from Python Modules

A related capability to the InitFunction is the so-called CaseFunction. This specifies one or more Python functions to be executed from a user-defined module(s) after all other setup tasks for a case have been performed. This allows the user to customize CFD settings or any other aspect of the inputs in ways that might not be possible with other Cape capabilities. In short, this can be used as a method of last resort if the environment does not support the level of customization required by the user.

Some common examples of CaseFunction include:

  • Specifying a different CFL number or some other CFD input as a function of freestream Mach number or angle of attack

  • Using different meshes according to Mach number or, for example, a control surface deflection setting

  • Adding an extra Label or tag input variable that allows the user to select from various options using the run matrix

  • Translating and/or rotating part of the mesh according to values in the run matrix (although this is typically better supported with the TriFunction, translation, rotation, or ConfigFunction keys)

  • Defining a run matrix variable and using it to turn on/off powered boundary conditions

  • Creating a function to alter the PBS setting s for each case

An example input from the JSON file is shown below.

"PythonPath": ["tools"],
"Modules": ["tfm", "freeair"],
"InitFunction": ["tfm.InitArchive"],
"CaseFunction": ["freeair.ApplyLabel", "freeair.ApplyTag"]

This instructs Cape to run the functions freeair.ApplyLabel() and then freeair.ApplyTag() after the case has been mostly set up but CFD input files have not been written yet. Some of the Python syntax for an example for FUN3D is shown below.

# Filter options based on the *Label* trajectory key
def ApplyLabel(cntl, i):
    # Get nahdle to FUN3D namelist
    f3d = cntl.opts["Fun3D"]
    # Key section names
    nsp = "nonlinear_solver_parameters"
    lsp = "linear_solver_parameers"
    ifm = "inviscid_flux_method"
    # Get *Label* value from the run matrix for case *i*
    lbl = cntl.x.Label[i]
    # Select default label if empty
    if lbl:
        # Status update
        print("    Using Label '%s'" % lbl)
    else:
        # Check Mach number for this case
        mach = cntl.x.mach[i]
        # Filter the default value based on Mach
        if mach < 1.4:
            # Select label to run uRANS
            lbl = 'd'
        else:
            # Different label to run RANS
            lbl = 'b'
    # Actually change the settings ...

As can be seen from this example, the inputs to a CaseFunction are the overall control object (cape.pycart.cntl.Cntl, cape.pyover.cntl.Cntl, or cape.pyfun.cntl.Cntl) and the case number i.