Skip to content

Commit d2063fe

Browse files
feat: add WorkflowTopology class and workflow_to_workflow_topology operator (#1902)
1 parent ce858d8 commit d2063fe

17 files changed

+1311
-12
lines changed

src/ansys/dpf/core/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@
118118
CustomTypeFieldsCollection:type = _CollectionFactory(CustomTypeField)
119119
GenericDataContainersCollection:type = _CollectionFactory(GenericDataContainer)
120120
StringFieldsCollection:type = _CollectionFactory(StringField)
121+
OperatorsCollection: type = _CollectionFactory(Operator)
121122
AnyCollection:type = _Collection
122123

123124
# for matplotlib

src/ansys/dpf/core/common.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import re
3333
import sys
3434
from enum import Enum
35+
from typing import Dict
3536

3637
from ansys.dpf.core.misc import module_exists
3738
from ansys.dpf.gate.common import locations, ProgressBarBase # noqa: F401
@@ -430,6 +431,51 @@ def type_to_special_dpf_constructors():
430431
return _type_to_special_dpf_constructors
431432

432433

434+
_derived_class_name_to_type = None
435+
436+
437+
def derived_class_name_to_type() -> Dict[str, type]:
438+
"""
439+
Returns a mapping of derived class names to their corresponding Python classes.
440+
441+
Returns
442+
-------
443+
dict[str, type]
444+
A dictionary mapping derived class names (str) to their corresponding
445+
Python class objects.
446+
"""
447+
global _derived_class_name_to_type
448+
if _derived_class_name_to_type is None:
449+
from ansys.dpf.core.workflow_topology import WorkflowTopology
450+
451+
_derived_class_name_to_type = {"WorkflowTopology": WorkflowTopology}
452+
return _derived_class_name_to_type
453+
454+
455+
def record_derived_class(class_name: str, py_class: type, overwrite: bool = False):
456+
"""
457+
Records a new derived class in the mapping of class names to their corresponding Python classes.
458+
459+
This function updates the global dictionary that maps derived class names (str) to their corresponding
460+
Python class objects (type). If the provided class name already exists in the dictionary, it will either
461+
overwrite the existing mapping or leave it unchanged based on the `overwrite` flag.
462+
463+
Parameters
464+
----------
465+
class_name : str
466+
The name of the derived class to be recorded.
467+
py_class : type
468+
The Python class type corresponding to the derived class.
469+
overwrite : bool, optional
470+
A flag indicating whether to overwrite an existing entry for the `class_name`.
471+
If `True`, the entry will be overwritten. If `False` (default), the entry will
472+
not be overwritten if it already exists.
473+
"""
474+
recorded_classes = derived_class_name_to_type()
475+
if overwrite or class_name not in recorded_classes:
476+
recorded_classes[class_name] = py_class
477+
478+
433479
def create_dpf_instance(type, internal_obj, server):
434480
spe_constructors = type_to_special_dpf_constructors()
435481
if type in spe_constructors:
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# Copyright (C) 2020 - 2024 ANSYS, Inc. and/or its affiliates.
2+
# SPDX-License-Identifier: MIT
3+
#
4+
#
5+
# Permission is hereby granted, free of charge, to any person obtaining a copy
6+
# of this software and associated documentation files (the "Software"), to deal
7+
# in the Software without restriction, including without limitation the rights
8+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
# copies of the Software, and to permit persons to whom the Software is
10+
# furnished to do so, subject to the following conditions:
11+
#
12+
# The above copyright notice and this permission notice shall be included in all
13+
# copies or substantial portions of the Software.
14+
#
15+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
# SOFTWARE.
22+
23+
"""
24+
CustomContainerBase
25+
===================
26+
This module contains the `CustomContainerBase` class, which serves as a base
27+
for creating wrappers around `GenericDataContainer` objects.
28+
29+
These wrappers provide an interface for accessing and managing data in
30+
generic containers, enabling more intuitive usage and the addition of custom
31+
behaviors tailored to specific use cases.
32+
"""
33+
34+
from ansys.dpf.core.generic_data_container import GenericDataContainer
35+
36+
37+
class CustomContainerBase:
38+
"""
39+
Base class for custom container wrappers.
40+
41+
This class provides a common interface for managing an underlying
42+
`GenericDataContainer` object.
43+
"""
44+
45+
def __init__(self, container: GenericDataContainer) -> None:
46+
"""
47+
Initialize the base container with a `GenericDataContainer`.
48+
49+
Parameters
50+
----------
51+
container : GenericDataContainer
52+
The underlying data container to be wrapped by this class.
53+
"""
54+
self._container = container

src/ansys/dpf/core/dpf_operator.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,7 @@ def _type_to_output_method(self):
384384
mesh_info,
385385
collection_base,
386386
any,
387+
custom_container_base,
387388
)
388389

389390
out = [
@@ -481,6 +482,15 @@ def _type_to_output_method(self):
481482
self._api.operator_getoutput_as_any,
482483
lambda obj, type: any.Any(server=self._server, any_dpf=obj).cast(type),
483484
),
485+
(
486+
custom_container_base.CustomContainerBase,
487+
self._api.operator_getoutput_generic_data_container,
488+
lambda obj, type: type(
489+
container=generic_data_container.GenericDataContainer(
490+
generic_data_container=obj, server=self._server
491+
)
492+
),
493+
),
484494
]
485495
if hasattr(self._api, "operator_getoutput_generic_data_container"):
486496
out.append(
@@ -726,8 +736,10 @@ def default_config(name, server=None):
726736

727737
def __del__(self):
728738
try:
729-
if self._internal_obj is not None:
730-
self._deleter_func[0](self._deleter_func[1](self))
739+
if hasattr(self, "_deleter_func"):
740+
obj = self._deleter_func[1](self)
741+
if obj is not None:
742+
self._deleter_func[0](obj)
731743
except:
732744
warnings.warn(traceback.format_exc())
733745

src/ansys/dpf/core/helpers/utils.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
import inspect
2424
import sys
25+
from typing import Any, Optional
2526

2627

2728
def _sort_supported_kwargs(bound_method, **kwargs):
@@ -48,3 +49,42 @@ def _sort_supported_kwargs(bound_method, **kwargs):
4849
warnings.warn(txt)
4950
# Return the accepted arguments
5051
return kwargs_in
52+
53+
54+
def indent(text: Any, subsequent_indent: str = "", initial_indent: Optional[str] = None) -> str:
55+
"""Indents each line of a given text.
56+
57+
Parameters
58+
----------
59+
text : Any
60+
The input text to be indented. If it is not already a string, it will be converted to one.
61+
subsequent_indent : str, optional
62+
The string to prefix all lines of the text after the first line. Default is an empty string.
63+
initial_indent : Optional[str], optional
64+
The string to prefix the first line of the text. If not provided, `subsequent_indent` will be used.
65+
66+
Returns
67+
-------
68+
str
69+
The indented text with specified prefixes applied to each line.
70+
71+
Examples
72+
--------
73+
>>> text = "Hello\\nWorld"
74+
>>> print(indent(text, subsequent_indent=" ", initial_indent="--> "))
75+
--> Hello
76+
World
77+
"""
78+
if initial_indent is None:
79+
initial_indent = subsequent_indent
80+
81+
if not isinstance(text, str):
82+
text = str(text)
83+
84+
lines = text.rstrip().splitlines()
85+
indented_lines = [
86+
f"{initial_indent if index == 0 else subsequent_indent}{line}"
87+
for (index, line) in enumerate(lines)
88+
]
89+
90+
return "\n".join(indented_lines)

src/ansys/dpf/core/outputs.py

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -81,17 +81,24 @@ def get_data(self):
8181
elif type_output == "int32":
8282
type_output = types.int
8383

84+
output = self._operator.get_output(self._pin, type_output)
85+
8486
type_output_derive_class = self._spec.name_derived_class
87+
if type_output_derive_class == "":
88+
return output
89+
90+
from ansys.dpf.core.common import derived_class_name_to_type
91+
92+
derived_type = derived_class_name_to_type().get(type_output_derive_class)
93+
if derived_type is not None:
94+
return derived_type(output)
8595

86-
if type_output_derive_class != "":
87-
out_type = [
88-
type_tuple
89-
for type_tuple in self._operator._type_to_output_method
90-
if type_output_derive_class in type_tuple
91-
]
92-
return out_type[0][0](self._operator.get_output(self._pin, type_output))
93-
else:
94-
return self._operator.get_output(self._pin, type_output)
96+
derived_types = [
97+
type_tuple
98+
for type_tuple in self._operator._type_to_output_method
99+
if type_output_derive_class in type_tuple
100+
]
101+
return derived_types[0][0](output)
95102

96103
def __call__(self):
97104
return self.get_data()

src/ansys/dpf/core/workflow.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,7 @@ def _type_to_output_method(self):
333333
collection_base,
334334
streams_container,
335335
)
336+
from ansys.dpf.core.custom_container_base import CustomContainerBase
336337

337338
out = [
338339
(streams_container.StreamsContainer, self._api.work_flow_getoutput_streams),
@@ -421,6 +422,15 @@ def _type_to_output_method(self):
421422
self._api.work_flow_getoutput_as_any,
422423
lambda obj, type: any.Any(server=self._server, any_dpf=obj).cast(type),
423424
),
425+
(
426+
CustomContainerBase,
427+
self._api.work_flow_getoutput_generic_data_container,
428+
lambda obj, type: type(
429+
container=generic_data_container.GenericDataContainer(
430+
generic_data_container=obj, server=self._server
431+
)
432+
),
433+
),
424434
]
425435
if hasattr(self._api, "work_flow_connect_generic_data_container"):
426436
out.append(
@@ -953,6 +963,26 @@ def to_graphviz(self, path: Union[os.PathLike, str]):
953963
"""Saves the workflow to a GraphViz file."""
954964
return self._api.work_flow_export_graphviz(self, str(path))
955965

966+
@version_requires("10.0")
967+
def get_topology(self):
968+
"""Get the topology of the workflow.
969+
970+
Returns
971+
-------
972+
workflow_topology : workflow_topology.WorkflowTopology
973+
974+
Notes
975+
-----
976+
Available from 10.0 server version.
977+
"""
978+
workflow_to_workflow_topology_op = dpf_operator.Operator(
979+
"workflow_to_workflow_topology", server=self._server
980+
)
981+
workflow_to_workflow_topology_op.inputs.workflow.connect(self)
982+
workflow_topology = workflow_to_workflow_topology_op.outputs.workflow_topology()
983+
984+
return workflow_topology
985+
956986
def __del__(self):
957987
try:
958988
if hasattr(self, "_internal_obj"):
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Copyright (C) 2020 - 2024 ANSYS, Inc. and/or its affiliates.
2+
# SPDX-License-Identifier: MIT
3+
#
4+
#
5+
# Permission is hereby granted, free of charge, to any person obtaining a copy
6+
# of this software and associated documentation files (the "Software"), to deal
7+
# in the Software without restriction, including without limitation the rights
8+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
# copies of the Software, and to permit persons to whom the Software is
10+
# furnished to do so, subject to the following conditions:
11+
#
12+
# The above copyright notice and this permission notice shall be included in all
13+
# copies or substantial portions of the Software.
14+
#
15+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
# SOFTWARE.
22+
23+
from .workflow_topology import WorkflowTopology
24+
from .operator_connection import OperatorConnection
25+
from .data_connection import DataConnection
26+
from .exposed_pin import ExposedPin

0 commit comments

Comments
 (0)