diff --git a/src/ansys/dpf/core/elements.py b/src/ansys/dpf/core/elements.py index 70c9c478d62..2cff1b09988 100644 --- a/src/ansys/dpf/core/elements.py +++ b/src/ansys/dpf/core/elements.py @@ -25,6 +25,7 @@ from __future__ import annotations from enum import Enum +from typing import TYPE_CHECKING import numpy as np @@ -34,6 +35,9 @@ from ansys.dpf.core.element_descriptor import ElementDescriptor from ansys.dpf.gate import integral_types +if TYPE_CHECKING: # pragma: no cover + from ansys.dpf.core.scoping import Scoping + class Element: """ @@ -492,7 +496,7 @@ def __get_element(self, elementindex=None, elementid=None): return Element(self._mesh, elementid, elementindex, nodesOut) @property - def scoping(self) -> scoping.Scoping: + def scoping(self) -> Scoping: """ Scoping of the elements. diff --git a/src/ansys/dpf/core/meshes_container.py b/src/ansys/dpf/core/meshes_container.py index 6b722bff1a6..98a583ac27e 100644 --- a/src/ansys/dpf/core/meshes_container.py +++ b/src/ansys/dpf/core/meshes_container.py @@ -27,6 +27,8 @@ Contains classes associated with the DPF MeshesContainer. """ +from __future__ import annotations + from ansys.dpf.core import errors as dpf_errors, meshed_region from ansys.dpf.core.collection_base import CollectionBase from ansys.dpf.core.plotter import DpfPlotter @@ -159,14 +161,14 @@ def get_meshes(self, label_space): """ return super()._get_entries(label_space) - def get_mesh(self, label_space_or_index): + def get_mesh(self, label_space_or_index: int | dict[str, int]): """Retrieve the mesh at a requested index or label space. Raises an exception if the request returns more than one mesh. Parameters ---------- - label_space_or_index : dict[str,int] , int + label_space_or_index: Scoping of the requested mesh, such as ``{"time": 1, "complex": 0}`` or the index of the mesh. diff --git a/src/ansys/dpf/core/nodes.py b/src/ansys/dpf/core/nodes.py index 668922148ea..11748960e0e 100644 --- a/src/ansys/dpf/core/nodes.py +++ b/src/ansys/dpf/core/nodes.py @@ -22,11 +22,18 @@ """Nodes.""" +from __future__ import annotations + +from typing import TYPE_CHECKING + import numpy as np from ansys.dpf.core.check_version import version_requires from ansys.dpf.core.common import locations, nodal_properties +if TYPE_CHECKING: # pragma: no cover + from ansys.dpf.core.scoping import Scoping + class Node: """ @@ -194,13 +201,13 @@ def __get_node(self, nodeindex=None, nodeid=None): return Node(self._mesh, nodeid, nodeindex, node_coordinates) @property - def scoping(self): + def scoping(self) -> Scoping: """ Scoping of the nodes. Returns ------- - scoping : Scoping + scoping: Scoping of the nodes. Examples diff --git a/src/ansys/dpf/core/plotter.py b/src/ansys/dpf/core/plotter.py index 2978046899f..8a892f1d1e4 100644 --- a/src/ansys/dpf/core/plotter.py +++ b/src/ansys/dpf/core/plotter.py @@ -30,7 +30,6 @@ from __future__ import annotations -import os from pathlib import Path import sys import tempfile @@ -48,6 +47,7 @@ if TYPE_CHECKING: # pragma: no cover from ansys.dpf.core import Operator, Result + from ansys.dpf.core.field import Field from ansys.dpf.core.fields_container import FieldsContainer from ansys.dpf.core.meshed_region import MeshedRegion @@ -233,6 +233,37 @@ def get_label_at_grid_point(index): ) return label_actors + def add_scoping( + self, + scoping: dpf.core.Scoping, + mesh: dpf.core.MeshedRegion, + show_mesh: bool = False, + **kwargs, + ): + # Add the mesh to the scene with low opacity + if show_mesh: + self._plotter.add_mesh(mesh=mesh.grid, opacity=0.3) + + scoping_mesh = None + + # If the scoping is nodal, use the add_points_label method + if scoping.location == locations.nodal: + node_indexes = np.where(np.isin(mesh.nodes.scoping.ids, scoping.ids))[0] + # grid_points = [mesh.grid.points[node_index] for node_index in node_indexes] + scoping_mesh = mesh.grid.extract_points(ind=node_indexes, include_cells=False) + # If the scoping is elemental, extract their edges and use active scalars to color them + if scoping.location == locations.elemental: + element_indexes = np.where(np.isin(mesh.elements.scoping.ids, scoping.ids))[0] + scoping_mesh = mesh.grid.extract_cells(ind=element_indexes) + + # If the scoping is faces, extract their edges and use active scalars to color them + if scoping.location == locations.faces: + raise NotImplementedError("Cannot plot a face scoping.") + + # Filter kwargs + kwargs_in = _sort_supported_kwargs(bound_method=self._plotter.add_mesh, **kwargs) + self._plotter.add_mesh(mesh=scoping_mesh, **kwargs_in) + def add_field( self, field, @@ -688,6 +719,55 @@ def add_field( **kwargs, ) + def add_scoping( + self, + scoping: dpf.core.Scoping, + mesh: dpf.core.MeshedRegion, + show_mesh: bool = False, + **kwargs, + ): + """Add a scoping to the plotter. + + A mesh is required to translate the scoping into entities to plot. + Tou can plot the mesh along with the scoping entities using ``show_mesh``. + + Parameters + ---------- + scoping: + Scoping with a mesh-based location and IDs of entities to plot. + mesh: + ``MeshedRegion`` to plot the field on. + show_mesh: + Whether to show the mesh along with the scoping entities. + **kwargs : optional + Additional keyword arguments for the plotter. More information + are available at :func:`pyvista.plot`. + + Examples + -------- + >>> from ansys.dpf import core as dpf + >>> from ansys.dpf.core import examples + >>> model = dpf.Model(examples.download_cfx_mixing_elbow()) + >>> mesh = model.metadata.meshed_region + >>> node_scoping = dpf.Scoping( + ... location=dpf.locations.nodal, + ... ids=mesh.nodes.scoping.ids[0:100] + ...) + >>> element_scoping = dpf.Scoping( + ... location=dpf.locations.elemental, + ... ids=mesh.elements.scoping.ids[0:100] + ...) + >>> from ansys.dpf.core.plotter import DpfPlotter + >>> plt = DpfPlotter() + >>> plt.add_scoping(node_scoping, mesh, show_mesh=True, color="red") + >>> plt.add_scoping(element_scoping, mesh, color="green") + >>> plt.show_figure() + + """ + self._internal_plotter.add_scoping( + scoping=scoping, mesh=mesh, show_mesh=show_mesh, **kwargs + ) + def show_figure(self, **kwargs): """Plot the figure built by the plotter object. diff --git a/src/ansys/dpf/core/scoping.py b/src/ansys/dpf/core/scoping.py index 4a2457e1b91..597d42175ef 100644 --- a/src/ansys/dpf/core/scoping.py +++ b/src/ansys/dpf/core/scoping.py @@ -491,6 +491,48 @@ def as_local_scoping(self): """ # noqa: E501 return _LocalScoping(self) + def plot(self, mesh, show_mesh: bool = False, **kwargs): + """Plot the entities of the mesh corresponding to the scoping. + + Parameters + ---------- + mesh: + Mesh to use to translate the scoping into mesh entities. + show_mesh: + Whether to also show the mesh with low opacity. + **kwargs : optional + Additional keyword arguments for the plotter. More information + are available at :func:`pyvista.plot`. + + Returns + ------- + (cpos, image): + Returns what the pyvista.show() method returns based on arguments. + + Examples + -------- + >>> from ansys.dpf import core as dpf + >>> from ansys.dpf.core import examples + >>> model = dpf.Model(examples.download_cfx_mixing_elbow()) + >>> mesh = model.metadata.meshed_region + >>> node_scoping = dpf.Scoping( + ... location=dpf.locations.nodal, + ... ids=mesh.nodes.scoping.ids[0:100] + ...) + >>> node_scoping.plot(mesh=mesh, color="red") + >>> element_scoping = dpf.Scoping( + ... location=dpf.locations.elemental, + ... ids=mesh.elements.scoping.ids[0:100] + ...) + >>> element_scoping.plot(mesh=mesh, color="green") + + """ + from ansys.dpf.core.plotter import DpfPlotter + + plt = DpfPlotter(**kwargs) + plt.add_scoping(scoping=self, mesh=mesh, show_mesh=show_mesh, **kwargs) + return plt.show_figure(**kwargs) + class _LocalScoping(Scoping): """Caches the internal data of the scoping so that it can be modified locally. diff --git a/src/ansys/dpf/core/scopings_container.py b/src/ansys/dpf/core/scopings_container.py index c076b9fdc64..1ce834b6ada 100644 --- a/src/ansys/dpf/core/scopings_container.py +++ b/src/ansys/dpf/core/scopings_container.py @@ -28,9 +28,17 @@ Contains classes associated to the DPF ScopingsContainer """ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import ansys.dpf.core as dpf from ansys.dpf.core import scoping from ansys.dpf.core.collection_base import CollectionBase +if TYPE_CHECKING: # pragma: no cover + from ansys.dpf.core import MeshedRegion, MeshesContainer + class ScopingsContainer(CollectionBase[scoping.Scoping]): """A class used to represent a ScopingsContainer which contains scopings split on a given space. @@ -125,3 +133,83 @@ def add_scoping(self, label_space, scoping): DPF scoping to add. """ return super()._add_entry(label_space, scoping) + + def plot( + self, + mesh: MeshedRegion | MeshesContainer, + show_mesh: bool = False, + colors: list[str] = None, + **kwargs, + ): + """Plot the entities of the mesh or meshes corresponding to the scopings. + + Parameters + ---------- + mesh: + Mesh or meshes to use to translate the scopings into mesh entities. + Associates each scoping to a mesh using labels if ``mesh`` is a MeshesContainer. + show_mesh: + Whether to also show the mesh with low opacity. + colors: + List of colors to use for the scoping entities. + **kwargs : optional + Additional keyword arguments for the plotter. More information + are available at :func:`pyvista.plot`. + + Returns + ------- + (cpos, image): + Returns what the pyvista.show() method returns based on arguments. + + Examples + -------- + >>> from ansys.dpf import core as dpf + >>> from ansys.dpf.core import examples + >>> model = dpf.Model(examples.download_cfx_mixing_elbow()) + >>> mesh = model.metadata.meshed_region + >>> node_scoping_1 = dpf.Scoping( + ... location=dpf.locations.nodal, + ... ids=mesh.nodes.scoping.ids[0:100] + ...) + >>> node_scoping_2 = dpf.Scoping( + ... location=dpf.locations.nodal, + ... ids=mesh.nodes.scoping.ids[300:400] + ...) + >>> node_sc = dpf.ScopingsContainer() + >>> node_sc.add_label(label="scoping", default_value=1) + >>> node_sc.add_scoping(label_space={"scoping": 1}, scoping=node_scoping_1) + >>> node_sc.add_scoping(label_space={"scoping": 2}, scoping=node_scoping_2) + >>> node_sc.plot(mesh=mesh, show_mesh=True) + + """ + from itertools import cycle + + from ansys.dpf.core.plotter import DpfPlotter + + colors_cycle = cycle( + colors if colors else ["red", "blue", "green", "orange", "black", "yellow"] + ) + plt = DpfPlotter(**kwargs) + for i, scoping_i in enumerate(self): + if isinstance(mesh, dpf.MeshedRegion): + show_mesh_i = show_mesh if i == 0 else False + mesh_i = mesh + elif isinstance(mesh, dpf.MeshesContainer): + show_mesh_i = True + mesh_i = mesh.get_mesh(label_space_or_index=self.get_label_space(index=i)) + if mesh_i is None: + raise ValueError( + f"ScopingsContainer.plot: could not associate a mesh to the scoping for label '{self.get_label_space(index=i)}'." + ) + else: + raise ValueError( + f"ScopingsContainer.plot: type '{type(mesh)}' is not a valid type for argument 'mesh'." + ) + plt.add_scoping( + scoping=scoping_i, + mesh=mesh_i, + color=next(colors_cycle), + show_mesh=show_mesh_i, + **kwargs, + ) + return plt.show_figure(**kwargs) diff --git a/tests/test_plotter.py b/tests/test_plotter.py index ac3176134e6..70599d73945 100644 --- a/tests/test_plotter.py +++ b/tests/test_plotter.py @@ -28,7 +28,11 @@ from ansys.dpf import core from ansys.dpf.core import Model, Operator, element_types, errors as dpf_errors, misc from ansys.dpf.core.plotter import plot_chart -from conftest import SERVERS_VERSION_GREATER_THAN_OR_EQUAL_TO_5_0, running_docker +from conftest import ( + SERVERS_VERSION_GREATER_THAN_OR_EQUAL_TO_5_0, + SERVERS_VERSION_GREATER_THAN_OR_EQUAL_TO_7_0, + running_docker, +) if misc.module_exists("pyvista"): HAS_PYVISTA = True @@ -809,3 +813,80 @@ def test_plot_polyhedron(): # Plot the MeshedRegion mesh.plot() + + +@pytest.mark.skipif(not HAS_PYVISTA, reason="This test requires pyvista") +@pytest.mark.skipif( + not SERVERS_VERSION_GREATER_THAN_OR_EQUAL_TO_7_0, + reason="cff::cas::meshes_provider requires DPF 24R1", +) +def test_plotter_add_scoping(fluent_mixing_elbow_steady_state): + mesh: core.MeshedRegion = core.operators.mesh.mesh_provider( + data_sources=fluent_mixing_elbow_steady_state() + ).eval() + node_scoping = core.Scoping(location=core.locations.nodal, ids=mesh.nodes.scoping.ids[0:100]) + element_scoping = core.Scoping( + location=core.locations.elemental, ids=mesh.elements.scoping.ids[0:100] + ) + plt = DpfPlotter() + plt.add_scoping(node_scoping, mesh, show_mesh=True, color="red") + plt.add_scoping(element_scoping, mesh, color="green") + plt.show_figure() + + face_scoping = core.Scoping(location=core.locations.faces, ids=mesh.faces.scoping.ids[0:100]) + with pytest.raises(NotImplementedError): + plt.add_scoping(face_scoping, mesh) + + +@pytest.mark.skipif(not HAS_PYVISTA, reason="This test requires pyvista") +@pytest.mark.skipif( + not SERVERS_VERSION_GREATER_THAN_OR_EQUAL_TO_7_0, + reason="cff::cas::meshes_provider requires DPF 24R1", +) +def test_scoping_plot(fluent_mixing_elbow_steady_state): + mesh: core.MeshedRegion = core.operators.mesh.mesh_provider( + data_sources=fluent_mixing_elbow_steady_state() + ).eval() + node_scoping = core.Scoping(location=core.locations.nodal, ids=mesh.nodes.scoping.ids[0:100]) + node_scoping.plot(mesh=mesh, color="red") + element_scoping = core.Scoping( + location=core.locations.elemental, ids=mesh.elements.scoping.ids[0:100] + ) + element_scoping.plot(mesh=mesh, color="green") + + +@pytest.mark.skipif(not HAS_PYVISTA, reason="This test requires pyvista") +@pytest.mark.skipif( + not SERVERS_VERSION_GREATER_THAN_OR_EQUAL_TO_7_0, + reason="cff::cas::meshes_provider requires DPF 24R1", +) +def test_scopings_container_plot(fluent_mixing_elbow_steady_state): + mesh: core.MeshedRegion = core.operators.mesh.mesh_provider( + data_sources=fluent_mixing_elbow_steady_state() + ).eval() + node_scoping_1 = core.Scoping(location=core.locations.nodal, ids=mesh.nodes.scoping.ids[0:100]) + node_scoping_2 = core.Scoping( + location=core.locations.nodal, ids=mesh.nodes.scoping.ids[300:400] + ) + node_sc = core.ScopingsContainer() + node_sc.add_label(label="scoping", default_value=1) + node_sc.add_scoping(label_space={"scoping": 1}, scoping=node_scoping_1) + node_sc.add_scoping(label_space={"scoping": 2}, scoping=node_scoping_2) + node_sc.plot(mesh=mesh, show_mesh=True) + + meshes: core.MeshesContainer = core.operators.mesh.meshes_provider( + data_sources=fluent_mixing_elbow_steady_state() + ).eval() + + with pytest.raises(ValueError, match="could not associate a mesh to the scoping for label"): + node_sc.plot(mesh=meshes) + + label_space = {"time": 1, "zone": 4} + node_scoping_3 = core.Scoping( + location=core.locations.nodal, ids=meshes.get_mesh(label_space).nodes.scoping.ids[0:100] + ) + node_sc_2 = core.ScopingsContainer() + node_sc_2.add_label(label="time", default_value=1) + node_sc_2.add_label(label="zone") + node_sc_2.add_scoping(label_space=label_space, scoping=node_scoping_3) + node_sc_2.plot(mesh=meshes)