Skip to content

Commit 9ab5943

Browse files
PProfiziansys-akarcher
authored andcommitted
Enable plots of ElementalNodal data (#511)
1 parent 04874b8 commit 9ab5943

File tree

5 files changed

+284
-37
lines changed

5 files changed

+284
-37
lines changed

examples/06-plotting/00-basic_plotting.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,8 @@
5252
# - The "off_screen" keyword only works when "notebook=False" to prevent the GUI from appearing.
5353

5454

55-
# Plot a field on its supporting mesh (field location must be Elemental or Nodal)
55+
# Plot a field on its supporting mesh
5656
stress = model.results.stress()
57-
stress.inputs.requested_location.connect(dpf.locations.nodal)
5857
fc = stress.outputs.fields_container()
5958
field = fc[0]
6059
field.plot(notebook=False, shell_layers=None, show_axes=True, title="Field", text="Field plot")

src/ansys/dpf/core/elements.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -677,7 +677,7 @@ def map_scoping(self, external_scope):
677677
678678
Examples
679679
--------
680-
Return the indices that map a field to an elements collection.
680+
Return the indices that map a field to an Elements collection.
681681
682682
>>> import ansys.dpf.core as dpf
683683
>>> from ansys.dpf.core import examples
@@ -689,7 +689,7 @@ def map_scoping(self, external_scope):
689689
690690
"""
691691
if external_scope.location in ["Nodal", "NodalElemental"]:
692-
raise ValueError('Input scope location must be "Nodal"')
692+
raise ValueError('Input scope location must be "Elemental"')
693693
arr = np.array(list(map(self.mapping_id_to_index.get, external_scope.ids)))
694694
mask = arr != None
695695
ind = arr[mask].astype(np.int32)

src/ansys/dpf/core/meshed_region.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333
import traceback
3434
import warnings
3535

36+
import numpy as np
37+
3638
from ansys.dpf.core import field, property_field, scoping, server as server_module
3739
from ansys.dpf.core.cache import class_handling_cache
3840
from ansys.dpf.core.check_version import meets_version, server_meet_version, version_requires
@@ -713,3 +715,48 @@ def is_empty(self) -> bool:
713715
no_elements = self.elements.n_elements == 0
714716
no_nodes = self.nodes.n_nodes == 0
715717
return no_nodes and no_faces and no_elements
718+
719+
def location_data_len(self, location: locations) -> int:
720+
"""Return the data length for a given mesh location.
721+
722+
Accepted mesh locations are nodal, elemental, faces, and elemental_nodal.
723+
724+
Parameters
725+
----------
726+
location:
727+
The mesh location to compute data length for.
728+
Can be nodal, elemental, faces, or elemental_nodal.
729+
730+
Returns
731+
-------
732+
data_size:
733+
If location is nodal, return the number of nodes.
734+
If location is elemental, return the number of elements.
735+
If location is faces, return the number of faces.
736+
If location is elemental nodal, return the sum of the number of nodes per element.
737+
"""
738+
if location == locations.nodal:
739+
return len(self.nodes)
740+
elif location == locations.elemental:
741+
return len(self.elements)
742+
elif location == locations.faces:
743+
return len(self.faces)
744+
elif location == locations.elemental_nodal:
745+
return np.sum(self.get_elemental_nodal_size_list())
746+
elif location == locations.overall:
747+
return len(self.elements)
748+
else:
749+
raise TypeError(f"Location {location} is not recognized.")
750+
751+
def get_elemental_nodal_size_list(self) -> np.ndarray:
752+
"""Return the array of number of nodes per element in the mesh."""
753+
# Get the field of element types
754+
element_types_field = self.elements.element_types_field
755+
# get the number of nodes for each possible element type
756+
size_map = dict(
757+
[(e_type.value, element_types.descriptor(e_type).n_nodes) for e_type in element_types]
758+
)
759+
keys = list(size_map.keys())
760+
sort_idx = np.argsort(keys)
761+
idx = np.searchsorted(keys, element_types_field.data, sorter=sort_idx)
762+
return np.asarray(list(size_map.values()))[sort_idx][idx]

src/ansys/dpf/core/plotter.py

Lines changed: 119 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ def add_mesh(self, meshed_region, deform_by=None, scale_factor=1.0, as_linear=Tr
147147
grid = meshed_region._as_vtk(
148148
meshed_region.nodes.coordinates_field, as_linear=as_linear
149149
)
150+
meshed_region._full_grid = grid
150151
meshed_region.as_linear = as_linear
151152
else:
152153
grid = meshed_region.grid
@@ -317,8 +318,14 @@ def add_field(
317318
show_min = False
318319
elif location == locations.overall:
319320
mesh_location = meshed_region.elements
321+
elif location == locations.elemental_nodal:
322+
mesh_location = meshed_region.elements
323+
# If ElementalNodal, first extend results to mid-nodes
324+
field = dpf.core.operators.averaging.extend_to_mid_nodes(field=field).eval()
320325
else:
321-
raise ValueError("Only elemental, nodal or faces location are supported for plotting.")
326+
raise ValueError(
327+
"Only elemental, elemental nodal, nodal, faces, or overall location are supported for plotting."
328+
)
322329

323330
# Treat multilayered shells
324331
if not isinstance(shell_layer, eshell_layers):
@@ -333,13 +340,27 @@ def add_field(
333340
)
334341
field = change_shell_layer_op.get_output(0, core.types.field)
335342

343+
location_data_len = meshed_region.location_data_len(location)
336344
component_count = field.component_count
337345
if component_count > 1:
338-
overall_data = np.full((len(mesh_location), component_count), np.nan)
346+
overall_data = np.full((location_data_len, component_count), np.nan)
339347
else:
340-
overall_data = np.full(len(mesh_location), np.nan)
348+
overall_data = np.full(location_data_len, np.nan)
341349
if location != locations.overall:
342350
ind, mask = mesh_location.map_scoping(field.scoping)
351+
352+
# Rework ind and mask to take into account n_nodes per element if ElementalNodal
353+
if location == locations.elemental_nodal:
354+
n_nodes_list = meshed_region.get_elemental_nodal_size_list().astype(np.int32)
355+
first_index = np.insert(np.cumsum(n_nodes_list)[:-1], 0, 0).astype(np.int32)
356+
mask_2 = np.asarray(
357+
[mask_i for i, mask_i in enumerate(mask) for _ in range(n_nodes_list[ind[i]])]
358+
)
359+
ind_2 = np.asarray(
360+
[first_index[ind_i] + j for ind_i in ind for j in range(n_nodes_list[ind_i])]
361+
)
362+
mask = mask_2
363+
ind = ind_2
343364
overall_data[ind] = field.data[mask]
344365
else:
345366
overall_data[:] = field.data[0]
@@ -348,12 +369,22 @@ def add_field(
348369
# Have to remove any active scalar field from the pre-existing grid object,
349370
# otherwise we get two scalar bars when calling several plot_contour on the same mesh
350371
# but not for the same field. The PyVista UnstructuredGrid keeps memory of it.
351-
if not deform_by:
352-
grid = meshed_region.grid
353-
else:
372+
if location == locations.elemental_nodal:
373+
as_linear = False
374+
if deform_by:
354375
grid = meshed_region._as_vtk(
355-
meshed_region.deform_by(deform_by, scale_factor), as_linear
376+
meshed_region.deform_by(deform_by, scale_factor), as_linear=as_linear
356377
)
378+
else:
379+
if as_linear != meshed_region.as_linear:
380+
grid = meshed_region._as_vtk(
381+
meshed_region.nodes.coordinates_field, as_linear=as_linear
382+
)
383+
meshed_region.as_linear = as_linear
384+
else:
385+
grid = meshed_region.grid
386+
if location == locations.elemental_nodal:
387+
grid = grid.shrink(1.0)
357388
grid.set_active_scalars(None)
358389
self._plotter.add_mesh(grid, scalars=overall_data, **kwargs_in)
359390

@@ -976,15 +1007,14 @@ def plot_contour(
9761007
import warnings
9771008

9781009
warnings.simplefilter("ignore")
979-
9801010
if isinstance(field_or_fields_container, (dpf.core.Field, dpf.core.FieldsContainer)):
9811011
fields_container = None
9821012
if isinstance(field_or_fields_container, dpf.core.Field):
9831013
fields_container = dpf.core.FieldsContainer(
9841014
server=field_or_fields_container._server
9851015
)
986-
fields_container.add_label(DefinitionLabels.time)
987-
fields_container.add_field({DefinitionLabels.time: 1}, field_or_fields_container)
1016+
fields_container.add_label("id")
1017+
fields_container.add_field({"id": 1}, field_or_fields_container)
9881018
elif isinstance(field_or_fields_container, dpf.core.FieldsContainer):
9891019
fields_container = field_or_fields_container
9901020
else:
@@ -1022,43 +1052,96 @@ def plot_contour(
10221052
unit = field.unit
10231053
break
10241054

1055+
# If ElementalNodal, first extend results to mid-nodes
1056+
if location == locations.elemental_nodal:
1057+
fields_container = dpf.core.operators.averaging.extend_to_mid_nodes_fc(
1058+
fields_container=fields_container
1059+
).eval()
1060+
1061+
location_data_len = mesh.location_data_len(location)
10251062
if location == locations.nodal:
10261063
mesh_location = mesh.nodes
10271064
elif location == locations.elemental:
10281065
mesh_location = mesh.elements
10291066
elif location == locations.faces:
10301067
mesh_location = mesh.faces
1068+
elif location == locations.elemental_nodal:
1069+
mesh_location = mesh.elements
10311070
else:
1032-
raise ValueError("Only elemental, nodal or faces location are supported for plotting.")
1071+
raise ValueError(
1072+
"Only elemental, elemental nodal, nodal or faces location are supported for plotting."
1073+
)
10331074

10341075
# pre-loop: check if shell layers for each field, if yes, set the shell layers
1035-
changeOp = core.Operator("change_shellLayers")
1036-
for field in fields_container:
1037-
shell_layer_check = field.shell_layers
1038-
if shell_layer_check in [
1039-
eshell_layers.topbottom,
1040-
eshell_layers.topbottommid,
1041-
]:
1042-
changeOp.inputs.fields_container.connect(fields_container)
1043-
sl = eshell_layers.top
1044-
if shell_layers is not None:
1045-
if not isinstance(shell_layers, eshell_layers):
1046-
raise TypeError(
1047-
"shell_layer attribute must be a core.shell_layers instance."
1048-
)
1049-
sl = shell_layers
1050-
changeOp.inputs.e_shell_layer.connect(sl.value) # top layers taken
1051-
fields_container = changeOp.get_output(0, core.types.fields_container)
1052-
break
1076+
changeOp = core.operators.utility.change_shell_layers()
1077+
if location == locations.elemental_nodal:
1078+
# change_shell_layers does not support elemental_nodal when given a fields_container
1079+
new_fields_container = dpf.core.FieldsContainer()
1080+
for l in fields_container.labels:
1081+
new_fields_container.add_label(l)
1082+
for i, field in enumerate(fields_container):
1083+
label_space_i = fields_container.get_label_space(i)
1084+
shell_layer_check = field.shell_layers
1085+
if shell_layer_check in [
1086+
eshell_layers.topbottom,
1087+
eshell_layers.topbottommid,
1088+
]:
1089+
changeOp.inputs.fields_container.connect(field)
1090+
changeOp.inputs.merge.connect(True)
1091+
sl = eshell_layers.top
1092+
if shell_layers is not None:
1093+
if not isinstance(shell_layers, eshell_layers):
1094+
raise TypeError(
1095+
"shell_layer attribute must be a core.shell_layers instance."
1096+
)
1097+
sl = shell_layers
1098+
changeOp.inputs.e_shell_layer.connect(sl.value) # top layers taken
1099+
field = changeOp.get_output(0, core.types.field)
1100+
new_fields_container.add_field(label_space=label_space_i, field=field)
1101+
fields_container = new_fields_container
1102+
else:
1103+
for field in fields_container:
1104+
shell_layer_check = field.shell_layers
1105+
if shell_layer_check in [
1106+
eshell_layers.topbottom,
1107+
eshell_layers.topbottommid,
1108+
]:
1109+
changeOp.inputs.fields_container.connect(fields_container)
1110+
sl = eshell_layers.top
1111+
if shell_layers is not None:
1112+
if not isinstance(shell_layers, eshell_layers):
1113+
raise TypeError(
1114+
"shell_layer attribute must be a core.shell_layers instance."
1115+
)
1116+
sl = shell_layers
1117+
changeOp.inputs.e_shell_layer.connect(sl.value) # top layers taken
1118+
fields_container = changeOp.get_output(0, core.types.fields_container)
1119+
break
10531120

10541121
# Merge field data into a single array
10551122
if component_count > 1:
1056-
overall_data = np.full((len(mesh_location), component_count), np.nan)
1123+
overall_data = np.full((location_data_len, component_count), np.nan)
10571124
else:
1058-
overall_data = np.full(len(mesh_location), np.nan)
1125+
overall_data = np.full(location_data_len, np.nan)
1126+
1127+
# field._data_pointer gives the first index of each entity data
1128+
# (should be of size nb_elements)
10591129

10601130
for field in fields_container:
10611131
ind, mask = mesh_location.map_scoping(field.scoping)
1132+
if location == locations.elemental_nodal:
1133+
# Rework ind and mask to take into account n_nodes per element
1134+
# entity_index_map = field._data_pointer
1135+
n_nodes_list = mesh.get_elemental_nodal_size_list().astype(np.int32)
1136+
first_index = np.insert(np.cumsum(n_nodes_list)[:-1], 0, 0).astype(np.int32)
1137+
mask_2 = np.asarray(
1138+
[mask_i for i, mask_i in enumerate(mask) for _ in range(n_nodes_list[ind[i]])]
1139+
)
1140+
ind_2 = np.asarray(
1141+
[first_index[ind_i] + j for ind_i in ind for j in range(n_nodes_list[ind_i])]
1142+
)
1143+
mask = mask_2
1144+
ind = ind_2
10621145
overall_data[ind] = field.data[mask]
10631146

10641147
# create the plotter and add the meshes
@@ -1087,15 +1170,20 @@ def plot_contour(
10871170
bound_method=self._internal_plotter._plotter.add_mesh, **kwargs
10881171
)
10891172
as_linear = True
1173+
if location == locations.elemental_nodal:
1174+
as_linear = False
10901175
if deform_by:
10911176
grid = mesh._as_vtk(mesh.deform_by(deform_by, scale_factor), as_linear=as_linear)
10921177
self._internal_plotter.add_scale_factor_legend(scale_factor, **kwargs)
10931178
else:
10941179
if as_linear != mesh.as_linear:
10951180
grid = mesh._as_vtk(mesh.nodes.coordinates_field, as_linear=as_linear)
1181+
mesh._full_grid = grid
10961182
mesh.as_linear = as_linear
10971183
else:
10981184
grid = mesh.grid
1185+
if location == locations.elemental_nodal:
1186+
grid = grid.shrink(1.0)
10991187
grid.clear_data()
11001188
self._internal_plotter._plotter.add_mesh(grid, scalars=overall_data, **kwargs_in)
11011189

0 commit comments

Comments
 (0)