diff --git a/examples/10-mesh_operations/00-create_polygons_and_polyhedrons.py b/examples/10-mesh_operations/00-create_polygons_and_polyhedrons.py index 79f4c13aa9..584c3eaf99 100644 --- a/examples/10-mesh_operations/00-create_polygons_and_polyhedrons.py +++ b/examples/10-mesh_operations/00-create_polygons_and_polyhedrons.py @@ -268,6 +268,7 @@ connectivity_f_n_shell = dpf.PropertyField() for face_nodes_index_shell, face_nodes_shell in enumerate(polygon_faces_node_connectivity): connectivity_f_n_shell.append(face_nodes_shell, face_nodes_index_shell) +connectivity_f_n_shell.scoping = mesh_scoping_factory.face_scoping(list(range(30))) mesh_shell_only.set_property_field( property_name="faces_nodes_connectivity", value=connectivity_f_n_shell ) @@ -277,6 +278,7 @@ connectivity_f_n_solid = dpf.PropertyField() for face_nodes_index_solid, face_nodes_solid in enumerate(polyhedron_faces_node_connectivity): connectivity_f_n_solid.append(face_nodes_solid, face_nodes_index_solid) +connectivity_f_n_solid.scoping = mesh_scoping_factory.face_scoping(list(range(24))) mesh_solid_only.set_property_field( property_name="faces_nodes_connectivity", value=connectivity_f_n_solid ) @@ -335,7 +337,7 @@ fcs_types_shell = dpf.PropertyField() for face_index_solid, fctype_shell in enumerate(FT_shell_line): fcs_types_shell.append(fctype_shell, face_index_solid) -fcs_types_shell.scoping = mesh_scoping_factory.elemental_scoping(list(range(30))) +fcs_types_shell.scoping = mesh_scoping_factory.face_scoping(list(range(30))) mesh_shell_only.set_property_field(property_name="faces_type", value=fcs_types_shell) # SolidOnly @@ -351,7 +353,7 @@ fcs_types_solid = dpf.PropertyField() for face_index_shell, fctype_solid in enumerate(FT_tot): fcs_types_solid.append(fctype_solid, face_index_shell) -fcs_types_solid.scoping = mesh_scoping_factory.elemental_scoping(list(range(24))) +fcs_types_solid.scoping = mesh_scoping_factory.face_scoping(list(range(24))) mesh_solid_only.set_property_field(property_name="faces_type", value=fcs_types_solid) ############################################################################### diff --git a/src/ansys/dpf/core/common.py b/src/ansys/dpf/core/common.py index 9bb359e810..7b06e7cab2 100644 --- a/src/ansys/dpf/core/common.py +++ b/src/ansys/dpf/core/common.py @@ -228,6 +228,22 @@ class nodal_properties: nodal_connectivity = "reverse_connectivity" +class face_properties: + """Contains strings to define face property fields. + + Attributes + ---------- + faces_type = "faces_type" + face type property data is provided + + faces_nodes_connectivity = "faces_nodes_connectivity" + faces connectivity property data is provided + """ + + faces_type = "faces_type" + faces_nodes_connectivity = "faces_nodes_connectivity" + + class config_options: """Contains strings to define configuration options. diff --git a/src/ansys/dpf/core/examples/examples.py b/src/ansys/dpf/core/examples/examples.py index 999dc6a355..a85da77728 100644 --- a/src/ansys/dpf/core/examples/examples.py +++ b/src/ansys/dpf/core/examples/examples.py @@ -12,6 +12,7 @@ from ansys.dpf.core import server as server_module from ansys.dpf.core.core import upload_file_in_tmp_folder from ansys.dpf.core import path_utilities +from ansys.dpf.core import DataSources if os.environ.get("DPF_DOCKER", "").lower() == "true": # must pass a path that can be accessed by a docker image with @@ -422,3 +423,26 @@ def find_distributed_msup_folder( """ return find_files(distributed_msup_folder, should_upload, server, return_local_path) + + +def fluid_axial_model() -> DataSources: + """Download the files and create a DataSources. + + Returns + ------- + DataSources + DataSources to the example file. + + Examples + -------- + + >>> from ansys.dpf.core import examples + >>> ds = examples.fluid_axial_model() + """ + from .downloads import download_fluent_axial_comp + + aux = download_fluent_axial_comp() + ds = DataSources() + ds.set_result_file_path(aux["cas"][0], "cas") + ds.add_file_path(aux["dat"][0], "dat") + return ds diff --git a/src/ansys/dpf/core/faces.py b/src/ansys/dpf/core/faces.py new file mode 100644 index 0000000000..8c6fe5a3ab --- /dev/null +++ b/src/ansys/dpf/core/faces.py @@ -0,0 +1,432 @@ +""" +.. _ref_faces_apis: + +Faces +======== +""" +import numpy as np +from ansys.dpf.core import scoping +from ansys.dpf.core.common import face_properties +from ansys.dpf.core.elements import element_types +from ansys.dpf.core.check_version import version_requires + + +@version_requires("7.0") +class Face: + """ + Contains all properties of a face of a mesh. + + The face is created from the + :class:`MeshedRegion ` class. + Properties include the face ID, index, type, shape, and connectivity. + + Parameters + ---------- + mesh : :class:`ansys.dpf.core.meshed_region.MeshedRegion` + Mesh containing the face. + faceid : int + Number (ID) of the face. + index : int + Fortran-based (1-based) index of the face in the result. + nodes : list + List of DPF nodes belonging to the face. + + Examples + -------- + Extract a single face from a meshed region. + + >>> import ansys.dpf.core as dpf + >>> from ansys.dpf.core import examples + >>> model = dpf.Model(examples.fluid_axial_model()) + >>> faces = model.metadata.meshed_region.faces + >>> face = faces[0] + + List the coordinates belonging to the first node of the face. + + >>> face.nodes[0].coordinates + [-0.030426240620025163, -0.05908951107677226, -0.034248966723680496] + + """ + + def __init__(self, mesh, faceid, index, nodes): + self._id = faceid + self._index = index + self._nodes = nodes + self._mesh = mesh + + @property + def node_ids(self): + """ + IDs of all nodes in the face. + + Returns + -------- + list + List of IDs for all nodes in the face. + + Examples + -------- + >>> import ansys.dpf.core as dpf + >>> from ansys.dpf.core import examples + >>> model = dpf.Model(examples.fluid_axial_model()) + >>> faces = model.metadata.meshed_region.faces + >>> face = faces[0] + >>> face.node_ids + [11291, 11416, 11455, 11325] + + """ + return [node.id for node in self._nodes] + + @property + def id(self) -> int: + """ + ID of the face. + + Returns + ------- + int + ID of the face. + + """ + return self._id + + @property + def index(self) -> int: + """ + Index of the face in the result. + + Returns + ------- + int + Index of the face in the result. This uses zero-based indexing starting at ``0``. + + """ + return self._index + + @property + def nodes(self): + """ + All nodes in the face. + Returns + -------- + list + List of all nodes in the face. + + Examples + -------- + >>> import ansys.dpf.core as dpf + >>> from ansys.dpf.core import examples + >>> model = dpf.Model(examples.fluid_axial_model()) + >>> faces = model.metadata.meshed_region.faces + >>> face = faces[0] + >>> first_node = face.nodes[0] + + """ + return self._nodes + + @property + def n_nodes(self) -> int: + """ + Number of nodes in the face. + + Returns + ------- + int + Number of nodes. + + """ + return len(self._nodes) + + def __str__(self): + txt = "DPF Face %d\n" % self.id + txt += "\tIndex: %7d\n" % self.index + txt += "\tNodes: %7d\n" % self.n_nodes + txt += f"\tType: {self.type}\n" + return txt + + @property + def type(self) -> int: + """ + Type of the face. + + Returns + ------- + int + Type of the face. For more information, see + :class:`ansys.dpf.core.elements.element_types`. + + Examples + -------- + >>> import ansys.dpf.core as dpf + >>> from ansys.dpf.core import examples + >>> model = dpf.Model(examples.fluid_axial_model()) + >>> faces = model.metadata.meshed_region.faces + >>> face = faces[0] + >>> face.type + + + """ + return self._get_type() + + def _get_type(self): + """Retrieve the Ansys element type.""" + return element_types( + self._mesh.property_field(face_properties.faces_type).get_entity_data(self._index)[0] + ) + + @property + def connectivity(self): + """ + Ordered list of node indices of the face. + + Returns + -------- + list + Ordered list of node indices. + + """ + list = [] + for node in self._nodes: + list.append(node.index) + return list + + +@version_requires("7.0") +class Faces: + """ + Contains faces belonging to a meshed region. + + Parameters + ---------- + mesh : str + Name of the meshed region. + + Examples + -------- + >>> import ansys.dpf.core as dpf + >>> from ansys.dpf.core import examples + >>> model = dpf.Model(examples.fluid_axial_model()) + >>> faces = model.metadata.meshed_region.faces + >>> faces.n_faces + 44242 + + """ + + def __init__(self, mesh): + self._mesh = mesh + self._server = mesh._server + self._mapping_id_to_index = None + + def __str__(self): + return "DPF Faces object with %d faces" % len(self) + + def __getitem__(self, index): + """Retrieves face based on an index.""" + return self.face_by_index(index) + + def __len__(self): + return self.n_faces + + def __iter__(self): + for i in range(len(self)): + yield self[i] + + def face_by_id(self, id) -> Face: + """ + Retrieve a face by face ID. + + Parameters + ---------- + id : int + Number (ID) of the face. + + Returns + ------- + Face + Face object. + + """ + return self.__get_face(faceid=id) + + def face_by_index(self, index) -> Face: + """ + Retrieve a face using its index. + + Parameters + ---------- + index : int + Zero-based index. + + Returns + ------- + Face + Yield face. + + Examples + -------- + faces.face_by_index(0) + + Notes + ----- + This is equivalent to ``faces[0]`` + + """ + return self.__get_face(faceindex=index) + + def __get_face(self, faceindex=None, faceid=None): + """ + Retrieve the face by ID or index. + + Parameters + ---------- + faceid : int, optional + ID of the face. The default is ``None``. + faceindex : int, optional + Index of the face. The default is ``None``. + + Returns + ------- + face : Face + """ + if faceindex is None: + faceindex = self._mesh.property_field(face_properties.faces_type).scoping.index(faceid) + elif faceid is None: + faceid = self._mesh.property_field(face_properties.faces_type).scoping.id(faceindex) + nodeIdx = self._mesh.property_field( + face_properties.faces_nodes_connectivity + ).get_entity_data(faceindex) + nodesOut = [self._mesh.nodes.node_by_index(node_index) for node_index in nodeIdx] + return Face(self._mesh, faceid, faceindex, nodesOut) + + @property + def scoping(self) -> scoping.Scoping: + """ + Scoping of the faces. + + Returns + ------- + :class:`ansys.dpf.core.scoping.Scoping` + + Examples + -------- + >>> import ansys.dpf.core as dpf + >>> from ansys.dpf.core import examples + >>> model = dpf.Model(examples.fluid_axial_model()) + >>> faces = model.metadata.meshed_region.faces + >>> my_scoping = faces.scoping + + """ + return self._mesh.property_field(face_properties.faces_type).scoping + + @property + def faces_type_field(self): + """ + Field of all faces types. + + Returns + ------- + :class:`ansys.dpf.core.field.Field` + Field of all faces types. + + Examples + -------- + >>> import ansys.dpf.core as dpf + >>> from ansys.dpf.core import examples + >>> model = dpf.Model(examples.fluid_axial_model()) + >>> faces = model.metadata.meshed_region.faces + >>> field = faces.faces_type_field + >>> print(field.data) + [16 16 16 ... 16 16 16] + + """ + return self._mesh.field_of_properties(face_properties.faces_type) + + @property + def faces_nodes_connectivity_field(self): + """ + Field containing for each face ID the node indices connected to the face. + + Returns + ------- + :class:`ansys.dpf.core.field.Field` + Field containing for each face ID the node indices connected to the face. + + Examples + -------- + >>> import ansys.dpf.core as dpf + >>> from ansys.dpf.core import examples + >>> model = dpf.Model(examples.fluid_axial_model()) + >>> faces = model.metadata.meshed_region.faces + >>> field = faces.faces_nodes_connectivity_field + >>> field.get_entity_data(1) + DPFArray([11415, 11347, 11387, 11454]) + + """ + return self._mesh.property_field(face_properties.faces_nodes_connectivity) + + @property + def n_faces(self) -> int: + """Number of faces""" + return self._mesh._api.meshed_region_get_num_faces(self._mesh) + + def _build_mapping_id_to_index(self): + """Retrieve the mapping between the IDs and indices of the entity.""" + return {eid: i for i, eid in enumerate(self.scoping.ids)} + + @property + def mapping_id_to_index(self) -> dict: + """ + Mapping between the IDs and indices of the entity. + + This property is useful for mapping scalar results from a field to the meshed region. + + Examples + -------- + >>> import ansys.dpf.core as dpf + >>> from ansys.dpf.core import examples + >>> model = dpf.Model(examples.fluid_axial_model()) + >>> meshed_region = model.metadata.meshed_region + >>> map = meshed_region.faces.mapping_id_to_index + + """ + if self._mapping_id_to_index is None: + self._mapping_id_to_index = self._build_mapping_id_to_index() + return self._mapping_id_to_index + + def map_scoping(self, external_scope): + """ + Retrieve the indices to map the scoping of these faces to + the scoping of a field. + + Parameters + ---------- + external_scope : :class:`ansys.dpf.core.scoping.Scoping` + Scoping to map to. + + Returns + ------- + indices : numpy.ndarray + List of indices to map from the external scope to the + scoping of these faces. + mask : numpy.ndarray + Members of the external scope that are in the face scoping. + + Examples + -------- + Return the indices that map a field to a faces collection. + + >>> import ansys.dpf.core as dpf + >>> from ansys.dpf.core import examples + >>> model = dpf.Model(examples.fluid_axial_model()) + >>> faces = model.metadata.meshed_region.faces + >>> m = model.results.mass_flow_rate() + >>> field = m.outputs.fields_container()[0] + >>> ind, mask = faces.map_scoping(field.scoping) + + """ + if external_scope.location in ["Nodal", "NodalElemental", "Elemental", "ElementalNodal"]: + raise ValueError('Input scope location must be "Faces"') + arr = np.array(list(map(self.mapping_id_to_index.get, external_scope.ids))) + mask = arr != None + ind = arr[mask].astype(np.int32) + return ind, mask diff --git a/src/ansys/dpf/core/mesh_scoping_factory.py b/src/ansys/dpf/core/mesh_scoping_factory.py index 3ddd67534d..8c52ce3dbf 100644 --- a/src/ansys/dpf/core/mesh_scoping_factory.py +++ b/src/ansys/dpf/core/mesh_scoping_factory.py @@ -49,6 +49,26 @@ def elemental_scoping(element_ids, server=None): return scoping +def face_scoping(face_ids, server=None): + """Create a specific face :class:`ansys.dpf.core.Scoping` associated with a mesh. + + Parameters + ---------- + face_ids : list[int] + List of IDs for the faces. + server : DpfServer, optional + Server with the channel connected to the remote or local instance. + The default is ``None``, in which case an attempt is made to use the + global server. + + Returns + ------- + scoping : Scoping + """ + scoping = Scoping(server=server, ids=face_ids, location=locations.faces) + return scoping + + def named_selection_scoping(named_selection_name, model, server=None): """Create a specific :class:`ansys.dpf.core.Scoping` associated with a specified model's mesh. diff --git a/src/ansys/dpf/core/meshed_region.py b/src/ansys/dpf/core/meshed_region.py index c988e14ac7..b3276aca09 100644 --- a/src/ansys/dpf/core/meshed_region.py +++ b/src/ansys/dpf/core/meshed_region.py @@ -12,6 +12,7 @@ from ansys.dpf.core.common import locations, types, nodal_properties from ansys.dpf.core.elements import Elements, element_types from ansys.dpf.core.nodes import Nodes +from ansys.dpf.core.faces import Faces from ansys.dpf.core.plotter import DpfPlotter, Plotter from ansys.dpf.core.cache import class_handling_cache from ansys.dpf.core import server as server_module @@ -166,6 +167,29 @@ def elements(self): """ return Elements(self) + @property + def faces(self): + """ + All face properties of the mesh, such as faces_nodes_connectivity and face types. + + Returns + ------- + faces : Faces + Faces belonging to the meshed region. + + Examples + -------- + >>> import ansys.dpf.core as dpf + >>> from ansys.dpf.core import examples + >>> model = dpf.Model(examples.find_static_rst()) + >>> meshed_region = model.metadata.meshed_region + >>> faces = meshed_region.faces + >>> print(faces) + DPF Faces object with 0 faces + + """ + return Faces(self) + @property def nodes(self): """ @@ -463,6 +487,7 @@ def _as_vtk(self, coordinates=None, as_linear=True, include_ids=False): grid["node_ids"] = self._nodeids grid["element_ids"] = self._elementids + self.as_linear = as_linear # store as_linear to avoid passing through here again return grid @property diff --git a/src/ansys/dpf/core/plotter.py b/src/ansys/dpf/core/plotter.py index 10de6861a1..fd125fe35b 100644 --- a/src/ansys/dpf/core/plotter.py +++ b/src/ansys/dpf/core/plotter.py @@ -814,8 +814,10 @@ def plot_contour( mesh_location = mesh.nodes elif location == locations.elemental: mesh_location = mesh.elements + elif location == locations.faces: + mesh_location = mesh.faces else: - raise ValueError("Only elemental or nodal location are supported for plotting.") + raise ValueError("Only elemental, nodal or faces location are supported for plotting.") # pre-loop: check if shell layers for each field, if yes, set the shell layers changeOp = core.Operator("change_shellLayers") diff --git a/tests/test_examples.py b/tests/test_examples.py index 0567b2ee91..0098ef13f1 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -6,6 +6,7 @@ from conftest import running_docker from ansys.dpf import core as dpf from ansys.dpf.core import Model +from ansys.dpf.core import DataSources from ansys.dpf.core import examples @@ -84,6 +85,11 @@ def test_download_cfx_mixing_elbow(): assert isinstance(Model(path), Model) +def test_fluid_axial_model(): + ds = examples.fluid_axial_model() + assert isinstance(ds, DataSources) + + list_examples = [ "simple_bar", "static_rst", diff --git a/tests/test_faces.py b/tests/test_faces.py new file mode 100644 index 0000000000..0400f47733 --- /dev/null +++ b/tests/test_faces.py @@ -0,0 +1,78 @@ +import pytest +import conftest +import platform +from ansys.dpf import core as dpf +from ansys.dpf.core.elements import element_types +from ansys.dpf.core import mesh_scoping_factory + + +@pytest.fixture() +def model_faces(fluent_axial_comp): + model = dpf.Model(fluent_axial_comp) + faces = model.metadata.meshed_region.faces + return faces + + +@pytest.mark.skipif(platform.system() == "Linux", reason="CFF not available for Linux InProcess.") +@pytest.mark.skipif( + not conftest.SERVERS_VERSION_GREATER_THAN_OR_EQUAL_TO_7_0, + reason="mesh faces were not supported before 7.0", +) +def test_faces(model_faces): + assert str(model_faces) == "DPF Faces object with 44242 faces" + assert len(model_faces) == 44242 + assert model_faces.n_faces == 44242 + assert model_faces.scoping.location == dpf.locations.faces + assert model_faces.scoping.size == 44242 + assert model_faces.faces_type_field.scoping.ids[45] == model_faces.scoping.ids[45] + assert model_faces.faces_type_field.get_entity_data(789)[0] == model_faces[789].type.value + for n in range(model_faces[2000].n_nodes): + assert ( + model_faces.faces_nodes_connectivity_field.get_entity_data(2000)[n] + == model_faces[2000].connectivity[n] + ) + + my_sco = mesh_scoping_factory.face_scoping([1100, 2400]) + ind, mask = model_faces.map_scoping(my_sco) + + assert ind[0] == 97 + assert mask[1] == True + + +@pytest.mark.skipif(platform.system() == "Linux", reason="CFF not available for Linux InProcess.") +@pytest.mark.skipif( + not conftest.SERVERS_VERSION_GREATER_THAN_OR_EQUAL_TO_7_0, + reason="mesh faces were not supported before 7.0", +) +def test_face(model_faces): + face = model_faces.face_by_id(4500) + ref_str = """DPF Face 4500 +\tIndex: 3497 +\tNodes: 4 +\tType: element_types.Quad4 +""" + assert str(face) == ref_str + assert face.node_ids == [4688, 4679, 4663, 4677] + assert face.id == 4500 + assert face.type == element_types.Quad4 + assert face.n_nodes == 4 + assert face.index == 3497 + assert len(face.nodes) == 4 + + ref_node_str = """DPF Node 4677 +Index: 4676 +Location: [-0.022856459489947675, -0.08534214957826106, -0.013310679234564304] +""" + + assert str(face.nodes[3]) == ref_node_str + + +@pytest.mark.skipif( + not conftest.SERVERS_VERSION_GREATER_THAN_OR_EQUAL_TO_7_0, + reason="faces location was not supported before 7.0", +) +def test_face_scoping(): + faces_sco = mesh_scoping_factory.face_scoping([56, 78, 4]) + assert faces_sco.location == dpf.locations.faces + assert faces_sco.size == 3 + assert faces_sco.ids[2] == 4 diff --git a/tests/test_plotter.py b/tests/test_plotter.py index 9435c4eb06..02078dbe7d 100644 --- a/tests/test_plotter.py +++ b/tests/test_plotter.py @@ -9,6 +9,7 @@ from ansys.dpf.core import misc from ansys.dpf.core.plotter import plot_chart from conftest import running_docker, SERVERS_VERSION_GREATER_THAN_OR_EQUAL_TO_5_0 +from ansys.dpf.core import element_types if misc.module_exists("pyvista"): HAS_PYVISTA = True @@ -710,8 +711,13 @@ def test_plot_polyhedron(): ] # Define the element connectivity element_connectivity = [i for face in faces_connectivity for i in face] + # Define the faces connectivity of the element elements_faces = [[0, 1, 2, 3, 4, 5, 6]] + # Define the types of faces in the mesh + faces_types = [[element_types.Polygon.value]] * 7 + # Define the types of elements in the mesh + cell_types = [[element_types.Polyhedron.value]] # Create mesh object and add nodes and elements mesh = core.MeshedRegion() @@ -719,6 +725,12 @@ def test_plot_polyhedron(): mesh.nodes.add_node(index, node_coordinates) mesh.elements.add_solid_element(0, element_connectivity) + # Set the "cell_types" PropertyField + cell_types_f = core.PropertyField() + for cell_index, cell_type in enumerate(cell_types): + cell_types_f.append(cell_type, cell_index) + mesh.set_property_field("eltype", cell_types_f) + # Set the "faces_nodes_connectivity" PropertyField connectivity_f = core.PropertyField() for face_index, face_connectivity in enumerate(faces_connectivity): @@ -731,5 +743,11 @@ def test_plot_polyhedron(): elements_faces_f.append(element_faces, element_index) mesh.set_property_field("elements_faces_connectivity", elements_faces_f) + # Set the "faces_types" PropertyField + faces_types_f = core.PropertyField() + for face_index, face_type in enumerate(faces_types): + faces_types_f.append(face_type, face_index) + mesh.set_property_field("faces_type", faces_types_f) + # Plot the MeshedRegion mesh.plot()