Note
Go to the end to download the full example code.
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.
For Windows, download this
PowerShell script
.For Linux, download this
Shell script
.
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 variableTEMP
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.
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 52.928 seconds)