Skip to content

Commit a5dbf84

Browse files
authored
feat(plotter): enable plots of mesh scopings (#2377)
1 parent fb1fd63 commit a5dbf84

File tree

7 files changed

+311
-7
lines changed

7 files changed

+311
-7
lines changed

src/ansys/dpf/core/elements.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from __future__ import annotations
2626

2727
from enum import Enum
28+
from typing import TYPE_CHECKING
2829

2930
import numpy as np
3031

@@ -34,6 +35,9 @@
3435
from ansys.dpf.core.element_descriptor import ElementDescriptor
3536
from ansys.dpf.gate import integral_types
3637

38+
if TYPE_CHECKING: # pragma: no cover
39+
from ansys.dpf.core.scoping import Scoping
40+
3741

3842
class Element:
3943
"""
@@ -492,7 +496,7 @@ def __get_element(self, elementindex=None, elementid=None):
492496
return Element(self._mesh, elementid, elementindex, nodesOut)
493497

494498
@property
495-
def scoping(self) -> scoping.Scoping:
499+
def scoping(self) -> Scoping:
496500
"""
497501
Scoping of the elements.
498502

src/ansys/dpf/core/meshes_container.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
Contains classes associated with the DPF MeshesContainer.
2828
"""
2929

30+
from __future__ import annotations
31+
3032
from ansys.dpf.core import errors as dpf_errors, meshed_region
3133
from ansys.dpf.core.collection_base import CollectionBase
3234
from ansys.dpf.core.plotter import DpfPlotter
@@ -159,14 +161,14 @@ def get_meshes(self, label_space):
159161
"""
160162
return super()._get_entries(label_space)
161163

162-
def get_mesh(self, label_space_or_index):
164+
def get_mesh(self, label_space_or_index: int | dict[str, int]):
163165
"""Retrieve the mesh at a requested index or label space.
164166
165167
Raises an exception if the request returns more than one mesh.
166168
167169
Parameters
168170
----------
169-
label_space_or_index : dict[str,int] , int
171+
label_space_or_index:
170172
Scoping of the requested mesh, such as ``{"time": 1, "complex": 0}``
171173
or the index of the mesh.
172174

src/ansys/dpf/core/nodes.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,18 @@
2222

2323
"""Nodes."""
2424

25+
from __future__ import annotations
26+
27+
from typing import TYPE_CHECKING
28+
2529
import numpy as np
2630

2731
from ansys.dpf.core.check_version import version_requires
2832
from ansys.dpf.core.common import locations, nodal_properties
2933

34+
if TYPE_CHECKING: # pragma: no cover
35+
from ansys.dpf.core.scoping import Scoping
36+
3037

3138
class Node:
3239
"""
@@ -194,13 +201,13 @@ def __get_node(self, nodeindex=None, nodeid=None):
194201
return Node(self._mesh, nodeid, nodeindex, node_coordinates)
195202

196203
@property
197-
def scoping(self):
204+
def scoping(self) -> Scoping:
198205
"""
199206
Scoping of the nodes.
200207
201208
Returns
202209
-------
203-
scoping : Scoping
210+
scoping:
204211
Scoping of the nodes.
205212
206213
Examples

src/ansys/dpf/core/plotter.py

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@
3030

3131
from __future__ import annotations
3232

33-
import os
3433
from pathlib import Path
3534
import sys
3635
import tempfile
@@ -48,6 +47,7 @@
4847

4948
if TYPE_CHECKING: # pragma: no cover
5049
from ansys.dpf.core import Operator, Result
50+
from ansys.dpf.core.field import Field
5151
from ansys.dpf.core.fields_container import FieldsContainer
5252
from ansys.dpf.core.meshed_region import MeshedRegion
5353

@@ -233,6 +233,37 @@ def get_label_at_grid_point(index):
233233
)
234234
return label_actors
235235

236+
def add_scoping(
237+
self,
238+
scoping: dpf.core.Scoping,
239+
mesh: dpf.core.MeshedRegion,
240+
show_mesh: bool = False,
241+
**kwargs,
242+
):
243+
# Add the mesh to the scene with low opacity
244+
if show_mesh:
245+
self._plotter.add_mesh(mesh=mesh.grid, opacity=0.3)
246+
247+
scoping_mesh = None
248+
249+
# If the scoping is nodal, use the add_points_label method
250+
if scoping.location == locations.nodal:
251+
node_indexes = np.where(np.isin(mesh.nodes.scoping.ids, scoping.ids))[0]
252+
# grid_points = [mesh.grid.points[node_index] for node_index in node_indexes]
253+
scoping_mesh = mesh.grid.extract_points(ind=node_indexes, include_cells=False)
254+
# If the scoping is elemental, extract their edges and use active scalars to color them
255+
if scoping.location == locations.elemental:
256+
element_indexes = np.where(np.isin(mesh.elements.scoping.ids, scoping.ids))[0]
257+
scoping_mesh = mesh.grid.extract_cells(ind=element_indexes)
258+
259+
# If the scoping is faces, extract their edges and use active scalars to color them
260+
if scoping.location == locations.faces:
261+
raise NotImplementedError("Cannot plot a face scoping.")
262+
263+
# Filter kwargs
264+
kwargs_in = _sort_supported_kwargs(bound_method=self._plotter.add_mesh, **kwargs)
265+
self._plotter.add_mesh(mesh=scoping_mesh, **kwargs_in)
266+
236267
def add_field(
237268
self,
238269
field,
@@ -688,6 +719,55 @@ def add_field(
688719
**kwargs,
689720
)
690721

722+
def add_scoping(
723+
self,
724+
scoping: dpf.core.Scoping,
725+
mesh: dpf.core.MeshedRegion,
726+
show_mesh: bool = False,
727+
**kwargs,
728+
):
729+
"""Add a scoping to the plotter.
730+
731+
A mesh is required to translate the scoping into entities to plot.
732+
Tou can plot the mesh along with the scoping entities using ``show_mesh``.
733+
734+
Parameters
735+
----------
736+
scoping:
737+
Scoping with a mesh-based location and IDs of entities to plot.
738+
mesh:
739+
``MeshedRegion`` to plot the field on.
740+
show_mesh:
741+
Whether to show the mesh along with the scoping entities.
742+
**kwargs : optional
743+
Additional keyword arguments for the plotter. More information
744+
are available at :func:`pyvista.plot`.
745+
746+
Examples
747+
--------
748+
>>> from ansys.dpf import core as dpf
749+
>>> from ansys.dpf.core import examples
750+
>>> model = dpf.Model(examples.download_cfx_mixing_elbow())
751+
>>> mesh = model.metadata.meshed_region
752+
>>> node_scoping = dpf.Scoping(
753+
... location=dpf.locations.nodal,
754+
... ids=mesh.nodes.scoping.ids[0:100]
755+
...)
756+
>>> element_scoping = dpf.Scoping(
757+
... location=dpf.locations.elemental,
758+
... ids=mesh.elements.scoping.ids[0:100]
759+
...)
760+
>>> from ansys.dpf.core.plotter import DpfPlotter
761+
>>> plt = DpfPlotter()
762+
>>> plt.add_scoping(node_scoping, mesh, show_mesh=True, color="red")
763+
>>> plt.add_scoping(element_scoping, mesh, color="green")
764+
>>> plt.show_figure()
765+
766+
"""
767+
self._internal_plotter.add_scoping(
768+
scoping=scoping, mesh=mesh, show_mesh=show_mesh, **kwargs
769+
)
770+
691771
def show_figure(self, **kwargs):
692772
"""Plot the figure built by the plotter object.
693773

src/ansys/dpf/core/scoping.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -491,6 +491,48 @@ def as_local_scoping(self):
491491
""" # noqa: E501
492492
return _LocalScoping(self)
493493

494+
def plot(self, mesh, show_mesh: bool = False, **kwargs):
495+
"""Plot the entities of the mesh corresponding to the scoping.
496+
497+
Parameters
498+
----------
499+
mesh:
500+
Mesh to use to translate the scoping into mesh entities.
501+
show_mesh:
502+
Whether to also show the mesh with low opacity.
503+
**kwargs : optional
504+
Additional keyword arguments for the plotter. More information
505+
are available at :func:`pyvista.plot`.
506+
507+
Returns
508+
-------
509+
(cpos, image):
510+
Returns what the pyvista.show() method returns based on arguments.
511+
512+
Examples
513+
--------
514+
>>> from ansys.dpf import core as dpf
515+
>>> from ansys.dpf.core import examples
516+
>>> model = dpf.Model(examples.download_cfx_mixing_elbow())
517+
>>> mesh = model.metadata.meshed_region
518+
>>> node_scoping = dpf.Scoping(
519+
... location=dpf.locations.nodal,
520+
... ids=mesh.nodes.scoping.ids[0:100]
521+
...)
522+
>>> node_scoping.plot(mesh=mesh, color="red")
523+
>>> element_scoping = dpf.Scoping(
524+
... location=dpf.locations.elemental,
525+
... ids=mesh.elements.scoping.ids[0:100]
526+
...)
527+
>>> element_scoping.plot(mesh=mesh, color="green")
528+
529+
"""
530+
from ansys.dpf.core.plotter import DpfPlotter
531+
532+
plt = DpfPlotter(**kwargs)
533+
plt.add_scoping(scoping=self, mesh=mesh, show_mesh=show_mesh, **kwargs)
534+
return plt.show_figure(**kwargs)
535+
494536

495537
class _LocalScoping(Scoping):
496538
"""Caches the internal data of the scoping so that it can be modified locally.

src/ansys/dpf/core/scopings_container.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,17 @@
2828
Contains classes associated to the DPF ScopingsContainer
2929
"""
3030

31+
from __future__ import annotations
32+
33+
from typing import TYPE_CHECKING
34+
35+
import ansys.dpf.core as dpf
3136
from ansys.dpf.core import scoping
3237
from ansys.dpf.core.collection_base import CollectionBase
3338

39+
if TYPE_CHECKING: # pragma: no cover
40+
from ansys.dpf.core import MeshedRegion, MeshesContainer
41+
3442

3543
class ScopingsContainer(CollectionBase[scoping.Scoping]):
3644
"""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):
125133
DPF scoping to add.
126134
"""
127135
return super()._add_entry(label_space, scoping)
136+
137+
def plot(
138+
self,
139+
mesh: MeshedRegion | MeshesContainer,
140+
show_mesh: bool = False,
141+
colors: list[str] = None,
142+
**kwargs,
143+
):
144+
"""Plot the entities of the mesh or meshes corresponding to the scopings.
145+
146+
Parameters
147+
----------
148+
mesh:
149+
Mesh or meshes to use to translate the scopings into mesh entities.
150+
Associates each scoping to a mesh using labels if ``mesh`` is a MeshesContainer.
151+
show_mesh:
152+
Whether to also show the mesh with low opacity.
153+
colors:
154+
List of colors to use for the scoping entities.
155+
**kwargs : optional
156+
Additional keyword arguments for the plotter. More information
157+
are available at :func:`pyvista.plot`.
158+
159+
Returns
160+
-------
161+
(cpos, image):
162+
Returns what the pyvista.show() method returns based on arguments.
163+
164+
Examples
165+
--------
166+
>>> from ansys.dpf import core as dpf
167+
>>> from ansys.dpf.core import examples
168+
>>> model = dpf.Model(examples.download_cfx_mixing_elbow())
169+
>>> mesh = model.metadata.meshed_region
170+
>>> node_scoping_1 = dpf.Scoping(
171+
... location=dpf.locations.nodal,
172+
... ids=mesh.nodes.scoping.ids[0:100]
173+
...)
174+
>>> node_scoping_2 = dpf.Scoping(
175+
... location=dpf.locations.nodal,
176+
... ids=mesh.nodes.scoping.ids[300:400]
177+
...)
178+
>>> node_sc = dpf.ScopingsContainer()
179+
>>> node_sc.add_label(label="scoping", default_value=1)
180+
>>> node_sc.add_scoping(label_space={"scoping": 1}, scoping=node_scoping_1)
181+
>>> node_sc.add_scoping(label_space={"scoping": 2}, scoping=node_scoping_2)
182+
>>> node_sc.plot(mesh=mesh, show_mesh=True)
183+
184+
"""
185+
from itertools import cycle
186+
187+
from ansys.dpf.core.plotter import DpfPlotter
188+
189+
colors_cycle = cycle(
190+
colors if colors else ["red", "blue", "green", "orange", "black", "yellow"]
191+
)
192+
plt = DpfPlotter(**kwargs)
193+
for i, scoping_i in enumerate(self):
194+
if isinstance(mesh, dpf.MeshedRegion):
195+
show_mesh_i = show_mesh if i == 0 else False
196+
mesh_i = mesh
197+
elif isinstance(mesh, dpf.MeshesContainer):
198+
show_mesh_i = True
199+
mesh_i = mesh.get_mesh(label_space_or_index=self.get_label_space(index=i))
200+
if mesh_i is None:
201+
raise ValueError(
202+
f"ScopingsContainer.plot: could not associate a mesh to the scoping for label '{self.get_label_space(index=i)}'."
203+
)
204+
else:
205+
raise ValueError(
206+
f"ScopingsContainer.plot: type '{type(mesh)}' is not a valid type for argument 'mesh'."
207+
)
208+
plt.add_scoping(
209+
scoping=scoping_i,
210+
mesh=mesh_i,
211+
color=next(colors_cycle),
212+
show_mesh=show_mesh_i,
213+
**kwargs,
214+
)
215+
return plt.show_figure(**kwargs)

0 commit comments

Comments
 (0)