diff --git a/ansys/dpf/core/errors.py b/ansys/dpf/core/errors.py index 78aa8ef28df..5b6f36cb615 100644 --- a/ansys/dpf/core/errors.py +++ b/ansys/dpf/core/errors.py @@ -1,5 +1,31 @@ from grpc._channel import _InactiveRpcError, _MultiThreadedRendezvous +_COMPLEX_PLOTTING_ERROR_MSG = """ +Complex fields can not be plotted. Use operators to get the amplitude +or the result at a defined sweeping phase before plotting. +""" + +_FIELD_CONTAINER_PLOTTING_MSG = """" +This fields_container contains multiple fields. Only one time-step +result can be plotted at a time. Extract a field with +``fields_container[index]``. +""" + + +class ComplexPlottingError(ValueError): + """Raised when attempting to plot a field with complex data""" + + def __init__(self, msg=_COMPLEX_PLOTTING_ERROR_MSG): + ValueError.__init__(self, msg) + + +class FieldContainerPlottingError(ValueError): + """Raised when attempting to plot a fields_container containing + multiple fields.""" + + def __init__(self, msg=_FIELD_CONTAINER_PLOTTING_MSG): + ValueError.__init__(self, msg) + class InvalidANSYSVersionError(RuntimeError): """Raised when ANSYS is an invalid version""" diff --git a/ansys/dpf/core/meshed_region.py b/ansys/dpf/core/meshed_region.py index 4dc3bbc21d5..190c2b2fd3e 100644 --- a/ansys/dpf/core/meshed_region.py +++ b/ansys/dpf/core/meshed_region.py @@ -7,23 +7,31 @@ class MeshedRegion: - """A class used to represent a Mesh""" + """A class used to represent a Mesh from DPF. - def __init__(self, mesh, channel=None): - """Initialize the mesh with MeshedRegion message + Parameters + ---------- + mesh : ansys.grpc.dpf.meshed_region_pb2.MeshedRegion - Parameters - ---------- - mesh : ansys.grpc.dpf.meshed_region_pb2.MeshedRegion + Attributes + ---------- + nodes : ansys.dpf.core.meshed_region.Nodes + Entity containing all the nodal properties - Attributes - ---------- - nodes : ansys.dpf.core.meshed_region.Nodes - Entity containing all the nodal properties + elements : ansys.dpf.core.meshed_region.Elements + Entity containing all the elemental properties - elements : ansys.dpf.core.meshed_region.Elements - Entity containing all the elemental properties - """ + Examples + -------- + Extract a meshed region from a model. + + >>> import ansys.dpf.core as dpf + >>> from ansys.dpf.core import examples + >>> model = dpf.Model(examples.static_rst) + >>> meshed_region = model.meshed_region + """ + + def __init__(self, mesh, channel=None): if channel is None: channel = dpf.core._global_channel() @@ -55,21 +63,45 @@ def _get_scoping(self, loc=locations.nodal): ids of the elements or nodes of the mesh """ request = meshed_region_pb2.LocationRequest(mesh=self._message) - request.loc.location =loc + request.loc.location = loc out = self._stub.List(request) - return scoping.Scoping(scoping=out,channel=self._channel) + return scoping.Scoping(scoping=out, channel=self._channel) - @property def elements(self): - """ returns instance of Elements which contains all the elemental properties""" - if self._elements == None: - self._elements= Elements(self) + """Returns elements collection containing all elements + belonging to this meshed region. + + Returns + ------- + Elements + Elements belonging to this meshed region. + + Examples + -------- + >>> elements = meshed_region.elements + >>> print(elements) + DPF Elements object with 24982 elements + """ + if self._elements is None: + self._elements = Elements(self) return self._elements @property def nodes(self): - """ returns instance of Nodes which contains all the nodal properties""" + """Returns nodes collection. + + Returns + ------- + Nodes + Nodes collection contains all the nodal properties of the + nodes belonging to this mesh region. + + Examples + -------- + >>> nodes = meshed_region.nodes + DPF Nodes object with 71987 nodes + """ if self._nodes is None: self._nodes = Nodes(self) return self._nodes @@ -79,7 +111,6 @@ def unit(self): """Unit type""" return self._get_unit() - # TODO: Depreciate in favor of unit property def _get_unit(self): """Returns the unit type @@ -101,7 +132,6 @@ def _connect(self): """Connect to the grpc service containing the reader""" return meshed_region_pb2_grpc.MeshedRegionServiceStub(self._channel) - def __str__(self): txt = 'Meshed Region\n' txt += '\t%d nodes\n' % self.nodes.n_nodes @@ -164,64 +194,204 @@ def _as_vtk(self, as_linear=True): @property def grid(self): - """Return full grid by default""" + """VTK pyvista UnstructuredGrid + + Returns + pyvista.UnstructuredGrid + UnstructuredGrid of the mesh. + + Examples + -------- + >>> grid = meshed_region.grid + >>> grid + UnstructuredGrid (0x7f9a64b41910) + N Cells: 24982 + N Points: 71987 + X Bounds: -7.297e-01, 3.703e+00 + Y Bounds: -1.299e+00, 1.331e+00 + Z Bounds: -6.268e-02, 7.495e+00 + N Arrays: 3 + + Plot this grid directly + + >>> grid.plot() + + Extract the surface mesh of this grid + + >>> mesh = grid.extract_surface() + >>> mesh + PolyData (0x7f9a5d150b40) + N Cells: 11190 + N Points: 8855 + X Bounds: -7.273e-01, 3.700e+00 + Y Bounds: -1.299e+00, 1.329e+00 + Z Bounds: -6.087e-02, 7.495e+00 + N Arrays: 5 + + Access the corresponding node and element IDs of the surface mesh + + >>> mesh.point_arrays + pyvista DataSetAttributes + Association: POINT + Contains keys: + node_ids + vtkOriginalPointIds + + >>> mesh.point_arrays['node_ids'] + pyvista_ndarray([ 1, 179, 65561, ..., 72150, 72145, 72144]) + """ if self._full_grid is None: self._full_grid = self._as_vtk() return self._full_grid - - def plot(self, field_or_fields_container=None, notebook=None, shell_layers=None): + + def plot(self, field_or_fields_container=None, notebook=None, + shell_layers=None, off_screen=None, show_axes=True, **kwargs): """Plot the field/fields container on mesh. - + Parameters ---------- field_or_fields_container dpf.core.Field or dpf.core.FieldsContainer - - notebook (default: None) - bool, that specifies if the plotting is in the notebook (2D) or not (3D) - + + notebook : bool, optional + That specifies if the plotting is in the notebook (2D) or not (3D). + shell_layers : core.ShellLayers, optional - Enum used to set the shell layers if the model to plot + Enum used to set the shell layers if the model to plot contains shell elements. + + off_screen : bool, optional + Renders off screen when ``True``. Useful for automated screenshots. + + show_axes : bool, optional + Shows a vtk axes widget. Enabled by default. + + **kwargs : optional + Additional keyword arguments for the plotter. See + ``help(pyvista.plot)`` for additional keyword arguments. + + Examples + -------- + Plot the displacement field from an example file + + >>> import ansys.dpf.core as dpf + >>> from ansys.dpf.core import examples + >>> model = dpf.Model(examples.static_rst) + >>> disp = model.results.displacement() + >>> field = disp.outputs.fields_container()[0] + >>> model.metadata.meshed_region.plot(field) """ pl = _DpfPlotter(self) if field_or_fields_container is not None: - pl.plot_contour(field_or_fields_container, notebook, shell_layers) - else: - pl.plot_mesh(notebook) + return pl.plot_contour(field_or_fields_container, notebook, shell_layers, + off_screen, show_axes, **kwargs) + + # otherwise, simply plot self + return pl.plot_mesh(notebook) class Node: - """A class used to represent a Node""" + """A DPF Node + + Created from an element or a meshed region. + + Examples + -------- + >>> import ansys.dpf.core as dpf + >>> from ansys.dpf.core import examples + >>> model = dpf.Model(examples.static_rst) + >>> nodes = model.metadata.meshed_region.nodes + + Initialize a node from a nodes collection + + >>> node = nodes[0] + >>> print(node) + DPF Node 63631 + Index: 63247 + Location: [-0.72324787407068, 0.80845567299105, 1.2400404500674] + + Initialize a node from an element + + >>> element = model.metadata.meshed_region.elements[0] + >>> node = element.nodes[0] + """ + def __init__(self, mesh, nodeid, index, coordinates): self._id = nodeid self._index = index self._coordinates = coordinates self._mesh = mesh - + @property - def index(self): + def index(self) -> int: + """Fortran index of the node in the model""" return self._index @property - def id(self): + def id(self) -> int: + """Node number""" return self._id @property def coordinates(self): + """Cartesian coordinates of the node. + + Examples + -------- + >>> node.coordinates + [-0.72324787407068, 0.80845567299105, 1.2400404500674] + """ return self._coordinates def __str__(self): - txt = 'DPF Node %d\n' % self.id - txt += 'Index: %d\n' % self.index - txt += f'{self.coordinates}\n' + txt = 'DPF Node %7d\n' % self.id + txt += 'Index: %7d\n' % self.index + txt += f'Location: {self.coordinates}\n' return txt - - class Element: - """A class used to represent an Element""" + """A DPF element. + + Created from a ``MeshedRegion``. + + Parameters + ---------- + mesh : MeshedRegion + ``MeshedRegion`` containing this element. + + elementid : int + Element ID. This is the element number corresponding to the element. + + index : int + Index of the element. Fortran based index of the element in + the result. + + nodes : list[Node] + List of DPF nodes belonging to the element. + + Examples + -------- + Extract a single element from a meshed region + + >>> import ansys.dpf.core as dpf + >>> from ansys.dpf.core import examples + >>> model = dpf.Model(examples.static_rst) + >>> elements = model.metadata.meshed_region.elements + >>> element = elements[0] + >>> print(element) + DPF Element 29502 + Index: 1 + Nodes: 10 + Type: 0 + Shape: Solid + + List the coordinates belonging to the first node of the element + + >>> element.nodes[0].coordinates + [-0.72324787407068, 0.80845567299105, 1.2400404500674] + """ + def __init__(self, mesh, elementid, index, nodes): self._id = elementid self._index = index @@ -230,61 +400,77 @@ def __init__(self, mesh, elementid, index, nodes): @property def node_ids(self): - node_ids=[] - for node in self._nodes: - node_ids.append(node.id) - return node_ids + """IDs of all the nodes in this element + + Returns + -------- + list + IDs of all the nodes in this element + + Examples + -------- + >>> element.node_ids + [1, 2, 3, 4, 5, 6, 7, 8] + """ + return [node.id for node in self._nodes] @property - def id(self): + def id(self) -> int: + """Element number""" return self._id @property - def index(self): + def index(self) -> int: + """Fortran based index of the element in the result""" return self._index @property def nodes(self): + """List of Nodes + + Examples + -------- + >>> print(element.nodes[1]) + DPF Node 63631 + Index: 63247 + Location: [-0.72324787407068, 0.80845567299105, 1.2400404500674] + """ return self._nodes @property - def n_nodes(self): + def n_nodes(self) -> int: + """Number of nodes""" return len(self._nodes) def __str__(self): txt = 'DPF Element %d\n' % self.id - txt += '\tIndex: %d\n' % self.index - txt += '\tNumber of nodes: %d\n' % self.n_nodes + txt += '\tIndex: %7d\n' % self.index + txt += '\tNodes: %7d\n' % self.n_nodes + txt += '\tType: %7d\n' % self.type + txt += '\tShape: %7s\n' % self.shape.capitalize() return txt - - @property - def element_type(self): - return self._get_element_type() - + @property - def element_shape(self): - return self._get_element_shape() - - def _get_element_type(self): - """Returns the element type of the element - - Returns - ------- - element_type : int - """ + @protect_grpc + def type(self) -> int: + """Ansys element type""" request = meshed_region_pb2.ElementalPropertyRequest() request.mesh.CopyFrom(self._mesh._message) request.index = self.index - # request.property = meshed_region_pb2.ElementalPropertyType.ELEMENT_TYPE request.property = meshed_region_pb2.ELEMENT_TYPE return self._mesh._stub.GetElementalProperty(request).prop - def _get_element_shape(self): - """Returns the element shape (beam, shell or solid) of the element + @property + @protect_grpc + def shape(self) -> str: + """Element shape. - Returns - ------- - element_shape : str + Can be ``'shell'``, ``'solid'``, ``'beam'``, or ``'unknown_shape'``. + + Examples + -------- + >>> element.shape + 'solid' """ request = meshed_region_pb2.ElementalPropertyRequest() request.mesh.CopyFrom(self._mesh._message) @@ -293,15 +479,28 @@ def _get_element_shape(self): prop = self._mesh._stub.GetElementalProperty(request).prop return meshed_region_pb2.ElementShape.Name(prop).lower() - class Nodes(): - """Class to encapsulate mesh nodes""" + """Collection of DPF Nodes. + + Created from a MeshedRegion + + Examples + -------- + >>> import ansys.dpf.core as dpf + >>> from ansys.dpf.core import examples + >>> model = dpf.Model(examples.static_rst) + >>> meshed_region = model.meshed_region + >>> nodes = model.metadata.meshed_region.nodes + >>> print(nodes) + DPF Nodes object with 71987 nodes + """ + def __init__(self, mesh): self._mesh = mesh self._mapping_id_to_index = None def __str__(self): - return 'DPF Nodes object with %d nodes\n' % len(self) + return f'DPF Node collection with {len(self)} nodes\n' def __getitem__(self, index): """Returns node based on index""" @@ -348,17 +547,36 @@ def __get_node(self, nodeindex=None, nodeid=None): request.index = nodeindex nodeOut = self._mesh._stub.Get(request).node return Node(self._mesh, nodeOut.id, nodeOut.index, nodeOut.coordinates) - + @property def scoping(self): + """Return the scoping of the Nodes + + Returns + ------- + scoping.Scoping + Scoping of the Nodes + + Examples + -------- + Get the ids of all the nodes in this collection + + >>> nodes.scoping.ids + [1, + 2, + 3, + 4, + ...] + """ return self._mesh._get_scoping(loc=dpf.core.locations.nodal) - + @property def n_nodes(self): """Number of nodes""" return self.scoping.size - + @property + @protect_grpc def coordinates_field(self): """Coordinates field @@ -366,16 +584,24 @@ def coordinates_field(self): ------- coordinates_field : Field field of all the nodes coordinates - """ - return self._get_coordinates_field() - @protect_grpc - def _get_coordinates_field(self): - """ - Returns - ------- - coordinates_field : Field - field of all the nodes coordinates + Examples + -------- + >>> print(nodes.coordinates_field) + DPF Field + Location: Nodal + 71987 id(s) + Shape: (71987, 3) + + Extract the array of coordinates the coordinates field + >>> nodes.coordinates_field.data + array([[ 3.40556124, -0.24838723, 0.69582925], + [ 3.49706859, -0.151947 , 0.6686485 ], + [ 3.43478821, -0.24973448, 0.69217843], + ..., + [ 3.44598692, -0.10708114, 0.64389383], + [ 3.453663 , -0.14285579, 0.61316773], + [ 3.39599888, -0.22926613, 0.66507732]]) """ request = meshed_region_pb2.ListPropertyRequest() request.mesh.CopyFrom(self._mesh._message) @@ -386,13 +612,7 @@ def _get_coordinates_field(self): def _build_mapping_id_to_index(self): """Return a mapping between ids and indices of the entity.""" - dic_out = {} - ids = self._mesh.nodes.scoping.ids - i = 0 - for node_id in ids: - dic_out[node_id] = i - i += 1 - return dic_out + return {eid: i for i, eid in enumerate(self.scoping.ids)} @property def mapping_id_to_index(self): @@ -402,7 +622,17 @@ def mapping_id_to_index(self): class Elements(): - """Class to encapsulate mesh elements""" + """Elements belonging to a ``meshed_region``. + + Examples + -------- + >>> import ansys.dpf.core as dpf + >>> from ansys.dpf.core import examples + >>> model = dpf.Model(examples.static_rst) + >>> elements = model.metadata.meshed_region.elements + >>> print(elements) + DPF Elements object with 24982 elements + """ def __init__(self, mesh): self._mesh = mesh @@ -413,19 +643,53 @@ def __str__(self): def __getitem__(self, index): """Returns element based on index""" - return self._mesh.element_by_index(index) + return self.element_by_index(index) def __len__(self): return self.n_elements def __iter__(self): for i in range(len(self)): - yield self[i] + yield self[i] + + def element_by_id(self, id) -> Element: + """Return an element using its element number (id). - def element_by_id(self, id): + Parameters + ---------- + id : int + Element number. + + Returns + ------- + Element + DPF Element + + """ return self.__get_element(elementid=id) - def element_by_index(self, index): + def element_by_index(self, index) -> Element: + """Return an element using its index. + + Parameters + ---------- + index : int + Zero-based index. + + Returns + ------- + Element + DPF Element. + + Examples + -------- + elements.element_by_index(0) + + Notes + ----- + This is equivalent to ``elements[0]`` + + """ return self.__get_element(elementindex=index) def __get_element(self, elementindex=None, elementid=None): @@ -455,28 +719,34 @@ def __get_element(self, elementindex=None, elementid=None): for node in elementOut.nodes: nodesOut.append(Node(self._mesh, node.id, node.index, node.coordinates)) return Element(self._mesh, elementOut.id, elementOut.index, nodesOut) - + @property - def scoping(self): + def scoping(self) -> scoping.Scoping: + """The Scoping of the elements. + + Examples + -------- + >>> print(elements.scoping) + DPF Scoping Object + Size: 24982 + Location: Elemental + """ return self._mesh._get_scoping(loc=locations.elemental) @property def element_types_field(self): """Element types field - - Returns - ------- - element_types_field : Field - field of all the element types - """ - return self._get_element_types_field() - def _get_element_types_field(self): - """ Returns ------- element_types_field : Field - field of all the element types + Field of all the element types. + + Examples + -------- + >>> field = elements.element_types_field + >>> field.data + array([0, 0, 0, ..., 0, 0, 0], dtype=int32) """ request = meshed_region_pb2.ListPropertyRequest() request.mesh.CopyFrom(self._mesh._message) @@ -486,22 +756,21 @@ def _get_element_types_field(self): return field.Field(self._mesh._channel, field=fieldOut) @property + @protect_grpc def materials_field(self): """Materials field - - Returns - ------- - materials_field : Field - field of all the materials ids - """ - return self._get_materials_field() - def _get_materials_field(self): - """ Returns ------- - materials_field : Field - field of all the materials ids + Field + Field of all the materials ids. + + Examples + -------- + Extract the material ids from the materials_field + + >>> elements.materials_field.data + array([1, 1, 1, ..., 1, 1, 1], dtype=int32) """ request = meshed_region_pb2.ListPropertyRequest() request.mesh.CopyFrom(self._mesh._message) @@ -511,18 +780,10 @@ def _get_materials_field(self): return field.Field(self._mesh._channel, field=fieldOut) @property + @protect_grpc def connectivities_field(self): """Connectivity field - - Returns - ------- - connectivities_field : Field - Field of all the connectivities (nodes indices associated to an element) - """ - return self._get_connectivities_field() - def _get_connectivities_field(self): - """ Returns ------- connectivities_field : Field @@ -536,22 +797,32 @@ def _get_connectivities_field(self): return field.Field(self._mesh._channel, field=fieldOut) @property - def n_elements(self): + def n_elements(self) -> int: """Number of elements""" return self.scoping.size def _build_mapping_id_to_index(self): """Return a mapping between ids and indices of the entity.""" - dic_out = {} - ids = self._mesh.elements.scoping.ids - i = 0 - for element_id in ids: - dic_out[element_id] = i - i += 1 - return dic_out + return {eid: i for i, eid in enumerate(self.scoping.ids)} @property - def mapping_id_to_index(self): + def mapping_id_to_index(self) -> dict: + """Mapping between the ids and indices of the entity. + + Useful for mapping scalar results from a field to this meshed region. + + Examples + -------- + >>> meshed_region.mapping_id_to_index + {28947: 0, + 29502: 1, + 29101: 2, + 28563: 3, + 29503: 4, + ... + } + + """ if self._mapping_id_to_index is None: self._mapping_id_to_index = self._build_mapping_id_to_index() return self._mapping_id_to_index diff --git a/ansys/dpf/core/plotter.py b/ansys/dpf/core/plotter.py index d124b0969a1..bf12913499d 100644 --- a/ansys/dpf/core/plotter.py +++ b/ansys/dpf/core/plotter.py @@ -1,5 +1,5 @@ -"""Dpf plotter class is contained in this module. -Allows to plot a mesh and a fields container +"""Dpf plotter class is contained in this module. +Allows to plot a mesh and a fields container using pyvista.""" import pyvista as pv @@ -7,17 +7,20 @@ import os import sys import numpy as np + from ansys import dpf from ansys.dpf import core from ansys.dpf.core.common import locations, ShellLayers, DefinitionLabels +from ansys.dpf.core import errors as dpf_errors + class Plotter: def __init__(self, mesh): self._mesh = mesh - + def plot_mesh(self, notebook=None): """Plot the mesh using pyvista. - + Parameters ---------- notebook : bool, optional @@ -33,7 +36,7 @@ def plot_chart(self, fields_container): """Plot the minimum/maximum result values over time if the time_freq_support contains several time_steps (for example: transient analysis) - + Parameters ---------- field_container @@ -75,9 +78,12 @@ def plot_chart(self, fields_container): pyplot.title( substr[0] + ": min/max values over time") return pyplot.legend() - def plot_contour(self, field_or_fields_container, notebook=None, shell_layers = None): + def plot_contour(self, field_or_fields_container, notebook=None, + shell_layers=None, off_screen=None, show_axes=True, **kwargs): """Plot the contour result on its mesh support. - Can not plot fields container containing results at several time steps. + + Can not plot fields container containing results at several + time steps. Parameters ---------- @@ -89,106 +95,108 @@ def plot_contour(self, field_or_fields_container, notebook=None, shell_layers = iPython notebook if available. When ``False``, plot external to the notebook with an interactive window. When ``True``, always plot within a notebook. - + shell_layers : core.ShellLayers, optional - Enum used to set the shell layers if the model to plot + Enum used to set the shell layers if the model to plot contains shell elements. + + off_screen : bool, optional + Renders off screen when ``True``. Useful for automated screenshots. + + show_axes : bool, optional + Shows a vtk axes widget. Enabled by default. + + **kwargs : optional + Additional keyword arguments for the plotter. See + ``help(pyvista.plot)`` for additional keyword arguments. """ if not sys.warnoptions: import warnings warnings.simplefilter("ignore") - - if isinstance(field_or_fields_container, dpf.core.Field) or isinstance(field_or_fields_container, dpf.core.FieldsContainer): + + if isinstance(field_or_fields_container, (dpf.core.Field, dpf.core.FieldsContainer)): fields_container = None if isinstance(field_or_fields_container, dpf.core.Field): fields_container = dpf.core.FieldsContainer() fields_container.add_label(DefinitionLabels.time) - fields_container.add_field({DefinitionLabels.time:1}, field_or_fields_container) + fields_container.add_field({DefinitionLabels.time: 1}, field_or_fields_container) elif isinstance(field_or_fields_container, dpf.core.FieldsContainer): fields_container = field_or_fields_container else: - raise Exception("Field or Fields Container only can be plotted.") - - #pre-loop to check if the there are several time steps + raise TypeError("Only field or fields_container can be plotted.") + + # pre-loop to check if the there are several time steps labels = fields_container.get_label_space(0) if DefinitionLabels.complex in labels.keys(): - raise Exception("Complex field can not be plotted. Use operators to get the amplitude or the result at a defined sweeping phase before plotting.") + raise dpf_errors.ComplexPlottingError if DefinitionLabels.time in labels.keys(): - i = 1 - size = len(fields_container) first_time = labels[DefinitionLabels.time] - while i < size: + for i in range(1, len(fields_container)): label = fields_container.get_label_space(i) if label[DefinitionLabels.time] != first_time: - raise Exception("Several time steps are contained in this fields container. Only one time-step result can be plotted.") - i += 1 - - plotter = pv.Plotter(notebook=notebook) + raise dpf_errors.FieldContainerPlottingError + mesh = self._mesh - grid = mesh.grid - nan_color = "grey" - - #get mesh scoping + + # get mesh scoping mesh_scoping = None m_id_to_index = None location = None component_count = None name = None - #pre-loop to get location and component count + + # pre-loop to get location and component count for field in fields_container: if len(field.data) != 0: location = field.location component_count = field.component_count name = field.name.split("_")[0] break - - if (location == locations.nodal): + + if location == locations.nodal: mesh_scoping = mesh.nodes.scoping m_id_to_index = mesh.nodes.mapping_id_to_index - elif(location == locations.elemental): + elif location == locations.elemental: mesh_scoping = mesh.elements.scoping m_id_to_index = mesh.elements.mapping_id_to_index else: raise Exception("Only elemental or nodal location are supported for plotting.") - - #request all data to compute the final field to plot - overall_data = np.empty((len(mesh_scoping), component_count)) - overall_data[:] = np.nan - - #pre-loop: check if shell layers for each field, if yes, set the shell layers + + # pre-loop: check if shell layers for each field, if yes, set the shell layers changeOp = core.Operator("change_shellLayers") for field in fields_container: shell_layer_check = field.shell_layers - if (shell_layer_check == ShellLayers.TOPBOTTOM - or shell_layer_check == ShellLayers.TOPBOTTOMMID): + if shell_layer_check in [ShellLayers.TOPBOTTOM, ShellLayers.TOPBOTTOMMID]: changeOp.inputs.fields_container.connect(fields_container) sl = ShellLayers.TOP if (shell_layers is not None): if not isinstance(shell_layers, ShellLayers): raise TypeError("shell_layer attribute must be a core.ShellLayers instance.") sl = shell_layers - changeOp.inputs.e_shell_layer.connect(sl.value) #top layers taken + changeOp.inputs.e_shell_layer.connect(sl.value) # top layers taken fields_container = changeOp.outputs.fields_container() break - - #loop: merge fields + + # Merge field data into a single array + overall_data = np.full((len(mesh_scoping), component_count), np.nan) for field in fields_container: - data = field.data - scop_ids = field.scoping.ids - size = len(scop_ids) - i = 0 - while i < size: - ind = m_id_to_index[scop_ids[i]] - overall_data[ind] = data[i] - i += 1 - - #add meshes - plotter.add_mesh(grid, scalars = overall_data, stitle = name, nan_color=nan_color, show_edges=True) - - #show result - plotter.add_axes() + ind = list(map(m_id_to_index.get, field.scoping.ids)) + overall_data[ind] = field.data + + # create the plotter and add the meshes + plotter = pv.Plotter(notebook=notebook, off_screen=off_screen) + + # add meshes + kwargs.setdefault('show_edges', True) + kwargs.setdefault('nan_color', 'grey') + kwargs.setdefault('stitle', name) + plotter.add_mesh(mesh.grid, scalars=overall_data, **kwargs) + + # show result + if show_axes: + plotter.add_axes() return plotter.show() - + def _plot_contour_using_vtk_file(self, fields_container, notebook=None): """Plot the contour result on its mesh support. The obtained figure depends on the support (can be a meshed_region or a time_freq_support). diff --git a/ansys/dpf/core/scoping.py b/ansys/dpf/core/scoping.py index d4927aeb201..07781fa4784 100644 --- a/ansys/dpf/core/scoping.py +++ b/ansys/dpf/core/scoping.py @@ -81,6 +81,10 @@ def _set_ids(self, ids): ids : list of int The ids to set """ + # must convert to a list for gRPC + if isinstance(ids, range): + ids = list(ids) + request = scoping_pb2.UpdateRequest() request.ids.ids.rep_int.extend(ids) request.scoping.CopyFrom(self._message) @@ -90,21 +94,25 @@ def _get_ids(self): """ Returns ------- - ids : list of int + ids : list[int] + List of ids. """ service = self._stub.List(self._message) - tupleMetaData = service.initial_metadata() - for iMeta in range(len(tupleMetaData)): - if (tupleMetaData[iMeta].key == 'size_tot'): - totsize = int(tupleMetaData[iMeta].value) - out = [None]*totsize - i = 0 - for dataout in service: - for idata in dataout.ids.rep_int: - out[i] = idata - i += 1 + + # Get total size, removed as it's unnecessary since Python has + # to create a list from the ids + # + # tupleMetaData = service.initial_metadata() + # for iMeta in range(len(tupleMetaData)): + # if (tupleMetaData[iMeta].key == 'size_tot'): + # totsize = int(tupleMetaData[iMeta].value) + + out = [] + for chunk in service: + out.extend(chunk.ids.rep_int) return out + def set_id(self, index, scopingid): """Set the id of an index of the scoping @@ -179,16 +187,16 @@ def location(self, value): def _connect(self): """Connect to the grpc service containing the reader""" return scoping_pb2_grpc.ScopingServiceStub(self._channel) - + def __len__(self): return self._count() - + def __del__(self): try: self._stub.Delete(self._message) except: pass - + def __getitem__(self, key): """Returns the id at a requested index""" return self.id(key) @@ -198,6 +206,7 @@ def size(self): return self._count() def __str__(self): - txt = 'DPF Scoping Object with\n' - txt += '\tSize: %d\n' % self.size + txt = 'DPF Scoping Object\n' + txt += f'Size: {self.size}\n' + txt += f'Location: {self.location}\n' return txt diff --git a/docs/source/api/index.rst b/docs/source/api/index.rst index e3fe9d86b60..38915f8217f 100644 --- a/docs/source/api/index.rst +++ b/docs/source/api/index.rst @@ -51,3 +51,21 @@ simplify the creation of new chained operators. .. automodule:: ansys.dpf.plotting :members: + + +MeshedRegion Class +------------------ +.. autoclass:: ansys.dpf.core.meshed_region.MeshedRegion + :members: + + +Elements Class +-------------- +.. autoclass:: ansys.dpf.core.meshed_region.Elements + :members: + + +Element Class +-------------- +.. autoclass:: ansys.dpf.core.meshed_region.Element + :members: diff --git a/tests/test_meshregion.py b/tests/test_meshregion.py index f8b18b78709..78ba82a5fba 100644 --- a/tests/test_meshregion.py +++ b/tests/test_meshregion.py @@ -37,8 +37,8 @@ def test_vtk_grid_from_model(simple_bar_model): def test_get_element_type_meshedregion(simple_bar_model): mesh = simple_bar_model.metadata.meshed_region - assert mesh.elements.element_by_index(1).element_type == 11 - assert mesh.elements.element_by_index(1).element_shape == 'solid' + assert mesh.elements.element_by_index(1).type == 11 + assert mesh.elements.element_by_index(1).shape == 'solid' def test_get_unit_meshedregion(simple_bar_model): diff --git a/tests/test_plotter.py b/tests/test_plotter.py index d4757dcf645..5dc7f1c1b22 100644 --- a/tests/test_plotter.py +++ b/tests/test_plotter.py @@ -1,11 +1,13 @@ +import unittest + from pyvista.plotting.renderer import CameraPosition +import pytest from ansys import dpf from ansys.dpf.core import Model, Operator from ansys.dpf.core.plotter import Plotter as DpfPlotter from ansys.dpf import core -import unittest -import pytest +from ansys.dpf.core import errors as dpf_errors def test_chart_plotter(plate_msup): @@ -189,49 +191,22 @@ def test_plot_fields_on_mesh_scoping(multishells): mesh.plot(s[0]) -class SeveralTimeSteps(unittest.TestCase): - - @pytest.fixture(autouse=True) - def set_filepath(self, plate_msup): - self._filepath = plate_msup - - def test_throw_on_several_time_steps(self): - model = core.Model(self._filepath) - scoping = core.Scoping() - scoping.ids = list(range(3, len(model.metadata.time_freq_support.frequencies) + 1)) - stress = model.results.displacement() - stress.inputs.time_scoping.connect(scoping) - fc = stress.outputs.fields_container() - mesh = model.metadata.meshed_region - self.assertRaises(Exception, mesh.plot, fc) - try: - mesh.plot(fc) - except Exception as e: - message = "Several time steps are contained in this fields container. Only one time-step result can be plotted." - e2 = Exception(message) - assert e.args == e2.args - assert type(e) == type(e2) - - -class ComplexFile(unittest.TestCase): - - @pytest.fixture(autouse=True) - def set_filepath(self, complex_model): - self._filepath = complex_model - - def test_throw_complex_file(self): - model = core.Model(self._filepath) - stress = model.results.displacement() - fc = stress.outputs.fields_container() - mesh = model.metadata.meshed_region - self.assertRaises(Exception, mesh.plot, fc) - try: - mesh.plot(fc) - except Exception as e: - message = "Complex field can not be plotted. Use operators to get the amplitude or the result at a defined sweeping phase before plotting." - e2 = Exception(message) - assert e.args == e2.args - assert type(e) == type(e2) - - - +def test_throw_on_several_time_steps(plate_msup): + model = core.Model(plate_msup) + scoping = core.Scoping() + scoping.ids = range(3, len(model.metadata.time_freq_support.frequencies) + 1) + stress = model.results.displacement() + stress.inputs.time_scoping.connect(scoping) + fc = stress.outputs.fields_container() + mesh = model.metadata.meshed_region + with pytest.raises(dpf_errors.FieldContainerPlottingError): + mesh.plot(fc) + + +def test_throw_complex_file(complex_model): + model = core.Model(complex_model) + stress = model.results.displacement() + fc = stress.outputs.fields_container() + mesh = model.metadata.meshed_region + with pytest.raises(dpf_errors.ComplexPlottingError): + mesh.plot(fc)