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 plugin 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 your 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.

To uninstall the ansys-dpf-core module from the Ansys installation:

  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 plugin package with multiple operators.

Basic operator plugin#

To create a basic operator plugin, 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, specify the following:

  • Name for the custom operator

  • Description of what the operator does

  • Dictionary for each input and output pin. This dictionary 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 lets you 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.

Plugin package with multiple operators#

To create a plugin package with multiple operators or with complex routines, 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 plugin package with dependencies consists of a folder with the necessary files. Assume that the name of your plugin 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 plugin package, 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 plugin package. The XML file must have the same name as the plugin 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:

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

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

  3. Place the ZIP file in the plugin 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 plugin 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 plugin 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 plugin 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 plugin 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 ref_custom_operator in the API reference and Examples of creating custom operator plugins in Examples.