diff --git a/ansys/fluent/core/session.py b/ansys/fluent/core/session.py index 42c244f5ab1d..b86015cd8979 100644 --- a/ansys/fluent/core/session.py +++ b/ansys/fluent/core/session.py @@ -116,6 +116,7 @@ def __init__( self, ip: str = None, port: int = None, + password: str = None, channel: grpc.Channel = None, cleanup_on_exit: bool = True, ): @@ -133,6 +134,8 @@ def __init__( Port to connect to existing Fluent instance. Used only when ``channel`` is ``None``. Defaults value can be set by the environment variable ``PYFLUENT_FLUENT_PORT=``. + password : str, optional + Password to connect to existing Fluent instance. channel : grpc.Channel, optional Grpc channel to use to connect to existing Fluent instance. ip and port arguments will be ignored when channel is @@ -154,7 +157,9 @@ def __init__( "The port to connect to Fluent session is not provided." ) self._channel = grpc.insecure_channel(f"{ip}:{port}") - self._metadata: List[Tuple[str, str]] = [] + self._metadata: List[Tuple[str, str]] = ( + [("password", password)] if password else [] + ) self._id = f"session-{next(Session._id_iter)}" self._settings_root = None @@ -229,8 +234,12 @@ def create_from_server_info_file( Session instance """ ip, port, password = _parse_server_info_file(server_info_filepath) - session = Session(ip=ip, port=port, cleanup_on_exit=cleanup_on_exit) - session._metadata.append(("password", password)) + session = Session( + ip=ip, + port=port, + password=password, + cleanup_on_exit=cleanup_on_exit, + ) return session @property diff --git a/ansys/fluent/post/__init__.py b/ansys/fluent/post/__init__.py index 316f135d5ba7..1c416234ae48 100644 --- a/ansys/fluent/post/__init__.py +++ b/ansys/fluent/post/__init__.py @@ -84,5 +84,4 @@ def _update_vtk_version(): if import_errors: raise ImportError("\n".join(import_errors)) - -import ansys.fluent.post.pyvista as pyvista # noqa: F401 +from ansys.fluent.post._config import get_config, set_config # noqa: F401 diff --git a/ansys/fluent/post/_config.py b/ansys/fluent/post/_config.py new file mode 100644 index 000000000000..cbef3608fc7c --- /dev/null +++ b/ansys/fluent/post/_config.py @@ -0,0 +1,38 @@ +"""Global configuration state for post.""" +import threading + +_global_config = { + "blocking": False, +} +_threadlocal = threading.local() + + +def _get_threadlocal_config(): + if not hasattr(_threadlocal, "global_config"): + _threadlocal.global_config = _global_config.copy() + return _threadlocal.global_config + + +def get_config() -> dict: + """ + Retrieve post configuration. + + Returns + ------- + config : dict + Keys are parameter names that can be passed to :func:`set_config`. + """ + return _get_threadlocal_config().copy() + + +def set_config(blocking: bool = False): + """ + Set post configuration. + + Parameters + ---------- + blocking : bool, default=False + If True, then graphics/plot display will block the current thread. + """ + local_config = _get_threadlocal_config() + local_config["blocking"] = blocking diff --git a/ansys/fluent/post/matplotlib/matplot_objects.py b/ansys/fluent/post/matplotlib/matplot_objects.py index ffaf9f8ca62b..dcfc2d65ee18 100644 --- a/ansys/fluent/post/matplotlib/matplot_objects.py +++ b/ansys/fluent/post/matplotlib/matplot_objects.py @@ -33,11 +33,7 @@ def __init__(self, session, local_surfaces_provider=None): else: self.__dict__ = session_state self._local_surfaces_provider = ( - lambda: local_surfaces_provider - if local_surfaces_provider - else self.Surfaces - if hasattr(self, "Surfaces") - else [] + lambda: local_surfaces_provider or getattr(self, "Surfaces", []) ) def _init_module(self, obj, mod): diff --git a/ansys/fluent/post/matplotlib/matplot_windows_manager.py b/ansys/fluent/post/matplotlib/matplot_windows_manager.py index 7d072738c446..b4772067053f 100644 --- a/ansys/fluent/post/matplotlib/matplot_windows_manager.py +++ b/ansys/fluent/post/matplotlib/matplot_windows_manager.py @@ -4,8 +4,10 @@ from typing import List, Optional, Union import numpy as np + from ansys.fluent.core.session import Session from ansys.fluent.core.utils.generic import AbstractSingletonMeta, in_notebook +from ansys.fluent.post import get_config from ansys.fluent.post.matplotlib.plotter_defns import Plotter, ProcessPlotter from ansys.fluent.post.post_object_defns import GraphicsDefn, PlotDefn from ansys.fluent.post.post_windows_manager import ( @@ -40,6 +42,9 @@ def plot(self, data): def set_properties(self, properties): self.plot_pipe.send({"properties": properties}) + def save_graphic(self, name: str): + self.plot_pipe.send({"save_graphic": name}) + def is_closed(self): if self._closed: return True @@ -82,15 +87,13 @@ def __init__(self, id: str, post_object: Union[GraphicsDefn, PlotDefn]): self.animate: bool = False self.close: bool = False self.refresh: bool = False - if in_notebook(): - self.plotter() def plot(self): """Draw plot.""" if not self.post_object: return xy_data = self._get_xy_plot_data() - if in_notebook(): + if in_notebook() or get_config()["blocking"]: self.plotter.set_properties(self.properties) else: try: @@ -106,7 +109,7 @@ def plot(self): def _get_plotter(self): return ( Plotter(self.id) - if in_notebook() + if in_notebook() or get_config()["blocking"] else _ProcessPlotterHandle(self.id) ) @@ -244,6 +247,31 @@ def plot( window.post_object = object window.plot() + def save_graphic( + self, + window_id: str, + format: str, + ) -> None: + """ + Save graphics. + + Parameters + ---------- + window_id : str + Window id for which graphic should be saved. + format : str + Graphic format. Supported formats are eps, jpeg, jpg, + pdf, pgf, png, ps, raw, rgba, svg, svgz, tif and tiff. + + Raises + ------ + ValueError + If window does not support specified format. + """ + window = self._post_windows.get(window_id) + if window: + window.plotter.save_graphic(f"{window_id}.{format}") + def refresh_windows( self, session_id: Optional[str] = "", @@ -332,7 +360,10 @@ def _open_window( if ( window and not window.plotter.is_closed() - and (not in_notebook() or window.refresh) + and ( + not (in_notebook() or get_config()["blocking"]) + or window.refresh + ) ): window.refresh = False else: diff --git a/ansys/fluent/post/matplotlib/plotter_defns.py b/ansys/fluent/post/matplotlib/plotter_defns.py index 1a4b094da368..80173336da6e 100644 --- a/ansys/fluent/post/matplotlib/plotter_defns.py +++ b/ansys/fluent/post/matplotlib/plotter_defns.py @@ -50,6 +50,7 @@ def __init__( self._max_x = None self._data = {} self._closed = False + self._visible = False if not remote_process: self.fig = plt.figure(num=self._window_id) self.ax = self.fig.add_subplot(111) @@ -98,6 +99,9 @@ def plot(self, data: dict) -> None: self.ax.set_ylim( self._min_y - y_range * 0.2, self._max_y + y_range * 0.2 ) + if not self._visible: + self._visible = True + plt.show() def close(self): """Close window.""" @@ -108,6 +112,17 @@ def is_closed(self): """Check if window is closed.""" return self._closed + def save_graphic(self, file_name: str): + """ + Save graphics. + + Parameters + ---------- + file_name : str + File name to save graphic. + """ + plt.savefig(file_name) + def set_properties(self, properties: dict): """ Set plot properties. @@ -131,6 +146,7 @@ def set_properties(self, properties: dict): def __call__(self): """Reset and show plot.""" self._reset() + self._visible = True plt.show() # private methods @@ -193,6 +209,9 @@ def _call_back(self): if "properties" in data: properties = data["properties"] self.set_properties(properties) + elif "save_graphic" in data: + name = data["save_graphic"] + self.save_graphic(name) else: self.plot(data) self.fig.canvas.draw() @@ -209,4 +228,5 @@ def __call__(self, pipe): timer = self.fig.canvas.new_timer(interval=10) timer.add_callback(self._call_back) timer.start() + self._visible = True plt.show() diff --git a/ansys/fluent/post/post_windows_manager.py b/ansys/fluent/post/post_windows_manager.py index f4edc8974163..aff61aedade4 100644 --- a/ansys/fluent/post/post_windows_manager.py +++ b/ansys/fluent/post/post_windows_manager.py @@ -83,6 +83,29 @@ def plot( """ pass + @abstractmethod + def save_graphic( + self, + window_id: str, + format: str, + ) -> None: + """ + Save graphics. + + Parameters + ---------- + window_id : str + Window id for which graphic should be saved. + format : str + Graphic format. + + Raises + ------ + ValueError + If window does not support specified format. + """ + pass + @abstractmethod def refresh_windows( self, diff --git a/ansys/fluent/post/pyvista/pyvista_objects.py b/ansys/fluent/post/pyvista/pyvista_objects.py index 8a65cae39f50..c540151784b0 100644 --- a/ansys/fluent/post/pyvista/pyvista_objects.py +++ b/ansys/fluent/post/pyvista/pyvista_objects.py @@ -45,11 +45,7 @@ def __init__(self, session, local_surfaces_provider=None): else: self.__dict__ = session_state self._local_surfaces_provider = ( - lambda: local_surfaces_provider - if local_surfaces_provider - else self.Surfaces - if hasattr(self, "Surfaces") - else [] + lambda: local_surfaces_provider or getattr(self, "Surfaces", []) ) def _init_module(self, obj, mod): diff --git a/ansys/fluent/post/pyvista/pyvista_windows_manager.py b/ansys/fluent/post/pyvista/pyvista_windows_manager.py index 60374b7b8752..ad9fcf9a4f32 100644 --- a/ansys/fluent/post/pyvista/pyvista_windows_manager.py +++ b/ansys/fluent/post/pyvista/pyvista_windows_manager.py @@ -9,6 +9,7 @@ from ansys.fluent.core.session import Session from ansys.fluent.core.utils.generic import AbstractSingletonMeta, in_notebook +from ansys.fluent.post import get_config from ansys.fluent.post.post_object_defns import GraphicsDefn, PlotDefn from ansys.fluent.post.post_windows_manager import ( PostWindow, @@ -33,16 +34,16 @@ def __init__(self, id: str, post_object: Union[GraphicsDefn, PlotDefn]): self.post_object: Union[GraphicsDefn, PlotDefn] = post_object self.id: str = id self.plotter: Union[BackgroundPlotter, pv.Plotter] = ( - pv.Plotter() - if in_notebook() + pv.Plotter(title=f"PyFluent ({self.id})") + if in_notebook() or get_config()["blocking"] else BackgroundPlotter(title=f"PyFluent ({self.id})") ) self.animate: bool = False self.close: bool = False self.refresh: bool = False self.update: bool = False + self._visible: bool = False self._init_properties() - self.plotter.show() def plot(self): """Plot graphics.""" @@ -64,6 +65,9 @@ def plot(self): if self.animate: plotter.write_frame() plotter.camera = camera.copy() + if not self._visible: + plotter.show() + self._visible = True # private methods @@ -415,7 +419,7 @@ def open_window(self, window_id: Optional[str] = None) -> str: with self._condition: if not window_id: window_id = self._get_unique_window_id() - if in_notebook(): + if in_notebook() or get_config()["blocking"]: self._open_window_notebook(window_id) else: self._open_and_plot_console(None, window_id) @@ -473,11 +477,36 @@ def plot( with self._condition: if not window_id: window_id = self._get_unique_window_id() - if in_notebook(): + if in_notebook() or get_config()["blocking"]: self._plot_notebook(object, window_id) else: self._open_and_plot_console(object, window_id) + def save_graphic( + self, + window_id: str, + format: str, + ) -> None: + """ + Save graphics. + + Parameters + ---------- + window_id : str + Window id for which graphic should be saved. + format : str + Graphic format. Supported formats are svg, eps, ps, pdf and tex. + + Raises + ------ + ValueError + If window does not support specified format. + """ + with self._condition: + window = self._post_windows.get(window_id) + if window: + window.plotter.save_graphic(f"{window_id}.{format}") + def refresh_windows( self, session_id: Optional[str] = "", @@ -561,7 +590,7 @@ def close_windows( for window_id in windows_id: window = self._post_windows.get(window_id) if window: - if in_notebook(): + if in_notebook() or get_config()["blocking"]: window.plotter.close() window.close = True diff --git a/doc/source/api/post/index.rst b/doc/source/api/post/index.rst index 99712efeec0b..e8ecb11032bf 100644 --- a/doc/source/api/post/index.rst +++ b/doc/source/api/post/index.rst @@ -70,8 +70,8 @@ environment and PyVista is used to visualze the extracted data. print(name) # iso surface - surface1.surface_type.iso_surface.field= "velocity-magnitude" - surface1.surface_type.iso_surface.rendering= "contour" + surface1.surface.iso_surface.field= "velocity-magnitude" + surface1.surface.iso_surface.rendering= "contour" # display contour1.display() @@ -90,13 +90,13 @@ environment and data is plotted in MatplotLib. .. code:: python # import module - from ansys.fluent.post.matplotlib import XYPlots + from ansys.fluent.post.matplotlib import Plots - # get the xyplots object for the session - xyplots_session1 = XYPlots(session) + # get the plots object for the session + plots_session1 = Plots(session) - #get plot object - plot1=xyplots_session1["plot-1"] + #get xyplot object + plot1=plots_session1.XYPlots["plot-1"] #set properties plot1.surfaces_list = ["symmetry"] diff --git a/tests/test_post.py b/tests/test_post.py index 110312def71a..892301cd404a 100644 --- a/tests/test_post.py +++ b/tests/test_post.py @@ -90,16 +90,15 @@ class MockLocalObjectDataExtractor: def __init__(self, obj=None): if not MockLocalObjectDataExtractor._session_data: - pickle_obj = open( + with open( str( Path(MockLocalObjectDataExtractor._session_dump).resolve() ), "rb", - ) - MockLocalObjectDataExtractor._session_data = pickle.load( - pickle_obj - ) - pickle_obj.close() + ) as pickle_obj: + MockLocalObjectDataExtractor._session_data = pickle.load( + pickle_obj + ) self.field_info = lambda: MockFieldInfo( MockLocalObjectDataExtractor._session_data ) @@ -109,16 +108,41 @@ def __init__(self, obj=None): self.id = lambda: 1 -def test_create_graphics_objects(): +def test_graphics_operations(): pyvista_graphics1 = Graphics(session=None) pyvista_graphics2 = Graphics(session=None) - pyvista_graphics1.Contours["contour-1"] - pyvista_graphics2.Contours["contour-2"] + contour1 = pyvista_graphics1.Contours["contour-1"] + contour2 = pyvista_graphics2.Contours["contour-2"] + # create assert pyvista_graphics1 is not pyvista_graphics2 assert pyvista_graphics1.Contours is pyvista_graphics2.Contours assert list(pyvista_graphics1.Contours) == ["contour-1", "contour-2"] + contour2.field = "temperature" + contour2.surfaces_list = contour2.surfaces_list.allowed_values + + contour1.field = "pressure" + contour1.surfaces_list = contour2.surfaces_list.allowed_values[0] + + # copy + pyvista_graphics2.Contours["contour-3"] = contour1() + contour3 = pyvista_graphics2.Contours["contour-3"] + assert contour3() == contour1() + + # update + contour3.update(contour2()) + assert contour3() == contour2() + + # del + assert list(pyvista_graphics1.Contours) == [ + "contour-1", + "contour-2", + "contour-3", + ] + del pyvista_graphics1.Contours["contour-3"] + assert list(pyvista_graphics1.Contours) == ["contour-1", "contour-2"] + def test_contour_object():