Skip to content

Commit 3fca741

Browse files
anslpaakaszynskiMaxJPRey
authored
Plotter: multiple mesh (#130)
* meshes_container.plot() * raise if meshes_container and fields_container does not have the same scope (MeshesContainer.plot(fields_container) * Fix unit test * Add docstring to plotter.add_field() and plotter.show_figure() * use field support in case of plotter.add_field(field, mesh=none) is set * meshes_container.plot() without data, docstring added * plotter.add_mesh() and testing added * path plot working * Plotter updated (fixes for path) * Fix unit tests -> all working * Remove the mesh attribute from the plotter * flake8 compliancy * Fix doc API strings * Flake8 compliancy * flake8 compliancy * Fix examples * Black refactore of plotter.py and meshes_container.py to be flake8 compliant * Re-run ci/cd * Revert "Fix examples" This reverts commit 431c100. * Update plotter.__init__ to keep the mesh parameter as an argument taken into account * Fix flake8 compliancy (remove try import pyvista in the plot_contour method) * Argument order for plotter.plot_contour (fix unit tests) * Fix indentation * remove mesh= * create _InternalPlotter and DpfPlotter * Update meshes_container plotting * update tests * fix meshes_container.plot() * meshes_container.plot() -> notebook and colors supported * CI/CD fixes * Fix meshes_container.plot(fc) (colors) * Add examples. Still missin: example to compare results * Flake8 compliancy * Fix CI/CD (hasattr(pl, scalar_bar) issue * Example that compares results * Refactore examples - plotting section added * Change model that we plot the path on * Flake8 compliancy * Add README.txt * Update tests/test_plotter.py Co-authored-by: Alex Kaszynski <[email protected]> * Update tests/test_plotter.py Co-authored-by: Alex Kaszynski <[email protected]> * Apply suggestions from code review Update PR regarding review Co-authored-by: Alex Kaszynski <[email protected]> * Update with review comments (local) * flake8 compliancy * flake8 compliancy * class DpfPlotter link added in examples * Remove "please" mention * Fix merge issues * Update example on path (read stresses) * Update ansys/dpf/core/meshes_container.py Update with PR review Co-authored-by: Maxime Rey <[email protected]> * Update examples with comments (review) Co-authored-by: Alex Kaszynski <[email protected]> Co-authored-by: Maxime Rey <[email protected]>
1 parent 3d9c327 commit 3fca741

File tree

6 files changed

+532
-18
lines changed

6 files changed

+532
-18
lines changed

ansys/dpf/core/meshes_container.py

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
from ansys import dpf
88
from ansys.dpf.core.collection import Collection
99
from ansys.dpf.core.common import types
10+
from ansys.dpf.core.plotter import DpfPlotter
11+
from ansys.dpf.core import errors as dpf_errors
1012

1113

1214
class MeshesContainer(Collection):
@@ -26,7 +28,7 @@ class MeshesContainer(Collection):
2628

2729
def __init__(self, meshes_container=None, server=None):
2830
"""Initialize the scoping with either an optional scoping
29-
message or by connecting to a stub."""
31+
message or by connecting to a stub. """
3032
if server is None:
3133
server = dpf.core._global_server()
3234

@@ -37,6 +39,57 @@ def __init__(self, meshes_container=None, server=None):
3739
self, types.meshed_region, collection=meshes_container, server=self._server
3840
)
3941

42+
def plot(self, fields_container=None, **kwargs):
43+
"""Plot the meshes container with a specific result if
44+
fields_container is specified.
45+
46+
Parameters
47+
----------
48+
fields_container : FieldsContainer, optional
49+
Data to plot. The default is ``None``.
50+
51+
Examples
52+
--------
53+
>>> from ansys.dpf import core as dpf
54+
>>> from ansys.dpf.core import examples
55+
>>> model = dpf.Model(examples.multishells_rst)
56+
>>> mesh = model.metadata.meshed_region
57+
>>> split_mesh_op = dpf.Operator("split_mesh")
58+
>>> split_mesh_op.connect(7, mesh)
59+
>>> split_mesh_op.connect(13, "mat")
60+
>>> meshes_cont = split_mesh_op.outputs.mesh_controller()
61+
>>> disp_op = dpf.Operator("U")
62+
>>> disp_op.connect(7, meshes_cont)
63+
>>> ds = dpf.DataSources(examples.multishells_rst)
64+
>>> disp_op.connect(4, ds)
65+
>>> disp_fc = disp_op.outputs.fields_container()
66+
>>> meshes_cont.plot(disp_fc)
67+
68+
"""
69+
kwargs.setdefault("show_edges", True)
70+
notebook = kwargs.pop("notebook", None)
71+
pl = DpfPlotter(notebook=notebook)
72+
if fields_container is not None:
73+
for i in range(len(fields_container)):
74+
label_space = fields_container.get_label_space(i)
75+
mesh_to_send = self.get_mesh(label_space)
76+
if mesh_to_send is None:
77+
raise dpf_errors.DpfValueError(
78+
"Meshes container and result fields "
79+
"container do not have the same scope. "
80+
"Plotting can not proceed. "
81+
)
82+
field = fields_container[i]
83+
pl.add_field(field, mesh_to_send, **kwargs)
84+
else:
85+
from random import random
86+
random_color = "color" not in kwargs
87+
for mesh in self:
88+
if random_color:
89+
kwargs["color"] = [random(), random(), random()]
90+
pl.add_mesh(mesh, **kwargs)
91+
pl.show_figure(**kwargs)
92+
4093
def get_meshes(self, label_space):
4194
"""Retrieve the meshes at a label space.
4295

ansys/dpf/core/plotter.py

Lines changed: 165 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,151 @@
1515
from ansys.dpf.core.check_version import meets_version
1616

1717

18+
class _InternalPlotter:
19+
def __init__(self, **kwargs):
20+
try:
21+
import pyvista as pv
22+
except ModuleNotFoundError:
23+
raise ModuleNotFoundError(
24+
"To use plotting capabilities, please install pyvista "
25+
"with :\n pip install pyvista>=0.24.0"
26+
)
27+
mesh = kwargs.pop("mesh", None)
28+
self._plotter = pv.Plotter(**kwargs)
29+
if mesh is not None:
30+
self._plotter.add_mesh(mesh.grid)
31+
32+
def add_mesh(self, meshed_region, **kwargs):
33+
has_attribute_scalar_bar = False
34+
try:
35+
has_attribute_scalar_bar = hasattr(self._plotter, 'scalar_bar')
36+
except:
37+
has_attribute_scalar_bar = False
38+
39+
if not has_attribute_scalar_bar:
40+
kwargs.setdefault("stitle", "Mesh")
41+
else:
42+
if self._plotter.scalar_bar.GetTitle() is None:
43+
kwargs.setdefault("stitle", "Mesh")
44+
kwargs.setdefault("show_edges", True)
45+
kwargs.setdefault("nan_color", "grey")
46+
self._plotter.add_mesh(meshed_region.grid, **kwargs)
47+
48+
def add_field(self, field, meshed_region=None, **kwargs):
49+
name = field.name.split("_")[0]
50+
kwargs.setdefault("stitle", name)
51+
kwargs.setdefault("show_edges", True)
52+
kwargs.setdefault("nan_color", "grey")
53+
54+
# get the meshed region location
55+
if meshed_region is None:
56+
meshed_region = field.meshed_region
57+
location = field.location
58+
if location == locations.nodal:
59+
mesh_location = meshed_region.nodes
60+
elif location == locations.elemental:
61+
mesh_location = meshed_region.elements
62+
else:
63+
raise ValueError(
64+
"Only elemental or nodal location are supported for plotting."
65+
)
66+
component_count = field.component_count
67+
if component_count > 1:
68+
overall_data = np.full((len(mesh_location), component_count), np.nan)
69+
else:
70+
overall_data = np.full(len(mesh_location), np.nan)
71+
ind, mask = mesh_location.map_scoping(field.scoping)
72+
overall_data[ind] = field.data[mask]
73+
74+
# plot
75+
self._plotter.add_mesh(meshed_region.grid, scalars=overall_data, **kwargs)
76+
77+
def show_figure(self, **kwargs):
78+
background = kwargs.pop("background", None)
79+
if background is not None:
80+
self._plotter.set_background(background)
81+
82+
# show result
83+
show_axes = kwargs.pop("show_axes", None)
84+
if show_axes:
85+
self._plotter.add_axes()
86+
return self._plotter.show()
87+
88+
89+
class DpfPlotter:
90+
def __init__(self, **kwargs):
91+
self._internal_plotter = _InternalPlotter(**kwargs)
92+
93+
def add_mesh(self, meshed_region, **kwargs):
94+
"""Add a mesh to plot.
95+
96+
Parameters
97+
----------
98+
meshed_region : MeshedRegion
99+
MeshedRegion to plot.
100+
101+
Examples
102+
--------
103+
>>> from ansys.dpf import core as dpf
104+
>>> from ansys.dpf.core import examples
105+
>>> model = dpf.Model(examples.multishells_rst)
106+
>>> mesh = model.metadata.meshed_region
107+
>>> from ansys.dpf.core.plotter import DpfPlotter
108+
>>> pl = DpfPlotter()
109+
>>> pl.add_mesh(mesh)
110+
111+
"""
112+
self._internal_plotter.add_mesh(meshed_region=meshed_region, **kwargs)
113+
114+
def add_field(self, field, meshed_region=None, **kwargs):
115+
"""Add a field containing data to the plotter.
116+
117+
A meshed_region to plot on can be added.
118+
If no ``meshed_region`` is specified, the field
119+
support will be used. Ensure that the field
120+
support is a ``meshed_region``.
121+
122+
Parameters
123+
----------
124+
field : Field
125+
Field data to plot
126+
meshed_region : MeshedRegion, optional
127+
``MeshedRegion`` to plot the field on.
128+
129+
Examples
130+
--------
131+
>>> from ansys.dpf import core as dpf
132+
>>> from ansys.dpf.core import examples
133+
>>> model = dpf.Model(examples.multishells_rst)
134+
>>> mesh = model.metadata.meshed_region
135+
>>> field = model.results.displacement().outputs.fields_container()[0]
136+
>>> from ansys.dpf.core.plotter import DpfPlotter
137+
>>> pl = DpfPlotter()
138+
>>> pl.add_field(field, mesh)
139+
140+
"""
141+
self._internal_plotter.add_field(field=field,
142+
meshed_region=meshed_region,
143+
**kwargs)
144+
145+
def show_figure(self, **kwargs):
146+
"""Plot the figure built by the plotter object.
147+
148+
Examples
149+
--------
150+
>>> from ansys.dpf import core as dpf
151+
>>> from ansys.dpf.core import examples
152+
>>> model = dpf.Model(examples.multishells_rst)
153+
>>> mesh = model.metadata.meshed_region
154+
>>> field = model.results.displacement().outputs.fields_container()[0]
155+
>>> from ansys.dpf.core.plotter import DpfPlotter
156+
>>> pl = DpfPlotter()
157+
>>> pl.add_field(field, mesh)
158+
>>> pl.show_figure()
159+
160+
"""
161+
self._internal_plotter.show_figure(**kwargs)
162+
18163
def plot_chart(fields_container):
19164
"""Plot the minimum/maximum result values over time.
20165
@@ -51,7 +196,8 @@ class Plotter:
51196
52197
"""
53198

54-
def __init__(self, mesh):
199+
def __init__(self, mesh, **kwargs):
200+
self._internal_plotter = _InternalPlotter(mesh=mesh, **kwargs)
55201
self._mesh = mesh
56202

57203
def plot_mesh(self, **kwargs):
@@ -140,6 +286,7 @@ def plot_contour(
140286
shell_layers=None,
141287
off_screen=None,
142288
show_axes=True,
289+
meshed_region=None,
143290
**kwargs
144291
):
145292
"""Plot the contour result on its mesh support.
@@ -202,7 +349,10 @@ def plot_contour(
202349
if label[DefinitionLabels.time] != first_time:
203350
raise dpf_errors.FieldContainerPlottingError
204351

205-
mesh = self._mesh
352+
if meshed_region is not None:
353+
mesh = meshed_region
354+
else:
355+
mesh = self._mesh
206356

207357
# get mesh scoping
208358
location = None
@@ -261,41 +411,39 @@ def plot_contour(
261411
cpos = kwargs.pop("cpos", None)
262412
return_cpos = kwargs.pop("return_cpos", None)
263413

264-
try:
265-
import pyvista as pv
266-
except ModuleNotFoundError:
267-
raise ModuleNotFoundError(
268-
"To use plotting capabilities, please install pyvista "
269-
"with :\n pip install pyvista>=0.24.0"
270-
)
271-
plotter = pv.Plotter(notebook=notebook, off_screen=off_screen)
414+
# plotter = pv.Plotter(notebook=notebook, off_screen=off_screen)
415+
if notebook is not None:
416+
self._internal_plotter._plotter.notebook = notebook
417+
if off_screen is not None:
418+
self._internal_plotter._plotter.off_screen = off_screen
272419

273420
# add meshes
274421
kwargs.setdefault("show_edges", True)
275422
kwargs.setdefault("nan_color", "grey")
276423
kwargs.setdefault("stitle", name)
277424
text = kwargs.pop('text', None)
278425
if text is not None:
279-
plotter.add_text(text, position='lower_edge')
280-
plotter.add_mesh(mesh.grid, scalars=overall_data, **kwargs)
426+
self._internal_plotter._plotter.add_text(text, position='lower_edge')
427+
self._internal_plotter._plotter.add_mesh(mesh.grid, scalars=overall_data, **kwargs)
281428

282429
if background is not None:
283-
plotter.set_background(background)
430+
self._internal_plotter._plotter.set_background(background)
284431

285432
if cpos is not None:
286-
plotter.camera_position = cpos
433+
self._internal_plotter._plotter.camera_position = cpos
287434

288435
# show result
289436
if show_axes:
290-
plotter.add_axes()
437+
self._internal_plotter._plotter.add_axes()
291438
if return_cpos is None:
292-
return plotter.show()
439+
return self._internal_plotter._plotter.show()
293440
else:
441+
import pyvista as pv
294442
pv_version = pv.__version__
295443
version_to_reach = '0.32.0'
296444
meet_ver = meets_version(pv_version, version_to_reach)
297445
if meet_ver:
298-
return plotter.show(return_cpos=return_cpos)
446+
return self._internal_plotter._plotter.show(return_cpos=return_cpos)
299447
else:
300448
txt = """To use the return_cpos option, please upgrade
301449
your pyvista module with a version higher than """
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
"""
2+
.. _plot_on_path:
3+
4+
Plot results on a specific path
5+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
6+
This example shows how to get a result mapped over a specific path,
7+
and how to plot it.
8+
9+
"""
10+
11+
from ansys.dpf import core as dpf
12+
from ansys.dpf.core import examples
13+
from ansys.dpf.core.plotter import DpfPlotter
14+
15+
###############################################################################
16+
# Path plotting
17+
# ~~~~~~~~~~~~~
18+
# We will use an :class:`ansys.dpf.core.plotter.DpfPlotter` to plot a mapped result over
19+
# a defined path of coordinates.
20+
21+
# First, we need to create the model, request its mesh and its
22+
# displacement data
23+
model = dpf.Model(examples.static_rst)
24+
mesh = model.metadata.meshed_region
25+
stress_fc = model.results.stress().eqv().outputs.fields_container()
26+
27+
###############################################################################
28+
# Then, we create a coordinates field to map on
29+
coordinates = []
30+
ref = [0.024, 0.03, 0.003]
31+
coordinates.append(ref)
32+
for i in range(1, 51):
33+
coord_copy = ref.copy()
34+
coord_copy[1] = coord_copy[0] + i * 0.001
35+
coordinates.append(coord_copy)
36+
field_coord = dpf.fields_factory.create_3d_vector_field(len(coordinates))
37+
field_coord.data = coordinates
38+
field_coord.scoping.ids = list(range(1, len(coordinates) + 1))
39+
40+
###############################################################################
41+
# Let's now compute the mapped data using the mapping operator
42+
mapping_operator = dpf.Operator("mapping")
43+
mapping_operator.inputs.fields_container.connect(stress_fc)
44+
mapping_operator.inputs.coordinates.connect(field_coord)
45+
mapping_operator.inputs.mesh.connect(mesh)
46+
mapping_operator.inputs.create_support.connect(True)
47+
fields_mapped = mapping_operator.outputs.fields_container()
48+
49+
###############################################################################
50+
# Here, we request the mapped field data and its mesh
51+
field_m = fields_mapped[0]
52+
mesh_m = field_m.meshed_region
53+
54+
###############################################################################
55+
# Now we create the plotter and add fields and meshes
56+
pl = DpfPlotter()
57+
58+
pl.add_field(field_m, mesh_m)
59+
pl.add_mesh(mesh, style="surface", show_edges=True,
60+
color="w", opacity=0.3)
61+
62+
# Finally we plot the result
63+
pl.show_figure(show_axes=True)

0 commit comments

Comments
 (0)