diff --git a/PyPEEC/lib_check/check_data_visualization.py b/PyPEEC/lib_check/check_data_visualization.py index bfa12a2f..4df9c93e 100644 --- a/PyPEEC/lib_check/check_data_visualization.py +++ b/PyPEEC/lib_check/check_data_visualization.py @@ -221,6 +221,7 @@ def _check_data_plotter_matplotlib(data_plot): if data_plot not in ["convergence", "residuum"]: raise CheckError("data_plot: specified data plot is invalid") + def _check_data_plotter_pyvista(data_plot): """ Check the data describing a PyVista plot (for the plotter). @@ -272,7 +273,7 @@ def _check_data_plotter_item(data_plotter): raise CheckError("plot_framework: plot framework should be a string") # check the plot data for the framework - if plot_framework== "matplotlib": + if plot_framework == "matplotlib": _check_data_plotter_matplotlib(data_plot) elif plot_framework == "pyvista": _check_data_plotter_pyvista(data_plot) @@ -344,4 +345,3 @@ def check_data_viewer(data_viewer): # check items for dat_tmp in data_viewer: _check_data_viewer_item(dat_tmp) - diff --git a/PyPEEC/lib_utils/plotgui.py b/PyPEEC/lib_utils/plotgui.py index ab24c403..b9bd6e74 100644 --- a/PyPEEC/lib_utils/plotgui.py +++ b/PyPEEC/lib_utils/plotgui.py @@ -3,8 +3,8 @@ The Qt framework is used for the GUI. WARNING: This module is using different more or less dirty hacks. - This module is likely to be non-portable. - This module is likely to break with some version of the dependencies. + This module is likely to be non-portable across platforms (tested on Linux x64). + This module is likely to break with newer/older versions of the dependencies. """ __author__ = "Thomas Guillod" @@ -28,10 +28,11 @@ mpl.use('QtAgg') -def open_pyvista(data_window, is_blocking): +def open_pyvista(data_window, is_interactive): """ Get a PyVista plotter. - If the call is non-blocking, the window is not shown. + If the mode is set to interactive, the plotter is customized (title, size, and menu). + If the mode is set to non-interactive, a basic plotter is returned. """ # get the data @@ -39,9 +40,9 @@ def open_pyvista(data_window, is_blocking): show_menu = data_window["show_menu"] size = data_window["size"] - # create the figure (blocking or non-blocking) - if is_blocking: - # get Qt plotter if blocking + # create the figure + if is_interactive: + # get Qt plotter if interactive pl = pvqt.BackgroundPlotter( show=True, toolbar=show_menu, @@ -52,64 +53,71 @@ def open_pyvista(data_window, is_blocking): # set icon pl.set_icon(PATH_ROOT + "/icon.png") else: - # get standard plotter if non-blocking + # get standard plotter if non-interactive pl = pv.Plotter(off_screen=True) return pl -def open_matplotlib(data_window, is_blocking): +def open_matplotlib(data_window, is_interactive): + """ + Get a Matplotlib figure. + If the mode is set to interactive, the figure is customized (title, size, and menu). + If the mode is set to non-interactive, a basic figure is returned. + """ + # get the data title = data_window["title"] show_menu = data_window["show_menu"] size = data_window["size"] - # create the figure (blocking or non-blocking) - if is_blocking: - (fig, ax) = plt.subplots(tight_layout=True) + # get the figure + (fig, ax) = plt.subplots(tight_layout=True) - # set the Qt options + # create the figure + if is_interactive: + # get the window size and icon (sizex, sizey) = size icn = qtu.QIcon(PATH_ROOT + "/icon.png") + # set the Qt options man = plt.get_current_fig_manager() man.window.setWindowTitle(title) man.window.setWindowIcon(icn) man.window.resize(sizex, sizey) - if show_menu: man.toolbar.show() else: man.toolbar.hide() - else: - # get a default plot is non-blocking - (fig, ax) = plt.subplots() return fig -def close_pyvista(pl, is_blocking): +def close_pyvista(pl, is_interactive): """ - Close a PyVista plotter (only if the call is non-blocking). + Close a PyVista plotter (only if the mode is set to non-interactive). """ - # close plotter if non-blocking - if not is_blocking: + if not is_interactive: pl.close() pl.deep_clean() -def close_matplotlib(fig, is_blocking): - if not is_blocking: +def close_matplotlib(fig, is_interactive): + """ + Close a Matplotlib figure (only if the mode is set to non-interactive). + """ + + if not is_interactive: plt.close(fig) -def open_app(is_blocking): +def open_app(is_interactive): """ - Create a master Qt app for all the plotter windows. + Create a master Qt app for all the GUI windows (only if the mode is set to interactive). """ - if is_blocking: + if is_interactive: app = qtw.QApplication([]) else: app = None @@ -117,12 +125,13 @@ def open_app(is_blocking): return app -def run_app(app, is_blocking): +def run_app(app, is_interactive): """ - Enter the event loop (only if the call is non-blocking). + Enter the event loop (only if the mode is set to interactive). + This will create a blocking call until all the windows are closed. """ - if is_blocking: + if is_interactive: plt.show(block=False) exit_code = app.exec_() return exit_code == 0 diff --git a/PyPEEC/lib_visualization/manage_matplotlib.py b/PyPEEC/lib_visualization/manage_matplotlib.py index f57ae657..665a6771 100644 --- a/PyPEEC/lib_visualization/manage_matplotlib.py +++ b/PyPEEC/lib_visualization/manage_matplotlib.py @@ -1,8 +1,23 @@ +""" +Different functions for plotting solver results with Matplotlib. + +For the plotter, the following plots are available: + - plot of the convergence of the matrix solver + - histogram of the final residuum of the solution +""" + +__author__ = "Thomas Guillod" +__copyright__ = "(c) 2023 - Dartmouth College" + import numpy as np import matplotlib.pyplot as plt def _get_plot_residuum(fig, res_raw): + """ + Plot the final residuum (absolute value) with an histogram. + """ + # activate the figure plt.figure(fig) @@ -17,7 +32,11 @@ def _get_plot_residuum(fig, res_raw): plt.title("Solver Residuum") -def _get_plot_congervence(fig, res_iter): +def _get_plot_convergence(fig, res_iter): + """ + Plot the convergence of the iterative matrix solver. + """ + # activate the figure plt.figure(fig) @@ -33,13 +52,17 @@ def _get_plot_congervence(fig, res_iter): def get_plot_plotter(fig, solver_status, data_plot): + """ + Plot the solver status (for the plotter). + """ + # get the data res_raw = solver_status["res_raw"] res_iter = solver_status["res_iter"] # get the main plot if data_plot == "convergence": - _get_plot_congervence(fig, res_iter) + _get_plot_convergence(fig, res_iter) elif data_plot == "residuum": _get_plot_residuum(fig, res_raw) else: diff --git a/PyPEEC/lib_visualization/manage_pyvista.py b/PyPEEC/lib_visualization/manage_pyvista.py index c6c59d7a..22b60a0a 100644 --- a/PyPEEC/lib_visualization/manage_pyvista.py +++ b/PyPEEC/lib_visualization/manage_pyvista.py @@ -1,12 +1,12 @@ """ Different functions for plotting voxel structures with PyVista. -For the viewer and the plotter, the following object are plotter: +For the viewer and the plotter, the following object are shown: - the complete voxel structure (as wireframe) - the structure containing non-empty voxels (as wireframe) - the defined point cloud (as points) -For the plotter, the following plots are available: +For the viewer, the following plots are available: - the domains are shown for the non-empty voxels - the connected components for the non-empty voxels diff --git a/PyPEEC/plotter.py b/PyPEEC/plotter.py index 6592c32f..4f52858e 100644 --- a/PyPEEC/plotter.py +++ b/PyPEEC/plotter.py @@ -7,6 +7,10 @@ - current density - magnetic field +Two different mode are available for the plots: + - interactive mode: the plots are shown (blocking call at the end) + - silent mode: nothing is shown and the program exit (for debugging and testing) + The plotting is done with PyVista with the Qt framework. """ @@ -64,7 +68,7 @@ def _get_grid_voxel(data_solution, data_point): return grid, voxel, point, solver_status -def _get_plot(grid, voxel, point, solver_status, data_plotter, is_blocking): +def _get_plot(grid, voxel, point, solver_status, data_plotter, is_interactive): """ Make a plot with the specified user settings. The plot contains the following elements: @@ -81,27 +85,27 @@ def _get_plot(grid, voxel, point, solver_status, data_plotter, is_blocking): # make the plots if plot_framework == "pyvista": # get the plotter (with the Qt framework) - pl = plotgui.open_pyvista(data_window, is_blocking) + pl = plotgui.open_pyvista(data_window, is_interactive) # make the plot manage_pyvista.get_plot_plotter(pl, grid, voxel, point, data_plot) - # close plotter if non-blocking - plotgui.close_pyvista(pl, is_blocking) + # close plotter if non-interactive + plotgui.close_pyvista(pl, is_interactive) elif plot_framework == "matplotlib": # get the figure (with the Qt framework) - fig = plotgui.open_matplotlib(data_window, is_blocking) + fig = plotgui.open_matplotlib(data_window, is_interactive) # make the plot manage_matplotlib.get_plot_plotter(fig, solver_status, data_plot) - # close figure if non-blocking - plotgui.close_matplotlib(fig, is_blocking) + # close figure if non-interactive + plotgui.close_matplotlib(fig, is_interactive) else: raise ValueError("invalid plot framework") -def run(data_solution, data_point, data_plotter, is_blocking): +def run(data_solution, data_point, data_plotter, is_interactive): """ Main script for plotting the solution of a FFT-PEEC problem. """ @@ -119,7 +123,7 @@ def run(data_solution, data_point, data_plotter, is_blocking): # create the Qt app (should be at the beginning) logger.info("create the GUI application") - app = plotgui.open_app(is_blocking) + app = plotgui.open_app(is_interactive) # handle the data logger.info("parse the voxel geometry and the data") @@ -129,7 +133,7 @@ def run(data_solution, data_point, data_plotter, is_blocking): logger.info("generate the different plots") for i, dat_tmp in enumerate(data_plotter): logger.info("plotting %d / %d" % (i + 1, len(data_plotter))) - _get_plot(grid, voxel, point, solver_status, dat_tmp, is_blocking) + _get_plot(grid, voxel, point, solver_status, dat_tmp, is_interactive) except CheckError as ex: logger.error("check error : " + str(ex)) return False @@ -141,6 +145,6 @@ def run(data_solution, data_point, data_plotter, is_blocking): logger.info("successful termination") # enter the event loop (should be at the end, blocking call) - status = plotgui.run_app(app, is_blocking) + status = plotgui.run_app(app, is_interactive) return status diff --git a/PyPEEC/script.py b/PyPEEC/script.py index b47d780a..0e2290ab 100644 --- a/PyPEEC/script.py +++ b/PyPEEC/script.py @@ -48,7 +48,7 @@ def run_mesher(file_mesher, file_voxel): return status -def run_viewer(file_voxel, file_point, file_viewer, is_blocking): +def run_viewer(file_voxel, file_point, file_viewer, is_interactive): """ Load the voxel structure and plot the results. """ @@ -61,7 +61,7 @@ def run_viewer(file_voxel, file_point, file_viewer, is_blocking): data_viewer = fileio.load_json(file_viewer) # call the viewer - status = viewer.run(data_voxel, data_point, data_viewer, is_blocking) + status = viewer.run(data_voxel, data_point, data_viewer, is_interactive) except FileError as ex: logger.error("check error : " + str(ex)) return False @@ -93,7 +93,7 @@ def run_solver(file_voxel, file_problem, file_solution): return status -def run_plotter(file_solution, file_point, file_plotter, is_blocking): +def run_plotter(file_solution, file_point, file_plotter, is_interactive): """ Load the solver solution and plot the results. """ @@ -106,7 +106,7 @@ def run_plotter(file_solution, file_point, file_plotter, is_blocking): data_plotter = fileio.load_json(file_plotter) # call the plotter - status = plotter.run(data_solution, data_point, data_plotter, is_blocking) + status = plotter.run(data_solution, data_point, data_plotter, is_interactive) except FileError as ex: logger.error("check error : " + str(ex)) return False @@ -182,12 +182,12 @@ def main_viewer(): "--silent", help="if set, do not display the plots", action="store_false", - dest="is_blocking", + dest="is_interactive", ) # parse and call args = parser.parse_args() - status = run_viewer(args.file_voxel, args.file_point, args.file_viewer, args.is_blocking) + status = run_viewer(args.file_voxel, args.file_point, args.file_viewer, args.is_interactive) sys.exit(int(not status)) @@ -266,10 +266,10 @@ def main_plotter(): "--silent", help="if set, do not display the plots", action="store_false", - dest="is_blocking", + dest="is_interactive", ) # parse and call args = parser.parse_args() - status = run_plotter(args.file_solution, args.file_point, args.file_plotter, args.is_blocking) + status = run_plotter(args.file_solution, args.file_point, args.file_plotter, args.is_interactive) sys.exit(int(not status)) diff --git a/PyPEEC/viewer.py b/PyPEEC/viewer.py index 7717f4a9..d2cd1feb 100644 --- a/PyPEEC/viewer.py +++ b/PyPEEC/viewer.py @@ -4,6 +4,10 @@ - the different domain composing the voxel structure - the connected components of the voxel structure +Two different mode are available for the plots: + - interactive mode: the plots are shown (blocking call at the end) + - silent mode: nothing is shown and the program exit (for debugging and testing) + The plotting is done with PyVista with the Qt framework. """ @@ -50,7 +54,7 @@ def _get_grid_voxel(data_voxel, data_point): return grid, voxel, point -def _get_plot(grid, voxel, point, data_viewer, is_blocking): +def _get_plot(grid, voxel, point, data_viewer, is_interactive): """ Make a plot with the voxel structure and the domains. """ @@ -60,16 +64,16 @@ def _get_plot(grid, voxel, point, data_viewer, is_blocking): data_plot = data_viewer["data_plot"] # get the plotter (with the Qt framework) - pl = plotgui.open_pyvista(data_window, is_blocking) + pl = plotgui.open_pyvista(data_window, is_interactive) # make the plot manage_pyvista.get_plot_viewer(pl, grid, voxel, point, data_plot) - # close plotter if non-blocking - plotgui.close_pyvista(pl, is_blocking) + # close plotter if non-interactive + plotgui.close_pyvista(pl, is_interactive) -def run(data_voxel, data_point, data_viewer, is_blocking): +def run(data_voxel, data_point, data_viewer, is_interactive): """ Main script for visualizing a 3D voxel structure. """ @@ -86,7 +90,7 @@ def run(data_voxel, data_point, data_viewer, is_blocking): # create the Qt app (should be at the beginning) logger.info("create the GUI application") - app = plotgui.open_app(is_blocking) + app = plotgui.open_app(is_interactive) # handle the data logger.info("parse the voxel geometry and the data") @@ -96,7 +100,7 @@ def run(data_voxel, data_point, data_viewer, is_blocking): logger.info("generate the different plots") for i, dat_tmp in enumerate(data_viewer): logger.info("plotting %d / %d" % (i + 1, len(data_viewer))) - _get_plot(grid, voxel, point, dat_tmp, is_blocking) + _get_plot(grid, voxel, point, dat_tmp, is_interactive) except CheckError as ex: logger.error("check error : " + str(ex)) return False @@ -108,6 +112,6 @@ def run(data_voxel, data_point, data_viewer, is_blocking): logger.info("successful termination") # enter the event loop (should be at the end, blocking call) - status = plotgui.run_app(app, is_blocking) + status = plotgui.run_app(app, is_interactive) return status diff --git a/examples/run_visualization.py b/examples/run_visualization.py index 5b5b8ebe..c4ae8cc9 100644 --- a/examples/run_visualization.py +++ b/examples/run_visualization.py @@ -173,7 +173,7 @@ def get_data_viewer(): """ Get the options for visualizing the voxel structure. Each element in the list represents a different plot. - This structure is used by the viewer. + This list is used by the viewer. """ data_viewer = [ @@ -202,7 +202,7 @@ def get_data_plotter(): """ Get the options for plotting the solver solution. Each element in the list represents a different plot. - This structure is used by the plotter. + This list is used by the plotter. """ # get the plots