From feded1457f1922a17f6d2965399f342de06f4bc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matt=C3=A9o=20Baussart?= Date: Fri, 15 Nov 2024 15:31:03 +0100 Subject: [PATCH 01/24] feat: add WorkflowTopology class and workflow_to_workflow_topology operator --- src/ansys/dpf/core/__init__.py | 1 + src/ansys/dpf/core/custom_container_base.py | 28 +++++ src/ansys/dpf/core/dpf_operator.py | 6 +- src/ansys/dpf/core/helpers/utils.py | 16 +++ src/ansys/dpf/core/workflow.py | 8 ++ .../core/workflow_topology/data_connection.py | 89 +++++++++++++ .../dpf/core/workflow_topology/exposed_pin.py | 92 ++++++++++++++ .../workflow_topology/operator_connection.py | 101 +++++++++++++++ .../workflow_topology/workflow_topology.py | 117 ++++++++++++++++++ 9 files changed, 456 insertions(+), 2 deletions(-) create mode 100644 src/ansys/dpf/core/custom_container_base.py create mode 100644 src/ansys/dpf/core/workflow_topology/data_connection.py create mode 100644 src/ansys/dpf/core/workflow_topology/exposed_pin.py create mode 100644 src/ansys/dpf/core/workflow_topology/operator_connection.py create mode 100644 src/ansys/dpf/core/workflow_topology/workflow_topology.py diff --git a/src/ansys/dpf/core/__init__.py b/src/ansys/dpf/core/__init__.py index 1c885d1d70b..89711fcf5ff 100644 --- a/src/ansys/dpf/core/__init__.py +++ b/src/ansys/dpf/core/__init__.py @@ -118,6 +118,7 @@ CustomTypeFieldsCollection:type = _CollectionFactory(CustomTypeField) GenericDataContainersCollection:type = _CollectionFactory(GenericDataContainer) StringFieldsCollection:type = _CollectionFactory(StringField) +OperatorsCollection: type = _CollectionFactory(Operator) AnyCollection:type = _Collection # for matplotlib diff --git a/src/ansys/dpf/core/custom_container_base.py b/src/ansys/dpf/core/custom_container_base.py new file mode 100644 index 00000000000..282bea71c7c --- /dev/null +++ b/src/ansys/dpf/core/custom_container_base.py @@ -0,0 +1,28 @@ +# Copyright (C) 2020 - 2024 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. + +from ansys.dpf.core.generic_data_container import GenericDataContainer + + +class CustomContainerBase: + def __init__(self, container: GenericDataContainer) -> None: + self._container = container diff --git a/src/ansys/dpf/core/dpf_operator.py b/src/ansys/dpf/core/dpf_operator.py index b0e2add66b5..c990030a88d 100644 --- a/src/ansys/dpf/core/dpf_operator.py +++ b/src/ansys/dpf/core/dpf_operator.py @@ -726,8 +726,10 @@ def default_config(name, server=None): def __del__(self): try: - if self._internal_obj is not None: - self._deleter_func[0](self._deleter_func[1](self)) + if hasattr(self, "_deleter_func"): + obj = self._deleter_func[1](self) + if obj is not None: + self._deleter_func[0](obj) except: warnings.warn(traceback.format_exc()) diff --git a/src/ansys/dpf/core/helpers/utils.py b/src/ansys/dpf/core/helpers/utils.py index 395f4b4ea54..e1708e7e1d3 100644 --- a/src/ansys/dpf/core/helpers/utils.py +++ b/src/ansys/dpf/core/helpers/utils.py @@ -48,3 +48,19 @@ def _sort_supported_kwargs(bound_method, **kwargs): warnings.warn(txt) # Return the accepted arguments return kwargs_in + + +def indent(text, subsequent_indent="", initial_indent=None): + if initial_indent is None: + initial_indent = subsequent_indent + + if not isinstance(text, str): + text = str(text) + + lines = text.rstrip().splitlines() + indented_lines = [ + f"{initial_indent if index == 0 else subsequent_indent}{line}" + for (index, line) in enumerate(lines) + ] + + return "\n".join(indented_lines) diff --git a/src/ansys/dpf/core/workflow.py b/src/ansys/dpf/core/workflow.py index d582f5c1762..ba4356fb4a1 100644 --- a/src/ansys/dpf/core/workflow.py +++ b/src/ansys/dpf/core/workflow.py @@ -43,6 +43,7 @@ server_meet_version_and_raise, ) from ansys.dpf.core import server as server_module +from ansys.dpf.core.workflow_topology.workflow_topology import WorkflowTopology from ansys.dpf.gate import ( workflow_abstract_api, workflow_grpcapi, @@ -953,6 +954,13 @@ def to_graphviz(self, path: Union[os.PathLike, str]): """Saves the workflow to a GraphViz file.""" return self._api.work_flow_export_graphviz(self, str(path)) + def get_topology(self): + workflow_to_workflow_topology_op = dpf_operator.Operator("workflow_to_workflow_topology") + workflow_to_workflow_topology_op.inputs.workflow.connect(self) + workflow_topology_container = workflow_to_workflow_topology_op.outputs.workflow_topology() + + return WorkflowTopology(workflow_topology_container) + def __del__(self): try: if hasattr(self, "_internal_obj"): diff --git a/src/ansys/dpf/core/workflow_topology/data_connection.py b/src/ansys/dpf/core/workflow_topology/data_connection.py new file mode 100644 index 00000000000..a02908c48ad --- /dev/null +++ b/src/ansys/dpf/core/workflow_topology/data_connection.py @@ -0,0 +1,89 @@ +# Copyright (C) 2020 - 2024 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. + +from ansys.dpf.core.custom_container_base import CustomContainerBase + + +class DataConnection(CustomContainerBase): + def __init__(self, container): + super().__init__(container) + self._source_data = None + self._target_operator = None + self._target_pin_id = None + + @property + def source_data(self): + if self._source_data is None: + self._source_data = self._container.get_property("source_data") + + return self._source_data + + @property + def target_operator(self): + from ansys.dpf.core.dpf_operator import Operator + + if self._target_operator is None: + self._target_operator = self._container.get_property("target_operator", Operator) + + return self._target_operator + + @property + def target_pin_id(self): + if self._target_pin_id is None: + self._target_pin_id = self._container.get_property("target_pin_id", int) + + return self._target_pin_id + + def __str__(self): + from ansys.dpf.core.helpers.utils import indent + + indents = " " + return ( + "DataConnection with properties:\n" + " - source_data:\n" + f"{indent(self.source_data, indents)}\n" + " - target_operator:\n" + f"{indent(self.target_operator.name, indents)}\n" + " - target_pin_id:\n" + f"{indent(self.target_pin_id, indents)}" + ) + + +class DataConnectionsCollection: + def __init__(self, collection): + self._collection = collection + + def __len__(self): + return len(self._collection) + + def __getitem__(self, index): + return DataConnection(self._collection[index]) + + def __iter__(self): + for i in range(len(self)): + yield self[i] + + def __str__(self): + from ansys.dpf.core.helpers.utils import indent + + indents = (" ", " - ") + return "\n".join([indent(data_connection, *indents) for data_connection in self]) diff --git a/src/ansys/dpf/core/workflow_topology/exposed_pin.py b/src/ansys/dpf/core/workflow_topology/exposed_pin.py new file mode 100644 index 00000000000..c30665fe522 --- /dev/null +++ b/src/ansys/dpf/core/workflow_topology/exposed_pin.py @@ -0,0 +1,92 @@ +# Copyright (C) 2020 - 2024 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. + +from ansys.dpf.core.custom_container_base import CustomContainerBase + + +class ExposedPin(CustomContainerBase): + def __init__(self, container): + super().__init__(container) + self._name = None + self._operator = None + self._pin_id = None + + @property + def name(self): + if self._name is None: + self._name = self._container.get_property("name", str) + + return self._name + + @property + def operator(self): + from ansys.dpf.core.dpf_operator import Operator + + if self._operator is None: + self._operator = self._container.get_property("operator", Operator) + + return self._operator + + @property + def pin_id(self): + if self._pin_id is None: + self._pin_id = self._container.get_property("pin_id", int) + + return self._pin_id + + def __str__(self): + from ansys.dpf.core.helpers.utils import indent + + indents = " " + return ( + "ExposedPin with properties:\n" + " - name:\n" + f"{indent(self.name, indents)}\n" + " - operator:\n" + f"{indent(self.operator.name, indents)}\n" + " - pin_id:\n" + f"{indent(self.pin_id, indents)}" + ) + + def __repr__(self) -> str: + return self.__str__() + + +class ExposedPinsCollection: + def __init__(self, collection): + self._collection = collection + + def __len__(self): + return len(self._collection) + + def __getitem__(self, index): + return ExposedPin(self._collection[index]) + + def __iter__(self): + for i in range(len(self)): + yield self[i] + + def __str__(self): + from ansys.dpf.core.helpers.utils import indent + + indents = (" ", " - ") + return "\n".join([indent(exposed_pin, *indents) for exposed_pin in self]) diff --git a/src/ansys/dpf/core/workflow_topology/operator_connection.py b/src/ansys/dpf/core/workflow_topology/operator_connection.py new file mode 100644 index 00000000000..638866b9d65 --- /dev/null +++ b/src/ansys/dpf/core/workflow_topology/operator_connection.py @@ -0,0 +1,101 @@ +# Copyright (C) 2020 - 2024 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. + +from ansys.dpf.core.custom_container_base import CustomContainerBase + + +class OperatorConnection(CustomContainerBase): + def __init__(self, container): + super().__init__(container) + self._source_operator = None + self._source_pin_id = None + self._target_operator = None + self._target_pin_id = None + + @property + def source_operator(self): + from ansys.dpf.core.dpf_operator import Operator + + if self._source_operator is None: + self._source_operator = self._container.get_property("source_operator", Operator) + + return self._source_operator + + @property + def source_pin_id(self): + if self._source_pin_id is None: + self._source_pin_id = self._container.get_property("source_pin_id", int) + + return self._source_pin_id + + @property + def target_operator(self): + from ansys.dpf.core.dpf_operator import Operator + + if self._target_operator is None: + self._target_operator = self._container.get_property("target_operator", Operator) + + return self._target_operator + + @property + def target_pin_id(self): + if self._target_pin_id is None: + self._target_pin_id = self._container.get_property("target_pin_id", int) + + return self._target_pin_id + + def __str__(self): + from ansys.dpf.core.helpers.utils import indent + + indents = " " + return ( + "OperatorConnection with properties:\n" + " - source_operator:\n" + f"{indent(self.source_operator.name, indents)}\n" + " - source_pin_id:\n" + f"{indent(self.source_pin_id, indents)}\n" + " - target_operator:\n" + f"{indent(self.target_operator.name, indents)}\n" + " - target_pin_id:\n" + f"{indent(self.target_pin_id, indents)}" + ) + + +class OperatorConnectionsCollection: + def __init__(self, collection): + self._collection = collection + + def __len__(self): + return len(self._collection) + + def __getitem__(self, index): + return OperatorConnection(self._collection[index]) + + def __iter__(self): + for i in range(len(self)): + yield self[i] + + def __str__(self): + from ansys.dpf.core.helpers.utils import indent + + indents = (" ", " - ") + return "\n".join([indent(operator_connection, *indents) for operator_connection in self]) diff --git a/src/ansys/dpf/core/workflow_topology/workflow_topology.py b/src/ansys/dpf/core/workflow_topology/workflow_topology.py new file mode 100644 index 00000000000..99cfecd3df1 --- /dev/null +++ b/src/ansys/dpf/core/workflow_topology/workflow_topology.py @@ -0,0 +1,117 @@ +# Copyright (C) 2020 - 2024 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. + +from ansys.dpf.core.custom_container_base import CustomContainerBase + + +class WorkflowTopology(CustomContainerBase): + def __init__(self, *args) -> None: + super().__init__(*args) + + self._operators = None + self._operator_connections = None + self._data_connections = None + self._exposed_inputs = None + self._exposed_outputs = None + + @property + def operators(self): + from ansys.dpf.core import OperatorsCollection + + if self._operators is None: + self._operators = self._container.get_property("operators", OperatorsCollection) + + return self._operators + + @property + def operator_connections(self): + from ansys.dpf.core import GenericDataContainersCollection + from ansys.dpf.core.workflow_topology.operator_connection import ( + OperatorConnectionsCollection, + ) + + if self._operator_connections is None: + self._operator_connections = OperatorConnectionsCollection( + self._container.get_property( + "operator_connections", GenericDataContainersCollection + ) + ) + + return self._operator_connections + + @property + def data_connections(self): + from ansys.dpf.core import GenericDataContainersCollection + from ansys.dpf.core.workflow_topology.data_connection import DataConnectionsCollection + + if self._data_connections is None: + self._data_connections = DataConnectionsCollection( + self._container.get_property("data_connections", GenericDataContainersCollection) + ) + + return self._data_connections + + @property + def exposed_inputs(self): + from ansys.dpf.core import GenericDataContainersCollection + from ansys.dpf.core.workflow_topology.exposed_pin import ExposedPinsCollection + + if self._exposed_inputs is None: + self._exposed_inputs = ExposedPinsCollection( + self._container.get_property("exposed_inputs", GenericDataContainersCollection) + ) + + return self._exposed_inputs + + @property + def exposed_outputs(self): + from ansys.dpf.core import GenericDataContainersCollection + from ansys.dpf.core.workflow_topology.exposed_pin import ExposedPinsCollection + + if self._exposed_outputs is None: + self._exposed_outputs = ExposedPinsCollection( + self._container.get_property("exposed_outputs", GenericDataContainersCollection) + ) + + return self._exposed_outputs + + def __str__(self): + from ansys.dpf.core.helpers.utils import indent + + def indent_operators(operators): + indents = (" ", " - ") + return "\n".join([indent(operator.name, *indents) for operator in operators]) + + indents = " " + return ( + "WorkflowTopology with properties:\n" + f" - operators (len: {len(self.operators)}):\n" + f"{indent_operators(self.operators)}\n" + f" - operator_connections (len: {len(self.operator_connections)}):\n" + f"{indent(self.operator_connections, indents)}\n" + f" - data_connections (len: {len(self.data_connections)}):\n" + f"{indent(self.data_connections, indents)}\n" + f" - exposed_inputs (len: {len(self.exposed_inputs)}):\n" + f"{indent(self.exposed_inputs, indents)}\n" + f" - exposed_outputs (len: {len(self.exposed_outputs)}):\n" + f"{indent(self.exposed_outputs, indents)}" + ) From 4ff8c4e38b4b9fdd2abb8030b644728b3e6108c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matt=C3=A9o=20Baussart?= Date: Thu, 21 Nov 2024 17:08:31 +0100 Subject: [PATCH 02/24] fix: updated Workflow.get_topology() to use derived classes --- src/ansys/dpf/core/dpf_operator.py | 6 ++++++ src/ansys/dpf/core/workflow.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/ansys/dpf/core/dpf_operator.py b/src/ansys/dpf/core/dpf_operator.py index c990030a88d..1bb84edef85 100644 --- a/src/ansys/dpf/core/dpf_operator.py +++ b/src/ansys/dpf/core/dpf_operator.py @@ -385,6 +385,7 @@ def _type_to_output_method(self): collection_base, any, ) + from ansys.dpf.core.workflow_topology import workflow_topology out = [ (any.Any, self._api.operator_getoutput_as_any), @@ -481,6 +482,11 @@ def _type_to_output_method(self): self._api.operator_getoutput_as_any, lambda obj, type: any.Any(server=self._server, any_dpf=obj).cast(type), ), + ( + workflow_topology.WorkflowTopology, + None, + "WorkflowTopology", + ), ] if hasattr(self._api, "operator_getoutput_generic_data_container"): out.append( diff --git a/src/ansys/dpf/core/workflow.py b/src/ansys/dpf/core/workflow.py index ba4356fb4a1..23b1ede54f1 100644 --- a/src/ansys/dpf/core/workflow.py +++ b/src/ansys/dpf/core/workflow.py @@ -959,7 +959,7 @@ def get_topology(self): workflow_to_workflow_topology_op.inputs.workflow.connect(self) workflow_topology_container = workflow_to_workflow_topology_op.outputs.workflow_topology() - return WorkflowTopology(workflow_topology_container) + return workflow_topology_container def __del__(self): try: From f8467b6b4ac4c0bc232b67efdd1ae5f936da3b7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matt=C3=A9o=20Baussart?= Date: Tue, 26 Nov 2024 15:46:18 +0100 Subject: [PATCH 03/24] feat: add tests for WorkflowTopology --- src/ansys/dpf/core/workflow.py | 4 +- tests/test_workflow_topology.py | 182 ++++++++++++++++++++++++++++++++ 2 files changed, 185 insertions(+), 1 deletion(-) create mode 100644 tests/test_workflow_topology.py diff --git a/src/ansys/dpf/core/workflow.py b/src/ansys/dpf/core/workflow.py index 23b1ede54f1..3f4e2525744 100644 --- a/src/ansys/dpf/core/workflow.py +++ b/src/ansys/dpf/core/workflow.py @@ -955,7 +955,9 @@ def to_graphviz(self, path: Union[os.PathLike, str]): return self._api.work_flow_export_graphviz(self, str(path)) def get_topology(self): - workflow_to_workflow_topology_op = dpf_operator.Operator("workflow_to_workflow_topology") + workflow_to_workflow_topology_op = dpf_operator.Operator( + "workflow_to_workflow_topology", server=self._server + ) workflow_to_workflow_topology_op.inputs.workflow.connect(self) workflow_topology_container = workflow_to_workflow_topology_op.outputs.workflow_topology() diff --git a/tests/test_workflow_topology.py b/tests/test_workflow_topology.py new file mode 100644 index 00000000000..f384acef9d9 --- /dev/null +++ b/tests/test_workflow_topology.py @@ -0,0 +1,182 @@ +# Copyright (C) 2020 - 2024 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. + +import pytest + +from ansys import dpf +import ansys.dpf.core.operators as op + + +def test_instantiate_workflow_to_workflow_topology_op(server_type): + workflow_to_workflow_topology_op = dpf.core.Operator( + "workflow_to_workflow_topology", server=server_type + ) + + assert workflow_to_workflow_topology_op + + +def workflow_forward(server_type) -> dpf.core.Workflow: + """ + ┌─────────┐ ┌────────────┐ ┌────────────┐ ┌──────────┐ + │"Input 0"├─────►│forward_op_1├─────►│forward_op_1├─────►│"Output 0"│ + └─────────┘ └────────────┘ └────────────┘ └──────────┘ + ┌───────┐ ┌────────────┐ ┌──────────┐ + │"hello"├──────────►│forward_op_3├─────►│"Output 1"│ + └───────┘ └────────────┘ └──────────┘ + """ + + forward_op_1 = op.utility.forward(server=server_type) + forward_op_2 = op.utility.forward(server=server_type) + forward_op_3 = op.utility.forward(server=server_type) + + forward_op_2.inputs.connect(forward_op_1.outputs) + forward_op_3.inputs.connect("hello") + + workflow = dpf.core.Workflow(server=server_type) + + workflow.add_operators([forward_op_1, forward_op_2, forward_op_3]) + + workflow.set_input_name("Input 0", forward_op_1.inputs.any) + workflow.set_output_name("Output 0", forward_op_2.outputs.any) + workflow.set_output_name("Output 1", forward_op_3.outputs.any) + + return workflow + + +def workflow_forward_5(server_type) -> dpf.core.Workflow: + """ + ┌─────────┐ ┌──────────┐ + │"Input 0"├──┐ ┌──►│"Output 0"│ + └─────────┘ │ │ └──────────┘ + ┌─────────┐ │ │ ┌──────────┐ + │"Input 1"├──┤ ├──►│"Output 1"│ + └─────────┘ │ │ └──────────┘ + ┌─────────┐ │ ┌──────────┐ │ ┌──────────┐ + │"Input 2"├──┼──►│forward_op├──┼──►│"Output 2"│ + └─────────┘ │ └──────────┘ │ └──────────┘ + ┌─────────┐ │ │ ┌──────────┐ + │"Input 3"├──┤ ├──►│"Output 3"│ + └─────────┘ │ │ └──────────┘ + ┌─────────┐ │ │ ┌──────────┐ + │"Input 4"├──┘ └──►│"Output 4"│ + └─────────┘ └──────────┘ + """ + + forward_op = op.utility.forward(server=server_type) + + workflow = dpf.core.Workflow(server=server_type) + + workflow.add_operators([forward_op]) + + for i in range(5): + workflow.set_input_name(f"Input {i}", forward_op, i) + workflow.set_output_name(f"Output {i}", forward_op, i) + + return workflow + + +def workflow_disp_min_max(server_type) -> dpf.core.Workflow: + """ + ┌──────────────┐ ┌───────┐ ┌─────────────┐ ┌─────┐ + │"data_sources"├─────►│disp_op├─────►│min_max_fc_op├──┬──►│"min"│ + └──────────────┘ └───────┘ └─────────────┘ │ └─────┘ + │ ┌─────┐ + └──►│"max"│ + └─────┘ + """ + + disp_op = op.result.displacement(server=server_type) + min_max_fc_op = op.min_max.min_max_fc(disp_op, server=server_type) + + workflow = dpf.core.Workflow(server=server_type) + + workflow.add_operators([disp_op, min_max_fc_op]) + + workflow.set_input_name("data_sources", disp_op.inputs.data_sources) + workflow.set_output_name("min", min_max_fc_op.outputs.field_min) + workflow.set_output_name("max", min_max_fc_op.outputs.field_max) + + return workflow + + +workflows = { + "workflow_forward": workflow_forward, + "workflow_forward_5": workflow_forward_5, + "workflow_disp_min_max": workflow_disp_min_max, +} +workflow_topologies = { + "workflow_forward": { + "operators": 3, + "operator_connections": 1, + "data_connections": 1, + "exposed_inputs": 1, + "exposed_outputs": 2, + }, + "workflow_forward_5": { + "operators": 1, + "operator_connections": 0, + "data_connections": 0, + "exposed_inputs": 5, + "exposed_outputs": 5, + }, + "workflow_disp_min_max": { + "operators": 2, + "operator_connections": 1, + "data_connections": 0, + "exposed_inputs": 1, + "exposed_outputs": 2, + }, +} + + +@pytest.fixture( + params=list(workflows.values()), + ids=list(workflows.keys()), +) +def workflow(server_type, request) -> dpf.core.Workflow: + wf = request.param(server_type) + wf.name = list(workflows.keys())[request.param_index] + return wf + + +@pytest.fixture() +def expected_workflow_topology(workflow): + return workflow_topologies[workflow.name] + + +def test_workflow_get_topology(workflow): + workflow_topology = workflow.get_topology() + + assert workflow_topology + + +def test_workflow_topology_sizes(workflow, expected_workflow_topology): + workflow_topology = workflow.get_topology() + + assert len(workflow_topology.operators) == expected_workflow_topology["operators"] + assert ( + len(workflow_topology.operator_connections) + == expected_workflow_topology["operator_connections"] + ) + assert len(workflow_topology.data_connections) == expected_workflow_topology["data_connections"] + assert len(workflow_topology.exposed_inputs) == expected_workflow_topology["exposed_inputs"] + assert len(workflow_topology.exposed_outputs) == expected_workflow_topology["exposed_outputs"] From 2078cd06360a5d6c3efd4539be1e80c13179897b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matt=C3=A9o=20Baussart?= Date: Tue, 26 Nov 2024 15:56:45 +0100 Subject: [PATCH 04/24] fix: add version conditions on new tests --- tests/test_workflow_topology.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/test_workflow_topology.py b/tests/test_workflow_topology.py index f384acef9d9..8a819b88a4e 100644 --- a/tests/test_workflow_topology.py +++ b/tests/test_workflow_topology.py @@ -24,6 +24,7 @@ from ansys import dpf import ansys.dpf.core.operators as op +import conftest def test_instantiate_workflow_to_workflow_topology_op(server_type): @@ -163,12 +164,20 @@ def expected_workflow_topology(workflow): return workflow_topologies[workflow.name] +@pytest.mark.skipif( + not conftest.SERVERS_VERSION_GREATER_THAN_OR_EQUAL_TO_10_0, + reason="any does not support operator below 8.0", +) def test_workflow_get_topology(workflow): workflow_topology = workflow.get_topology() assert workflow_topology +@pytest.mark.skipif( + not conftest.SERVERS_VERSION_GREATER_THAN_OR_EQUAL_TO_10_0, + reason="any does not support operator below 8.0", +) def test_workflow_topology_sizes(workflow, expected_workflow_topology): workflow_topology = workflow.get_topology() From 46c79f87bd598e755349c6a34f1a1549ceedf10d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matt=C3=A9o=20Baussart?= Date: Tue, 26 Nov 2024 16:09:12 +0100 Subject: [PATCH 05/24] fix: !! (I forgot one) --- src/ansys/dpf/core/workflow.py | 1 - tests/test_workflow_topology.py | 20 ++++++++++++-------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/ansys/dpf/core/workflow.py b/src/ansys/dpf/core/workflow.py index 3f4e2525744..a75fa5297dd 100644 --- a/src/ansys/dpf/core/workflow.py +++ b/src/ansys/dpf/core/workflow.py @@ -43,7 +43,6 @@ server_meet_version_and_raise, ) from ansys.dpf.core import server as server_module -from ansys.dpf.core.workflow_topology.workflow_topology import WorkflowTopology from ansys.dpf.gate import ( workflow_abstract_api, workflow_grpcapi, diff --git a/tests/test_workflow_topology.py b/tests/test_workflow_topology.py index 8a819b88a4e..b6a7998ba41 100644 --- a/tests/test_workflow_topology.py +++ b/tests/test_workflow_topology.py @@ -27,14 +27,6 @@ import conftest -def test_instantiate_workflow_to_workflow_topology_op(server_type): - workflow_to_workflow_topology_op = dpf.core.Operator( - "workflow_to_workflow_topology", server=server_type - ) - - assert workflow_to_workflow_topology_op - - def workflow_forward(server_type) -> dpf.core.Workflow: """ ┌─────────┐ ┌────────────┐ ┌────────────┐ ┌──────────┐ @@ -164,6 +156,18 @@ def expected_workflow_topology(workflow): return workflow_topologies[workflow.name] +@pytest.mark.skipif( + not conftest.SERVERS_VERSION_GREATER_THAN_OR_EQUAL_TO_10_0, + reason="any does not support operator below 8.0", +) +def test_instantiate_workflow_to_workflow_topology_op(server_type): + workflow_to_workflow_topology_op = dpf.core.Operator( + "workflow_to_workflow_topology", server=server_type + ) + + assert workflow_to_workflow_topology_op + + @pytest.mark.skipif( not conftest.SERVERS_VERSION_GREATER_THAN_OR_EQUAL_TO_10_0, reason="any does not support operator below 8.0", From 9880d99bd4873df4a1e6fdf7c4adbfe8df73802e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matt=C3=A9o=20Baussart?= Date: Wed, 27 Nov 2024 10:36:23 +0100 Subject: [PATCH 06/24] feat: add test for WorkflowTopology.__str__() --- tests/test_workflow_topology.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/tests/test_workflow_topology.py b/tests/test_workflow_topology.py index b6a7998ba41..8ff9a11d894 100644 --- a/tests/test_workflow_topology.py +++ b/tests/test_workflow_topology.py @@ -158,7 +158,7 @@ def expected_workflow_topology(workflow): @pytest.mark.skipif( not conftest.SERVERS_VERSION_GREATER_THAN_OR_EQUAL_TO_10_0, - reason="any does not support operator below 8.0", + reason="Operator `workflow_to_workflow_topology` does not exist below 10.0", ) def test_instantiate_workflow_to_workflow_topology_op(server_type): workflow_to_workflow_topology_op = dpf.core.Operator( @@ -170,7 +170,7 @@ def test_instantiate_workflow_to_workflow_topology_op(server_type): @pytest.mark.skipif( not conftest.SERVERS_VERSION_GREATER_THAN_OR_EQUAL_TO_10_0, - reason="any does not support operator below 8.0", + reason="Operator `workflow_to_workflow_topology` does not exist below 10.0", ) def test_workflow_get_topology(workflow): workflow_topology = workflow.get_topology() @@ -180,7 +180,7 @@ def test_workflow_get_topology(workflow): @pytest.mark.skipif( not conftest.SERVERS_VERSION_GREATER_THAN_OR_EQUAL_TO_10_0, - reason="any does not support operator below 8.0", + reason="Operator `workflow_to_workflow_topology` does not exist below 10.0", ) def test_workflow_topology_sizes(workflow, expected_workflow_topology): workflow_topology = workflow.get_topology() @@ -193,3 +193,14 @@ def test_workflow_topology_sizes(workflow, expected_workflow_topology): assert len(workflow_topology.data_connections) == expected_workflow_topology["data_connections"] assert len(workflow_topology.exposed_inputs) == expected_workflow_topology["exposed_inputs"] assert len(workflow_topology.exposed_outputs) == expected_workflow_topology["exposed_outputs"] + + +@pytest.mark.skipif( + not conftest.SERVERS_VERSION_GREATER_THAN_OR_EQUAL_TO_10_0, + reason="Operator `workflow_to_workflow_topology` does not exist below 10.0", +) +def test_workflow_topology_str(workflow): + workflow_topology = workflow.get_topology() + + # We only check that it does not raise + assert str(workflow_topology) From 87aacb8d7a37a0f5937f3cf198e548e9d684f9af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matt=C3=A9o=20Baussart?= Date: Wed, 27 Nov 2024 10:54:14 +0100 Subject: [PATCH 07/24] fix: remove leftover code --- src/ansys/dpf/core/workflow_topology/exposed_pin.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/ansys/dpf/core/workflow_topology/exposed_pin.py b/src/ansys/dpf/core/workflow_topology/exposed_pin.py index c30665fe522..efadaf27b3d 100644 --- a/src/ansys/dpf/core/workflow_topology/exposed_pin.py +++ b/src/ansys/dpf/core/workflow_topology/exposed_pin.py @@ -67,9 +67,6 @@ def __str__(self): f"{indent(self.pin_id, indents)}" ) - def __repr__(self) -> str: - return self.__str__() - class ExposedPinsCollection: def __init__(self, collection): From 4dfcee50b119c8391dc9371a8b771db9de05dd97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matt=C3=A9o=20Baussart?= Date: Fri, 29 Nov 2024 15:02:09 +0100 Subject: [PATCH 08/24] fix: add docstrings and typehinting --- src/ansys/dpf/core/custom_container_base.py | 15 ++ src/ansys/dpf/core/helpers/utils.py | 27 +++- src/ansys/dpf/core/workflow.py | 13 +- .../core/workflow_topology/data_connection.py | 133 ++++++++++++++-- .../dpf/core/workflow_topology/exposed_pin.py | 133 ++++++++++++++-- .../workflow_topology/operator_connection.py | 147 +++++++++++++++--- .../workflow_topology/workflow_topology.py | 102 +++++++++--- 7 files changed, 497 insertions(+), 73 deletions(-) diff --git a/src/ansys/dpf/core/custom_container_base.py b/src/ansys/dpf/core/custom_container_base.py index 282bea71c7c..580ddcf15c9 100644 --- a/src/ansys/dpf/core/custom_container_base.py +++ b/src/ansys/dpf/core/custom_container_base.py @@ -24,5 +24,20 @@ class CustomContainerBase: + """ + Base class for custom container wrappers. + + This class provides a common interface for managing an underlying + `GenericDataContainer` object. + """ + def __init__(self, container: GenericDataContainer) -> None: + """ + Initialize the base container with a `GenericDataContainer`. + + Parameters + ---------- + container : GenericDataContainer + The underlying data container to be wrapped by this class. + """ self._container = container diff --git a/src/ansys/dpf/core/helpers/utils.py b/src/ansys/dpf/core/helpers/utils.py index e1708e7e1d3..4d1497a99b2 100644 --- a/src/ansys/dpf/core/helpers/utils.py +++ b/src/ansys/dpf/core/helpers/utils.py @@ -22,6 +22,7 @@ import inspect import sys +from typing import Any, Optional def _sort_supported_kwargs(bound_method, **kwargs): @@ -50,7 +51,31 @@ def _sort_supported_kwargs(bound_method, **kwargs): return kwargs_in -def indent(text, subsequent_indent="", initial_indent=None): +def indent(text: Any, subsequent_indent: str = "", initial_indent: Optional[str] = None) -> str: + """ + Indents each line of a given text. + + Parameters: + ---------- + text : Any + The input text to be indented. If it is not already a string, it will be converted to one. + subsequent_indent : str, optional + The string to prefix all lines of the text after the first line. Default is an empty string. + initial_indent : Optional[str], optional + The string to prefix the first line of the text. If not provided, `subsequent_indent` will be used. + + Returns: + ------- + str + The indented text with specified prefixes applied to each line. + + Example: + -------- + >>> text = "Hello\nWorld" + >>> print(indent(text, subsequent_indent=" ", initial_indent="--> ")) + --> Hello + World + """ if initial_indent is None: initial_indent = subsequent_indent diff --git a/src/ansys/dpf/core/workflow.py b/src/ansys/dpf/core/workflow.py index a75fa5297dd..ec58ead101f 100644 --- a/src/ansys/dpf/core/workflow.py +++ b/src/ansys/dpf/core/workflow.py @@ -53,6 +53,7 @@ object_handler, integral_types, ) +from ansys.dpf.core.workflow_topology import workflow_topology LOG = logging.getLogger(__name__) LOG.setLevel("DEBUG") @@ -953,14 +954,20 @@ def to_graphviz(self, path: Union[os.PathLike, str]): """Saves the workflow to a GraphViz file.""" return self._api.work_flow_export_graphviz(self, str(path)) - def get_topology(self): + def get_topology(self) -> workflow_topology.WorkflowTopology: + """Get the topology of the workflow. + + Returns + ---------- + workflow_topology : workflowTopology.WorkflowTopology + """ workflow_to_workflow_topology_op = dpf_operator.Operator( "workflow_to_workflow_topology", server=self._server ) workflow_to_workflow_topology_op.inputs.workflow.connect(self) - workflow_topology_container = workflow_to_workflow_topology_op.outputs.workflow_topology() + workflow_topology = workflow_to_workflow_topology_op.outputs.workflow_topology() - return workflow_topology_container + return workflow_topology def __del__(self): try: diff --git a/src/ansys/dpf/core/workflow_topology/data_connection.py b/src/ansys/dpf/core/workflow_topology/data_connection.py index a02908c48ad..aad8a92bd56 100644 --- a/src/ansys/dpf/core/workflow_topology/data_connection.py +++ b/src/ansys/dpf/core/workflow_topology/data_connection.py @@ -20,40 +20,91 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +from typing import Any, Iterator, Optional +from ansys.dpf.core import GenericDataContainersCollection from ansys.dpf.core.custom_container_base import CustomContainerBase +from ansys.dpf.core.dpf_operator import Operator +from ansys.dpf.core.generic_data_container import GenericDataContainer class DataConnection(CustomContainerBase): - def __init__(self, container): + """ + Represents a connection between a data and an operator in a workflow. + + This class provides access to the source data and target operator, as well as its pin ID. + """ + + def __init__(self, container: GenericDataContainer) -> None: + """ + Initialize an DataConnection object. + + Parameters + ---------- + container : GenericDataContainer + The underlying data container that holds the connection's information. + """ super().__init__(container) - self._source_data = None - self._target_operator = None - self._target_pin_id = None + + self._source_data: Optional[Any] = None + self._target_operator: Optional[Operator] = None + self._target_pin_id: Optional[int] = None @property - def source_data(self): + def source_data(self) -> Any: + """ + Retrieve the source data of the connection. + + Returns + ------- + Any + The data serving as the source of this connection. + """ if self._source_data is None: self._source_data = self._container.get_property("source_data") return self._source_data @property - def target_operator(self): - from ansys.dpf.core.dpf_operator import Operator - + def target_operator(self) -> Operator: + """ + Retrieve the target operator of the connection. + + Returns + ------- + Operator + The operator serving as the target of this connection. + """ if self._target_operator is None: self._target_operator = self._container.get_property("target_operator", Operator) return self._target_operator @property - def target_pin_id(self): + def target_pin_id(self) -> int: + """ + Retrieve the pin ID of the target operator. + + Returns + ------- + int + The pin ID of the target operator. + """ if self._target_pin_id is None: self._target_pin_id = self._container.get_property("target_pin_id", int) return self._target_pin_id - def __str__(self): + def __str__(self) -> str: + """ + Return a string representation of the data connection. + + This includes the source data and target operator, with its pin ID. + + Returns + ------- + str + String representation of the data connection. + """ from ansys.dpf.core.helpers.utils import indent indents = " " @@ -69,20 +120,72 @@ def __str__(self): class DataConnectionsCollection: - def __init__(self, collection): + """ + Represents a collection of data connections in a workflow. + + This class provides iterable access to all data connections, allowing retrieval + of individual connections or iteration through the entire collection. + """ + + def __init__(self, collection: GenericDataContainersCollection) -> None: + """ + Initialize an DataConnectionsCollection object. + + Parameters + ---------- + collection : GenericDataContainersCollection + The underlying collection of data connections. + """ self._collection = collection - def __len__(self): + def __len__(self) -> int: + """ + Return the number of data connections in the collection. + + Returns + ------- + int + The number of data connections. + """ return len(self._collection) - def __getitem__(self, index): + def __getitem__(self, index: int) -> DataConnection: + """ + Retrieve a data connection by its index. + + Parameters + ---------- + index : int + The index of the data connection to retrieve. + + Returns + ------- + DataConnection + The data connection at the specified index. + """ return DataConnection(self._collection[index]) - def __iter__(self): + def __iter__(self) -> Iterator[DataConnection]: + """ + Iterate over the data connections in the collection. + + Yields + ------ + DataConnection + The next data connection in the collection. + """ for i in range(len(self)): yield self[i] - def __str__(self): + def __str__(self) -> str: + """ + Return a string representation of the data connections collection. + + Returns + ------- + str + String representation of the collection. + """ from ansys.dpf.core.helpers.utils import indent indents = (" ", " - ") diff --git a/src/ansys/dpf/core/workflow_topology/exposed_pin.py b/src/ansys/dpf/core/workflow_topology/exposed_pin.py index efadaf27b3d..fc8f1b0b8db 100644 --- a/src/ansys/dpf/core/workflow_topology/exposed_pin.py +++ b/src/ansys/dpf/core/workflow_topology/exposed_pin.py @@ -20,40 +20,91 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +from typing import Iterator, Optional +from ansys.dpf.core import GenericDataContainersCollection from ansys.dpf.core.custom_container_base import CustomContainerBase +from ansys.dpf.core.dpf_operator import Operator +from ansys.dpf.core.generic_data_container import GenericDataContainer class ExposedPin(CustomContainerBase): - def __init__(self, container): + """ + Represents an exposed input or output pin in a workflow. + + This class provides access to the name and the associated operator, as well as its pin ID. + """ + + def __init__(self, container: GenericDataContainer) -> None: + """ + Initialize an ExposedPin object. + + Parameters + ---------- + container : GenericDataContainer + The underlying data container that holds the exposed pin's information. + """ super().__init__(container) - self._name = None - self._operator = None - self._pin_id = None + + self._name: Optional[str] = None + self._operator: Optional[Operator] = None + self._pin_id: Optional[int] = None @property - def name(self): + def name(self) -> str: + """ + Retrieve the name of the exposed pin. + + Returns + ------- + str + The name of the exposed pin. + """ if self._name is None: self._name = self._container.get_property("name", str) return self._name @property - def operator(self): - from ansys.dpf.core.dpf_operator import Operator - + def operator(self) -> Operator: + """ + Retrieve the operator associated with the exposed pin. + + Returns + ------- + Operator + The operator associated with this exposed pin. + """ if self._operator is None: self._operator = self._container.get_property("operator", Operator) return self._operator @property - def pin_id(self): + def pin_id(self) -> int: + """ + Retrieve the pin ID of the operator. + + Returns + ------- + int + The pin ID of the operator. + """ if self._pin_id is None: self._pin_id = self._container.get_property("pin_id", int) return self._pin_id - def __str__(self): + def __str__(self) -> str: + """ + Return a string representation of the exposed pin. + + This includes the name and associated operator, with its pin ID. + + Returns + ------- + str + String representation of the exposed pin. + """ from ansys.dpf.core.helpers.utils import indent indents = " " @@ -69,20 +120,72 @@ def __str__(self): class ExposedPinsCollection: - def __init__(self, collection): + """ + Represents a collection of exposed pins in a workflow. + + This class provides iterable access to all exposed pins, allowing retrieval + of individual exposed pins or iteration through the entire collection. + """ + + def __init__(self, collection: GenericDataContainersCollection) -> None: + """ + Initialize an ExposedPinsCollection object. + + Parameters + ---------- + collection : GenericDataContainersCollection + The underlying collection of exposed pins. + """ self._collection = collection - def __len__(self): + def __len__(self) -> int: + """ + Return the number of exposed pins in the collection. + + Returns + ------- + int + The number of exposed pins. + """ return len(self._collection) - def __getitem__(self, index): + def __getitem__(self, index: int) -> ExposedPin: + """ + Retrieve an exposed pin by its index. + + Parameters + ---------- + index : int + The index of the exposed pin to retrieve. + + Returns + ------- + ExposedPin + The exposed pin at the specified index. + """ return ExposedPin(self._collection[index]) - def __iter__(self): + def __iter__(self) -> Iterator[ExposedPin]: + """ + Iterate over the exposed pins in the collection. + + Yields + ------ + ExposedPin + The next exposed pin in the collection. + """ for i in range(len(self)): yield self[i] - def __str__(self): + def __str__(self) -> str: + """ + Return a string representation of the exposed pins collection. + + Returns + ------- + str + String representation of the collection. + """ from ansys.dpf.core.helpers.utils import indent indents = (" ", " - ") diff --git a/src/ansys/dpf/core/workflow_topology/operator_connection.py b/src/ansys/dpf/core/workflow_topology/operator_connection.py index 638866b9d65..76fb8753675 100644 --- a/src/ansys/dpf/core/workflow_topology/operator_connection.py +++ b/src/ansys/dpf/core/workflow_topology/operator_connection.py @@ -20,50 +20,107 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +from typing import Iterator, Optional +from ansys.dpf.core import GenericDataContainersCollection from ansys.dpf.core.custom_container_base import CustomContainerBase +from ansys.dpf.core.dpf_operator import Operator +from ansys.dpf.core.generic_data_container import GenericDataContainer class OperatorConnection(CustomContainerBase): - def __init__(self, container): + """ + Represents a connection between two operators in a workflow. + + This class provides access to the source and target operators, as well as their respective pin IDs. + """ + + def __init__(self, container: GenericDataContainer) -> None: + """ + Initialize an OperatorConnection object. + + Parameters + ---------- + container : GenericDataContainer + The underlying data container that holds the connection's information. + """ super().__init__(container) - self._source_operator = None - self._source_pin_id = None - self._target_operator = None - self._target_pin_id = None - @property - def source_operator(self): - from ansys.dpf.core.dpf_operator import Operator + self._source_operator: Optional[Operator] = None + self._source_pin_id: Optional[int] = None + self._target_operator: Optional[Operator] = None + self._target_pin_id: Optional[int] = None + @property + def source_operator(self) -> Operator: + """ + Retrieve the source operator of the connection. + + Returns + ------- + Operator + The operator serving as the source of this connection. + """ if self._source_operator is None: self._source_operator = self._container.get_property("source_operator", Operator) return self._source_operator @property - def source_pin_id(self): + def source_pin_id(self) -> int: + """ + Retrieve the pin ID of the source operator. + + Returns + ------- + int + The pin ID of the source operator. + """ if self._source_pin_id is None: self._source_pin_id = self._container.get_property("source_pin_id", int) return self._source_pin_id @property - def target_operator(self): - from ansys.dpf.core.dpf_operator import Operator - + def target_operator(self) -> Operator: + """ + Retrieve the target operator of the connection. + + Returns + ------- + Operator + The operator serving as the target of this connection. + """ if self._target_operator is None: self._target_operator = self._container.get_property("target_operator", Operator) return self._target_operator @property - def target_pin_id(self): + def target_pin_id(self) -> int: + """ + Retrieve the pin ID of the target operator. + + Returns + ------- + int + The pin ID of the target operator. + """ if self._target_pin_id is None: self._target_pin_id = self._container.get_property("target_pin_id", int) return self._target_pin_id - def __str__(self): + def __str__(self) -> str: + """ + Return a string representation of the operator connection. + + This includes the source and target operators and their respective pin IDs. + + Returns + ------- + str + String representation of the operator connection. + """ from ansys.dpf.core.helpers.utils import indent indents = " " @@ -81,20 +138,72 @@ def __str__(self): class OperatorConnectionsCollection: - def __init__(self, collection): + """ + Represents a collection of operator connections in a workflow. + + This class provides iterable access to all operator connections, allowing retrieval + of individual connections or iteration through the entire collection. + """ + + def __init__(self, collection: GenericDataContainersCollection) -> None: + """ + Initialize an OperatorConnectionsCollection object. + + Parameters + ---------- + collection : GenericDataContainersCollection + The underlying collection of operator connections. + """ self._collection = collection - def __len__(self): + def __len__(self) -> int: + """ + Return the number of operator connections in the collection. + + Returns + ------- + int + The number of operator connections. + """ return len(self._collection) - def __getitem__(self, index): + def __getitem__(self, index: int) -> OperatorConnection: + """ + Retrieve an operator connection by its index. + + Parameters + ---------- + index : int + The index of the operator connection to retrieve. + + Returns + ------- + OperatorConnection + The operator connection at the specified index. + """ return OperatorConnection(self._collection[index]) - def __iter__(self): + def __iter__(self) -> Iterator[OperatorConnection]: + """ + Iterate over the operator connections in the collection. + + Yields + ------ + OperatorConnection + The next operator connection in the collection. + """ for i in range(len(self)): yield self[i] - def __str__(self): + def __str__(self) -> str: + """ + Return a string representation of the operator connections collection. + + Returns + ------- + str + String representation of the collection. + """ from ansys.dpf.core.helpers.utils import indent indents = (" ", " - ") diff --git a/src/ansys/dpf/core/workflow_topology/workflow_topology.py b/src/ansys/dpf/core/workflow_topology/workflow_topology.py index 99cfecd3df1..55bae9a50d7 100644 --- a/src/ansys/dpf/core/workflow_topology/workflow_topology.py +++ b/src/ansys/dpf/core/workflow_topology/workflow_topology.py @@ -20,21 +20,47 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +from typing import Optional +from ansys.dpf.core import OperatorsCollection from ansys.dpf.core.custom_container_base import CustomContainerBase +from ansys.dpf.core.generic_data_container import GenericDataContainer +from ansys.dpf.core.workflow_topology.data_connection import DataConnectionsCollection +from ansys.dpf.core.workflow_topology.exposed_pin import ExposedPinsCollection +from ansys.dpf.core.workflow_topology.operator_connection import OperatorConnectionsCollection class WorkflowTopology(CustomContainerBase): - def __init__(self, *args) -> None: - super().__init__(*args) - - self._operators = None - self._operator_connections = None - self._data_connections = None - self._exposed_inputs = None - self._exposed_outputs = None + """ + Represents the topology of a workflow, including its operators, connections, and exposed input/output pins. + """ + + def __init__(self, container: GenericDataContainer) -> None: + """ + Initialize a WorkflowTopology object. + + Parameters + ---------- + container : GenericDataContainer + The underlying data container that holds the workflow topology information. + """ + super().__init__(container) + + self._operators: Optional[OperatorsCollection] = None + self._operator_connections: Optional[OperatorConnectionsCollection] = None + self._data_connections: Optional[DataConnectionsCollection] = None + self._exposed_inputs: Optional[ExposedPinsCollection] = None + self._exposed_outputs: Optional[ExposedPinsCollection] = None @property - def operators(self): + def operators(self) -> OperatorsCollection: + """ + Retrieve the operators in the workflow. + + Returns + ------- + OperatorsCollection + A collection of all the operators in the workflow. + """ from ansys.dpf.core import OperatorsCollection if self._operators is None: @@ -43,11 +69,16 @@ def operators(self): return self._operators @property - def operator_connections(self): + def operator_connections(self) -> OperatorConnectionsCollection: + """ + Retrieve the operator connections in the workflow. + + Returns + ------- + OperatorConnectionsCollection + A collection of all the operator connections in the workflow. + """ from ansys.dpf.core import GenericDataContainersCollection - from ansys.dpf.core.workflow_topology.operator_connection import ( - OperatorConnectionsCollection, - ) if self._operator_connections is None: self._operator_connections = OperatorConnectionsCollection( @@ -59,9 +90,16 @@ def operator_connections(self): return self._operator_connections @property - def data_connections(self): + def data_connections(self) -> DataConnectionsCollection: + """ + Retrieve the data connections in the workflow. + + Returns + ------- + OperatorConnectionsCollection + A collection of all the data connections in the workflow. + """ from ansys.dpf.core import GenericDataContainersCollection - from ansys.dpf.core.workflow_topology.data_connection import DataConnectionsCollection if self._data_connections is None: self._data_connections = DataConnectionsCollection( @@ -71,9 +109,16 @@ def data_connections(self): return self._data_connections @property - def exposed_inputs(self): + def exposed_inputs(self) -> ExposedPinsCollection: + """ + Retrieve the exposed inputs in the workflow. + + Returns + ------- + ExposedPinsCollection + A collection of all the exposed inputs in the workflow. + """ from ansys.dpf.core import GenericDataContainersCollection - from ansys.dpf.core.workflow_topology.exposed_pin import ExposedPinsCollection if self._exposed_inputs is None: self._exposed_inputs = ExposedPinsCollection( @@ -83,9 +128,16 @@ def exposed_inputs(self): return self._exposed_inputs @property - def exposed_outputs(self): + def exposed_outputs(self) -> ExposedPinsCollection: + """ + Retrieve the exposed outputs in the workflow. + + Returns + ------- + ExposedPinsCollection + A collection of all the exposed outputs in the workflow. + """ from ansys.dpf.core import GenericDataContainersCollection - from ansys.dpf.core.workflow_topology.exposed_pin import ExposedPinsCollection if self._exposed_outputs is None: self._exposed_outputs = ExposedPinsCollection( @@ -94,7 +146,17 @@ def exposed_outputs(self): return self._exposed_outputs - def __str__(self): + def __str__(self) -> str: + """ + Return a string representation of the workflow topology. + + The string provides details about the workflow's operators, connections, and exposed pins. + + Returns + ------- + str + String representation of the workflow topology. + """ from ansys.dpf.core.helpers.utils import indent def indent_operators(operators): From 2bc8fcd3923b93dce8a1a3e7b8f85c05cd9c5926 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matt=C3=A9o=20Baussart?= Date: Fri, 29 Nov 2024 16:10:35 +0100 Subject: [PATCH 09/24] feat: add titles and descriptions --- src/ansys/dpf/core/custom_container_base.py | 11 +++++++++++ .../dpf/core/workflow_topology/data_connection.py | 8 ++++++++ src/ansys/dpf/core/workflow_topology/exposed_pin.py | 9 +++++++++ .../dpf/core/workflow_topology/workflow_topology.py | 8 ++++++++ 4 files changed, 36 insertions(+) diff --git a/src/ansys/dpf/core/custom_container_base.py b/src/ansys/dpf/core/custom_container_base.py index 580ddcf15c9..44df0131449 100644 --- a/src/ansys/dpf/core/custom_container_base.py +++ b/src/ansys/dpf/core/custom_container_base.py @@ -20,6 +20,17 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +""" +CustomContainerBase +=================== +This module contains the `CustomContainerBase` class, which serves as a base +for creating wrappers around `GenericDataContainer` objects. + +These wrappers provide an interface for accessing and managing data in +generic containers, enabling more intuitive usage and the addition of custom +behaviors tailored to specific use cases. +""" + from ansys.dpf.core.generic_data_container import GenericDataContainer diff --git a/src/ansys/dpf/core/workflow_topology/data_connection.py b/src/ansys/dpf/core/workflow_topology/data_connection.py index aad8a92bd56..5ccf6e9246d 100644 --- a/src/ansys/dpf/core/workflow_topology/data_connection.py +++ b/src/ansys/dpf/core/workflow_topology/data_connection.py @@ -20,6 +20,14 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +""" +DataConnection +============== +This module contains the `DataConnection` and `DataConnectionsCollection` +classes, which represent individual connections between data and operator, +and a collection of such connections within a workflow, respectively. +""" + from typing import Any, Iterator, Optional from ansys.dpf.core import GenericDataContainersCollection from ansys.dpf.core.custom_container_base import CustomContainerBase diff --git a/src/ansys/dpf/core/workflow_topology/exposed_pin.py b/src/ansys/dpf/core/workflow_topology/exposed_pin.py index fc8f1b0b8db..761730a657e 100644 --- a/src/ansys/dpf/core/workflow_topology/exposed_pin.py +++ b/src/ansys/dpf/core/workflow_topology/exposed_pin.py @@ -20,6 +20,15 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +""" +ExposedPin +========== +This module contains the `ExposedPin` and `ExposedPinsCollection` classes, +which represent individual exposed pins and a collection of exposed pins in a workflow, +respectively. These classes enable easy access to the pins that serve as input/output points +for the workflow. +""" + from typing import Iterator, Optional from ansys.dpf.core import GenericDataContainersCollection from ansys.dpf.core.custom_container_base import CustomContainerBase diff --git a/src/ansys/dpf/core/workflow_topology/workflow_topology.py b/src/ansys/dpf/core/workflow_topology/workflow_topology.py index 55bae9a50d7..1b1d1ad8c90 100644 --- a/src/ansys/dpf/core/workflow_topology/workflow_topology.py +++ b/src/ansys/dpf/core/workflow_topology/workflow_topology.py @@ -20,6 +20,14 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +""" +WorkflowTopology +================ +This module contains the `WorkflowTopology` class, which represents +the structure and relationships within a workflow, including its operators, +connections, and exposed input/output pins. +""" + from typing import Optional from ansys.dpf.core import OperatorsCollection from ansys.dpf.core.custom_container_base import CustomContainerBase From 0e59baf7cb71a11893859104cadee7f07e12ebc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matt=C3=A9o=20Baussart?= Date: Fri, 29 Nov 2024 16:14:06 +0100 Subject: [PATCH 10/24] fix: add missing title and description for OperatorConnection --- .../dpf/core/workflow_topology/operator_connection.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/ansys/dpf/core/workflow_topology/operator_connection.py b/src/ansys/dpf/core/workflow_topology/operator_connection.py index 76fb8753675..09891ff8da1 100644 --- a/src/ansys/dpf/core/workflow_topology/operator_connection.py +++ b/src/ansys/dpf/core/workflow_topology/operator_connection.py @@ -20,6 +20,14 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +""" +OperatorConnection +================== +This module contains the `OperatorConnection` and `OperatorConnectionsCollection` +classes, which represent individual connections between operators and a +collection of such connections within a workflow, respectively. +""" + from typing import Iterator, Optional from ansys.dpf.core import GenericDataContainersCollection from ansys.dpf.core.custom_container_base import CustomContainerBase From 09abcd7afc709085a25d06bee3ddd84e3196aa84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matt=C3=A9o=20Baussart?= Date: Fri, 29 Nov 2024 16:42:50 +0100 Subject: [PATCH 11/24] fix: fix circular dependence --- src/ansys/dpf/core/workflow.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/ansys/dpf/core/workflow.py b/src/ansys/dpf/core/workflow.py index ec58ead101f..ad7c37241b4 100644 --- a/src/ansys/dpf/core/workflow.py +++ b/src/ansys/dpf/core/workflow.py @@ -53,7 +53,6 @@ object_handler, integral_types, ) -from ansys.dpf.core.workflow_topology import workflow_topology LOG = logging.getLogger(__name__) LOG.setLevel("DEBUG") @@ -954,7 +953,7 @@ def to_graphviz(self, path: Union[os.PathLike, str]): """Saves the workflow to a GraphViz file.""" return self._api.work_flow_export_graphviz(self, str(path)) - def get_topology(self) -> workflow_topology.WorkflowTopology: + def get_topology(self): """Get the topology of the workflow. Returns From 596278861dd499a40e81c9b94856594c2d865157 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matt=C3=A9o=20Baussart?= Date: Mon, 2 Dec 2024 12:33:28 +0100 Subject: [PATCH 12/24] feat: add get_output with CustomContainerBase and derived class --- src/ansys/dpf/core/common.py | 12 ++++++++++++ src/ansys/dpf/core/dpf_operator.py | 11 ++++++++--- src/ansys/dpf/core/outputs.py | 10 ++++------ src/ansys/dpf/core/workflow.py | 10 ++++++++++ 4 files changed, 34 insertions(+), 9 deletions(-) diff --git a/src/ansys/dpf/core/common.py b/src/ansys/dpf/core/common.py index 1c13185b643..6257d4420a4 100644 --- a/src/ansys/dpf/core/common.py +++ b/src/ansys/dpf/core/common.py @@ -430,6 +430,18 @@ def type_to_special_dpf_constructors(): return _type_to_special_dpf_constructors +_derived_class_name_to_type = None + + +def derived_class_name_to_type(): + global _derived_class_name_to_type + if _derived_class_name_to_type is None: + from ansys.dpf.core.workflow_topology.workflow_topology import WorkflowTopology + + _derived_class_name_to_type = {"WorkflowTopology": WorkflowTopology} + return _derived_class_name_to_type + + def create_dpf_instance(type, internal_obj, server): spe_constructors = type_to_special_dpf_constructors() if type in spe_constructors: diff --git a/src/ansys/dpf/core/dpf_operator.py b/src/ansys/dpf/core/dpf_operator.py index 1bb84edef85..9e83d09445f 100644 --- a/src/ansys/dpf/core/dpf_operator.py +++ b/src/ansys/dpf/core/dpf_operator.py @@ -386,6 +386,7 @@ def _type_to_output_method(self): any, ) from ansys.dpf.core.workflow_topology import workflow_topology + from ansys.dpf.core.custom_container_base import CustomContainerBase out = [ (any.Any, self._api.operator_getoutput_as_any), @@ -483,9 +484,13 @@ def _type_to_output_method(self): lambda obj, type: any.Any(server=self._server, any_dpf=obj).cast(type), ), ( - workflow_topology.WorkflowTopology, - None, - "WorkflowTopology", + CustomContainerBase, + self._api.operator_getoutput_generic_data_container, + lambda obj, type: type( + container=generic_data_container.GenericDataContainer( + generic_data_container=obj, server=self._server + ) + ), ), ] if hasattr(self._api, "operator_getoutput_generic_data_container"): diff --git a/src/ansys/dpf/core/outputs.py b/src/ansys/dpf/core/outputs.py index e7dc9c3da62..dec87843cf2 100644 --- a/src/ansys/dpf/core/outputs.py +++ b/src/ansys/dpf/core/outputs.py @@ -84,12 +84,10 @@ def get_data(self): type_output_derive_class = self._spec.name_derived_class if type_output_derive_class != "": - out_type = [ - type_tuple - for type_tuple in self._operator._type_to_output_method - if type_output_derive_class in type_tuple - ] - return out_type[0][0](self._operator.get_output(self._pin, type_output)) + from ansys.dpf.core.common import derived_class_name_to_type + + out_type = derived_class_name_to_type()[type_output_derive_class] + return out_type(self._operator.get_output(self._pin, type_output)) else: return self._operator.get_output(self._pin, type_output) diff --git a/src/ansys/dpf/core/workflow.py b/src/ansys/dpf/core/workflow.py index ad7c37241b4..0045a0fdba1 100644 --- a/src/ansys/dpf/core/workflow.py +++ b/src/ansys/dpf/core/workflow.py @@ -333,6 +333,7 @@ def _type_to_output_method(self): collection_base, streams_container, ) + from ansys.dpf.core.custom_container_base import CustomContainerBase out = [ (streams_container.StreamsContainer, self._api.work_flow_getoutput_streams), @@ -421,6 +422,15 @@ def _type_to_output_method(self): self._api.work_flow_getoutput_as_any, lambda obj, type: any.Any(server=self._server, any_dpf=obj).cast(type), ), + ( + CustomContainerBase, + self._api.work_flow_getoutput_generic_data_container, + lambda obj, type: type( + container=generic_data_container.GenericDataContainer( + generic_data_container=obj, server=self._server + ) + ), + ), ] if hasattr(self._api, "work_flow_connect_generic_data_container"): out.append( From f9ff71e0ba07efdfcd36987c94a20e0c65500e85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matt=C3=A9o=20Baussart?= Date: Mon, 2 Dec 2024 12:44:54 +0100 Subject: [PATCH 13/24] fix: simplify import of WorkflowTopology --- src/ansys/dpf/core/common.py | 2 +- src/ansys/dpf/core/dpf_operator.py | 5 ++-- src/ansys/dpf/core/workflow.py | 2 +- .../dpf/core/workflow_topology/__init__.py | 26 +++++++++++++++++++ 4 files changed, 30 insertions(+), 5 deletions(-) create mode 100644 src/ansys/dpf/core/workflow_topology/__init__.py diff --git a/src/ansys/dpf/core/common.py b/src/ansys/dpf/core/common.py index 6257d4420a4..fc3d5b574c9 100644 --- a/src/ansys/dpf/core/common.py +++ b/src/ansys/dpf/core/common.py @@ -436,7 +436,7 @@ def type_to_special_dpf_constructors(): def derived_class_name_to_type(): global _derived_class_name_to_type if _derived_class_name_to_type is None: - from ansys.dpf.core.workflow_topology.workflow_topology import WorkflowTopology + from ansys.dpf.core.workflow_topology import WorkflowTopology _derived_class_name_to_type = {"WorkflowTopology": WorkflowTopology} return _derived_class_name_to_type diff --git a/src/ansys/dpf/core/dpf_operator.py b/src/ansys/dpf/core/dpf_operator.py index 9e83d09445f..07800918934 100644 --- a/src/ansys/dpf/core/dpf_operator.py +++ b/src/ansys/dpf/core/dpf_operator.py @@ -384,9 +384,8 @@ def _type_to_output_method(self): mesh_info, collection_base, any, + custom_container_base, ) - from ansys.dpf.core.workflow_topology import workflow_topology - from ansys.dpf.core.custom_container_base import CustomContainerBase out = [ (any.Any, self._api.operator_getoutput_as_any), @@ -484,7 +483,7 @@ def _type_to_output_method(self): lambda obj, type: any.Any(server=self._server, any_dpf=obj).cast(type), ), ( - CustomContainerBase, + custom_container_base.CustomContainerBase, self._api.operator_getoutput_generic_data_container, lambda obj, type: type( container=generic_data_container.GenericDataContainer( diff --git a/src/ansys/dpf/core/workflow.py b/src/ansys/dpf/core/workflow.py index 0045a0fdba1..d8b8b525315 100644 --- a/src/ansys/dpf/core/workflow.py +++ b/src/ansys/dpf/core/workflow.py @@ -968,7 +968,7 @@ def get_topology(self): Returns ---------- - workflow_topology : workflowTopology.WorkflowTopology + workflow_topology : workflow_topology.WorkflowTopology """ workflow_to_workflow_topology_op = dpf_operator.Operator( "workflow_to_workflow_topology", server=self._server diff --git a/src/ansys/dpf/core/workflow_topology/__init__.py b/src/ansys/dpf/core/workflow_topology/__init__.py new file mode 100644 index 00000000000..1b670cd7215 --- /dev/null +++ b/src/ansys/dpf/core/workflow_topology/__init__.py @@ -0,0 +1,26 @@ +# Copyright (C) 2020 - 2024 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. + +from .workflow_topology import WorkflowTopology +from .operator_connection import OperatorConnection +from .data_connection import DataConnection +from .exposed_pin import ExposedPin From 529909b4ab31ade904c42271fd20fffc93aee610 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matt=C3=A9o=20Baussart?= Date: Mon, 2 Dec 2024 13:22:37 +0100 Subject: [PATCH 14/24] feat: add tests for derived class outputs --- tests/test_operator.py | 33 +++++++++++++++++++++++++++++++++ tests/test_workflow.py | 21 +++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/tests/test_operator.py b/tests/test_operator.py index 8cfadd185b7..a92e7c4ad6c 100644 --- a/tests/test_operator.py +++ b/tests/test_operator.py @@ -35,6 +35,7 @@ from ansys.dpf.core import operators as ops from ansys.dpf.core.misc import get_ansys_path from ansys.dpf.core.operator_specification import Specification +from ansys.dpf.core.workflow_topology import WorkflowTopology import conftest from conftest import ( SERVERS_VERSION_GREATER_THAN_OR_EQUAL_TO_3_0, @@ -1424,3 +1425,35 @@ def test_operator_input_output_streams(server_in_process, simple_bar): time_provider.connect(pin=3, inpt=streams) times = time_provider.outputs.time_freq_support() assert times + + +@pytest.mark.skipif( + not conftest.SERVERS_VERSION_GREATER_THAN_OR_EQUAL_TO_10_0, + reason="Operator `workflow_to_workflow_topology` does not exist below 10.0", +) +def test_operator_outputs_derived_class(server_type): + workflow = dpf.core.Workflow(server=server_type) + + workflow_to_workflow_topology_op = dpf.core.Operator( + "workflow_to_workflow_topology", server=server_type + ) + workflow_to_workflow_topology_op.inputs.workflow.connect(workflow) + + workflow_topology = workflow_to_workflow_topology_op.outputs.workflow_topology() + assert workflow_topology + + +@pytest.mark.skipif( + not conftest.SERVERS_VERSION_GREATER_THAN_OR_EQUAL_TO_10_0, + reason="Operator `workflow_to_workflow_topology` does not exist below 10.0", +) +def test_operator_get_output_derived_class(server_type): + workflow = dpf.core.Workflow(server=server_type) + + workflow_to_workflow_topology_op = dpf.core.Operator( + "workflow_to_workflow_topology", server=server_type + ) + workflow_to_workflow_topology_op.inputs.workflow.connect(workflow) + + workflow_topology = workflow_to_workflow_topology_op.get_output(0, WorkflowTopology) + assert workflow_topology diff --git a/tests/test_workflow.py b/tests/test_workflow.py index 5576cbe5552..40cff92ecf5 100644 --- a/tests/test_workflow.py +++ b/tests/test_workflow.py @@ -27,6 +27,7 @@ import platform import ansys.dpf.core.operators as op +from ansys.dpf.core.workflow_topology import WorkflowTopology import conftest from ansys import dpf from ansys.dpf.core import misc @@ -1030,6 +1031,26 @@ def test_workflow_input_output_streams(server_in_process, simple_bar): assert times +@pytest.mark.skipif( + not conftest.SERVERS_VERSION_GREATER_THAN_OR_EQUAL_TO_10_0, + reason="Operator `workflow_to_workflow_topology` does not exist below 10.0", +) +def test_workflow_get_output_derived_class(server_type): + workflow = dpf.core.Workflow(server=server_type) + + workflow_to_workflow_topology_op = dpf.core.Operator( + "workflow_to_workflow_topology", server=server_type + ) + dpf_workflow_wrapper = dpf.core.Workflow(server=server_type) + dpf_workflow_wrapper.add_operator(workflow_to_workflow_topology_op) + dpf_workflow_wrapper.set_input_name("input", workflow_to_workflow_topology_op, 0) + dpf_workflow_wrapper.set_output_name("output", workflow_to_workflow_topology_op, 0) + dpf_workflow_wrapper.connect("input", workflow) + + workflow_topology = dpf_workflow_wrapper.get_output("output", WorkflowTopology) + assert workflow_topology + + def main(): test_connect_field_workflow() velocity_acceleration = conftest.resolve_test_file("velocity_acceleration.rst", "rst_operators") From 0d8d7fc71b5be504e02a2f6d15ac5debbb7d33e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matt=C3=A9o=20Baussart?= Date: Mon, 2 Dec 2024 14:02:14 +0100 Subject: [PATCH 15/24] fix: fix broken derived classes (mesh_info) --- src/ansys/dpf/core/outputs.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/ansys/dpf/core/outputs.py b/src/ansys/dpf/core/outputs.py index dec87843cf2..4109a669bc3 100644 --- a/src/ansys/dpf/core/outputs.py +++ b/src/ansys/dpf/core/outputs.py @@ -81,15 +81,24 @@ def get_data(self): elif type_output == "int32": type_output = types.int + output = self._operator.get_output(self._pin, type_output) + type_output_derive_class = self._spec.name_derived_class + if type_output_derive_class == "": + return output + + from ansys.dpf.core.common import derived_class_name_to_type - if type_output_derive_class != "": - from ansys.dpf.core.common import derived_class_name_to_type + derived_type = derived_class_name_to_type().get(type_output_derive_class) + if derived_type is not None: + return derived_type(output) - out_type = derived_class_name_to_type()[type_output_derive_class] - return out_type(self._operator.get_output(self._pin, type_output)) - else: - return self._operator.get_output(self._pin, type_output) + derived_types = [ + type_tuple + for type_tuple in self._operator._type_to_output_method + if type_output_derive_class in type_tuple + ] + return derived_types[0][0](output) def __call__(self): return self.get_data() From 2ba84755f223bcc2178bd3dde1a0dc12c59df9cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matt=C3=A9o=20Baussart?= Date: Mon, 2 Dec 2024 15:10:19 +0100 Subject: [PATCH 16/24] fix: fix docstring --- src/ansys/dpf/core/helpers/utils.py | 6 +++--- src/ansys/dpf/core/workflow.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ansys/dpf/core/helpers/utils.py b/src/ansys/dpf/core/helpers/utils.py index 4d1497a99b2..f5608980469 100644 --- a/src/ansys/dpf/core/helpers/utils.py +++ b/src/ansys/dpf/core/helpers/utils.py @@ -55,7 +55,7 @@ def indent(text: Any, subsequent_indent: str = "", initial_indent: Optional[str] """ Indents each line of a given text. - Parameters: + Parameters ---------- text : Any The input text to be indented. If it is not already a string, it will be converted to one. @@ -64,12 +64,12 @@ def indent(text: Any, subsequent_indent: str = "", initial_indent: Optional[str] initial_indent : Optional[str], optional The string to prefix the first line of the text. If not provided, `subsequent_indent` will be used. - Returns: + Returns ------- str The indented text with specified prefixes applied to each line. - Example: + Examples -------- >>> text = "Hello\nWorld" >>> print(indent(text, subsequent_indent=" ", initial_indent="--> ")) diff --git a/src/ansys/dpf/core/workflow.py b/src/ansys/dpf/core/workflow.py index d8b8b525315..8d0c681c677 100644 --- a/src/ansys/dpf/core/workflow.py +++ b/src/ansys/dpf/core/workflow.py @@ -967,7 +967,7 @@ def get_topology(self): """Get the topology of the workflow. Returns - ---------- + ------- workflow_topology : workflow_topology.WorkflowTopology """ workflow_to_workflow_topology_op = dpf_operator.Operator( From da603669ddacd092cd3e4098227ad078bf17fe16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matt=C3=A9o=20Baussart?= Date: Mon, 2 Dec 2024 16:20:02 +0100 Subject: [PATCH 17/24] fix: fix docstring escaping --- src/ansys/dpf/core/helpers/utils.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/ansys/dpf/core/helpers/utils.py b/src/ansys/dpf/core/helpers/utils.py index f5608980469..b43986b884b 100644 --- a/src/ansys/dpf/core/helpers/utils.py +++ b/src/ansys/dpf/core/helpers/utils.py @@ -52,8 +52,7 @@ def _sort_supported_kwargs(bound_method, **kwargs): def indent(text: Any, subsequent_indent: str = "", initial_indent: Optional[str] = None) -> str: - """ - Indents each line of a given text. + """Indents each line of a given text. Parameters ---------- @@ -71,7 +70,7 @@ def indent(text: Any, subsequent_indent: str = "", initial_indent: Optional[str] Examples -------- - >>> text = "Hello\nWorld" + >>> text = "Hello\\nWorld" >>> print(indent(text, subsequent_indent=" ", initial_indent="--> ")) --> Hello World From 0b7522d9da63c7a619f21737453dfc30976715c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matt=C3=A9o=20Baussart?= Date: Mon, 2 Dec 2024 17:01:16 +0100 Subject: [PATCH 18/24] fix: remove extra import --- src/ansys/dpf/core/workflow_topology/workflow_topology.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/ansys/dpf/core/workflow_topology/workflow_topology.py b/src/ansys/dpf/core/workflow_topology/workflow_topology.py index 1b1d1ad8c90..8728afb31a7 100644 --- a/src/ansys/dpf/core/workflow_topology/workflow_topology.py +++ b/src/ansys/dpf/core/workflow_topology/workflow_topology.py @@ -69,8 +69,6 @@ def operators(self) -> OperatorsCollection: OperatorsCollection A collection of all the operators in the workflow. """ - from ansys.dpf.core import OperatorsCollection - if self._operators is None: self._operators = self._container.get_property("operators", OperatorsCollection) From 71f35ae6213569a0f1e5791b00523715d0f483bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matt=C3=A9o=20Baussart?= Date: Fri, 6 Dec 2024 14:05:57 +0100 Subject: [PATCH 19/24] fix: add docstring/typehints; add exception in `Workflow.get_topology()` --- src/ansys/dpf/core/common.py | 11 ++++++++++- src/ansys/dpf/core/workflow.py | 8 +++++++- tests/conftest.py | 2 ++ tests/test_any.py | 6 ++---- tests/test_operator.py | 11 +++-------- tests/test_workflow.py | 6 ++---- tests/test_workflow_topology.py | 22 +++++----------------- 7 files changed, 31 insertions(+), 35 deletions(-) diff --git a/src/ansys/dpf/core/common.py b/src/ansys/dpf/core/common.py index fc3d5b574c9..a4ba808ec3c 100644 --- a/src/ansys/dpf/core/common.py +++ b/src/ansys/dpf/core/common.py @@ -433,7 +433,16 @@ def type_to_special_dpf_constructors(): _derived_class_name_to_type = None -def derived_class_name_to_type(): +def derived_class_name_to_type() -> dict[str, type]: + """ + Returns a mapping of derived class names to their corresponding Python classes. + + Returns + ------- + dict[str, type] + A dictionary mapping derived class names (str) to their corresponding + Python class objects. + """ global _derived_class_name_to_type if _derived_class_name_to_type is None: from ansys.dpf.core.workflow_topology import WorkflowTopology diff --git a/src/ansys/dpf/core/workflow.py b/src/ansys/dpf/core/workflow.py index 8d0c681c677..4520effe352 100644 --- a/src/ansys/dpf/core/workflow.py +++ b/src/ansys/dpf/core/workflow.py @@ -36,7 +36,7 @@ from typing import Union from ansys import dpf -from ansys.dpf.core import dpf_operator, inputs, outputs +from ansys.dpf.core import dpf_operator, errors, inputs, outputs from ansys.dpf.core.check_version import ( server_meet_version, version_requires, @@ -969,7 +969,13 @@ def get_topology(self): Returns ------- workflow_topology : workflow_topology.WorkflowTopology + + Notes + ----- + Available from 10.0 server version. """ + if not self._server.meet_version("10.0"): + raise errors.DpfVersionNotSupported("10.0") workflow_to_workflow_topology_op = dpf_operator.Operator( "workflow_to_workflow_topology", server=self._server ) diff --git a/tests/conftest.py b/tests/conftest.py index 73c3f59ce9e..c2e0f6ab956 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -391,6 +391,8 @@ def decorator(func): if version == "5.0" else not SERVERS_VERSION_GREATER_THAN_OR_EQUAL_TO_6_0 if version == "6.0" + else not SERVERS_VERSION_GREATER_THAN_OR_EQUAL_TO_10_0 + if version == "10.0" else True, reason=f"Requires server version greater than or equal to {version}", raises=core.errors.DpfVersionNotSupported, diff --git a/tests/test_any.py b/tests/test_any.py index 2ad90264665..e60688bf8c1 100644 --- a/tests/test_any.py +++ b/tests/test_any.py @@ -24,6 +24,7 @@ import conftest from ansys.dpf import core as dpf +from conftest import raises_for_servers_version_under @conftest.raises_for_servers_version_under("7.0") @@ -134,10 +135,7 @@ def test_cast_workflow_any(server_type): assert new_entity.input_names == [] -@pytest.mark.skipif( - not conftest.SERVERS_VERSION_GREATER_THAN_OR_EQUAL_TO_10_0, - reason="any does not support operator below 8.0", -) +@raises_for_servers_version_under("10.0") def test_cast_operator_any(server_type): entity = dpf.Operator(server=server_type, name="U") any_dpf = dpf.Any.new_from(entity) diff --git a/tests/test_operator.py b/tests/test_operator.py index a92e7c4ad6c..e233d30992a 100644 --- a/tests/test_operator.py +++ b/tests/test_operator.py @@ -45,6 +45,7 @@ SERVERS_VERSION_GREATER_THAN_OR_EQUAL_TO_7_0, SERVERS_VERSION_GREATER_THAN_OR_EQUAL_TO_8_0, ) +from conftest import raises_for_servers_version_under # Check for ANSYS installation env var HAS_AWP_ROOT212 = os.environ.get("AWP_ROOT212", False) is not False @@ -1427,10 +1428,7 @@ def test_operator_input_output_streams(server_in_process, simple_bar): assert times -@pytest.mark.skipif( - not conftest.SERVERS_VERSION_GREATER_THAN_OR_EQUAL_TO_10_0, - reason="Operator `workflow_to_workflow_topology` does not exist below 10.0", -) +@raises_for_servers_version_under("10.0") def test_operator_outputs_derived_class(server_type): workflow = dpf.core.Workflow(server=server_type) @@ -1443,10 +1441,7 @@ def test_operator_outputs_derived_class(server_type): assert workflow_topology -@pytest.mark.skipif( - not conftest.SERVERS_VERSION_GREATER_THAN_OR_EQUAL_TO_10_0, - reason="Operator `workflow_to_workflow_topology` does not exist below 10.0", -) +@raises_for_servers_version_under("10.0") def test_operator_get_output_derived_class(server_type): workflow = dpf.core.Workflow(server=server_type) diff --git a/tests/test_workflow.py b/tests/test_workflow.py index 40cff92ecf5..6034f22c3e8 100644 --- a/tests/test_workflow.py +++ b/tests/test_workflow.py @@ -31,6 +31,7 @@ import conftest from ansys import dpf from ansys.dpf.core import misc +from conftest import raises_for_servers_version_under if misc.module_exists("graphviz"): HAS_GRAPHVIZ = True @@ -1031,10 +1032,7 @@ def test_workflow_input_output_streams(server_in_process, simple_bar): assert times -@pytest.mark.skipif( - not conftest.SERVERS_VERSION_GREATER_THAN_OR_EQUAL_TO_10_0, - reason="Operator `workflow_to_workflow_topology` does not exist below 10.0", -) +@raises_for_servers_version_under("10.0") def test_workflow_get_output_derived_class(server_type): workflow = dpf.core.Workflow(server=server_type) diff --git a/tests/test_workflow_topology.py b/tests/test_workflow_topology.py index 8ff9a11d894..b7dedbf0804 100644 --- a/tests/test_workflow_topology.py +++ b/tests/test_workflow_topology.py @@ -24,7 +24,7 @@ from ansys import dpf import ansys.dpf.core.operators as op -import conftest +from conftest import raises_for_servers_version_under def workflow_forward(server_type) -> dpf.core.Workflow: @@ -156,10 +156,7 @@ def expected_workflow_topology(workflow): return workflow_topologies[workflow.name] -@pytest.mark.skipif( - not conftest.SERVERS_VERSION_GREATER_THAN_OR_EQUAL_TO_10_0, - reason="Operator `workflow_to_workflow_topology` does not exist below 10.0", -) +@raises_for_servers_version_under("10.0") def test_instantiate_workflow_to_workflow_topology_op(server_type): workflow_to_workflow_topology_op = dpf.core.Operator( "workflow_to_workflow_topology", server=server_type @@ -168,20 +165,14 @@ def test_instantiate_workflow_to_workflow_topology_op(server_type): assert workflow_to_workflow_topology_op -@pytest.mark.skipif( - not conftest.SERVERS_VERSION_GREATER_THAN_OR_EQUAL_TO_10_0, - reason="Operator `workflow_to_workflow_topology` does not exist below 10.0", -) +@raises_for_servers_version_under("10.0") def test_workflow_get_topology(workflow): workflow_topology = workflow.get_topology() assert workflow_topology -@pytest.mark.skipif( - not conftest.SERVERS_VERSION_GREATER_THAN_OR_EQUAL_TO_10_0, - reason="Operator `workflow_to_workflow_topology` does not exist below 10.0", -) +@raises_for_servers_version_under("10.0") def test_workflow_topology_sizes(workflow, expected_workflow_topology): workflow_topology = workflow.get_topology() @@ -195,10 +186,7 @@ def test_workflow_topology_sizes(workflow, expected_workflow_topology): assert len(workflow_topology.exposed_outputs) == expected_workflow_topology["exposed_outputs"] -@pytest.mark.skipif( - not conftest.SERVERS_VERSION_GREATER_THAN_OR_EQUAL_TO_10_0, - reason="Operator `workflow_to_workflow_topology` does not exist below 10.0", -) +@raises_for_servers_version_under("10.0") def test_workflow_topology_str(workflow): workflow_topology = workflow.get_topology() From 6052034ce4317fe945d29ec7a32c47595ed96a8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matt=C3=A9o=20Baussart?= Date: Fri, 6 Dec 2024 14:28:32 +0100 Subject: [PATCH 20/24] fix(test): revert some failing changes --- tests/test_operator.py | 11 ++++++++--- tests/test_workflow_topology.py | 6 +++++- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/tests/test_operator.py b/tests/test_operator.py index e233d30992a..a92e7c4ad6c 100644 --- a/tests/test_operator.py +++ b/tests/test_operator.py @@ -45,7 +45,6 @@ SERVERS_VERSION_GREATER_THAN_OR_EQUAL_TO_7_0, SERVERS_VERSION_GREATER_THAN_OR_EQUAL_TO_8_0, ) -from conftest import raises_for_servers_version_under # Check for ANSYS installation env var HAS_AWP_ROOT212 = os.environ.get("AWP_ROOT212", False) is not False @@ -1428,7 +1427,10 @@ def test_operator_input_output_streams(server_in_process, simple_bar): assert times -@raises_for_servers_version_under("10.0") +@pytest.mark.skipif( + not conftest.SERVERS_VERSION_GREATER_THAN_OR_EQUAL_TO_10_0, + reason="Operator `workflow_to_workflow_topology` does not exist below 10.0", +) def test_operator_outputs_derived_class(server_type): workflow = dpf.core.Workflow(server=server_type) @@ -1441,7 +1443,10 @@ def test_operator_outputs_derived_class(server_type): assert workflow_topology -@raises_for_servers_version_under("10.0") +@pytest.mark.skipif( + not conftest.SERVERS_VERSION_GREATER_THAN_OR_EQUAL_TO_10_0, + reason="Operator `workflow_to_workflow_topology` does not exist below 10.0", +) def test_operator_get_output_derived_class(server_type): workflow = dpf.core.Workflow(server=server_type) diff --git a/tests/test_workflow_topology.py b/tests/test_workflow_topology.py index b7dedbf0804..beea6c01d48 100644 --- a/tests/test_workflow_topology.py +++ b/tests/test_workflow_topology.py @@ -24,6 +24,7 @@ from ansys import dpf import ansys.dpf.core.operators as op +import conftest from conftest import raises_for_servers_version_under @@ -156,7 +157,10 @@ def expected_workflow_topology(workflow): return workflow_topologies[workflow.name] -@raises_for_servers_version_under("10.0") +@pytest.mark.skipif( + not conftest.SERVERS_VERSION_GREATER_THAN_OR_EQUAL_TO_10_0, + reason="Operator `workflow_to_workflow_topology` does not exist below 10.0", +) def test_instantiate_workflow_to_workflow_topology_op(server_type): workflow_to_workflow_topology_op = dpf.core.Operator( "workflow_to_workflow_topology", server=server_type From 5a026b3f273ad9eb5d701508484e33a349380b8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matt=C3=A9o=20Baussart?= Date: Fri, 6 Dec 2024 14:42:32 +0100 Subject: [PATCH 21/24] fix(test): revert some failing changes (bis) --- tests/test_any.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test_any.py b/tests/test_any.py index e60688bf8c1..8d45aef3bcd 100644 --- a/tests/test_any.py +++ b/tests/test_any.py @@ -24,7 +24,6 @@ import conftest from ansys.dpf import core as dpf -from conftest import raises_for_servers_version_under @conftest.raises_for_servers_version_under("7.0") @@ -135,7 +134,10 @@ def test_cast_workflow_any(server_type): assert new_entity.input_names == [] -@raises_for_servers_version_under("10.0") +@pytest.mark.skipif( + not conftest.SERVERS_VERSION_GREATER_THAN_OR_EQUAL_TO_10_0, + reason="any does not support operator below 10.0", +) def test_cast_operator_any(server_type): entity = dpf.Operator(server=server_type, name="U") any_dpf = dpf.Any.new_from(entity) From c04710534cb3bcecd9894a4b4b4033ec28c2a885 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matt=C3=A9o=20Baussart?= Date: Fri, 6 Dec 2024 14:59:57 +0100 Subject: [PATCH 22/24] fix(test): revert some failing changes (ter) --- tests/test_workflow.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test_workflow.py b/tests/test_workflow.py index 6034f22c3e8..40cff92ecf5 100644 --- a/tests/test_workflow.py +++ b/tests/test_workflow.py @@ -31,7 +31,6 @@ import conftest from ansys import dpf from ansys.dpf.core import misc -from conftest import raises_for_servers_version_under if misc.module_exists("graphviz"): HAS_GRAPHVIZ = True @@ -1032,7 +1031,10 @@ def test_workflow_input_output_streams(server_in_process, simple_bar): assert times -@raises_for_servers_version_under("10.0") +@pytest.mark.skipif( + not conftest.SERVERS_VERSION_GREATER_THAN_OR_EQUAL_TO_10_0, + reason="Operator `workflow_to_workflow_topology` does not exist below 10.0", +) def test_workflow_get_output_derived_class(server_type): workflow = dpf.core.Workflow(server=server_type) From 9e28ee6732cf6830b7522e935ca09eb0366dd940 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matt=C3=A9o=20Baussart?= Date: Fri, 6 Dec 2024 15:49:22 +0100 Subject: [PATCH 23/24] fix: change version check --- src/ansys/dpf/core/workflow.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/ansys/dpf/core/workflow.py b/src/ansys/dpf/core/workflow.py index 4520effe352..634bbfe5817 100644 --- a/src/ansys/dpf/core/workflow.py +++ b/src/ansys/dpf/core/workflow.py @@ -36,7 +36,7 @@ from typing import Union from ansys import dpf -from ansys.dpf.core import dpf_operator, errors, inputs, outputs +from ansys.dpf.core import dpf_operator, inputs, outputs from ansys.dpf.core.check_version import ( server_meet_version, version_requires, @@ -963,6 +963,7 @@ def to_graphviz(self, path: Union[os.PathLike, str]): """Saves the workflow to a GraphViz file.""" return self._api.work_flow_export_graphviz(self, str(path)) + @version_requires("10.0") def get_topology(self): """Get the topology of the workflow. @@ -974,8 +975,6 @@ def get_topology(self): ----- Available from 10.0 server version. """ - if not self._server.meet_version("10.0"): - raise errors.DpfVersionNotSupported("10.0") workflow_to_workflow_topology_op = dpf_operator.Operator( "workflow_to_workflow_topology", server=self._server ) From 139924725917f96bdde8200261db6e370ba64dc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matt=C3=A9o=20Baussart?= Date: Mon, 9 Dec 2024 16:39:22 +0100 Subject: [PATCH 24/24] feat: add ability to record derived types --- src/ansys/dpf/core/common.py | 27 ++++++++++++++++++++++++++- tests/test_operator.py | 25 +++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/src/ansys/dpf/core/common.py b/src/ansys/dpf/core/common.py index a4ba808ec3c..7e07167a207 100644 --- a/src/ansys/dpf/core/common.py +++ b/src/ansys/dpf/core/common.py @@ -32,6 +32,7 @@ import re import sys from enum import Enum +from typing import Dict from ansys.dpf.core.misc import module_exists from ansys.dpf.gate.common import locations, ProgressBarBase # noqa: F401 @@ -433,7 +434,7 @@ def type_to_special_dpf_constructors(): _derived_class_name_to_type = None -def derived_class_name_to_type() -> dict[str, type]: +def derived_class_name_to_type() -> Dict[str, type]: """ Returns a mapping of derived class names to their corresponding Python classes. @@ -451,6 +452,30 @@ def derived_class_name_to_type() -> dict[str, type]: return _derived_class_name_to_type +def record_derived_class(class_name: str, py_class: type, overwrite: bool = False): + """ + Records a new derived class in the mapping of class names to their corresponding Python classes. + + This function updates the global dictionary that maps derived class names (str) to their corresponding + Python class objects (type). If the provided class name already exists in the dictionary, it will either + overwrite the existing mapping or leave it unchanged based on the `overwrite` flag. + + Parameters + ---------- + class_name : str + The name of the derived class to be recorded. + py_class : type + The Python class type corresponding to the derived class. + overwrite : bool, optional + A flag indicating whether to overwrite an existing entry for the `class_name`. + If `True`, the entry will be overwritten. If `False` (default), the entry will + not be overwritten if it already exists. + """ + recorded_classes = derived_class_name_to_type() + if overwrite or class_name not in recorded_classes: + recorded_classes[class_name] = py_class + + def create_dpf_instance(type, internal_obj, server): spe_constructors = type_to_special_dpf_constructors() if type in spe_constructors: diff --git a/tests/test_operator.py b/tests/test_operator.py index a92e7c4ad6c..b4c2211cf6e 100644 --- a/tests/test_operator.py +++ b/tests/test_operator.py @@ -33,6 +33,8 @@ from ansys import dpf from ansys.dpf.core import errors from ansys.dpf.core import operators as ops +from ansys.dpf.core.common import derived_class_name_to_type, record_derived_class +from ansys.dpf.core.custom_container_base import CustomContainerBase from ansys.dpf.core.misc import get_ansys_path from ansys.dpf.core.operator_specification import Specification from ansys.dpf.core.workflow_topology import WorkflowTopology @@ -1457,3 +1459,26 @@ def test_operator_get_output_derived_class(server_type): workflow_topology = workflow_to_workflow_topology_op.get_output(0, WorkflowTopology) assert workflow_topology + + +def test_record_derived_type(): + class TestContainer(CustomContainerBase): + pass + + class TestContainer2(CustomContainerBase): + pass + + class_name = "TestContainer" + + derived_classes = derived_class_name_to_type() + assert class_name not in derived_classes + + record_derived_class(class_name, TestContainer) + assert class_name in derived_classes + assert derived_classes[class_name] is TestContainer + + record_derived_class(class_name, TestContainer2) + assert derived_classes[class_name] is TestContainer + + record_derived_class(class_name, TestContainer2, overwrite=True) + assert derived_classes[class_name] is TestContainer2