How to set dependent parameters in a parameter study using PyFoam with derivedParameters.py

This article describes how to set dependent parameters when running a parameter study using pyFoamRunParameterVariation.py, e.g. set the time step size as a function of the mesh spacing.

First, the basic usage is described with an example scenario. Afterwards, more information is given on possible pitfalls, additional options and how PyFoam invokes the script.

Example scenario: set the time step according to some stability criterion

Let’s assume you already have an existing parameter study consisting of a parameter file and a templated OpenFOAM test case. The time step size is already parametrized, meaning there is a controlDict.template (Remember, .template is PyFoam’s default extension for parametrized files) file containing deltaT @!delta_T!@ for the parametrized time step (Note: there are different choices to denote the tokens which are to be replaced, so it might as well be |-delta_T-| or something different).

However, rather to manually specify a set of time step sizes in the parameter file, e.g. delta_T(0.1 0.01 0.001); you want want evaluate a stability criterion which depends on your mesh resolution and set the time step accordingly. This is where derivedParameters.py comes into play, described in the following steps:

  1. Create the derivedParameters.py file in your template test case.

  2. In this Python file, implement your stability criterion in form of function, e.g.

    derivedParameters.py

    def computeTimeStepSize(values):
        dt = someFunctionOf(values)
        return dt
    

    Here, “values” is dictionary containing the current parameter vector.

  3. Set a variable, named after the corresponding token (so here it must be “delta_T”), to the values given by function defined above:

    derivedParameters.py

    delta_T = computeTimeStepSize(locals())
    

    Note that calling “locals()” returns a dictionary with the current parameter vector.

  4. That’s it! pyFoamRunParameterVariation should now set the time step according to the function computeTimeStepSize(values) (See below for the complete example code snippet).

Additional information

Options for PyFoam involving derivedParameters.py

  • --allow-derived-changes By default, PyFoam will complain if you explicitly specify a parameter in your parameter file and try to overwrite it in the derivedParameter.py script and cancel the parameter study. You can allow overwriting by calling pyFoamRunParameterVariation with the option --allow-derived-changes.
  • --derived-parameters-script=myScript.py By default, PyFoam will look for a file named derivedParameters.py. If you want to use a different file name, you can pass the option --derived-parameters-script=myScript.py specifying the name of your script.
  • There are certainly more, but I did not need them yet.

How to access values from the parameter file in the derivedParameters.py script

Calling “locals()” in the global scope of derivedParameters.py will give you a dictionary containing the current parameter vector. However, calling “locals()” inside a function definition will not work. Thus, you need to define your functions to accept at least one parameter to pass the parameter dictionary. If we stick to the example to the example above and assume that the mesh spacing is given by the key word “delta_x” in the parameter file, the complete derivedParameters.py script could look like this:

derivedParameters.py

def computeTimeStepSize(values):
	import math
 
	dx = values["delta_x"]
	dt = dx/math.pi
 
	return dt
 
delta_T = computeTimeStepSize(locals())

How to import modules in derivedParameters.py

For whatever reason, importing a module in the global scope of this script, e.g

import math
 
def computeTimeStepSize(values):
	return math.pi

results in an error stating that the name math is not known.

Moving the imports inside the function itself solves this problem.

How is this functionality implemented in PyFoam?

Essentially, the derivedParameter.py script is read as a string and then executed using Python’s built-in exec function ( exec(object, globals, locals), see here for details. PyFoam passes the current parameter vector as the “locals” argument (That’s why you need “locals()” inside the derivedParameters.py script to access them and importing modules as usual does not work). The source code related to this functionality can be found in the file PrepareCase.py of PyFoam. If you look it up, don’t be confused that it calls “exec_(…)”. “exec_(…)” is just a wrapper function whose definition depends on your Python version. In case of Python3, nothing more than calling Python3’s built-in “exec(…)” function happens.

Known issues:

PyFoam verison: 0.6.9

If you use PyFoam not only for the test case setup but also to run the solver, you may encounter an error like this:

Error in /usr/bin/pyFoamRunParameterVariation.py : Error in PyFoamParser: ‘Illegal character ‘<’ in line 36 (pos: 654)’ NONE

The cause is a line in the file PyFoamPrepareCaseParameters:

computeDeltaT <function computeDeltaT at 0x7f13ae8ba378>;

It seems that PyFoam writes all varibales defined in derivedParameters.py to this file, including those declaring functions.

Here is how to fix this:

  • Go to the PyFoam source folder (e.g. /usr/lib/python3.6/site-packages/PyFoam if you are using Arch Linux and installed PyFoam via pip).

  • In the Application directory, open RunParameterVariation.py

  • Add the function given below as member function of the class ‘RunParameterVariation’

    Illegal character fix

    def removeFunctionEntries(self, folder):
    	 # PyFoam crashes when in encounters characters like "<", "'" in the
    	 # PyFoamPrepareCaseParameters file. Unfortunately, this is written
    	 # for some unknown reason to the file if a function is declared
    	 # in derivedParameters.py
    	 # This function removes lines containing the keyword 'function'
    	 prepareCaseParameters = open(path.join(folder.name,"PyFoamPrepareCaseParameters"), 'r')
    	 outputBuffer = ""
    
    	 for line in prepareCaseParameters:
    		 if "function" in line:
    			 line = ""
    		 outputBuffer += line
    
    	 prepareCaseParameters.close()
    
    	 prepareCaseParameters = open(path.join(folder.name,"PyFoamPrepareCaseParameters"), 'w')
    	 prepareCaseParameters.write(outputBuffer)
    	 prepareCaseParameters.close()
    
  • Add a call to this member function after resetCustomers() and before initalization of run as shown below:

    resetCustomCounter()
    
    # ------ insert line below -------
    self.removeFunctionEntries(workCase)
    
    run=AnalyzedRunner(BoundingLogAnalyzer(progress=self.opts.progress,
    

The argument ‘workCase’ contains the path of the current variation.

See also