Custom operators#

In Ansys 2023 R1 and later, you can create custom operators in CPython. Creating custom operators consists of wrapping Python routines in a DPF-compliant way so that you can access them in the same way as you access the native operators in the ansys.dpf.core.dpf_operator.Operator class in PyDPF-Core or in any supported client API.

With support for custom operators, PyDPF-Core becomes a development tool offering:

  • Accessibility: A simple script can define a basic operator plugin.

  • Componentization: Operators with similar applications can be grouped in Python plug-in packages.

  • Easy distribution: Standard Python tools can be used to package, upload, and download custom operators.

  • Dependency management: Third-party Python modules can be added to the Python package.

  • Reusability: A documented and packaged operator can be reused in an infinite number of workflows.

  • Remotable and parallel computing: Native DPF capabilities are inherited by custom operators.

The only prerequisite for creating custom operators is to be familiar with native operators. For more information, see Operators.

Install module#

Once an Ansys-unified installation is complete, you must install the ansys-dpf-core module in the Ansys installer’s Python interpreter.

  1. Download the script for you operating system:

  2. Run the downloaded script for installing with optional arguments:

    • -awp_root: Path to the Ansys root installation folder. For example, the 2023 R1 installation folder ends with Ansys Inc/v231, and the default environment variable is AWP_ROOT231.

    • -pip_args: Optional arguments to add to the pip command. For example, --extra-index-url or --trusted-host.

If you ever want to uninstall the ansys-dpf-core module from the Ansys installation, you can do so.

  1. Download the script for your operating system:

  1. Run the downloaded script for uninstalling with the optional argument:

    • -awp_root: Path to the Ansys root installation folder. For example, the 2023 R1 installation folder ends with Ansys Inc/v231, and the default environment variable is AWP_ROOT231.

Create operators#

You can create a basic operator plugin or a plug-in package with multiple operators.

Basic operator plugin#

To create a basic operator plugin, you write a simple Python script. An operator implementation derives from the ansys.dpf.core.custom_operator.CustomOperatorBase class and a call to the ansys.dpf.core.custom_operator.record_operator() method.

This example script shows how you create a basic operator plugin:

from ansys.dpf import core as dpf
from ansys.dpf.core.custom_operator import CustomOperatorBase, record_operator  # noqa: F401
from ansys.dpf.core.operator_specification import CustomSpecification, SpecificationProperties, \
    PinSpecification


class CustomOperator(CustomOperatorBase):
    @property
    def name(self):
        return "name_of_my_custom_operator"

    @property
    def specification(self) -> CustomSpecification:
        spec = CustomSpecification()
        spec.description = "What the Operator does."
        spec.inputs = {
            0: PinSpecification("name_of_pin_0", [dpf.Field, dpf.FieldsContainer],
                                "Describe pin 0."),
        }
        spec.outputs = {
            0: PinSpecification("name_of_pin_0", [dpf.Field], "Describe pin 0."),
        }
        spec.properties = SpecificationProperties(
            user_name="user name",
            category="category",
            license="license",
        )
        return spec

    def run(self):
        field = self.get_input(0, dpf.Field)
        if field is None:
            field = self.get_input(0, dpf.FieldsContainer)[0]
        # compute data
        self.set_output(0, dpf.Field())
        self.set_succeeded()
def load_operators(*args):
    record_operator(CustomOperator, *args)

In the various properties for the class, you specify the following:

  • Name for the custom operator

  • Description of what the operator does

  • Dictionary for each input and output pin, which includes the name, a list of supported types, a description, and whether it is optional and/or ellipsis (meaning that the specification is valid for pins going from pin number x to infinity)

  • List for operator properties, including name to use in the documentation and code generation and the operator category. The optional license property allows to define a required license to check out when running the operator. Set it equal to any_dpf_supported_increments to allow any license currently accepted by DPF (see here)

For comprehensive examples on writing operator plugins, see Examples of creating custom operator plugins.

Plug-in package with multiple operators#

To create a plug-in package with multiple operators or with complex routines, you write a Python package. The benefits of writing packages rather than simple scripts are:

  • Componentization: You can split the code into several Python modules or files.

  • Distribution: You can use standard Python tools to upload and download packages.

  • Documentation: You can add README files, documentation, tests, and examples to the package.

A plug-in package with dependencies consists of a folder with the necessary files. Assume that the name of your plug-in package is custom_plugin. A folder with this name would contain four files:

  • __init__.py

  • operators.py

  • operators_loader.py

  • common.py

__init__.py file

The __init__.py file contains this code:

from operators_loader import load_operators

operators.py file

The operators.py file contains code like this:

from ansys.dpf import core as dpf
from ansys.dpf.core.custom_operator import CustomOperatorBase, record_operator  # noqa: F401
from ansys.dpf.core.operator_specification import CustomSpecification, SpecificationProperties, \
    PinSpecification


class CustomOperator(CustomOperatorBase):
    @property
    def name(self):
        return "name_of_my_custom_operator"

    @property
    def specification(self) -> CustomSpecification:
        spec = CustomSpecification()
        spec.description = "What the Operator does."
        spec.inputs = {
            0: PinSpecification("name_of_pin_0", [dpf.Field, dpf.FieldsContainer],
                                "Describe pin 0."),
        }
        spec.outputs = {
            0: PinSpecification("name_of_pin_0", [dpf.Field], "Describe pin 0."),
        }
        spec.properties = SpecificationProperties(
            user_name="user name",
            category="category",
            license="license",
        )
        return spec

    def run(self):
        field = self.get_input(0, dpf.Field)
        if field is None:
            field = self.get_input(0, dpf.FieldsContainer)[0]
        # compute data
        self.set_output(0, dpf.Field())
        self.set_succeeded()

operators_loader.py file

The operators_loader.py file contains code like this:

from custom_plugin import operators
from ansys.dpf.core.custom_operator import record_operator


def load_operators(*args):
    record_operator(operators.CustomOperator, *args)

common.py file

The common.py file contains the Python routines as classes and functions:

#write needed python routines as classes and functions here.

Third-party dependencies#

To add third-party modules as dependencies to a plug-in package, you should create and reference a folder or ZIP file with the sites of the dependencies in an XML file located next to the folder for the plug-in package. The XML file must have the same name as the plug-in package plus an .xml extension.

When the ansys.dpf.core.core.load_library() method is called, PyDPF-Core uses the site Python module to add custom sites to the path for the Python interpreter.

To create these custom sites, do the following:

  1. Install the requirements of the plug-in package in a Python virtual environment.

  2. Remove unnecessary folders from the site packages and compress them to a ZIP file.

  3. Place the ZIP file in the plug-in package.

  4. Reference the path to the ZIP file in the XML file as indicated above.

To simplify this step, you can add a requirements file in the plug-in package:

wheel
pygltflib

For this approach, do the following:

  1. Download the script for your operating system:

  1. Run the downloaded script with the mandatory arguments:

    • -pluginpath: Path to the folder with the plug-in package.

    • -zippath: Path and name for the ZIP file.

    Optional arguments are:

    • -pythonexe: Path to a Python executable of your choice.

    • -tempfolder: Path to a temporary folder to work in. The default is the environment variable TEMP on Windows and /tmp/ on Linux.

  2. Run the command for your operating system.

  • From Windows PowerShell, run:

    create_sites_for_python_operators.ps1 -pluginpath /path/to/plugin -zippath /path/to/plugin/assets/winx64.zip
    
  • From Linux Shell, run:

    create_sites_for_python_operators.sh -pluginpath /path/to/plugin -zippath /path/to/plugin/assets/linx64.zip
    

Assume once again that the name of your plug-in package is custom_plugin. A folder with this name would contain these files:

  • __init__.py

  • operators.py

  • operators_loader.py

  • common.py

  • winx64.zip

  • linx64.zip

  • custom_plugin.xml

__init__.py file

The __init__.py file contains this code:

from operators_loader import load_operators

operators.py file

The operators.py file contains code like this:

from ansys.dpf import core as dpf
from ansys.dpf.core.custom_operator import CustomOperatorBase, record_operator  # noqa: F401
from ansys.dpf.core.operator_specification import CustomSpecification, SpecificationProperties, \
    PinSpecification


class CustomOperator(CustomOperatorBase):
    @property
    def name(self):
        return "name_of_my_custom_operator"

    @property
    def specification(self) -> CustomSpecification:
        spec = CustomSpecification()
        spec.description = "What the Operator does."
        spec.inputs = {
            0: PinSpecification("name_of_pin_0", [dpf.Field, dpf.FieldsContainer],
                                "Describe pin 0."),
        }
        spec.outputs = {
            0: PinSpecification("name_of_pin_0", [dpf.Field], "Describe pin 0."),
        }
        spec.properties = SpecificationProperties(
            user_name="user name",
            category="category",
            license="license",
        )
        return spec

    def run(self):
        field = self.get_input(0, dpf.Field)
        if field is None:
            field = self.get_input(0, dpf.FieldsContainer)[0]
        # compute data
        self.set_output(0, dpf.Field())
        self.set_succeeded()

operators_loader.py file

The operators_loader.py file contains code like this:

from custom_plugin import operators
from ansys.dpf.core.custom_operator import record_operator


def load_operators(*args):
    record_operator(operators.CustomOperator, *args)


def load_operators(*args):
            record_operator(operators.CustomOperator, *args)

common.py file

The common.py file contains the Python routines as classes and functions:

#write needed python routines as classes and functions here.

requirements.txt file

The requirements.txt file contains code like this:

wheel
pygltflib

The ZIP files for Windows and Linux are included as assets:

  • winx64.zip

  • linx64.zip

custom_plugin.xml file

The custom_plugin.xml file contains code like this:

<?xml version="1.0"?>
<Environment>
	<Windows>
		<CUSTOM_SITE>$(THIS_XML_FOLDER)/custom_plugin/assets/winx64.zip;$(CUSTOM_SITE)</CUSTOM_SITE>
		<LOAD_DEFAULT_DPF_SITE>true</LOAD_DEFAULT_DPF_SITE>
	</Windows>
	<Linux>
		<CUSTOM_SITE>$(THIS_XML_FOLDER)/custom_plugin/assets/linx64.zip:$(CUSTOM_SITE)</CUSTOM_SITE>
		<LOAD_DEFAULT_DPF_SITE>true</LOAD_DEFAULT_DPF_SITE>
	</Linux>
</Environment>

Use custom operators#

Once a custom operator is created, you can use the ansys.dpf.core.core.load_library() method to load it. The first argument is the path to the directory with the plugin. The second argument is py_ plus any name identifying the plugin. The last argument is the function name for recording operators.

For a plugin that is a single script, the second argument should be py_ plus the name of the Python file:

dpf.load_library(
r"path/to/plugins",
"py_custom_plugin", #if the load_operators function is defined in path/to/plugins/custom_plugin.py
"load_operators")

For a plug-in package, the second argument should be py_ plus any name:

dpf.load_library(
r"path/to/plugins/custom_plugin",
"py_my_custom_plugin", #if the load_operators function is defined in path/to/plugins/custom_plugin/__init__.py
"load_operators")

Once the plugin is loaded, you can instantiate the custom operator:

new_operator = dpf.Operator("custom_operator") # if "custom_operator" is what is returned by the ``name`` property

References#

For more information, see Custom Operator Base in the API reference and Examples of creating custom operator plugins in Examples.