Write user defined Operators having third party dependencies#

This example shows how advanced DPF python plugins of Operators can be created as standard python packages and how third party python modules dependencies can be added to the package. For a first introduction on user defined python Operators see example Write user defined Operator and for a simpler example on user defined python Operators as a package see Write user defined Operators as a package.

This plugin will hold an Operator which implementation depends on a third party python module named gltf. This Operator takes a path, a mesh and 3D vector field in input and exports the mesh and the norm of the input field in a gltf file located at the given path.

Write Operator#

For this more advanced use case, a python package is created. Each Operator implementation derives from ansys.dpf.core.custom_operator.CustomOperatorBase and a call to ansys.dpf.core.custom_operator.record_operator() records the Operators of the plugin. The python package gltf_plugin is downloaded and displayed here:

import os
from ansys.dpf.core import examples

print('\033[1m gltf_plugin')
file_list = ["gltf_plugin/__init__.py", "gltf_plugin/operators.py",
             "gltf_plugin/operators_loader.py", "gltf_plugin/requirements.txt",
             "gltf_plugin/gltf_export.py", "gltf_plugin/texture.png", "gltf_plugin.xml"
             ]
plugin_path = None
GITHUB_SOURCE_URL = "https://github.com/pyansys/pydpf-core/raw/" \
                    "" \
                    "examples/first_python_plugins/python_plugins"

for file in file_list:
    EXAMPLE_FILE = GITHUB_SOURCE_URL + "/gltf_plugin/" + file
    operator_file_path = examples.downloads._retrieve_file(
        EXAMPLE_FILE, file, os.path.join("python_plugins", os.path.dirname(file)))

    print(f'\033[1m {file}\n \033[0m')
    if (os.path.splitext(file)[1] == ".py" or os.path.splitext(file)[1] == ".xml") \
            and file != "gltf_plugin/gltf_export.py":
        with open(operator_file_path, "r") as f:
            for line in f.readlines():
                print('\t\t\t' + line)
        print("\n\n")
        if plugin_path is None:
            plugin_path = os.path.dirname(operator_file_path)
gltf_plugin
gltf_plugin/__init__.py

                       from gltf_plugin.operators_loader import load_operators




gltf_plugin/operators.py

                       from gltf_plugin import gltf_export

                       from ansys.dpf.core.custom_operator import CustomOperatorBase, record_operator

                       from ansys.dpf.core.operator_specification import CustomSpecification, PinSpecification, SpecificationProperties

                       from ansys.dpf import core as dpf





                       class WriteGLTF(CustomOperatorBase):

                           def run(self):

                               path = self.get_input(0, str)

                               mesh = self.get_input(1, dpf.MeshedRegion)

                               field = self.get_input(2, dpf.Field)



                               mesh_element_types = mesh.elements.element_types_field.data_as_list

                               if mesh_element_types.count(dpf.element_types.Tri3.value) != len(mesh_element_types) or not mesh_element_types:

                                   raise Exception("Elements of mesh are not triangles.")



                               norm_op = dpf.operators.math.norm()

                               norm_op.inputs.field.connect(field)



                               min_max_op = dpf.operators.min_max.min_max()

                               min_max_op.inputs.field.connect(norm_op.outputs.field())

                               field_max = min_max_op.outputs.field_max().data[0]

                               field_min = min_max_op.outputs.field_min().data[0]

                               field_range = field_max - field_min



                               uv = []

                               for value in norm_op.outputs.field().data:

                                   uv.append([value / field_range, 0])



                               path = gltf_export.export(path, mesh.nodes.coordinates_field.data, mesh.elements.connectivities_field.data_as_list, uv)



                               self.set_output(0, path)

                               self.set_succeeded()



                           @property

                           def specification(self):

                               spec = CustomSpecification("Writes a GLTF file for a surface MeshedRegion with triangles elements and a Field using pygltflib python module.")

                               spec.inputs = {

                                   0: PinSpecification("path", type_names=str, document="path to write GLTF file"),

                                   1: PinSpecification("mesh", type_names=dpf.MeshedRegion),

                                   2: PinSpecification("field", type_names=dpf.Field, document="3D vector Field to export (ie displacement Field)."),

                               }

                               spec.outputs = {

                                   0: PinSpecification("path", type_names=str),

                               }

                               spec.properties = SpecificationProperties(user_name="GLTF export", category="serialization")

                               return spec



                           @property

                           def name(self):

                               return "gltf_export"




gltf_plugin/operators_loader.py

                       from gltf_plugin import operators

                       from ansys.dpf.core.custom_operator import record_operator





                       def load_operators(*args):

                           record_operator(operators.WriteGLTF, *args)




gltf_plugin/requirements.txt

gltf_plugin/gltf_export.py

gltf_plugin/texture.png

gltf_plugin.xml

                       <?xml version="1.0"?>

                       <Environment>

                               <Windows>

                                       <CUSTOM_SITE>$(THIS_XML_FOLDER)/gltf_plugin/assets/gltf_sites_winx64.zip;$(CUSTOM_SITE)</CUSTOM_SITE>

                                       <LOAD_DEFAULT_DPF_SITE>true</LOAD_DEFAULT_DPF_SITE>

                               </Windows>

                               <Linux>

                                       <CUSTOM_SITE>$(THIS_XML_FOLDER)/gltf_plugin/assets/gltf_sites_linx64.zip:$(CUSTOM_SITE)</CUSTOM_SITE>

                                       <LOAD_DEFAULT_DPF_SITE>true</LOAD_DEFAULT_DPF_SITE>

                               </Linux>

                       </Environment>

To add third party modules as dependencies to a custom DPF python plugin, a folder or zip file with the sites of the dependencies needs to be created and referenced in an xml located next to the plugin’s folder and having the same name as the plugin plus the .xml extension. The site python module is used by DPF when calling ansys.dpf.core.core.load_library() function to add these custom sites to the python interpreter path. To create these custom sites, the requirements of the custom plugin should be installed in a python virtual environment, the site-packages (with unnecessary folders removed) should be zipped and put with the plugin. The path to this zip should be referenced in the xml as done above.

To simplify this step, a requirements file can be added in the plugin, like:

print(f'\033[1m gltf_plugin/requirements.txt: \n \033[0m')
with open(os.path.join(plugin_path, "requirements.txt"), "r") as f:
    for line in f.readlines():
        print('\t\t\t' + line)
gltf_plugin/requirements.txt:

                       pygltflib

And this powershell script for windows or this shell script can be ran with the mandatory arguments:

  • -pluginpath : path to the folder of the plugin.

  • -zippath : output zip file name.

optional arguments are:

  • -pythonexe : path to a python executable of your choice.

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

For windows powershell, call:

create_sites_for_python_operators.ps1 -pluginpath /path/to/plugin -zippath /path/to/plugin/assets/winx64.zip # noqa: E501

For linux shell, call:

create_sites_for_python_operators.sh -pluginpath /path/to/plugin -zippath /path/to/plugin/assets/linx64.zip # noqa: E501
if os.name == "nt" and \
        not os.path.exists(os.path.join(plugin_path, 'assets', 'gltf_sites_winx64.zip')):
    CMD_FILE_URL = GITHUB_SOURCE_URL + "/create_sites_for_python_operators.ps1"
    cmd_file = examples.downloads._retrieve_file(
        CMD_FILE_URL, "create_sites_for_python_operators.ps1", "python_plugins")
    run_cmd = f"powershell {cmd_file}"
    args = f" -pluginpath \"{plugin_path}\" " \
           f"-zippath {os.path.join(plugin_path, 'assets', 'gltf_sites_winx64.zip')}"
    print(run_cmd + args)
    import subprocess
    process = subprocess.run(run_cmd + args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    if process.stderr:
        raise RuntimeError(
            "Installing pygltf in a virtual environment failed with error:\n"
            + process.stderr.decode())
    else:
        print("Installing pygltf in a virtual environment succeeded")
elif os.name == "posix" and \
        not os.path.exists(os.path.join(plugin_path, 'assets', 'gltf_sites_linx64.zip')):
    CMD_FILE_URL = GITHUB_SOURCE_URL + "/create_sites_for_python_operators.sh"
    cmd_file = examples.downloads._retrieve_file(
        CMD_FILE_URL, "create_sites_for_python_operators.ps1", "python_plugins"
    )
    run_cmd = f"{cmd_file}"
    args = f" -pluginpath \"{plugin_path}\" " \
           f"-zippath \"{os.path.join(plugin_path, 'assets', 'gltf_sites_linx64.zip')}\""
    print(run_cmd + args)
    os.system(f"chmod u=rwx,o=x {cmd_file}")
    os.system(run_cmd + args)
    print("\nInstalling pygltf in a virtual environment succeeded")
powershell c:\hostedtoolcache\windows\python\3.8.10\x64\lib\site-packages\ansys\dpf\core\examples\python_plugins\create_sites_for_python_operators.ps1 -pluginpath "c:\hostedtoolcache\windows\python\3.8.10\x64\lib\site-packages\ansys\dpf\core\examples\python_plugins\gltf_plugin" -zippath c:\hostedtoolcache\windows\python\3.8.10\x64\lib\site-packages\ansys\dpf\core\examples\python_plugins\gltf_plugin\assets\gltf_sites_winx64.zip
Installing pygltf in a virtual environment succeeded

Load Plugin#

Once a python plugin is written as a package, it can be loaded with the function ansys.dpf.core.core.load_library() taking as first argument the path to the directory of the plugin, as second argument py_ + any name identifying the plugin, and as last argument the function’s name exposed in the __init__.py file and used to record operators.

from ansys.dpf import core as dpf
from ansys.dpf.core import examples

# python plugins are not supported in process
dpf.start_local_server(config=dpf.AvailableServerConfigs.GrpcServer)

tmp = dpf.make_tmp_dir_server()
dpf.upload_files_in_folder(
    dpf.path_utilities.join(tmp, "plugins", "gltf_plugin"),
    plugin_path
)
dpf.upload_file(
    plugin_path + ".xml",
    dpf.path_utilities.join(tmp, "plugins", "gltf_plugin.xml")
)

dpf.load_library(
    dpf.path_utilities.join(tmp, "plugins", "gltf_plugin"),
    "py_dpf_gltf",
    "load_operators")
'py_dpf_gltf successfully loaded'

Once the Plugin loaded, Operators recorded in the plugin can be used with:

new_operator = dpf.Operator("gltf_export")

This new Operator gltf_export requires a triangle surface mesh, a displacement Field on this surface mesh as well as an export path as inputs. To demo this new Operator, a ansys.dpf.core.model.Model on a simple file is created, ansys.dpf.core.operators.mesh.tri_mesh_skin Operator is used to extract the surface of the mesh in triangles elements.

digraph workflow {
   graph [pad="0.5", nodesep="0.3", ranksep="0.3"]
   node [shape=box, style=filled, fillcolor="#ffcc00", margin="0"];
   rankdir=LR;
   splines=line;
   ds [label="data_sources", shape=box, style=filled, fillcolor=cadetblue2];
   ds -> mesh_provider [style=dashed];
   mesh_provider -> skin_mesh [splines=ortho];
   ds -> displacement [style=dashed];
   skin_mesh -> displacement [splines=ortho];
   skin_mesh -> gltf_export [splines=ortho];
   displacement -> gltf_export [splines=ortho];
}

Use the Custom Operator#

import os

model = dpf.Model(dpf.upload_file_in_tmp_folder(examples.static_rst))

mesh = model.metadata.meshed_region
skin_mesh = dpf.operators.mesh.tri_mesh_skin(mesh=mesh)

displacement = model.results.displacement()
displacement.inputs.mesh_scoping(skin_mesh)
displacement.inputs.mesh(skin_mesh)
new_operator.inputs.path(os.path.join(tmp, "out"))
new_operator.inputs.mesh(skin_mesh)
new_operator.inputs.field(displacement.outputs.fields_container()[0])
new_operator.run()

print("operator ran successfully")

dpf.download_file(os.path.join(tmp, "out.glb"), os.path.join(os.getcwd(), "out.glb"))
operator ran successfully

The gltf Operator output can be downloaded here.

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

Gallery generated by Sphinx-Gallery