diff --git a/src/ansys/dpf/core/errors.py b/src/ansys/dpf/core/errors.py index fb7332c57c0..63a0ff66a88 100644 --- a/src/ansys/dpf/core/errors.py +++ b/src/ansys/dpf/core/errors.py @@ -41,6 +41,12 @@ ``fields_container[index]``. """ +_EMPTY_MESH_PLOTTING_MSG = """" +The mesh support is empty. +Either provide one to the plot function called, or use MeshedRegion.plot +and provide the current data as parameter. +""" + class DpfValueError(ValueError): """Error raised when a specific DPF error value must be defined.""" @@ -80,6 +86,13 @@ def __init__(self, msg=_FIELD_CONTAINER_PLOTTING_MSG): ValueError.__init__(self, msg) +class EmptyMeshPlottingError(ValueError): + """Error raised when attempting to plot data with no mesh.""" + + def __init__(self, msg=_EMPTY_MESH_PLOTTING_MSG): + ValueError.__init__(self, msg) + + class InvalidANSYSVersionError(RuntimeError): """Error raised when the Ansys version is invalid.""" diff --git a/src/ansys/dpf/core/field.py b/src/ansys/dpf/core/field.py index f98b34fe8dc..3af42e6ef32 100644 --- a/src/ansys/dpf/core/field.py +++ b/src/ansys/dpf/core/field.py @@ -22,11 +22,21 @@ """Field.""" +from __future__ import annotations + +from typing import TYPE_CHECKING + import numpy as np from ansys import dpf from ansys.dpf.core import dimensionality, errors, meshed_region, scoping, time_freq_support -from ansys.dpf.core.common import _get_size_of_list, locations, natures, types +from ansys.dpf.core.common import ( + _get_size_of_list, + locations, + natures, + shell_layers as eshell_layers, + types, +) from ansys.dpf.core.field_base import _FieldBase, _LocalFieldBase from ansys.dpf.core.field_definition import FieldDefinition from ansys.dpf.gate import ( @@ -36,6 +46,12 @@ field_capi, field_grpcapi, ) +from ansys.dpf.gate.errors import DPFServerException + +if TYPE_CHECKING: # pragma: nocover + from ansys.dpf.core.dpf_operator import Operator + from ansys.dpf.core.meshed_region import MeshedRegion + from ansys.dpf.core.results import Result class Field(_FieldBase): @@ -500,7 +516,14 @@ def to_nodal(self): op.inputs.connect(self) return op.outputs.field() - def plot(self, shell_layers=None, deform_by=None, scale_factor=1.0, **kwargs): + def plot( + self, + shell_layers: eshell_layers = None, + deform_by: Union[Field, Result, Operator] = None, + scale_factor: float = 1.0, + meshed_region: MeshedRegion = None, + **kwargs, + ): """Plot the field or fields container on the mesh support if it exists. Warning @@ -522,21 +545,24 @@ def plot(self, shell_layers=None, deform_by=None, scale_factor=1.0, **kwargs): Parameters ---------- - shell_layers : shell_layers, optional + shell_layers: Enum used to set the shell layers if the model to plot - contains shell elements. The default is ``None``. - deform_by : Field, Result, Operator, optional + contains shell elements. Defaults to the top layer. + deform_by: Used to deform the plotted mesh. Must output a 3D vector field. - Defaults to None. - scale_factor : float, optional - Scaling factor to apply when warping the mesh. Defaults to 1.0. - **kwargs : optional + scale_factor: + Scaling factor to apply when warping the mesh. + meshed_region: + Mesh to plot the field on. + **kwargs: Additional keyword arguments for the plotter. For additional keyword arguments, see ``help(pyvista.plot)``. """ from ansys.dpf.core.plotter import Plotter - pl = Plotter(self.meshed_region, **kwargs) + if meshed_region is None: + meshed_region = self.meshed_region + pl = Plotter(meshed_region, **kwargs) return pl.plot_contour( self, shell_layers, @@ -691,7 +717,7 @@ def field_definition(self): def field_definition(self, value): return self._set_field_definition(value) - def _get_meshed_region(self): + def _get_meshed_region(self) -> MeshedRegion: """Retrieve the meshed region. Returns @@ -699,8 +725,15 @@ def _get_meshed_region(self): :class:`ansys.dpf.core.meshed_region.MeshedRegion` """ + try: + support = self._api.csfield_get_support_as_meshed_region(self) + except DPFServerException as e: + if "the field doesn't have this support type" in str(e): + support = None + else: + raise e return meshed_region.MeshedRegion( - mesh=self._api.csfield_get_support_as_meshed_region(self), + mesh=support, server=self._server, ) @@ -736,7 +769,7 @@ def time_freq_support(self, value): self._api.csfield_set_support(self, value) @property - def meshed_region(self): + def meshed_region(self) -> MeshedRegion: """Meshed region of the field. Return @@ -747,8 +780,8 @@ def meshed_region(self): return self._get_meshed_region() @meshed_region.setter - def meshed_region(self, value): - self._set_support(value, "MESHED_REGION") + def meshed_region(self, value: MeshedRegion): + self._set_support(support=value, support_type="MESHED_REGION") def __add__(self, field_b): """Add two fields. diff --git a/src/ansys/dpf/core/meshed_region.py b/src/ansys/dpf/core/meshed_region.py index f17bdf64673..193798edc69 100644 --- a/src/ansys/dpf/core/meshed_region.py +++ b/src/ansys/dpf/core/meshed_region.py @@ -35,7 +35,7 @@ from ansys.dpf.core import field, property_field, scoping, server as server_module from ansys.dpf.core.cache import class_handling_cache -from ansys.dpf.core.check_version import server_meet_version, version_requires +from ansys.dpf.core.check_version import meets_version, server_meet_version, version_requires from ansys.dpf.core.common import ( locations, nodal_properties, @@ -701,3 +701,15 @@ def field_of_properties(self, property_name): # Not sure we go through here since the only datatype not int is coordinates, # which is already dealt with previously. return field.Field(server=self._server, field=field_out) + + def is_empty(self) -> bool: + """Whether the mesh is empty. + + A mesh is considered empty when it has zero element, zero face, and zero node. + """ + no_faces = True + if meets_version(self._server.version, "7.0"): + no_faces = self.faces.n_faces == 0 + no_elements = self.elements.n_elements == 0 + no_nodes = self.nodes.n_nodes == 0 + return no_nodes and no_faces and no_elements diff --git a/src/ansys/dpf/core/plotter.py b/src/ansys/dpf/core/plotter.py index ef95b941ddb..23e20189e09 100644 --- a/src/ansys/dpf/core/plotter.py +++ b/src/ansys/dpf/core/plotter.py @@ -47,6 +47,8 @@ from ansys.dpf.core.nodes import Node, Nodes if TYPE_CHECKING: # pragma: no cover + from ansys.dpf.core import Operator, Result + from ansys.dpf.core.fields_container import FieldsContainer from ansys.dpf.core.meshed_region import MeshedRegion @@ -851,31 +853,32 @@ def plot_chart(fields_container, off_screen=False, screenshot=None): def plot_contour( self, - field_or_fields_container, - shell_layers=None, - meshed_region=None, - deform_by=None, - scale_factor=1.0, + field_or_fields_container: Union[Field, FieldsContainer], + shell_layers: eshell_layers = None, + meshed_region: MeshedRegion = None, + deform_by: Union[Field, Result, Operator] = None, + scale_factor: float = 1.0, **kwargs, ): """Plot the contour result on its mesh support. You cannot plot a fields container containing results at several - time steps. + time steps. Use :func:`FieldsContainer.animate` instead. Parameters ---------- - field_or_fields_container : dpf.core.Field or dpf.core.FieldsContainer + field_or_fields_container: Field or field container that contains the result to plot. - shell_layers : core.shell_layers, optional + shell_layers: Enum used to set the shell layers if the model to plot - contains shell elements. - deform_by : Field, Result, Operator, optional + contains shell elements. Defaults to the top layer. + meshed_region: + Mesh to plot the data on. + deform_by: Used to deform the plotted mesh. Must output a 3D vector field. - Defaults to None. - scale_factor : float, optional - Scaling factor to apply when warping the mesh. Defaults to 1.0. - **kwargs : optional + scale_factor: + Scaling factor to apply when warping the mesh. + **kwargs: Additional keyword arguments for the plotter. For more information, see ``help(pyvista.plot)``. """ @@ -912,6 +915,8 @@ def plot_contour( mesh = meshed_region else: mesh = self._mesh + if mesh.is_empty(): + raise dpf_errors.EmptyMeshPlottingError # get mesh scoping location = None diff --git a/tests/test_plotter.py b/tests/test_plotter.py index 4615b6d0441..ac3176134e6 100644 --- a/tests/test_plotter.py +++ b/tests/test_plotter.py @@ -252,6 +252,16 @@ def test_field_shell_plot_scoping_elemental(multishells): f.plot(shell_layers=core.shell_layers.top) +@pytest.mark.skipif(not HAS_PYVISTA, reason="Please install pyvista") +def test_field_plot_raise_empty_mesh(simple_bar): + ds = core.DataSources(simple_bar) + stream_prov = core.operators.metadata.streams_provider(data_sources=ds) + result_op = core.operators.result.displacement(streams_container=stream_prov) + field = result_op.outputs.fields_container()[0] + with pytest.raises(dpf_errors.EmptyMeshPlottingError): + field.plot() + + @pytest.mark.skipif(not HAS_PYVISTA, reason="Please install pyvista") def test_plotter_plot_contour_throw_shell_layers(multishells): model = core.Model(multishells)