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 onto 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.

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.

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

Next, set the specification property of your operator with:

  • a description of what the operator does

  • a dictionary for each input and output pin, including the name, a list of supported types, a description, and whether it is optional and/or ellipsis

  • a list for operator properties, including name to use in the documentation and the operator category. The optional license property lets you define a required license to check out when running the operator.

Finally, implement the operator behavior in its run method: request the inputs with get_input, perform operations on the data, set the outputs with set_output, and call set_succeeded.

The following code shows the contents of a file named custom_operator_example.py available under ansys.dpf.core.examples.python_plugins.

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"

    @property
    def specification(self) -> CustomSpecification:
        """Create the specification of the custom operator."""
        spec = CustomSpecification()
        spec.description = "What the Operator does. You can use MarkDown and LaTeX in descriptions."
        spec.inputs = {
            0: PinSpecification(
                name="input_0",
                type_names=[dpf.Field, dpf.FieldsContainer],
                document="Describe input pin 0.",
            ),
        }
        spec.outputs = {
            0: PinSpecification(
                name="output_0", type_names=[dpf.Field], document="Describe output pin 0."
            ),
        }
        spec.properties = SpecificationProperties(
            user_name="my custom operator",
            category="my_category",
            license="any_dpf_supported_increments",
        )

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

            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")
            )
        except ModuleNotFoundError as e:
            if "ansys.dpf.core.changelog" in str(e):
                pass
            else:
                raise e

        return spec

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

        In this example, the operator changes the name of a Field.
        """
        # Get the input as a Field
        field: dpf.Field = self.get_input(0, dpf.Field)
        # If None, try requesting as a FieldsContainer
        if field is None:
            field: dpf.FieldsContainer = self.get_input(0, dpf.FieldsContainer).get_field(0)
        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 and declare success
        self.set_output(0, field)
        self.set_succeeded()

Package as a plugin#

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

A DPF plugin contains Python modules with custom operator declarations. It also defines an entry-point for the DPF server to call, which records the operators into the server registry.

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

The CustomOperator class defined above, together with the load_operators function below, form a complete single-file DPF Python plugin.

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

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)

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 to load the plugin.

  • 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 plugin).

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

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
)

# 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, instantiate the custom operator based on its name, as returned by the name property.

my_custom_op = dpf.Operator(name="my_custom_operator", server=server)
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 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

Total running time of the script: (0 minutes 12.203 seconds)

Gallery generated by Sphinx-Gallery