Custom operators#

Note

This tutorial requires DPF 7.1 or above (2024 R1).

This tutorial shows the basics of creating a custom operator in Python and loading it ont a server for use.

Note

You can create custom operators in CPython using PyDPF-Core for use with DPF in Ansys 2023 R1 and later.

It first presents how to create a custom DPF operator in Python using PyDPF-Core.

It then shows how to make a plugin out of this single operator.

The next step is to load the plugin on the server to record its operators.

The final step is to instantiate the custom operator from the client API and use it.

Note

In this tutorial the DPF client API used is PyDPF-Core but, once recorded on the server, you can call the operators of the plugin using any of the DPF client APIs (C++, CPython, IronPython), as you would any other operator.

Download tutorial as Python script Download tutorial as Jupyter notebook

Create a custom operator#

To create a custom DPF operator using PyDPF-Core, define a custom operator class inheriting from the CustomOperatorBase class in a dedicated Python file.

The following are sections of a file named custom_operator_example.py available under ansys.dpf.core.examples.python_plugins:

First declare the custom operator class, with necessary imports and a first property to define the operator scripting name:

# Copyright (C) 2020 - 2025 ANSYS, Inc. and/or its affiliates.
# SPDX-License-Identifier: MIT
#
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

"""Example of a custom DPF operator in Python."""

from ansys.dpf import core as dpf
from ansys.dpf.core.custom_operator import CustomOperatorBase
from ansys.dpf.core.operator_specification import (
    CustomSpecification,
    PinSpecification,
    SpecificationProperties,
)


class CustomOperator(CustomOperatorBase):
    """Example of a custom DPF operator coded in Python."""

    @property
    def name(self):
        """Return the scripting name of the operator in Snake Case."""
        return "my_custom_operator"

Next, set the specification property of your operator with:

  • a description of what the operator does

  • a 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)

  • a 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)


    @property
    def specification(self) -> CustomSpecification:
        """Create the specification of the custom operator.

        The specification declares:
            - the description of the operator
            - the inputs of the operator
            - the outputs of the operator
            - the properties of the operator (a username, a category, a required license)
            - the changelog of the operator (starting with DPF 2026 R1)
        """
        # Instantiate the custom specification
        spec = CustomSpecification()
        # Set the description of the operator
        spec.description = "What the Operator does. You can use MarkDown and LaTeX in descriptions."
        # Define the inputs of the operator if any
        spec.inputs = {
            0: PinSpecification(
                name="input_0",
                type_names=[dpf.Field, dpf.FieldsContainer],
                document="Describe input pin 0.",
            ),
        }
        # Define the outputs of the operator if any
        spec.outputs = {
            0: PinSpecification(
                name="output_0", type_names=[dpf.Field], document="Describe output pin 0."
            ),
        }
        # Define the properties of the operator if any
        spec.properties = SpecificationProperties(
            user_name="my custom operator",  # Optional, defaults to the scripting name with spaces
            category="my_category",  # Optional, defaults to 'other'
            license="any_dpf_supported_increments",  # Optional, defaults to None
        )

        # Operator changelog and versioning is only available after DPF 2025R2
        try:
            from ansys.dpf.core.changelog import Changelog

            # Set the changelog of the operator to track changes
            spec.set_changelog(
                Changelog()
                .patch_bump("Describe a patch bump.")
                .major_bump("Describe a major bump.")
                .minor_bump("Describe a minor bump.")
                .expect_version("1.1.0")  # Checks the resulting version is as expected
            )
        except ModuleNotFoundError as e:
            if "ansys.dpf.core.changelog" in e.msg:
                pass
            else:
                raise e

        return spec

Next, implement the operator behavior in its run method:


    def run(self):
        """Run the operator and execute the logic implemented here.

        This method defines the behavior of the operator.

        Request the inputs with the method ``get_input``,
        perform operations on the data,
        then set the outputs with the method ``set_output``,
        and finally call ``set_succeeded``.

        In this example, the operator changes the name of a Field.

        """
        # First get the field in input by calling get_input for the different types supported
        # # Try requesting the input as a Field
        field: dpf.Field = self.get_input(0, dpf.Field)
        # # If function returns None, there is no Field connected to this input
        if field is None:
            # # Try requesting the input as a FieldsContainer
            field: dpf.FieldsContainer = self.get_input(0, dpf.FieldsContainer).get_field(0)
        # # If the input is optional, set its default value
        # # If the input is not optional and empty, raise an error
        if field is None:
            raise ValueError(
                "my_custom_operator: mandatory input 'input_0' is empty or of an unsupported type."
            )

        # Perform some operations on the data
        field.name = "new_field_name"

        # Set the output of the operator
        self.set_output(0, field)

        # And declare the operator run a success
        self.set_succeeded()

The CustomOperator class is now ready for packaging into any DPF Python plugin.

Package as a plugin#

You must package your custom operator as a plugin, which is what you can later load onto a running DPF server, or configure your installation to automatically load when starting a DPF server.

A DPF plugin contains Python modules with declarations of custom Python operators such as seen above. However, it also has to define an entry-point for the DPF server to call, which records the operators of the plugin into the server registry of available operators.

This is done by defining a function (DPF looks for a function named load_operators by default) somewhere in the plugin with signature *args and a call to the record_operator() method for each custom operator.

In this tutorial, the plugin is made of a single operator, in a single Python file. You can transform this single Python file into a DPF Python plugin very easily by adding load_operators(*args) function with a call to the record_operator() method at the end of the file.

def load_operators(*args):
    """Mandatory entry-point for the server to record the operators of the plugin."""
    from ansys.dpf.core.custom_operator import record_operator

    record_operator(CustomOperator, *args)

PS: You can declare several custom operator classes in the same file, with as many calls to record_operator() as necessary.

Load the plugin#

First, start a server in gRPC mode, which is the only server type supported for custom Python plugins.

import ansys.dpf.core as dpf

# Python plugins are not supported in process.
server = dpf.start_local_server(config=dpf.AvailableServerConfigs.GrpcServer, as_global=False)

With the server and custom plugin ready, use the load_library() method in a PyDPF-Core script to load it.

  • The first argument is the path to the directory with the plugin.

  • The second argument is py_<plugin>, where <plugin> is the name identifying the plugin (the name of the Python file for a single file).

  • The third argument is the name of the function in the plugin which records operators (load_operators by default).

# Get the path to the example plugin
from pathlib import Path
from ansys.dpf.core.examples.python_plugins import custom_operator_example
custom_operator_folder = Path(custom_operator_example.__file__).parent

# Load it on the server
dpf.load_library(
    filename=custom_operator_folder,  # Path to the plugin directory
    name="py_custom_operator_example",  # Look for a Python file named 'custom_operator_example.py'
    symbol="load_operators",  # Look for the entry-point where operators are recorded
    server=server,  # Load the plugin on the server previously started
    generate_operators=False,  # Do not generate the Python module for this operator
)

# You can verify the operator is now in the list of available operators on the server
assert "my_custom_operator" in dpf.dpf_operator.available_operator_names(server=server)

Use the custom operator#

Once the plugin is loaded, you can instantiate the custom operator based on its name.

my_custom_op = dpf.Operator(name="my_custom_operator", server=server) # as returned by the ``name`` property
print(my_custom_op)
DPF my_custom_operator Operator: 
  What the Operator does. You can use MarkDown and LaTeX in descriptions. 
  Inputs:
         input_0 [field, fields_container]: Describe input pin 0. 
  Outputs:
         output_0 [field]: Describe output pin 0. 

Finally, run it as any other operator.

# Create a bogus field to use as input
in_field = dpf.Field(server=server)
# Give it a name
in_field.name = "initial name"
print(in_field)
# Set it as input of the operator
my_custom_op.inputs.input_0.connect(in_field)
# Run the operator by requesting its output
out_field = my_custom_op.outputs.output_0()
print(out_field)
DPF initial name Field
  Location: Nodal
  Unit: 
  0 entities 
  Data: 3 components and 0 elementary data 

DPF new_field_name Field
  Location: Nodal
  Unit: 
  0 entities 
  Data: 3 components and 0 elementary data 

References#

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