diff --git a/doc/source/conf.py b/doc/source/conf.py index 869c55a1af..4422d8a8a6 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -135,6 +135,7 @@ autosummary_generate = False +autodoc_mock_imports = ["ansys.dpf.core.examples.python_plugins"] # Add any paths that contain templates here, relative to this directory. # templates_path = ['_templates'] diff --git a/doc/source/user_guide/custom_operator_example.py b/doc/source/user_guide/custom_operator_example.py deleted file mode 100644 index 5a62c9875a..0000000000 --- a/doc/source/user_guide/custom_operator_example.py +++ /dev/null @@ -1,36 +0,0 @@ -from ansys.dpf import core as dpf -from ansys.dpf.core.custom_operator import CustomOperatorBase, record_operator # noqa: F401 -from ansys.dpf.core.operator_specification import CustomSpecification, SpecificationProperties, \ - PinSpecification - - -class CustomOperator(CustomOperatorBase): - @property - def name(self): - return "name_of_my_custom_operator" - - @property - def specification(self) -> CustomSpecification: - spec = CustomSpecification() - spec.description = "What the Operator does." - spec.inputs = { - 0: PinSpecification("name_of_pin_0", [dpf.Field, dpf.FieldsContainer], - "Describe pin 0."), - } - spec.outputs = { - 0: PinSpecification("name_of_pin_0", [dpf.Field], "Describe pin 0."), - } - spec.properties = SpecificationProperties( - user_name="user name", - category="category", - license="license", - ) - return spec - - def run(self): - field = self.get_input(0, dpf.Field) - if field is None: - field = self.get_input(0, dpf.FieldsContainer)[0] - # compute data - self.set_output(0, dpf.Field()) - self.set_succeeded() diff --git a/doc/source/user_guide/custom_operators.rst b/doc/source/user_guide/custom_operators.rst deleted file mode 100644 index 72fb3e9d1c..0000000000 --- a/doc/source/user_guide/custom_operators.rst +++ /dev/null @@ -1,253 +0,0 @@ -.. _user_guide_custom_operators: - -================ -Custom operators -================ - -In Ansys 2023 R1 and later, you can create custom operators in CPython. Creating custom operators -consists of wrapping Python routines in a DPF-compliant way so that you can access them in the same way -as you access the native operators in the :class:`ansys.dpf.core.dpf_operator.Operator` class in -PyDPF-Core or in any supported client API. - -With support for custom operators, PyDPF-Core becomes a development tool offering: - -- **Accessibility:** A simple script can define a basic operator plugin. - -- **Componentization:** Operators with similar applications can be grouped in Python plugin packages. - -- **Easy distribution:** Standard Python tools can be used to package, upload, and download custom operators. - -- **Dependency management:** Third-party Python modules can be added to the Python package. - -- **Reusability:** A documented and packaged operator can be reused in an infinite number of workflows. - -- **Remotable and parallel computing:** Native DPF capabilities are inherited by custom operators. - -The only prerequisite for creating custom operators is to be familiar with native operators. -For more information, see :ref:`ref_user_guide_operators`. - -Install module --------------- - -Once an Ansys unified installation is complete, you must install the ``ansys-dpf-core`` module in the Ansys -installer's Python interpreter. - -#. Download the script for your operating system: - - - For Windows, download this :download:`PowerShell script `. - - For Linux, download this :download:`Shell script ` - -#. Run the downloaded script for installing with optional arguments: - - - ``-awp_root``: Path to the Ansys root installation folder. For example, the 2023 R1 installation folder ends - with ``Ansys Inc/v231``, and the default environment variable is ``AWP_ROOT231``. - - ``-pip_args``: Optional arguments to add to the ``pip`` command. For example, ``--extra-index-url`` or - ``--trusted-host``. - -To uninstall the ``ansys-dpf-core`` module from the Ansys installation: - -#. Download the script for your operating system: - - - For Windows, download this :download:`PowerShell script `. - - For Linux, download this :download:`Shell script `. - -3. Run the downloaded script for uninstalling with the optional argument: - - - ``-awp_root``: Path to the Ansys root installation folder. For example, the 2023 R1 installation folder ends - with ``Ansys Inc/v231``, and the default environment variable is ``AWP_ROOT231``. - - -Create operators ----------------- -You can create a basic operator plugin or a plugin package with multiple operators. - -Basic operator plugin -~~~~~~~~~~~~~~~~~~~~~ -To create a basic operator plugin, write a simple Python script. An operator implementation -derives from the :class:`ansys.dpf.core.custom_operator.CustomOperatorBase` class and a call to -the :func:`ansys.dpf.core.custom_operator.record_operator` method. - -This example script shows how you create a basic operator plugin: - -.. literalinclude:: custom_operator_example.py - - -.. code-block:: - - def load_operators(*args): - record_operator(CustomOperator, *args) - - -In the various properties for the class, specify the following: - -- Name for the custom operator -- Description of what the operator does -- Dictionary for each input and output pin. This dictionary includes the name, a list of supported types, a description, - and whether it is optional and/or ellipsis (meaning that the specification is valid for pins going from pin - number *x* to infinity) -- List for operator properties, including name to use in the documentation and code generation and the - operator category. The optional ``license`` property lets you define a required license to check out - when running the operator. Set it equal to ``any_dpf_supported_increments`` to allow any license - currently accepted by DPF (see :ref:`here`) - -For comprehensive examples on writing operator plugins, see :ref:`python_operators`. - - -Plugin package with multiple operators -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -To create a plugin package with multiple operators or with complex routines, write a -Python package. The benefits of writing packages rather than simple scripts are: - -- **Componentization:** You can split the code into several Python modules or files. -- **Distribution:** You can use standard Python tools to upload and download packages. -- **Documentation:** You can add README files, documentation, tests, and examples to the package. - -A plugin package with dependencies consists of a folder with the necessary files. Assume -that the name of your plugin package is ``custom_plugin``. A folder with this name would -contain four files: - -- ``__init__.py`` -- ``operators.py`` -- ``operators_loader.py`` -- ``common.py`` - -**__init__.py file** - -The ``__init__.py`` file contains this code:: - - from operators_loader import load_operators - - -**operators.py file** - -The ``operators.py`` file contains code like this: - -.. literalinclude:: custom_operator_example.py - - -**operators_loader.py file** - -The ``operators_loader.py`` file contains code like this:: - - from custom_plugin import operators - from ansys.dpf.core.custom_operator import record_operator - - - def load_operators(*args): - record_operator(operators.CustomOperator, *args) - - -**common.py file** - -The ``common.py`` file contains the Python routines as classes and functions:: - - #write needed python routines as classes and functions here. - -Third-party dependencies -^^^^^^^^^^^^^^^^^^^^^^^^ - -.. include:: custom_operators_deps.rst - - -Assume once again that the name of your plugin package is ``custom_plugin``. -A folder with this name would contain these files: - -- ``__init__.py`` -- ``operators.py`` -- ``operators_loader.py`` -- ``common.py`` -- ``winx64.zip`` -- ``linx64.zip`` -- ``custom_plugin.xml`` - -**__init__.py file** - -The ``__init__.py`` file contains this code:: - - from operators_loader import load_operators - - -**operators.py file** - -The ``operators.py`` file contains code like this: - -.. literalinclude:: custom_operator_example.py - - -**operators_loader.py file** - -The ``operators_loader.py`` file contains code like this:: - - from custom_plugin import operators - from ansys.dpf.core.custom_operator import record_operator - - - def load_operators(*args): - record_operator(operators.CustomOperator, *args) - - - def load_operators(*args): - record_operator(operators.CustomOperator, *args) - -**common.py file** - -The ``common.py`` file contains the Python routines as classes and functions:: - - #write needed python routines as classes and functions here. - - -**requirements.txt file** - -The ``requirements.txt`` file contains code like this: - -.. literalinclude:: /examples/07-python-operators/plugins/gltf_plugin/requirements.txt - -The ZIP files for Windows and Linux are included as assets: - -- ``winx64.zip`` -- ``linx64.zip`` - - -**custom_plugin.xml file** - -The ``custom_plugin.xml`` file contains code like this: - -.. literalinclude:: custom_plugin.xml - :language: xml - - -Use custom operators --------------------- - -Once a custom operator is created, you can use the :func:`ansys.dpf.core.core.load_library` method to load it. -The first argument is the path to the directory with the plugin. The second argument is ``py_`` plus any name -identifying the plugin. The last argument is the function name for recording operators. - -For a plugin that is a single script, the second argument should be ``py_`` plus the name of the Python file: - -.. code:: - - dpf.load_library( - r"path/to/plugins", - "py_custom_plugin", #if the load_operators function is defined in path/to/plugins/custom_plugin.py - "load_operators") - -For a plugin package, the second argument should be ``py_`` plus any name: - -.. code:: - - dpf.load_library( - r"path/to/plugins/custom_plugin", - "py_my_custom_plugin", #if the load_operators function is defined in path/to/plugins/custom_plugin/__init__.py - "load_operators") - -Once the plugin is loaded, you can instantiate the custom operator: - -.. code:: - - new_operator = dpf.Operator("custom_operator") # if "custom_operator" is what is returned by the ``name`` property - -References ----------- -For more information, see :py:mod:`Custom Operator Base ` in the **API reference** -and :ref:`python_operators` in **Examples**. diff --git a/doc/source/user_guide/how_to.rst b/doc/source/user_guide/how_to.rst index f435986884..fb51c3df75 100644 --- a/doc/source/user_guide/how_to.rst +++ b/doc/source/user_guide/how_to.rst @@ -15,15 +15,6 @@ How-tos .. image:: ../images/plotting/pontoon.png .. image:: ../images/plotting/pontoon_strain.png - - - .. grid-item-card:: Create custom operators - :link: user_guide_custom_operators - :link-type: ref - :text-align: center - - .. image:: ../images/drawings/plugin-logo.png - :width: 50% .. grid-item-card:: Use DPF Server package diff --git a/doc/source/user_guide/index.rst b/doc/source/user_guide/index.rst index 4cc30c3dad..8a962e990a 100644 --- a/doc/source/user_guide/index.rst +++ b/doc/source/user_guide/index.rst @@ -66,7 +66,7 @@ Troubleshooting tutorials/export_data/index.rst tutorials/plot/index.rst tutorials/animate/index.rst - tutorials/enriching_dpf_capabilities/index.rst + tutorials/custom_operators_and_plugins/index.rst tutorials/distributed_files/index.rst tutorials/dpf_server/index.rst tutorials/licensing/index.rst diff --git a/doc/source/user_guide/operators.rst b/doc/source/user_guide/operators.rst index f2bd57ecbd..9aa9a0d5b3 100644 --- a/doc/source/user_guide/operators.rst +++ b/doc/source/user_guide/operators.rst @@ -56,16 +56,19 @@ The library of DPF operators is large and includes file readers and mathematical geometrical, and logical transformations. For more information on this library, which is progressively enhanced, see :ref:`ref_dpf_operators_reference`. +Despite the large number of operators proposed by the standard DPF installation, you may want to +create your own operators and ship them as DPF plugins. +See the tutorials section on custom operators and plugins to learn more. -Create operators -~~~~~~~~~~~~~~~~ -Each operator is of type :py:mod:`Operator `. You can create an instance +Instantiate operators +~~~~~~~~~~~~~~~~~~~~~ +Each operator is of type :py:mod:`Operator `. You can instantiate an operator in Python with any of the derived classes available in the :py:mod:`ansys.dpf.core.operators` package or directly with the :py:mod:`Operator ` class using the internal name string that indicates the operator type. For more information, see :ref:`ref_dpf_operators_reference`. -This example shows how to create the displacement operator: +This example shows how to instantiate the displacement operator: .. code-block:: python diff --git a/doc/source/user_guide/create_sites_for_python_operators.ps1 b/doc/source/user_guide/tutorials/custom_operators_and_plugins/create_sites_for_python_operators.ps1 similarity index 100% rename from doc/source/user_guide/create_sites_for_python_operators.ps1 rename to doc/source/user_guide/tutorials/custom_operators_and_plugins/create_sites_for_python_operators.ps1 diff --git a/doc/source/user_guide/create_sites_for_python_operators.sh b/doc/source/user_guide/tutorials/custom_operators_and_plugins/create_sites_for_python_operators.sh similarity index 100% rename from doc/source/user_guide/create_sites_for_python_operators.sh rename to doc/source/user_guide/tutorials/custom_operators_and_plugins/create_sites_for_python_operators.sh diff --git a/doc/source/user_guide/tutorials/custom_operators_and_plugins/custom_operators.rst b/doc/source/user_guide/tutorials/custom_operators_and_plugins/custom_operators.rst new file mode 100644 index 0000000000..6e82a779ec --- /dev/null +++ b/doc/source/user_guide/tutorials/custom_operators_and_plugins/custom_operators.rst @@ -0,0 +1,167 @@ +.. _tutorials_custom_operators_and_plugins_custom_operator: + +================ +Custom operators +================ + +This tutorial shows the basics of creating a custom operator in Python and loading it ont 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 :ref:`create a custom DPF operator` +in Python using PyDPF-Core. + +It then shows how to :ref:`make a plugin` +out of this single operator. + +The next step is to :ref:`load the plugin on the server` to record its operators. + +The final step is to instantiate the custom operator from the client API and :ref:`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. + + +:jupyter-download-script:`Download tutorial as Python script` +:jupyter-download-notebook:`Download tutorial as Jupyter notebook` + + +.. _tutorials_custom_operators_and_plugins_custom_operator_create_custom_operator: + +Create a custom operator +------------------------ + +To create a custom DPF operator using PyDPF-Core, define a custom operator class inheriting from +the :class:`CustomOperatorBase ` class in a dedicated Python file. + +The following are sections of a file named `custom_operator_example.py` available under ``ansys.dpf.core.examples.python_plugins``: + +First declare the custom operator class, with necessary imports and a first property to define the operator scripting name: + +.. literalinclude:: /../../src/ansys/dpf/core/examples/python_plugins/custom_operator_example.py + :end-at: return "my_custom_operator" + +Next, set the `specification` property of your operator with: + +- a description of what the operator does +- a dictionary for each input and output pin. This dictionary includes the name, a list of supported types, a description, + and whether it is optional and/or ellipsis (meaning that the specification is valid for pins going from pin + number *x* to infinity) +- a list for operator properties, including name to use in the documentation and code generation and the + operator category. The optional ``license`` property lets you define a required license to check out + when running the operator. Set it equal to ``any_dpf_supported_increments`` to allow any license + currently accepted by DPF (see :ref:`here`) + +.. literalinclude:: /../../src/ansys/dpf/core/examples/python_plugins/custom_operator_example.py + :start-after: return "my_custom_operator" + :end-at: return spec + +Next, implement the operator behavior in its `run` method: + +.. literalinclude:: /../../src/ansys/dpf/core/examples/python_plugins/custom_operator_example.py + :start-after: return spec + :end-at: self.set_succeeded() + +The `CustomOperator` class is now ready for packaging into any DPF Python plugin. + +.. _tutorials_custom_operators_and_plugins_custom_operator_create_custom_plugin: + +Package as a plugin +------------------- + +You must package your custom operator as a `plugin`, +which is what you can later load onto a running DPF server, +or configure your installation to automatically load when starting a DPF server. + +A DPF plugin contains Python modules with declarations of custom Python operators such as seen above. +However, it also has to define an entry-point for the DPF server to call, +which records the operators of the plugin into the server registry of available operators. + +This is done by defining a function (DPF looks for a function named ``load_operators`` by default) +somewhere in the plugin with signature ``*args`` and a call to the +:func:`record_operator() ` method for each custom operator. + +In this tutorial, the plugin is made of a single operator, in a single Python file. +You can transform this single Python file into a DPF Python plugin very easily by adding +``load_operators(*args)`` function with a call to the +:func:`record_operator() ` method at the end of the file. + +.. literalinclude:: /../../src/ansys/dpf/core/examples/python_plugins/custom_operator_example.py + :start-at: def load_operators(*args): + +PS: You can declare several custom operator classes in the same file, with as many calls to +:func:`record_operator() ` as necessary. + +.. _tutorials_custom_operators_and_plugins_custom_operator_load_the_plugin: + +Load the plugin +--------------- + +First, start a server in gRPC mode, which is the only server type supported for custom Python plugins. + +.. jupyter-execute:: + + 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 :func:`load_library() ` method in a PyDPF-Core script to load it. + +- The first argument is the path to the directory with the plugin. +- The second argument is ``py_``, where is the name identifying the plugin (the name of the Python file for a single file). +- The third argument is the name of the function in the plugin which records operators (``load_operators`` by default). + +.. jupyter-execute:: + + # Get the path to the example plugin + 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 + ) + + # You can 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) + +.. _tutorials_custom_operators_and_plugins_custom_operator_use_the_custom_operator: + +Use the custom operator +----------------------- + +Once the plugin is loaded, you can instantiate the custom operator based on its name. + +.. jupyter-execute:: + + my_custom_op = dpf.Operator(name="my_custom_operator", server=server) # as returned by the ``name`` property + print(my_custom_op) + +Finally, run it as any other operator. + +.. jupyter-execute:: + + # Create a bogus 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) + +References +---------- +For more information, see :ref:`ref_custom_operator` in the **API reference** +and :ref:`python_operators` in **Examples**. diff --git a/doc/source/user_guide/custom_operators_deps.rst b/doc/source/user_guide/tutorials/custom_operators_and_plugins/custom_operators_deps.rst similarity index 92% rename from doc/source/user_guide/custom_operators_deps.rst rename to doc/source/user_guide/tutorials/custom_operators_and_plugins/custom_operators_deps.rst index 8872d77fd6..2ec95a1178 100644 --- a/doc/source/user_guide/custom_operators_deps.rst +++ b/doc/source/user_guide/tutorials/custom_operators_and_plugins/custom_operators_deps.rst @@ -23,8 +23,8 @@ For this approach, do the following: #. Download the script for your operating system: - - For Windows, download this :download:`PowerShell script `. - - For Linux, download this :download:`Shell script `. + - For Windows, download this :download:`PowerShell script `. + - For Linux, download this :download:`Shell script `. 3. Run the downloaded script with the mandatory arguments: diff --git a/doc/source/user_guide/tutorials/custom_operators_and_plugins/custom_plug_in_package.rst b/doc/source/user_guide/tutorials/custom_operators_and_plugins/custom_plug_in_package.rst new file mode 100644 index 0000000000..8d39c2fc34 --- /dev/null +++ b/doc/source/user_guide/tutorials/custom_operators_and_plugins/custom_plug_in_package.rst @@ -0,0 +1,90 @@ +.. _tutorials_others_custom_plug_ins_packages: + +======================================= +Plug-in package with multiple operators +======================================= + +This tutorial shows how to create, load and use a custom plug-in package with multiple operators or with complex routines + +Create the plug-in package +-------------------------- + +To create a plug-in package with multiple operators or with complex routines, you write a +Python package. The benefits of writing packages rather than simple scripts are: + +- **Componentization:** You can split the code into several Python modules or files. +- **Distribution:** You can use standard Python tools to upload and download packages. +- **Documentation:** You can add README files, documentation, tests, and examples to the package. + +A plug-in package with dependencies consists of a folder with the necessary files. Assume +that the name of your plug-in package is ``custom_plugin``. A folder with this name would +contain four files: + +- ``__init__.py`` +- ``operators.py`` +- ``operators_loader.py`` +- ``common.py`` + +**__init__.py file** + +The ``__init__.py`` file contains this code:: + + from operators_loader import load_operators + + +**operators.py file** + +The ``operators.py`` file contains code like this: + +.. literalinclude:: custom_operator_example.py + + +**operators_loader.py file** + +The ``operators_loader.py`` file contains code like this:: + + from custom_plugin import operators + from ansys.dpf.core.custom_operator import record_operator + + + def load_operators(*args): + record_operator(operators.CustomOperator, *args) + + +**common.py file** + +The ``common.py`` file contains the Python routines as classes and functions:: + + #write needed python routines as classes and functions here. + + +Load the plug-in package +------------------------ + +Use the :func:`load_library() ` method to load plug-in package. + +- The first argument is the path to the directory where the plug-in package is located. +- The second argument is ``py_`` where 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. + +.. code:: + + dpf.load_library( + r"path/to/plugins/custom_plugin", + "py_my_custom_plugin", #if the load_operators function is defined in path/to/plugins/custom_plugin/__init__.py + "load_operators") + + +Use the custom operators +------------------------ + +Once the plugin is loaded, you can instantiate the custom operator: + +.. code:: + + new_operator = dpf.Operator("custom_operator") # if "custom_operator" is what is returned by the ``name`` property + +References +---------- +For more information, see :ref:`ref_custom_operator` in the **API reference** +and :ref:`python_operators` in **Examples**. \ No newline at end of file diff --git a/doc/source/user_guide/tutorials/custom_operators_and_plugins/custom_plug_in_package_third_deps.rst b/doc/source/user_guide/tutorials/custom_operators_and_plugins/custom_plug_in_package_third_deps.rst new file mode 100644 index 0000000000..5c16f72844 --- /dev/null +++ b/doc/source/user_guide/tutorials/custom_operators_and_plugins/custom_plug_in_package_third_deps.rst @@ -0,0 +1,158 @@ +.. _tutorials_others_custom_plug_ins_packages_third_deps: + +============================================= +Plug-in package with third-party dependencies +============================================= + +This tutorial shows how to create, load and use a custom plug-in package with third-party dependencies + +Create the plug-in package +-------------------------- + +To create a plug-in package with multiple operators or with complex routines, you write a +Python package. + +A plug-in package with dependencies consists of a folder with the necessary files. Assume +that the name of your plug-in package is ``custom_plugin``. A folder with this name would +contain four files: + +- ``__init__.py`` +- ``operators.py`` +- ``operators_loader.py`` +- ``common.py`` + +**__init__.py file** + +The ``__init__.py`` file contains this code:: + + from operators_loader import load_operators + + +**operators.py file** + +The ``operators.py`` file contains code like this: + +.. literalinclude:: custom_operator_example.py + + +**operators_loader.py file** + +The ``operators_loader.py`` file contains code like this:: + + from custom_plugin import operators + from ansys.dpf.core.custom_operator import record_operator + + + def load_operators(*args): + record_operator(operators.CustomOperator, *args) + + +**common.py file** + +The ``common.py`` file contains the Python routines as classes and functions:: + + #write needed python routines as classes and functions here. + + +Third-party dependencies +------------------------ + +.. include:: custom_operators_deps.rst + + +Assume once again that the name of your plug-in package is ``custom_plugin``. +A folder with this name would contain these files: + +- ``__init__.py`` +- ``operators.py`` +- ``operators_loader.py`` +- ``common.py`` +- ``winx64.zip`` +- ``linx64.zip`` +- ``custom_plugin.xml`` + +**__init__.py file** + +The ``__init__.py`` file contains this code:: + + from operators_loader import load_operators + + +**operators.py file** + +The ``operators.py`` file contains code like this: + +.. literalinclude:: custom_operator_example.py + + +**operators_loader.py file** + +The ``operators_loader.py`` file contains code like this:: + + from custom_plugin import operators + from ansys.dpf.core.custom_operator import record_operator + + + def load_operators(*args): + record_operator(operators.CustomOperator, *args) + + + def load_operators(*args): + record_operator(operators.CustomOperator, *args) + +**common.py file** + +The ``common.py`` file contains the Python routines as classes and functions:: + + #write needed python routines as classes and functions here. + + +**requirements.txt file** + +The ``requirements.txt`` file contains code like this: + +.. literalinclude:: /examples/07-python-operators/plugins/gltf_plugin/requirements.txt + +The ZIP files for Windows and Linux are included as assets: + +- ``winx64.zip`` +- ``linx64.zip`` + + +**custom_plugin.xml file** + +The ``custom_plugin.xml`` file contains code like this: + +.. literalinclude:: custom_plugin.xml + :language: xml + + +Load the plug-in package +------------------------ + +Use the :func:`load_library() ` method to load plug-in package. + +- The first argument is the path to the directory where the plug-in package is located. +- The second argument is ``py_`` where 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. + +.. code:: + + dpf.load_library( + r"path/to/plugins/custom_plugin", + "py_my_custom_plugin", #if the load_operators function is defined in path/to/plugins/custom_plugin/__init__.py + "load_operators") + +Use the custom operators +-------------------- + +Once the plugin is loaded, you can instantiate the custom operator: + +.. code:: + + new_operator = dpf.Operator("custom_operator") # if "custom_operator" is what is returned by the ``name`` property + +References +---------- +For more information, see :ref:`ref_custom_operator` in the **API reference** +and :ref:`python_operators` in **Examples**. \ No newline at end of file diff --git a/doc/source/user_guide/custom_plugin.xml b/doc/source/user_guide/tutorials/custom_operators_and_plugins/custom_plugin.xml similarity index 100% rename from doc/source/user_guide/custom_plugin.xml rename to doc/source/user_guide/tutorials/custom_operators_and_plugins/custom_plugin.xml diff --git a/doc/source/user_guide/tutorials/custom_operators_and_plugins/index.rst b/doc/source/user_guide/tutorials/custom_operators_and_plugins/index.rst new file mode 100644 index 0000000000..ba0fdd44ed --- /dev/null +++ b/doc/source/user_guide/tutorials/custom_operators_and_plugins/index.rst @@ -0,0 +1,74 @@ +.. _ref_tutorials_custom_operators_and_plugins: + +Custom Operators and Plugins +---------------------------- +You can enhance and customize your DPF installation by creating new operators and libraries of operators, called 'plugins'. + +DPF offers multiple development APIs depending on your environment. + +With support for custom operators, PyDPF-Core becomes a development tool offering: + +- **Accessibility:** A simple script can define a basic operator plugin. + +- **Componentization:** Operators with similar applications can be grouped in Python plug-in packages. + +- **Easy distribution:** Standard Python tools can be used to package, upload, and download custom operators. + +- **Dependency management:** Third-party Python modules can be added to the Python package. + +- **Reusability:** A documented and packaged operator can be reused in an infinite number of workflows. + +- **Remotable and parallel computing:** Native DPF capabilities are inherited by custom operators. + +The only prerequisite for creating custom operators is to be familiar with native operators. +For more information, see :ref:`ref_user_guide_operators`. + +.. note: + You can create custom operators in CPython using PyDPF-Core for use with DPF in Ansys 2023 R1 and later. + +The following tutorials demonstrate how to develop such plugins using PyDPF-Core (CPython based) and how to use them. + +For comprehensive examples on writing operator plugins, see :ref:`python_operators`. + +.. grid:: 1 1 3 3 + :gutter: 2 + :padding: 2 + :margin: 2 + + .. grid-item-card:: Create a DPF plugin with a single operator + :link: tutorials_custom_operators_and_plugins_custom_operator + :link-type: ref + :text-align: center + + This tutorial shows how to create, load, and use a custom plugin containing a single custom operator. + + .. grid-item-card:: Create a DPF plugin with multiple operators + :link: tutorials_others_custom_plug_ins_packages + :link-type: ref + :text-align: center + + This tutorial shows how to create, load, and use a custom plugin with multiple operators or with complex routines. + + .. grid-item-card:: Create a custom DPF plugin with third-party dependencies using Python + :link: tutorials_others_custom_plug_ins_packages_third_deps + :link-type: ref + :text-align: center + + This tutorial shows how to create a Python plug-in package with third-party dependencies. + + .. grid-item-card:: Update PyDPF-Core in the DPF installation + :link: tutorials_custom_operators_update_pydpf_core + :link-type: ref + :text-align: center + + This tutorial shows how to update PyDPF-Core in your DPF installation. + +.. toctree:: + :maxdepth: 2 + :hidden: + + custom_operators.rst + custom_plug_in_package.rst + custom_plug_in_package_third_deps.rst + update_pydpf_core.rst + diff --git a/doc/source/user_guide/install_ansys_dpf_core_in_ansys.ps1 b/doc/source/user_guide/tutorials/custom_operators_and_plugins/install_ansys_dpf_core_in_ansys.ps1 similarity index 100% rename from doc/source/user_guide/install_ansys_dpf_core_in_ansys.ps1 rename to doc/source/user_guide/tutorials/custom_operators_and_plugins/install_ansys_dpf_core_in_ansys.ps1 diff --git a/doc/source/user_guide/install_ansys_dpf_core_in_ansys.sh b/doc/source/user_guide/tutorials/custom_operators_and_plugins/install_ansys_dpf_core_in_ansys.sh similarity index 100% rename from doc/source/user_guide/install_ansys_dpf_core_in_ansys.sh rename to doc/source/user_guide/tutorials/custom_operators_and_plugins/install_ansys_dpf_core_in_ansys.sh diff --git a/doc/source/user_guide/uninstall_ansys_dpf_core_in_ansys.ps1 b/doc/source/user_guide/tutorials/custom_operators_and_plugins/uninstall_ansys_dpf_core_in_ansys.ps1 similarity index 100% rename from doc/source/user_guide/uninstall_ansys_dpf_core_in_ansys.ps1 rename to doc/source/user_guide/tutorials/custom_operators_and_plugins/uninstall_ansys_dpf_core_in_ansys.ps1 diff --git a/doc/source/user_guide/uninstall_ansys_dpf_core_in_ansys.sh b/doc/source/user_guide/tutorials/custom_operators_and_plugins/uninstall_ansys_dpf_core_in_ansys.sh similarity index 100% rename from doc/source/user_guide/uninstall_ansys_dpf_core_in_ansys.sh rename to doc/source/user_guide/tutorials/custom_operators_and_plugins/uninstall_ansys_dpf_core_in_ansys.sh diff --git a/doc/source/user_guide/tutorials/custom_operators_and_plugins/update_pydpf_core.rst b/doc/source/user_guide/tutorials/custom_operators_and_plugins/update_pydpf_core.rst new file mode 100644 index 0000000000..29f4e1e927 --- /dev/null +++ b/doc/source/user_guide/tutorials/custom_operators_and_plugins/update_pydpf_core.rst @@ -0,0 +1,30 @@ + +Install module +-------------- + +Once an Ansys-unified installation is complete, you must install the ``ansys-dpf-core`` module in the Ansys +installer's Python interpreter. + +#. Download the script for you operating system: + + - For Windows, download this :download:`PowerShell script `. + - For Linux, download this :download:`Shell script ` + +#. Run the downloaded script for installing with optional arguments: + + - ``-awp_root``: Path to the Ansys root installation folder. For example, the 2023 R1 installation folder ends + with ``Ansys Inc/v231``, and the default environment variable is ``AWP_ROOT231``. + - ``-pip_args``: Optional arguments to add to the ``pip`` command. For example, ``--extra-index-url`` or + ``--trusted-host``. + +If you ever want to uninstall the ``ansys-dpf-core`` module from the Ansys installation, you can do so. + +#. Download the script for your operating system: + + - For Windows, download this :download:`PowerShell script `. + - For Linux, download this :download:`Shell script `. + +#. Run the downloaded script for uninstalling with the optional argument: + + - ``-awp_root``: Path to the Ansys root installation folder. For example, the 2023 R1 installation folder ends + with ``Ansys Inc/v231``, and the default environment variable is ``AWP_ROOT231``. \ No newline at end of file diff --git a/doc/source/user_guide/tutorials/enriching_dpf_capabilities/index.rst b/doc/source/user_guide/tutorials/enriching_dpf_capabilities/index.rst deleted file mode 100644 index 232c57c2a4..0000000000 --- a/doc/source/user_guide/tutorials/enriching_dpf_capabilities/index.rst +++ /dev/null @@ -1,42 +0,0 @@ -.. _ref_tutorials_enriching: - -========================== -Enriching DPF capabilities -========================== - -The available DPF capabilities loaded in a DPF application can be enhanced -by creating new operator’s libraries. DPF offers multiple development APIs -depending on your environment. - -These tutorials demonstrate how to develop those plugins for PyDPF-Core (CPython based) - -.. grid:: 1 1 3 3 - :gutter: 2 - :padding: 2 - :margin: 2 - - .. grid-item-card:: Create custom operators and plugins - :link: ref_tutorials - :link-type: ref - :text-align: center - - This tutorial - - .. grid-item-card:: Create a plug-in package with multiple operators - :link: ref_tutorials - :link-type: ref - :text-align: center - - This tutorial - - .. grid-item-card:: Create a plug-in package that has third-party dependencies - :link: ref_tutorials - :link-type: ref - :text-align: center - - This tutorial - -.. toctree:: - :maxdepth: 2 - :hidden: - diff --git a/doc/source/user_guide/tutorials/index.rst b/doc/source/user_guide/tutorials/index.rst index a24466eba5..ad5172161a 100644 --- a/doc/source/user_guide/tutorials/index.rst +++ b/doc/source/user_guide/tutorials/index.rst @@ -120,7 +120,7 @@ of our package background so you can understand how to work with it. :link-type: ref :text-align: center - Discover how to enhance DPF capabilities by creating new operator’s libraries. + Discover how to enhance DPF capabilities by creating your own operators and plugins. .. grid-item-card:: Post-process distributed files :link: ref_tutorials_distributed_files diff --git a/doc/source/user_guide/tutorials/transform_data/index.rst b/doc/source/user_guide/tutorials/transform_data/index.rst new file mode 100644 index 0000000000..1af0c017fc --- /dev/null +++ b/doc/source/user_guide/tutorials/transform_data/index.rst @@ -0,0 +1,49 @@ +.. _ref_tutorials_transform_data: + +============== +Transform data +============== + +An operator is the main object that is used to create, transform, and stream data in DPF. + +They can perform different modifications with the data: direct mathematical operations, +averaging in the mesh, changes in the model locations .... They can also be chained together +to create more complex operations and customizable results. + +The tutorials in this section aims to explain how to transform and operate on data to obtain +the desired input by using the DPF operators with PyDPF-Core. + +For more information on how to program with PyDPF-Core check the +:ref:`ref_tutorials_language_and_usage` tutorial. + + +.. grid:: 1 1 3 3 + :gutter: 2 + :padding: 2 + :margin: 2 + + .. grid-item-card:: Using operators + :link: ref_tutorials + :link-type: ref + :text-align: center + + This tutorial + + .. grid-item-card:: Create a workflow + :link: ref_tutorials + :link-type: ref + :text-align: center + + This tutorial + + .. grid-item-card:: Mathematical operations with PyDPF-Core data storage structures + :link: ref_tutorials + :link-type: ref + :text-align: center + + This tutorial + +.. toctree:: + :maxdepth: 2 + :hidden: + diff --git a/examples/08-python-operators/02-python_operators_with_dependencies.py b/examples/08-python-operators/02-python_operators_with_dependencies.py index cd61e584e9..87a75c7e31 100644 --- a/examples/08-python-operators/02-python_operators_with_dependencies.py +++ b/examples/08-python-operators/02-python_operators_with_dependencies.py @@ -57,13 +57,36 @@ # Download the ``gltf_plugin`` plug-in package that has already been # created for you. -import os from pathlib import Path -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 = Path(examples.download_gltf_plugin()) folder_root = Path(str(Path.cwd()).rsplit("pydpf-core", 1)[0]) / "pydpf-core" +source_path_in_repo = r"doc\source\examples\07-python-operators\plugins" +operator_folder = Path(folder_root) / Path(source_path_in_repo) +print(operator_folder) +plugin_path = None + +for file in file_list: + operator_file_path = Path(operator_folder) / Path(file) + + print(f"\033[1m {file}\n \033[0m") + if (Path(file).suffix in [".py", ".xml"]) and file != "gltf_plugin/gltf_export.py": + with Path(operator_file_path).open(mode="r") as f: + for line in f.readlines(): + print("\t\t\t" + line) + print("\n\n") + if plugin_path is None: + plugin_path = Path(operator_file_path).parent # %% # To add third-party modules as dependencies to a plug-in package, you must @@ -84,9 +107,8 @@ # 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") -requirements_path = plugin_path / "requirements.txt" -with requirements_path.open("r") as file: - for line in file.readlines(): +with (Path(plugin_path) / "requirements.txt").open(mode="r") as f: + for line in f.readlines(): print("\t\t\t" + line) @@ -94,9 +116,9 @@ # Download the script for your operating system. # # - For Windows, download this -# :download:`PowerShell script `. +# :download:`PowerShell script `. # - For Linux, download this -# :download:`Shell script `. +# :download:`Shell script `. # # Run the downloaded script with the mandatory arguments: # @@ -119,21 +141,27 @@ # # create_sites_for_python_operators.sh -pluginpath /path/to/plugin -zippath /path/to/plugin/assets/linx64.zip # noqa: E501 -site_path = plugin_path / "assets" / "gltf_sites_winx64.zip" -if os.name == "nt" and not site_path.exists(): +import os + +if os.name == "nt" and not (Path(plugin_path) / "assets" / "gltf_sites_winx64.zip").exists(): cmd_file = ( - folder_root / "doc" / "source" / "user_guide" / "create_sites_for_python_operators.ps1" + Path(folder_root) + / "doc" + / "source" + / "user_guide" + / "tutorials" + / "custom_operators_and_plugins" + / "create_sites_for_python_operators.ps1" ) args = [ "powershell", - str(cmd_file), + cmd_file, "-pluginpath", - str(plugin_path), + plugin_path, "-zippath", - str(plugin_path / "assets" / "gltf_sites_winx64.zip"), + Path(plugin_path) / "assets" / "gltf_sites_winx64.zip", ] print(args) - import subprocess process = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) @@ -147,15 +175,20 @@ ) else: print("Installing pygltf in a virtual environment succeeded") - -elif os.name == "posix" and not site_path.exists(): +elif os.name == "posix" and not (Path(plugin_path) / "assets" / "gltf_sites_linx64.zip").exists(): cmd_file = ( - folder_root / "doc" / "source" / "user_guide" / "create_sites_for_python_operators.sh" + Path(folder_root) + / "doc" + / "source" + / "user_guide" + / "tutorials" + / "custom_operators_and_plugins" + / "create_sites_for_python_operators.sh" ) run_cmd = f"{cmd_file}" args = ( f' -pluginpath "{plugin_path}" ' - f'-zippath "{plugin_path / "assets" / "gltf_sites_winx64.zip"}"' + f"-zippath \"{Path(plugin_path)/'assets'/'gltf_sites_linx64.zip'}\"" ) print(run_cmd + args) os.system(f"chmod u=rwx,o=x {cmd_file}") @@ -181,14 +214,14 @@ # Python plugins are not supported in process. dpf.start_local_server(config=dpf.AvailableServerConfigs.GrpcServer) -tmp = Path(dpf.make_tmp_dir_server()) -dpf.upload_files_in_folder(dpf.path_utilities.join(str(tmp), "plugins", "gltf_plugin"), plugin_path) +tmp = dpf.make_tmp_dir_server() +dpf.upload_files_in_folder(dpf.path_utilities.join(tmp, "plugins", "gltf_plugin"), plugin_path) dpf.upload_file( - str(plugin_path) + ".xml", dpf.path_utilities.join(str(tmp), "plugins", "gltf_plugin.xml") + str(plugin_path) + ".xml", dpf.path_utilities.join(tmp, "plugins", "gltf_plugin.xml") ) dpf.load_library( - dpf.path_utilities.join(str(tmp), "plugins", "gltf_plugin"), + dpf.path_utilities.join(tmp, "plugins", "gltf_plugin"), "py_dpf_gltf", "load_operators", ) @@ -237,14 +270,14 @@ displacement = model.results.displacement() displacement.inputs.mesh_scoping(skin_mesh) displacement.inputs.mesh(skin_mesh) -new_operator.inputs.path(str(tmp / "out")) +new_operator.inputs.path(Path(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(tmp / "out.glb", Path.cwd() / "out.glb") +dpf.download_file(Path(tmp) / "out.glb", Path.cwd() / "out.glb") # %% # You can download :download:`output ` from the ``gltf`` operator. diff --git a/src/ansys/dpf/core/examples/python_plugins/custom_operator_example.py b/src/ansys/dpf/core/examples/python_plugins/custom_operator_example.py new file mode 100644 index 0000000000..942c5a5bbc --- /dev/null +++ b/src/ansys/dpf/core/examples/python_plugins/custom_operator_example.py @@ -0,0 +1,129 @@ +# Copyright (C) 2020 - 2025 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +"""Example of a custom DPF operator in Python.""" + +from ansys.dpf import core as dpf +from ansys.dpf.core.changelog import Changelog +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. + + The specification declares: + - the description of the operator + - the inputs of the operator + - the outputs of the operator + - the properties of the operator (a username, a category, a required license) + - the changelog of the operator (starting with DPF 2026 R1) + """ + # Instantiate the custom specification + spec = CustomSpecification() + # Set the description of the operator + spec.description = "What the Operator does. You can use MarkDown and LaTeX in descriptions." + # Define the inputs of the operator if any + spec.inputs = { + 0: PinSpecification( + name="input_0", + type_names=[dpf.Field, dpf.FieldsContainer], + document="Describe input pin 0.", + ), + } + # Define the outputs of the operator if any + spec.outputs = { + 0: PinSpecification( + name="output_0", type_names=[dpf.Field], document="Describe output pin 0." + ), + } + # Define the properties of the operator if any + spec.properties = SpecificationProperties( + user_name="my custom operator", # Optional, defaults to the scripting name with spaces + category="my_category", # Optional, defaults to 'other' + license="any_dpf_supported_increments", # Optional, defaults to None + ) + # Set the changelog of the operator to track changes + 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") # Checks the resulting version is as expected + ) + return spec + + def run(self): + """Run the operator and execute the logic implemented here. + + This method defines the behavior of the operator. + + Request the inputs with the method ``get_input``, + perform operations on the data, + then set the outputs with the method ``set_output``, + and finally call ``set_succeeded``. + + In this example, the operator changes the name of a Field. + + """ + # First get the field in input by calling get_input for the different types supported + # # Try requesting the input as a Field + field: dpf.Field = self.get_input(0, dpf.Field) + # # If function returns None, there is no Field connected to this input + if field is None: + # # Try requesting the input as a FieldsContainer + field: dpf.FieldsContainer = self.get_input(0, dpf.FieldsContainer).get_field(0) + # # If the input is optional, set its default value + # # If the input is not optional and empty, raise an error + 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 of the operator + self.set_output(0, field) + + # And declare the operator run a success + self.set_succeeded() + + +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)