Create a plug-in package that has third-party dependencies#

This example shows how to create a Python plug-in package with third-party dependencies. You should be familiar with these examples before proceeding with this more advanced one:

This plug-in contains an operator whose implementation depends on a third-party Python module named gltf. This operator takes a path, a mesh, and a 3D vector field as inputs and then exports the mesh and the norm of the 3D vector field to a GLTF file at the given path.

Note

This example requires DPF 4.0 (Ansys 2022R2) or above. For more information, see Compatibility.

Create the plug-in package#

Each 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, which records the operators of the plug-in package.

Download the gltf_plugin plug-in package that has already been created for you.

import os

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


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",
]
import os

folder_root = os.path.join(os.getcwd().rsplit("pydpf-core", 1)[0], "pydpf-core")
source_path_in_repo = r"doc\source\examples\07-python-operators\plugins"
operator_folder = os.path.join(folder_root, source_path_in_repo)
print(operator_folder)
plugin_path = None

for file in file_list:
    operator_file_path = os.path.join(operator_folder, 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
D:\a\pydpf-core\pydpf-core\doc\source\examples\07-python-operators\plugins
 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

                        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 plug-in package, you must 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, DPF-Core uses the site Python module to add custom to the path for the Python interpreter.

To create these custom sites, requirements of the plug-in package should be installed in a Python virtual environment, the site-packages (with unnecessary folders removed) should be compressed to a ZIP file and placed with the plugin. The path to this ZIP file should be referenced in the XML as shown in the preceding code.

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

print("\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:

                       wheel

                       pygltflib

Download the script for your operating system.

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.

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 # noqa: E501
    
  • From Linux Shell, run:

    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 = os.path.join(
        folder_root,
        "doc",
        "source",
        "user_guide",
        "create_sites_for_python_operators.ps1",
    )
    args = [
        "powershell",
        cmd_file,
        "-pluginpath",
        plugin_path,
        "-zippath",
        os.path.join(plugin_path, "assets", "gltf_sites_winx64.zip"),
    ]
    print(args)
    import subprocess

    process = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    if process.stderr:
        raise RuntimeError(
            "Installing pygltf in a virtual environment failed with error:\n"
            + f"return code = {process.returncode}\n"
            + process.stderr.decode()
            + "\n\n and log:\n"
            + process.stdout.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 = os.path.join(
        folder_root,
        "doc",
        "source",
        "user_guide",
        "create_sites_for_python_operators.sh",
    )
    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', 'D:\\a\\pydpf-core\\pydpf-core\\doc\\source\\user_guide\\create_sites_for_python_operators.ps1', '-pluginpath', 'D:\\a\\pydpf-core\\pydpf-core\\doc\\source\\examples\\07-python-operators\\plugins\\gltf_plugin', '-zippath', 'D:\\a\\pydpf-core\\pydpf-core\\doc\\source\\examples\\07-python-operators\\plugins\\gltf_plugin\\assets\\gltf_sites_winx64.zip']
Installing pygltf in a virtual environment succeeded

Load the plug-in package#

You use the function ansys.dpf.core.core.load_library() to load the plug-in package.

  • The first argument is the path to the directory where the plug-in package is located.

  • The second argument is py_<package>, where <package> is the name identifying the plug-in package.

  • The third argument is the name of the function exposed in the __init__ file for the plug-in package that is 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'

Instantiate the operator.

new_operator = dpf.Operator("gltf_export")

This new gltf_export operator requires the following as inputs: a triangle surface mesh, a displacement field on this surface mesh, and a path to export the GLTF file to.

To demonstrate this new operator, a ansys.dpf.core.model.Model class is created on a simple file and the ansys.dpf.core.operators.mesh.tri_mesh_skin operator is used to extract the surface of the mesh in triangle 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.find_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

You can download output from the gltf operator.

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

Gallery generated by Sphinx-Gallery