Note
Click here to download the full example code
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];
}](../../_images/graphviz-53b3756a06ca6f5dc8f854c282527ba53ff01090.png)
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)