From 8aac8ec696c352ed853d56b0cc5fa2f7123458bd Mon Sep 17 00:00:00 2001 From: bbean Date: Mon, 5 Aug 2024 20:43:45 -0600 Subject: [PATCH 1/4] fix the window location upper_left_xy property in figure_management for tkinter backends --- .../common/lib/render/figure_management.py | 6 +- .../lib/render/test/test_figure_management.py | 55 ++++++++++++------- .../lib/render_control/RenderControlFigure.py | 27 ++++++--- 3 files changed, 58 insertions(+), 30 deletions(-) diff --git a/opencsp/common/lib/render/figure_management.py b/opencsp/common/lib/render/figure_management.py index 2545465e2..60a194352 100644 --- a/opencsp/common/lib/render/figure_management.py +++ b/opencsp/common/lib/render/figure_management.py @@ -230,7 +230,11 @@ def _setup_figure( upper_left_xy = figure_control.upper_left_xy x = upper_left_xy[0] y = upper_left_xy[1] - fig.canvas.manager.window.move(x, y) + window = fig.canvas.manager.window + if hasattr(window, "move"): + window.move(x, y) # qt + else: + window.geometry(f"+{x}+{y}") # tkinter # Copying this command, as from Randy, which suppresses duplicate axes in tile_figure(). ~ BGB plt.axis('off') diff --git a/opencsp/common/lib/render/test/test_figure_management.py b/opencsp/common/lib/render/test/test_figure_management.py index 61cf5ce33..aafa2fe75 100644 --- a/opencsp/common/lib/render/test/test_figure_management.py +++ b/opencsp/common/lib/render/test/test_figure_management.py @@ -1,18 +1,15 @@ -import os -import subprocess import sys -import time import unittest import matplotlib.pyplot as plt +from PIL import Image -import opencsp.common.lib.opencsp_path.opencsp_root_path as root_path -import opencsp.common.lib.process.subprocess_tools as st import opencsp.common.lib.render.figure_management as fm -import opencsp.common.lib.render.test.lib.RenderControlFigureRecordInfSave as rcfr_is +import opencsp.common.lib.render.view_spec as vs +import opencsp.common.lib.render_control.RenderControlAxis as rca import opencsp.common.lib.render_control.RenderControlFigure as rcfg +import opencsp.common.lib.render.test.lib.RenderControlFigureRecordInfSave as rcfr_is import opencsp.common.lib.tool.file_tools as ft -import opencsp.common.lib.tool.log_tools as lt is_original_call = "--funcname" in sys.argv """ Because we call this file again but with arguments, we need to know if @@ -21,21 +18,18 @@ class test_figure_management(unittest.TestCase): - dir_in = os.path.join('common', 'lib', 'render', 'test', 'data', 'input', 'figure_management') - dir_out = os.path.join('common', 'lib', 'render', 'test', 'data', 'output', 'figure_management') - - def __init__(self, *vargs, **kwargs): - super().__init__(*vargs, **kwargs) - self.dir_in = test_figure_management.dir_in - self.dir_out = test_figure_management.dir_out - @classmethod def setUpClass(cls) -> None: - ret = super().setUpClass() - ft.create_directories_if_necessary(cls.dir_out) + path, name, _ = ft.path_components(__file__) + cls.in_dir = ft.join(path, 'data/input', name.split('test_')[-1]) + cls.out_dir = ft.join(path, 'data/output', name.split('test_')[-1]) + ft.create_directories_if_necessary(cls.out_dir) if is_original_call: - ft.delete_files_in_directory(cls.dir_out, "*") - return ret + ft.delete_files_in_directory(cls.out_dir, "*") + return super().setUpClass() + + def setUp(self) -> None: + self.test_name = self.id().split('.')[-1] def tearDown(self): # Make sure we release all matplotlib resources. @@ -65,7 +59,7 @@ def test_save_all_figures_line(self): line = list(range(100)) view.draw_p_list(line) - figs_txts = fm.save_all_figures(self.dir_out) + figs_txts = fm.save_all_figures(self.out_dir) self.assert_exists(figs_txts, 1) def test_save_all_figures_two_lines(self): @@ -83,7 +77,7 @@ def test_save_all_figures_two_lines(self): line = lines[i] view.draw_p_list(line) - figs_txts = fm.save_all_figures(self.dir_out) + figs_txts = fm.save_all_figures(self.out_dir) self.assert_exists(figs_txts, 2) def _figure_manager_timeout_1(self): @@ -113,6 +107,25 @@ def _figure_manager_timeout_1(self): return fm + def test_upper_left_xy_no_exception(self): + """ + Verify that figure_management._setup_figure() with the figure control + parameter "upper_left_xy" set doesn't raise an exception. + """ + # TODO how to test that the window has actually been located correctly? + axis_control = rca.meters() + figure_control = rcfg.RenderControlFigure(tile=False, upper_left_xy=(100, 100)) + view_spec_2d = vs.view_spec_xy() + fig_record = fm.setup_figure( + figure_control, + axis_control, + view_spec_2d, + title=self.test_name, + code_tag=f"{__file__}.{self.test_name}", + equal=False, + ) + fig_record.view.show() + fig_record.close() if __name__ == '__main__': import argparse diff --git a/opencsp/common/lib/render_control/RenderControlFigure.py b/opencsp/common/lib/render_control/RenderControlFigure.py index 795c3929d..357858670 100644 --- a/opencsp/common/lib/render_control/RenderControlFigure.py +++ b/opencsp/common/lib/render_control/RenderControlFigure.py @@ -12,7 +12,7 @@ class RenderControlFigure: def __init__( self, tile=True, # True => Lay out figures in grid. False => Place at upper_left or default screen center. - tile_array=(3, 2), # (n_x, n_y) + tile_array: tuple[int, int] = (3, 2), # (n_x, n_y) tile_square=False, # Set to True for equal-axis 3d plots. figsize=(6.4, 4.8), # inch. upper_left_xy=None, # pixel. (0,0) --> Upper left corner of screen. @@ -35,13 +35,24 @@ def __init__( view.draw_pq_list(energy_values, style=style) view.show(block=True) - Args: - - tile (bool): True => Lay out figures in grid. False => Place at upper_left or default screen center. Default True - - tile_array (tuple[int]): How many tiles across and down (n_x, n_y). Default (3, 2) - - tile_square (bool): Set to True for equal-axis 3d plots. Default False - - figsize (tuple[float]): Size of the figure in inches. Default (6.4, 4.8) - - upper_left_xy (tuple[int]): Pixel placement for the first tile. (0,0) --> Upper left corner of screen. Default None - - grid (bool): Whether or not to draw grid lines. Note: this value seems to be inverted. Default True + Params: + ------- + tile : bool, optional + True => Lay out figures in grid. False => Place at upper_left or + default screen center. If True, then figsize, upper_left_xy, and + maximize are ignored. Default True + tile_array : tuple[int] | None, optional + How many tiles across and down (n_x, n_y). + tile_square : bool, optional + Set to True for equal-axis 3d plots. Default False + figsize : tuple[float], optional + Size of the figure in inches. Ignored if tile is True. Default (6.4, 4.8) + upper_left_xy : tuple[int], optional + Pixel placement for the first tile. (0,0) --> Upper left corner of + screen. Ignored if tile is True. Default None + grid : bool, optional + Whether or not to draw grid lines. Note: this value seems to be + inverted. Default True """ super(RenderControlFigure, self).__init__() From a9dabac647ca91122a99b5e4190d25164a773351 Mon Sep 17 00:00:00 2001 From: bbean Date: Mon, 5 Aug 2024 20:48:18 -0600 Subject: [PATCH 2/4] fix figure dpi sizing while saving --- opencsp/common/lib/render/View3d.py | 6 +++- .../lib/render/test/test_figure_management.py | 36 +++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/opencsp/common/lib/render/View3d.py b/opencsp/common/lib/render/View3d.py index 4a7c70c16..a7e725b0f 100644 --- a/opencsp/common/lib/render/View3d.py +++ b/opencsp/common/lib/render/View3d.py @@ -299,7 +299,11 @@ def save(self, output_dir, output_figure_body, format='png', dpi=300) -> str: # Save the figure. output_figure_dir_body_ext = output_figure_dir_body + '.' + format lt.info('In View3d.save(), saving figure: ' + output_figure_dir_body_ext) - # plt.savefig(output_figure_dir_body_ext, format=format, dpi=dpi) + self.view.set_size_inches(self.view.get_figwidth(), self.view.get_figheight(), forward=True) + # it could be that another backend requires the following instead: + # self.view.set_figwidth(self.view.get_figwidth() * dpi) + # self.view.set_figheight(self.view.get_figheight() * dpi) + self.view.set_dpi(dpi) self.view.savefig(output_figure_dir_body_ext, format=format, dpi=dpi) # Return the outptu file path and directory. return output_figure_dir_body_ext diff --git a/opencsp/common/lib/render/test/test_figure_management.py b/opencsp/common/lib/render/test/test_figure_management.py index aafa2fe75..fe2d80197 100644 --- a/opencsp/common/lib/render/test/test_figure_management.py +++ b/opencsp/common/lib/render/test/test_figure_management.py @@ -127,6 +127,42 @@ def test_upper_left_xy_no_exception(self): fig_record.view.show() fig_record.close() + def test_save_figsize(self): + """Verify that the size of the saved figure is as given in the save parameters.""" + # create and save the figure with pixel sizes: + # small: 900 x 600 + # regular: 1800 x 1200 + # large: 2700 x 1800 + axis_control = rca.meters() + figure_control = rcfg.RenderControlFigure(tile=False, figsize=(3, 2)) + view_spec_2d = vs.view_spec_xy() + fig_record = fm.setup_figure( + figure_control, + axis_control, + view_spec_2d, + title=self.test_name, + code_tag=f"{__file__}.{self.test_name}", + equal=False, + ) + # fig_record.view.show() + fig_record.save(self.out_dir, f"{self.test_name}_small", format="png", dpi=300, close_after_save=False) + fig_record.save(self.out_dir, f"{self.test_name}_regular", format="png", dpi=600, close_after_save=False) + fig_record.save(self.out_dir, f"{self.test_name}_large", format="png", dpi=900, close_after_save=False) + fig_record.view.show() + fig_record.close() + + # load the images and verify their size in pixels + with Image.open(ft.join(self.out_dir, f"{self.test_name}_small_xy.png")) as img_small: + self.assertEqual(img_small.width, 900) + self.assertEqual(img_small.height, 600) + with Image.open(ft.join(self.out_dir, f"{self.test_name}_regular_xy.png")) as img_regular: + self.assertEqual(img_regular.width, 1800) + self.assertEqual(img_regular.height, 1200) + with Image.open(ft.join(self.out_dir, f"{self.test_name}_large_xy.png")) as img_large: + self.assertEqual(img_large.width, 2700) + self.assertEqual(img_large.height, 1800) + + if __name__ == '__main__': import argparse From defee1341995741067f3a0cc10320e60b739ed98 Mon Sep 17 00:00:00 2001 From: bbean Date: Mon, 5 Aug 2024 20:48:57 -0600 Subject: [PATCH 3/4] add "maximize" option to figure_management --- .../common/lib/render/figure_management.py | 6 ++++++ .../lib/render/test/test_figure_management.py | 20 +++++++++++++++++++ .../lib/render_control/RenderControlFigure.py | 5 +++++ 3 files changed, 31 insertions(+) diff --git a/opencsp/common/lib/render/figure_management.py b/opencsp/common/lib/render/figure_management.py index 60a194352..0f7352e3c 100644 --- a/opencsp/common/lib/render/figure_management.py +++ b/opencsp/common/lib/render/figure_management.py @@ -235,6 +235,12 @@ def _setup_figure( window.move(x, y) # qt else: window.geometry(f"+{x}+{y}") # tkinter + if figure_control.maximize: + window = fig.canvas.manager.window + if hasattr(window, "showMaximized"): + window.showMaximized() # qt + else: + window.state("zoomed") # tkinter # Copying this command, as from Randy, which suppresses duplicate axes in tile_figure(). ~ BGB plt.axis('off') diff --git a/opencsp/common/lib/render/test/test_figure_management.py b/opencsp/common/lib/render/test/test_figure_management.py index fe2d80197..c282576f1 100644 --- a/opencsp/common/lib/render/test/test_figure_management.py +++ b/opencsp/common/lib/render/test/test_figure_management.py @@ -127,6 +127,26 @@ def test_upper_left_xy_no_exception(self): fig_record.view.show() fig_record.close() + def test_maximize_no_exception(self): + """ + Verify that figure_management._setup_figure() with the figure control + parameter "maximize" set doesn't raise an exception. + """ + # TODO how to test that the window has actually been maximized? + axis_control = rca.meters() + figure_control = rcfg.RenderControlFigure(tile=False, maximize=True) + view_spec_2d = vs.view_spec_xy() + fig_record = fm.setup_figure( + figure_control, + axis_control, + view_spec_2d, + title=self.test_name, + code_tag=f"{__file__}.{self.test_name}", + equal=False, + ) + fig_record.view.show() + fig_record.close() + def test_save_figsize(self): """Verify that the size of the saved figure is as given in the save parameters.""" # create and save the figure with pixel sizes: diff --git a/opencsp/common/lib/render_control/RenderControlFigure.py b/opencsp/common/lib/render_control/RenderControlFigure.py index 357858670..909c927ae 100644 --- a/opencsp/common/lib/render_control/RenderControlFigure.py +++ b/opencsp/common/lib/render_control/RenderControlFigure.py @@ -17,6 +17,7 @@ def __init__( figsize=(6.4, 4.8), # inch. upper_left_xy=None, # pixel. (0,0) --> Upper left corner of screen. grid=True, + maximize=False, ): # Whether or not to draw grid lines. """Set of controls for how to render figures. @@ -53,6 +54,9 @@ def __init__( grid : bool, optional Whether or not to draw grid lines. Note: this value seems to be inverted. Default True + maximize : bool, optional + Whether the figure should be maximized (made full screen) as soon as + it is made visible. Ignored if tile is True. Default False. """ super(RenderControlFigure, self).__init__() @@ -66,6 +70,7 @@ def __init__( # Figure size and placement. self.figsize = figsize self.upper_left_xy = upper_left_xy + self.maximize = maximize # Axis control. self.x_label = 'x (m)' From b59b13e281b51bea630e8cbd48cc1ed7a8a540da Mon Sep 17 00:00:00 2001 From: bbean Date: Mon, 5 Aug 2024 21:21:35 -0600 Subject: [PATCH 4/4] skip the test_maximize_no_exception in the case that we're running on the ubi8 test docker image --- .../lib/render/test/test_figure_management.py | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/opencsp/common/lib/render/test/test_figure_management.py b/opencsp/common/lib/render/test/test_figure_management.py index c282576f1..beaf3aa2a 100644 --- a/opencsp/common/lib/render/test/test_figure_management.py +++ b/opencsp/common/lib/render/test/test_figure_management.py @@ -136,16 +136,22 @@ def test_maximize_no_exception(self): axis_control = rca.meters() figure_control = rcfg.RenderControlFigure(tile=False, maximize=True) view_spec_2d = vs.view_spec_xy() - fig_record = fm.setup_figure( - figure_control, - axis_control, - view_spec_2d, - title=self.test_name, - code_tag=f"{__file__}.{self.test_name}", - equal=False, - ) - fig_record.view.show() - fig_record.close() + try: + fig_record = fm.setup_figure( + figure_control, + axis_control, + view_spec_2d, + title=self.test_name, + code_tag=f"{__file__}.{self.test_name}", + equal=False, + ) + fig_record.view.show() + fig_record.close() + except Exception as ex: + ubi8_msg = '_tkinter.TclError: bad argument "zoomed": must be normal, iconic, or withdrawn' + if ubi8_msg in str(ex): + # TODO how to make this test work on ubi8? + self.skipTest("Window 'maximize' state doesn't working on our ubi8 test docker image.") def test_save_figsize(self): """Verify that the size of the saved figure is as given in the save parameters."""