Python scriptning extends the functionality of *FUNCTION and *PARAMETER with a more powerful way of defining custom workflow. This document describes the basics of how it can used.
IMPETUS Afea Solver uses Python 3 and it is embedded in the Windows version. Linux users must install python3 from the package manager. For use of external modules installed from pip, Python 3 must also be installed externally on Windows systems.
Writing the Python script
If you are not familiar with Python, please, check out the tutorial and documentation below:
The basics steps are: 1 read the input data (parameters), 2 do the necessary calculations and 3 return the desired value. It's important that the function actually returns a value, else the return value will be set to zero. It's also important to know that function names are case sensitive.
The example below shows a module named py_module with the function func(). The function contains one parameter called time. This parameter is passed on from the input deck in the *FUNCTION command.
def func(time): if time < 1.0: return 0.5 else: return 1.0
Including a Python script (module) into the simulation
In order to use a function defined in Python, the file needs to be added with the *SCRIPT_PYTHON command. The name of the file must end with an .py extension. Script files loaded into Python are treated as modules with the file name as module name. Be aware that the name of the module is case sensitive.
The code above loads py_module.py as a module with the name py_module. It will later be used in the *FUNCTION command.
Calling Python function from *FUNCTION
Calling a Python function works just like calling one of the built-in functions. It must, however, be preceded by
the module name seperated with a period. Example:
epsp * py_module.funcA(fxc(1), pres) *FUNCTION
One can also call two different functions from two different modules. In this example, we load python_file_A.py and python_file_B.py. Both of these modules will work independently from each other.
Any parameters that can be used in *FUNCTION, can also be sent into the python function.
python_file_A.py python_file_B.py *FUNCTION
python_file_A.func(epsp) * python_file_B.func(t)
Calling Python function from *PARAMETER
Calling a Python function from *PARAMETER is essentially the same as calling from *FUNCTION. The difference is that the returned value is assigned to variable, and the function is only executed once during the initialization.
Python functions called from *PARAMETER can also return a Python list (array) of variables. If a list is returned from Python, then the varaible in the input deck will be defined as a vector.
For usage with *PARAMETER, the python file must be included before it is called from *PARAMETER.
value = python_file.func(1, 2)
Python code in *OBJECT
Sandboxing and restrictions
Python code can be used when creating Impetus objects (*OBJECT). Unlike the normal use described above, the Python environment is sandboxed when embedded into objects.
Sandboxing is a security measure that allows one to run untrusted code in a restricted environment. It cannot interact with other parts of the system. This can help protect the system from malicious code that might try to access sensitive data or perform other undesirable actions.
The sandbox environment does not allow for imports of libraries. The reason by restricting imports is that it prevents code from using system specific functions. Functions that can potentially harm the user and its system. The math library and the built-in impetus module are however enabled by default (no need to import them).
Use of print() has also been disabled. The reason here is because print() calls fwrite under the cover, which can be used for sending sensitive information to an external output, such as a file or a network connection.
The implementation of Sandboxing also comes with other restrictions. Functions defined in the same module are set in different scopes, so they cannot call each other. Consider the following example:
value = script.evaluate(2)
def increment(variable): return variable + 1 def evaluate(data): data = increment(data) return data
The function evaluate() calls increment() and this is valid for a normal use, both in standard Python and in Impetus. This is however not possible in the sandboxed environment, as increment() and evaluate() both resides in different scopes. One way to solve this is to create a Class. Both functions can then be wrapped in the class as methods. One can then create the class object and then call its methods. The methods within the same object are in the same scope and can therefore call each other.
# Define the class class MyClass: def __init__(self): return def increment(self, variable): return variable + 1 def evaluate(self, data): data = self.increment(data) return data # Create object, this needs to defined be in the file scope obj = MyClass() def evaluate(data): # Make obj global so it can be loaded and accessed from this function global obj # Call the evaluate function within the class object return obj.evaluate(data)
The evaluate() function is called from Impetus which then calls the obj.evaluate() method within the class object. The class object then calls the method self.increment().
Conclusion & example
- A script file (module) must be loaded with the *SCRIPT_PYTHON command.
- Both module names and functions are case sensitive.
- Functions must return a value, else it returns the value zero.
- Print statements are good for debug purposes but should otherwise be avoided.
- Python functions are called by both the module and function name, seperated by a period sign.
- Python in *OBJECT are executed in sandboxed environment. Implementation is more strict but provides extra layer of security.
The example below shows the usage of the output interval for imp-output. We send in the variables: t (time) and fxc (contact force) into the function fuser(). This function evaluates the time and the contact force. If the contact force is less than 1.0, a larger output interval is returned. In every other case, a smaller output interval will returned. One exception is when the time passes 3.0e-4, then it will return a larger output interval.
def fuser(t, fxc): if fxc < 1.0 or t > 3.0e-4: return 1.0e-4 else: return 1.0e-5