diff --git a/src/ansys/dpf/core/vtk_helper.py b/src/ansys/dpf/core/vtk_helper.py index 14030b163c..d4dff217f0 100644 --- a/src/ansys/dpf/core/vtk_helper.py +++ b/src/ansys/dpf/core/vtk_helper.py @@ -1,7 +1,6 @@ import numpy as np import pyvista as pv -import ansys.dpf.core as dpf -from ansys.dpf.core import errors +from typing import Union from vtk import ( VTK_HEXAHEDRON, VTK_LINE, @@ -22,6 +21,10 @@ VTK_WEDGE, vtkVersion, ) + +import ansys.dpf.core as dpf +from ansys.dpf.core import errors +from ansys.dpf.core.check_version import server_meet_version_and_raise from ansys.dpf.core.elements import element_types VTK9 = vtkVersion().GetVTKMajorVersion() >= 9 @@ -124,7 +127,7 @@ def __init__( ModuleNotFoundError.__init__(self, msg) -def dpf_mesh_to_vtk_op(mesh, nodes, as_linear): +def dpf_mesh_to_vtk_op(mesh, nodes=None, as_linear=True): """Return a pyvista unstructured grid given DPF node and element definitions from operators (server > 6.2) @@ -134,7 +137,7 @@ def dpf_mesh_to_vtk_op(mesh, nodes, as_linear): Meshed Region to export to pyVista format nodes : dpf.Field - Field containing the nodes of the mesh. + Field containing the node coordinates of the mesh. as_linear : bool Export quadratic surface elements as linear. @@ -333,25 +336,28 @@ def compute_offset(): return pv.UnstructuredGrid(offset, cells, vtk_cell_type, node_coordinates) -def dpf_mesh_to_vtk(mesh, nodes=None, as_linear=True): - """Return a pyvista unstructured grid given DPF node and element - definitions. +def dpf_mesh_to_vtk( + mesh: dpf.MeshedRegion, + nodes: Union[dpf.Field, None] = None, + as_linear: bool = True +) -> pv.UnstructuredGrid: + """Return a pyvista UnstructuredGrid given a pydpf MeshedRegion. Parameters ---------- mesh : dpf.MeshedRegion - Meshed Region to export to pyVista format + Meshed Region to export to pyVista format. nodes : dpf.Field, optional - Field containing the nodes of the mesh. + Field containing the node coordinates of the mesh (useful to get a deformed geometry). as_linear : bool, optional Export quadratic surface elements as linear. Returns ------- - grid : pyvista.UnstructuredGrid - Unstructured grid of the DPF mesh. + grid: + UnstructuredGrid corresponding to the DPF mesh. """ try: return dpf_mesh_to_vtk_op(mesh, nodes, as_linear) @@ -363,3 +369,277 @@ def vtk_update_coordinates(vtk_grid, coordinates_array): from copy import copy vtk_grid.points = copy(coordinates_array) + + +def dpf_meshes_to_vtk( + meshes_container: dpf.MeshesContainer, + nodes: Union[dpf.FieldsContainer, None] = None, + as_linear: bool = True +) -> pv.UnstructuredGrid: + """Return a pyvista UnstructuredGrid given a pydpf MeshedRegion. + + Parameters + ---------- + meshes_container: + MeshesContainer to export to pyVista format. + + nodes: + FieldsContainer containing the node coordinates for each mesh + (useful to get a deformed geometry). The labels must match a field to a mesh. + + as_linear : bool, optional + Export quadratic surface elements as linear. + + Returns + ------- + grid: + UnstructuredGrid corresponding to the DPF meshes. + """ + grids = [] + for i, mesh in enumerate(meshes_container): + nodes_i = None + if nodes: + nodes_i = nodes[i] + grids.append(dpf_mesh_to_vtk(mesh, nodes_i, as_linear)) + return pv.MultiBlock(grids).combine() + + +def dpf_field_to_vtk( + field: dpf.Field, + meshed_region: Union[dpf.MeshedRegion, None] = None, + nodes: Union[dpf.Field, None] = None, + as_linear: bool = True +) -> pv.UnstructuredGrid: + """Return a pyvista UnstructuredGrid given a DPF Field. + + Parameters + ---------- + field: + Field to export to pyVista format. + + meshed_region: + Mesh to associate to the field. + Useful for fluid results where the field is not automatically associated to its mesh. + + nodes: + Field containing the node coordinates of the mesh (useful to get a deformed geometry). + + as_linear: + Export quadratic surface elements as linear. + + Returns + ------- + grid: + UnstructuredGrid corresponding to the DPF Field. + """ + # Check Field location + supported_locations = [ + dpf.locations.nodal, dpf.locations.elemental, dpf.locations.faces, dpf.locations.overall + ] + if field.location not in supported_locations: + raise ValueError( + f"Supported field locations for translation to VTK are: {supported_locations}." + ) + + # Associate the provided mesh with the field + if meshed_region: + field.meshed_region = meshed_region + else: + try: + meshed_region = field.meshed_region + except errors.DPFServerException as e: + if "the field doesn't have this support type" in str(e): + raise ValueError("The field does not have a meshed_region.") + else: + raise e + except RuntimeError as e: + if "The field's support is not a mesh" in str(e): + raise ValueError("The field does not have a meshed_region.") + else: + raise e + + # Initialize the bare UnstructuredGrid + if meshed_region.nodes.n_nodes == 0: + raise ValueError("The field does not have a meshed_region.") + grid = dpf_mesh_to_vtk(mesh=meshed_region, nodes=nodes, as_linear=as_linear) + + # Map Field.data to the VTK mesh + overall_data = _map_field_to_mesh(field=field, meshed_region=meshed_region) + + # Update the UnstructuredGrid + if field.location == dpf.locations.nodal: + grid.point_data[field.name] = overall_data + else: + grid.cell_data[field.name] = overall_data + return grid + + +def dpf_fieldscontainer_to_vtk( + fields_container: dpf.FieldsContainer, + meshes_container: Union[dpf.MeshesContainer, None] = None, + nodes: Union[dpf.Field, None] = None, + as_linear: bool = True +) -> pv.UnstructuredGrid: + """Return a pyvista UnstructuredGrid given a DPF FieldsContainer. + + If the fields have different mesh supports, a global merged mesh support is created. + + Parameters + ---------- + fields_container: + FieldsContainer to export to pyVista format. + + meshes_container: + MeshesContainer with meshes to associate to the fields in the FieldsContainer. + Useful for fluid results where the fields are not automatically associated to their mesh. + + nodes: + Field containing the node coordinates of the mesh (useful to get a deformed geometry). + + as_linear: + Export quadratic surface elements as linear. + + Returns + ------- + grid: + UnstructuredGrid corresponding to the DPF Field. + """ + # Check Field location + supported_locations = [ + dpf.locations.nodal, dpf.locations.elemental, dpf.locations.faces, dpf.locations.overall + ] + if fields_container[0].location not in supported_locations: + raise ValueError( + f"Supported field locations for translation to VTK are: {supported_locations}." + ) + + # Associate the meshes in meshes_container to the corresponding fields if provided + if meshes_container: + for i, mesh in enumerate(meshes_container): + label_space = meshes_container.get_label_space(i) + fields_container.get_field( + label_space_or_index=label_space + ).meshed_region = meshes_container.get_mesh(label_space_or_index=label_space) + + # Initialize the bare UnstructuredGrid + # Loop on the fields to check if merging supports is necessary + meshes = [] + for field in fields_container: + if field.meshed_region not in meshes: + meshes.append(field.meshed_region) + if len(meshes)>1: + # Merge the meshed_regions + merge_op = dpf.operators.utility.merge_meshes(server=fields_container._server) + for i, mesh in enumerate(meshes): + merge_op.connect(i, mesh) + meshed_region = merge_op.eval() + else: + meshed_region = meshes[0] + if meshed_region.nodes.n_nodes == 0: + raise ValueError("The meshed_region of the fields contains no nodes.") + grid = dpf_mesh_to_vtk(mesh=meshed_region, nodes=nodes, as_linear=as_linear) + + for i, field in enumerate(fields_container): + # Map Field.data to the VTK mesh + overall_data = _map_field_to_mesh(field=field, meshed_region=meshed_region) + label_space = fields_container.get_label_space(i) + label_space = dict([(k, label_space[k]) for k in sorted(label_space.keys())]) + field.name = field.name+f" {label_space}" + # Update the UnstructuredGrid + if field.location == dpf.locations.nodal: + grid.point_data[field.name] = overall_data + else: + grid.cell_data[field.name] = overall_data + + return grid + + +def _map_field_to_mesh( + field: Union[dpf.Field, dpf.PropertyField], + meshed_region: dpf.MeshedRegion +) -> np.ndarray: + """Return an NumPy array of 'Field.data' mapped to the mesh on the field's location.""" + location = field.location + if location == dpf.locations.nodal: + mesh_location = meshed_region.nodes + elif location == dpf.locations.elemental: + mesh_location = meshed_region.elements + elif location == dpf.locations.faces: + mesh_location = meshed_region.faces + if len(mesh_location) == 0: + raise ValueError("No faces found to plot on") + elif location == dpf.locations.overall: + mesh_location = meshed_region.elements + else: + raise ValueError("Only elemental, nodal or faces location are supported for plotting.") + component_count = field.component_count + if component_count > 1: + overall_data = np.full((len(mesh_location), component_count), np.nan) + else: + overall_data = np.full(len(mesh_location), np.nan) + if location != dpf.locations.overall: + ind, mask = mesh_location.map_scoping(field.scoping) + overall_data[ind] = field.data[mask] + else: + overall_data[:] = field.data[0] + return overall_data + + +def dpf_property_field_to_vtk( + property_field: dpf.PropertyField, + meshed_region: dpf.MeshedRegion, + nodes: Union[dpf.Field, None] = None, + as_linear: bool = True +) -> pv.UnstructuredGrid: + """Return a pyvista UnstructuredGrid given a DPF PropertyField. + + ..note: + Available starting with DPF 2024.2.pre1. + + Parameters + ---------- + property_field: + PropertyField to export to pyVista format. + + meshed_region: + Mesh to associate to the property field. + + nodes: + Field containing the node coordinates of the mesh (useful to get a deformed geometry). + + as_linear: + Export quadratic surface elements as linear. + + Returns + ------- + grid: + UnstructuredGrid corresponding to the DPF PropertyField. + """ + server_meet_version_and_raise( + required_version="8.1", + server=meshed_region._server, + msg="Use of dpf_property_field_to_vtk requires DPF 2024.2.pre1 or above." + ) + # Check Field location + supported_locations = [ + dpf.locations.nodal, dpf.locations.elemental, dpf.locations.faces, dpf.locations.overall + ] + if property_field.location not in supported_locations: + raise ValueError( + f"Supported field locations for translation to VTK are: {supported_locations}." + ) + + # Initialize the bare UnstructuredGrid + if meshed_region.nodes.n_nodes == 0: + raise ValueError("The property field does not have a meshed_region.") + grid = dpf_mesh_to_vtk(mesh=meshed_region, nodes=nodes, as_linear=as_linear) + + # Map Field.data to the VTK mesh + overall_data = _map_field_to_mesh(field=property_field, meshed_region=meshed_region) + + # Update the UnstructuredGrid + if property_field.location == dpf.locations.nodal: + grid.point_data[property_field.name] = overall_data + else: + grid.cell_data[property_field.name] = overall_data + return grid diff --git a/tests/test_vtk_translate.py b/tests/test_vtk_translate.py new file mode 100644 index 0000000000..0de8a6cee6 --- /dev/null +++ b/tests/test_vtk_translate.py @@ -0,0 +1,162 @@ +import pytest +import conftest +import ansys.dpf.core as dpf +from ansys.dpf.core import errors, misc +from ansys.dpf.core.vtk_helper import \ + dpf_mesh_to_vtk, dpf_field_to_vtk, dpf_meshes_to_vtk, \ + dpf_fieldscontainer_to_vtk, dpf_property_field_to_vtk + +if misc.module_exists("pyvista"): + HAS_PYVISTA = True + import pyvista as pv +else: + HAS_PYVISTA = False + + +@pytest.mark.skipif(not HAS_PYVISTA, reason="Please install pyvista") +def test_dpf_mesh_to_vtk(simple_rst, server_type): + model = dpf.Model(simple_rst, server=server_type) + mesh = model.metadata.meshed_region + # Mesh to VTK + ug = dpf_mesh_to_vtk(mesh=mesh) + assert isinstance(ug, pv.UnstructuredGrid) + pv.plot(ug) + # With deformation + field = model.results.displacement.on_last_time_freq().eval()[0] + initial_coord = mesh.nodes.coordinates_field + updated_coord = (initial_coord + field).eval() + ug = dpf_mesh_to_vtk(mesh=mesh, nodes=updated_coord) + assert isinstance(ug, pv.UnstructuredGrid) + pv.plot(ug) + # As linear + ug = dpf_mesh_to_vtk(mesh=mesh, as_linear=True) + assert isinstance(ug, pv.UnstructuredGrid) + pv.plot(ug) + + +@pytest.mark.skipif( + not conftest.SERVERS_VERSION_GREATER_THAN_OR_EQUAL_TO_7_0, + reason="CFF source operators where not supported before 7.0,", +) +@pytest.mark.skipif(not HAS_PYVISTA, reason="Please install pyvista") +def test_dpf_field_to_vtk(simple_rst, fluent_mixing_elbow_steady_state, server_type): + model = dpf.Model(simple_rst, server=server_type) + mesh = model.metadata.meshed_region + field = model.results.displacement.on_last_time_freq().eval()[0] + field.name = "disp" + # Nodal Field to VTK + ug = dpf_field_to_vtk(field=field) + assert isinstance(ug, pv.UnstructuredGrid) + assert "disp" in ug.point_data.keys() + pv.plot(ug) + # With deformation + initial_coord = mesh.nodes.coordinates_field + updated_coord = (initial_coord + field).eval() + ug = dpf_field_to_vtk(field=field, nodes=updated_coord) + assert isinstance(ug, pv.UnstructuredGrid) + pv.plot(ug) + # As linear + ug = dpf_field_to_vtk(field=field, as_linear=True) + assert isinstance(ug, pv.UnstructuredGrid) + pv.plot(ug) + # Elemental Field to VTK + model = dpf.Model(fluent_mixing_elbow_steady_state(server=server_type), server=server_type) + field = model.results.dynamic_viscosity.on_last_time_freq().eval()[0] + field.name = "DV" + ug = dpf_field_to_vtk(field=field, meshed_region=model.metadata.meshed_region) + assert isinstance(ug, pv.UnstructuredGrid) + assert "DV" in ug.cell_data.keys() + pv.plot(ug) + + +@pytest.mark.skipif(not HAS_PYVISTA, reason="Please install pyvista") +def test_dpf_field_to_vtk_errors(simple_rst, server_type): + model = dpf.Model(simple_rst, server=server_type) + # Elemental Field to VTK + field = model.results.elemental_volume.on_last_time_freq().eval()[0] + with pytest.raises(ValueError, match="The field does not have a meshed_region."): + _ = dpf_field_to_vtk(field=field) + + +@pytest.mark.skipif( + not conftest.SERVERS_VERSION_GREATER_THAN_OR_EQUAL_TO_7_0, + reason="CFF source operators where not supported before 7.0,", +) +@pytest.mark.skipif(not HAS_PYVISTA, reason="Please install pyvista") +def test_dpf_meshes_to_vtk(fluent_axial_comp, server_type): + model = dpf.Model(fluent_axial_comp(server=server_type), server=server_type) + meshes_container = dpf.operators.mesh.meshes_provider( + data_sources=model, + server=server_type, + region_scoping=dpf.Scoping(ids=[13, 28], location=dpf.locations.zone, server=server_type) + ).eval() + assert len(meshes_container) == 2 + ug = dpf_meshes_to_vtk(meshes_container=meshes_container) + assert ug.GetNumberOfCells() == 13856 + pv.plot(ug) + + +@pytest.mark.skipif( + not conftest.SERVERS_VERSION_GREATER_THAN_OR_EQUAL_TO_7_0, + reason="CFF source operators where not supported before 7.0,", +) +@pytest.mark.skipif(not HAS_PYVISTA, reason="Please install pyvista") +def test_dpf_fieldscontainer_to_vtk(fluent_axial_comp, server_type): + model = dpf.Model(fluent_axial_comp(server=server_type), server=server_type) + zone_scoping = dpf.Scoping(ids=[13, 28], location=dpf.locations.zone, server=server_type) + # Elemental + fields_container = dpf.operators.result.enthalpy( + data_sources=model, + server=server_type, + region_scoping=zone_scoping, + ).eval() + assert len(fields_container) == 2 + meshes_container = dpf.operators.mesh.meshes_provider( + data_sources=model, + server=server_type, + region_scoping=zone_scoping, + ).eval() + ug = dpf_fieldscontainer_to_vtk( + fields_container=fields_container, meshes_container=meshes_container + ) + assert ug.GetNumberOfCells() == 13856 + assert sorted(list(ug.cell_data.keys())) == ["h {'time': 1, 'zone': 13}", + "h {'time': 1, 'zone': 28}"] + pv.plot(ug) + zone_scoping = dpf.Scoping(ids=[3, 4, 7], location=dpf.locations.zone, server=server_type) + # Faces + fields_container = dpf.operators.result.wall_shear_stress( + data_sources=model, + server=server_type, + region_scoping=zone_scoping, + ).eval() + assert len(fields_container) == 3 + meshes_container = dpf.operators.mesh.meshes_provider( + data_sources=model, + server=server_type, + region_scoping=zone_scoping + ).eval() + ug = dpf_fieldscontainer_to_vtk( + fields_container=fields_container, meshes_container=meshes_container + ) + assert ug.GetNumberOfCells() == 1144 + assert sorted(list(ug.cell_data.keys())) == [ + "tau_w {'time': 1, 'zone': 3}", + "tau_w {'time': 1, 'zone': 4}", + "tau_w {'time': 1, 'zone': 7}"] + pv.plot(ug) + + +@pytest.mark.xfail(raises=errors.DpfVersionNotSupported) +@pytest.mark.skipif(not HAS_PYVISTA, reason="Please install pyvista") +def test_dpf_property_field_to_vtk(simple_rst, server_type): + model = dpf.Model(simple_rst, server=server_type) + mesh = model.metadata.meshed_region + property_field = mesh.property_field(property_name="mat") + print(property_field) + property_field.name = "mat_id" + # PropertyField to VTK + ug = dpf_property_field_to_vtk(property_field=property_field, meshed_region=mesh) + assert isinstance(ug, pv.UnstructuredGrid) + assert "mat_id" in ug.cell_data.keys() + pv.plot(ug)