From 384b8926b65c2ea0fb95fbf8dbb9a29531c3f836 Mon Sep 17 00:00:00 2001 From: Massimo Capodiferro <77293250+maxcapodi78@users.noreply.github.com> Date: Fri, 18 Oct 2024 16:54:27 +0200 Subject: [PATCH 1/4] FEAT: New class ReportPlotter to handle all matplotlib plot (#5297) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: maxcapodi78 Co-authored-by: Samuelopez-ansys Co-authored-by: Sébastien Morais <146729917+SMoraisAnsys@users.noreply.github.com> Co-authored-by: Samuel Lopez <85613111+Samuelopez-ansys@users.noreply.github.com> --- _unittest/test_12_1_PostProcessing.py | 9 + _unittest/test_46_FarField.py | 67 +- doc/source/API/visualization/plot.rst | 5 +- src/ansys/aedt/core/generic/settings.py | 11 + .../aedt/core/modeler/pcb/object_3d_layout.py | 2 +- .../advanced/farfield_visualization.py | 80 +- .../core/visualization/plot/matplotlib.py | 1519 +++++++++++++++-- .../aedt/core/visualization/plot/pyvista.py | 13 +- .../aedt/core/visualization/post/common.py | 122 +- .../core/visualization/post/solution_data.py | 167 +- 10 files changed, 1722 insertions(+), 273 deletions(-) diff --git a/_unittest/test_12_1_PostProcessing.py b/_unittest/test_12_1_PostProcessing.py index d3f8d0a3f1d..ef9d993ff23 100644 --- a/_unittest/test_12_1_PostProcessing.py +++ b/_unittest/test_12_1_PostProcessing.py @@ -308,6 +308,11 @@ def test_09_manipulate_report(self): assert self.aedtapp.post.create_report_from_configuration( os.path.join(self.local_scratch.path, f"{plot.plot_name}.json"), solution_name=self.aedtapp.nominal_sweep ) + assert self.aedtapp.post.create_report_from_configuration( + os.path.join(self.local_scratch.path, f"{plot.plot_name}.json"), + solution_name=self.aedtapp.nominal_sweep, + matplotlib=True, + ) assert self.aedtapp.post.create_report( expressions="MaxMagDeltaS", variations={"Pass": ["All"]}, @@ -760,6 +765,7 @@ def test_67_sweep_from_json(self): local_path = os.path.dirname(os.path.realpath(__file__)) dict_vals = read_json(os.path.join(local_path, "example_models", "report_json", "Modal_Report_Simple.json")) assert self.aedtapp.post.create_report_from_configuration(report_settings=dict_vals) + assert self.aedtapp.post.create_report_from_configuration(report_settings=dict_vals, matplotlib=True) @pytest.mark.skipif( config["desktopVersion"] < "2022.2", reason="Not working in non graphical in version lower than 2022.2" @@ -769,6 +775,9 @@ def test_70_sweep_from_json(self): assert self.aedtapp.post.create_report_from_configuration( os.path.join(local_path, "example_models", "report_json", "Modal_Report.json") ) + assert self.aedtapp.post.create_report_from_configuration( + os.path.join(local_path, "example_models", "report_json", "Modal_Report.json"), matplotlib=True + ) def test_74_dynamic_update(self): val = self.aedtapp.post.update_report_dynamically diff --git a/_unittest/test_46_FarField.py b/_unittest/test_46_FarField.py index 83d053a8090..c047bb4c02d 100644 --- a/_unittest/test_46_FarField.py +++ b/_unittest/test_46_FarField.py @@ -26,9 +26,9 @@ import shutil from ansys.aedt.core.visualization.advanced.farfield_visualization import FfdSolutionData +from ansys.aedt.core.visualization.plot.matplotlib import ReportPlotter # from _unittest.conftest import config -from matplotlib.figure import Figure import pytest from pyvista.plotting.plotter import Plotter @@ -150,9 +150,9 @@ def test_04_far_field_data(self, local_scratch): ffdata.plot_cut(output_file=img3, show=False) assert os.path.exists(img3) curve_2d = ffdata.plot_cut(show=False) - assert isinstance(curve_2d, Figure) + assert isinstance(curve_2d, ReportPlotter) data = ffdata.plot_3d_chart(show=False) - assert isinstance(data, Figure) + assert isinstance(data, ReportPlotter) img4 = os.path.join(self.local_scratch.path, "ff_3d1.jpg") ffdata.plot_3d( @@ -161,6 +161,59 @@ def test_04_far_field_data(self, local_scratch): assert os.path.exists(img4) data_pyvista = ffdata.plot_3d(quantity="RealizedGain", show=False, background=[255, 0, 0], show_geometry=False) assert isinstance(data_pyvista, Plotter) + matplot_lib = ffdata.plot_cut( + quantity="RealizedGain", + primary_sweep="theta", + secondary_sweep_value=[-180, -75, 75], + title=f"Azimuth at {ffdata.frequency}Hz", + quantity_format="dB10", + ) + matplot_lib.add_note( + "Hello Pyaedt2", + [10, -10], + color=(1, 1, 0), + bold=True, + italic=True, + back_color=(0, 0, 1), + background_visibility=True, + ) + + matplot_lib.traces_by_index[0].trace_style = "--" + matplot_lib.x_scale = "log" + _ = matplot_lib.plot_2d() + matplot_lib.add_note( + "Hello Pyaedt", + [0, -10], + color=(1, 1, 0), + bold=False, + italic=False, + background_visibility=False, + ) + matplot_lib.x_scale = "linear" + matplot_lib.traces_by_index[0].trace_color = (1, 0, 0) + matplot_lib.grid_enable_minor_x = True + _ = matplot_lib.plot_2d() + + matplot_lib.traces["Phi=-180"].symbol_style = "v" + _ = matplot_lib.plot_2d() + + matplot_lib.apply_style("dark_background") + matplot_lib.add_limit_line( + [[0, 20, 120], [15, 5, 5]], + properties={ + "trace_color": (1, 0, 0), + }, + name="LimitLine1", + ) + _ = matplot_lib.plot_2d() + + matplot_lib.traces_by_index[0].trace_color = (1, 0, 0) + matplot_lib.grid_enable_minor_x = True + + matplot_lib.grid_enable_minor_x = False + matplot_lib.grid_enable_minor_y = False + + _ = matplot_lib.plot_2d() def test_05_antenna_plot(self, array_test): ffdata = array_test.get_antenna_data(sphere="3D") @@ -171,7 +224,7 @@ def test_05_antenna_plot(self, array_test): img1 = os.path.join(self.local_scratch.path, "contour.jpg") assert ffdata.farfield_data.plot_contour( quantity="RealizedGain", - title="Contour at {}Hz".format(ffdata.farfield_data.frequency), + title=f"Contour at {ffdata.farfield_data.frequency}Hz", output_file=img1, show=False, ) @@ -186,7 +239,7 @@ def test_05_antenna_plot(self, array_test): quantity="RealizedGain", primary_sweep="theta", secondary_sweep_value=[-180, -75, 75], - title="Azimuth at {}Hz".format(ffdata.farfield_data.frequency), + title=f"Azimuth at {ffdata.farfield_data.frequency}Hz", output_file=img2, show=False, ) @@ -197,7 +250,7 @@ def test_05_antenna_plot(self, array_test): quantity="RealizedGain", primary_sweep="phi", secondary_sweep_value=30, - title="Azimuth at {}Hz".format(ffdata.farfield_data.frequency), + title=f"Azimuth at {ffdata.farfield_data.frequency}Hz", output_file=img3, show=False, ) @@ -208,7 +261,7 @@ def test_05_antenna_plot(self, array_test): quantity="RealizedGain", primary_sweep="phi", secondary_sweep_value=30, - title="Azimuth at {}Hz".format(ffdata.farfield_data.frequency), + title=f"Azimuth at {ffdata.farfield_data.frequency}Hz", output_file=img3_polar, show=False, is_polar=True, diff --git a/doc/source/API/visualization/plot.rst b/doc/source/API/visualization/plot.rst index ce6d5f786e3..a00e71715c3 100644 --- a/doc/source/API/visualization/plot.rst +++ b/doc/source/API/visualization/plot.rst @@ -45,10 +45,7 @@ PyAEDT benefits of `matplotlib `_ package and allows to :toctree: _autosummary :nosignatures: - plot_polar_chart - plot_3d_chart - plot_2d_chart - plot_contour + ReportPlotter is_notebook diff --git a/src/ansys/aedt/core/generic/settings.py b/src/ansys/aedt/core/generic/settings.py index 2dddf658537..fe7fa4f3f43 100644 --- a/src/ansys/aedt/core/generic/settings.py +++ b/src/ansys/aedt/core/generic/settings.py @@ -218,6 +218,7 @@ def __init__(self): else: pyaedt_settings_path = os.path.join(os.environ["APPDATA"], "pyaedt_settings.yaml") self.load_yaml_configuration(pyaedt_settings_path) + self.__block_figure_plot = False # ########################## Logging properties ########################## @@ -230,6 +231,16 @@ def logger(self): def logger(self, val): self.__logger = val + @property + def block_figure_plot(self): + """Block matplotlib figure plot during python script run until the user close it manually. + Default is ``False``.""" + return self.__block_figure_plot + + @block_figure_plot.setter + def block_figure_plot(self, val): + self.__block_figure_plot = val + @property def enable_desktop_logs(self): """Enable or disable the logging to the AEDT message window.""" diff --git a/src/ansys/aedt/core/modeler/pcb/object_3d_layout.py b/src/ansys/aedt/core/modeler/pcb/object_3d_layout.py index 713487ff0e8..e2b918e649c 100644 --- a/src/ansys/aedt/core/modeler/pcb/object_3d_layout.py +++ b/src/ansys/aedt/core/modeler/pcb/object_3d_layout.py @@ -851,7 +851,7 @@ def plot( show_legend=True, save_plot=None, outline=None, - size=(2000, 1000), + size=(1920, 1440), plot_components_on_top=False, plot_components_on_bottom=False, show=True, diff --git a/src/ansys/aedt/core/visualization/advanced/farfield_visualization.py b/src/ansys/aedt/core/visualization/advanced/farfield_visualization.py index 012db470082..ebf75a67a61 100644 --- a/src/ansys/aedt/core/visualization/advanced/farfield_visualization.py +++ b/src/ansys/aedt/core/visualization/advanced/farfield_visualization.py @@ -36,11 +36,8 @@ from ansys.aedt.core.generic.general_methods import open_file from ansys.aedt.core.generic.general_methods import pyaedt_function_handler from ansys.aedt.core.visualization.advanced.touchstone_parser import read_touchstone +from ansys.aedt.core.visualization.plot.matplotlib import ReportPlotter from ansys.aedt.core.visualization.plot.matplotlib import is_notebook -from ansys.aedt.core.visualization.plot.matplotlib import plot_2d_chart -from ansys.aedt.core.visualization.plot.matplotlib import plot_3d_chart -from ansys.aedt.core.visualization.plot.matplotlib import plot_contour -from ansys.aedt.core.visualization.plot.matplotlib import plot_polar_chart from ansys.aedt.core.visualization.plot.pyvista import ModelPlotter from ansys.aedt.core.visualization.plot.pyvista import get_structured_mesh @@ -829,7 +826,7 @@ def plot_contour( Returns ------- - :class:`matplotlib.pyplot.Figure` + :class:`ansys.aedt.core.visualization.plot.matplotlib.ReportPlotter` Matplotlib figure object. Examples @@ -860,19 +857,25 @@ def plot_contour( ph, th = np.meshgrid(data["Phi"], data["Theta"][select]) # Convert to radians for polar plot. ph = np.radians(ph) if polar else ph - - return plot_contour( - plot_data=[data_to_plot, th, ph], - xlabel=r"$\phi$ (Degrees)", - ylabel=r"$\theta$ (Degrees)", - title=title, - levels=levels, + new = ReportPlotter() + new.show_legend = False + new.title = title + props = { + "x_label": r"$\phi$ (Degrees)", + "y_label": r"$\theta$ (Degrees)", + } + + new.add_trace([data_to_plot, th, ph], 2, props) + _ = new.plot_contour( + trace=0, polar=polar, - snapshot_path=output_file, + levels=levels, max_theta=max_theta, color_bar=quantity_format, + snapshot_path=output_file, show=show, ) + return new @pyaedt_function_handler() def plot_cut( @@ -924,7 +927,7 @@ def plot_cut( Returns ------- - :class:`matplotlib.pyplot.Figure` + :class:`ansys.aedt.core.visualization.plot.matplotlib.ReportPlotter` Matplotlib figure object. If ``show=True``, a Matplotlib figure instance of the plot is returned. If ``show=False``, the plotted curve is returned. @@ -963,7 +966,7 @@ def plot_cut( y = conversion_function(y, quantity_format) if not isinstance(y, np.ndarray): # pragma: no cover raise Exception("Format of quantity is wrong.") - curves.append([x, y, "{}={}".format(y_key, el)]) + curves.append([x, y, f"{y_key}={el}"]) elif isinstance(secondary_sweep_value, list): list_inserted = [] for el in secondary_sweep_value: @@ -973,7 +976,7 @@ def plot_cut( y = conversion_function(y, quantity_format) if not isinstance(y, np.ndarray): # pragma: no cover raise Exception("Format of quantity is wrong.") - curves.append([x, y, "{}={}".format(y_key, el)]) + curves.append([x, y, f"{y_key}={el}"]) list_inserted.append(theta_idx) else: theta_idx = self.__find_nearest(data[y_key], secondary_sweep_value) @@ -981,28 +984,20 @@ def plot_cut( y = conversion_function(y, quantity_format) if not isinstance(y, np.ndarray): # pragma: no cover raise Exception("Wrong format quantity.") - curves.append([x, y, "{}={}".format(y_key, data[y_key][theta_idx])]) - + curves.append([x, y, f"{y_key}={data[y_key][theta_idx]}"]) + + new = ReportPlotter() + new.show_legend = show_legend + new.title = title + props = {"x_label": x_key, "y_label": quantity} + for pdata in curves: + name = pdata[2] if len(pdata) > 2 else "Trace" + new.add_trace(pdata[:2], 0, props, name=name) if is_polar: - return plot_polar_chart( - curves, - xlabel=x_key, - ylabel=quantity, - title=title, - snapshot_path=output_file, - show_legend=show_legend, - show=show, - ) + _ = new.plot_polar(traces=None, snapshot_path=output_file, show=show) else: - return plot_2d_chart( - curves, - xlabel=x_key, - ylabel=quantity, - title=title, - snapshot_path=output_file, - show_legend=show_legend, - show=show, - ) + _ = new.plot_2d(None, output_file, show) + return new @pyaedt_function_handler() def plot_3d_chart( @@ -1041,10 +1036,9 @@ def plot_3d_chart( Returns ------- - :class:`matplotlib.pyplot.Figure` + :class:`ansys.aedt.core.visualization.plot.matplotlib.ReportPlotter` Matplotlib figure object. - If ``show=True``, a Matplotlib figure instance of the plot is returned. - If ``show=False``, the plotted curve is returned. + Examples -------- @@ -1078,7 +1072,13 @@ def plot_3d_chart( x = r * np.sin(theta_grid) * np.cos(phi_grid) y = r * np.sin(theta_grid) * np.sin(phi_grid) z = r * np.cos(theta_grid) - return plot_3d_chart([x, y, z], xlabel="Theta", ylabel="Phi", title=title, snapshot_path=output_file, show=show) + new = ReportPlotter() + new.show_legend = False + new.title = title + props = {"x_label": "Theta", "y_label": "Phi", "z_label": quantity} + new.add_trace([x, y, z], 2, props, quantity) + _ = new.plot_3d(trace=0, snapshot_path=output_file, show=show) + return new @pyaedt_function_handler() def plot_3d( diff --git a/src/ansys/aedt/core/visualization/plot/matplotlib.py b/src/ansys/aedt/core/visualization/plot/matplotlib.py index 21011d5a120..3a434d8407d 100644 --- a/src/ansys/aedt/core/visualization/plot/matplotlib.py +++ b/src/ansys/aedt/core/visualization/plot/matplotlib.py @@ -23,9 +23,11 @@ # SOFTWARE. import ast +import math +import os import warnings -from ansys.aedt.core.aedt_logger import pyaedt_logger +from ansys.aedt.core import settings from ansys.aedt.core.generic.general_methods import pyaedt_function_handler try: @@ -37,21 +39,10 @@ ) try: + from matplotlib.colors import Normalize from matplotlib.patches import PathPatch from matplotlib.path import Path import matplotlib.pyplot as plt - - rc_params = { - "axes.titlesize": 26, # Use these default settings for Matplotlb axes. - "axes.labelsize": 20, # Apply the settings only in this module. - "xtick.labelsize": 18, - "ytick.labelsize": 18, - } - - plt.ioff() - default_rc_params = plt.rcParams.copy() - plt.rcParams.update(rc_params) - except ImportError: warnings.warn( "The Matplotlib module is required to run some functionalities of PostProcess.\n" @@ -70,7 +61,7 @@ def is_notebook(): """ try: shell = get_ipython().__class__.__name__ - if shell == "ZMQInteractiveShell": + if shell in ["ZMQInteractiveShell"]: # pragma: no cover return True # Jupyter notebook or qtconsole else: return False @@ -78,12 +69,1287 @@ def is_notebook(): return False # Probably standard Python interpreter +def is_ipython(): + """Check if pyaedt is running in Jupyter or not. + + Returns + ------- + bool + """ + try: + shell = get_ipython().__class__.__name__ + if shell in ["TerminalInteractiveShell", "SpyderShell"]: + return True # Jupyter notebook or qtconsole + else: # pragma: no cover + return False + except NameError: + return False # Probably standard Python interpreter + + +class Note: + def __init__(self): + self._position = (0, 0) + self._text = "" + self._back_color = None + self._background_visibility = None + self._border_visibility = None + self._border_width = None + self._font = "Arial" + self._font_size = 12 + self._italic = False + self._bold = False + self._color = (0, 0, 0) + + @property + def text(self): + """Note text. + + Returns + ------- + str + """ + return self._text + + @text.setter + def text(self, value): + self._text = value + + @property + def background_color(self): + """Note color. + + Returns + ------- + tuple or list + """ + return self._back_color + + @background_color.setter + def background_color(self, value): + self._back_color = value + + @property + def background_visibility(self): + """Note background visibility. + + Returns + ------- + bool + """ + return self._background_visibility + + @background_visibility.setter + def background_visibility(self, value): + self._background_visibility = value + + @property + def border_visibility(self): + """Note border visibility. + + Returns + ------- + bool + """ + return self._border_visibility + + @border_visibility.setter + def border_visibility(self, value): + self._border_visibility = value + + @property + def border_width(self): + """Note border width. + + Returns + ------- + float + """ + return self._border_width + + @border_width.setter + def border_width(self, value): + self._border_width = value + + @property + def font(self): + """Note font. + + Returns + ------- + str + """ + return self._font + + @font.setter + def font(self, value): + self._font = value + + @property + def font_size(self): + """Note font size. + + Returns + ------- + bool + """ + return self._font_size + + @font_size.setter + def font_size(self, value): + self._font_size = value + + @property + def color(self): + """Note font color. + + Returns + ------- + list + """ + return self._color + + @color.setter + def color(self, value): + self._color = value + + @property + def bold(self): + """Note font bold. + + Returns + ------- + bool + """ + return self._bold + + @bold.setter + def bold(self, value): + self._bold = value + + @property + def italic(self): + """Note font italic. + + Returns + ------- + bool + """ + return self._italic + + @italic.setter + def italic(self, value): + self._italic = value + + +class Trace: + """Trace class.""" + + def __init__(self): + self.name = "" + self._cartesian_data = None + self._spherical_data = None + self.x_label = "" + self.y_label = "" + self.z_label = "" + self.__trace_style = "-" + self.__trace_width = 1.5 + self.__trace_color = None + self.__symbol_style = "" + self.__fill_symbol = False + self.__symbol_color = None + + @property + def trace_style(self): + """Matplotlib trace style. + + Returns + ------- + str + """ + return self.__trace_style + + @property + def trace_width(self): + """Trace width. + + Returns + ------- + float + """ + return self.__trace_width + + @property + def trace_color(self): + """Matplotlib trace color. It can be a tuple or a string of allowed colors. + + Returns + ------- + str, list + """ + return self.__trace_color + + @property + def symbol_style(self): + """Matplotlib symbol style. + + Returns + ------- + str + """ + return self.__symbol_style + + @property + def fill_symbol(self): + """Fill symbol. + + Returns + ------- + bool + """ + return self.__fill_symbol + + @trace_style.setter + def trace_style(self, val): + self.__trace_style = val + + @trace_width.setter + def trace_width(self, val): + self.__trace_width = val + + @trace_color.setter + def trace_color(self, val): + self.__trace_color = val + + @symbol_style.setter + def symbol_style(self, val): + self.__symbol_style = val + + @fill_symbol.setter + def fill_symbol(self, val): + self.__fill_symbol = val + + @property + def cartesian_data(self): + """Cartesian data [x,y,z]. + + Returns + ------- + List[:class:`numpy.array`] + List of data. + """ + return self._cartesian_data + + @cartesian_data.setter + def cartesian_data(self, val): + self._cartesian_data = [] + for el in val: + if isinstance(el, (list, tuple)): + self._cartesian_data.append(np.array(el)) + else: + self._cartesian_data.append(el) + if len(self._cartesian_data) == 2: + self._cartesian_data.append(np.zeros(len(val[-1]))) + self.car2spherical() + + @property + def spherical_data(self): + """Spherical data [r, theta, phi]. Angles are in degrees. + + Returns + ------- + List[:class:`numpy.array`] + List of data. + """ + return self._spherical_data + + @spherical_data.setter + def spherical_data(self, rthetaphi): + self._spherical_data = [] + for el in rthetaphi: + if isinstance(el, (list, tuple)): + self._spherical_data.append(np.array(el)) + else: + self._spherical_data.append(el) + self.spherical2car() + + @pyaedt_function_handler() + def car2polar(self, x, y, is_degree=False): + """Convert cartesian data to polar. + + Parameters + ---------- + x : list + X data. + y : list + Y data. + is_degree : bool, optional + Whether to return data in degree or radians. + + Returns + ------- + list, list + R and theta. + """ + rate = 1 + if not is_degree: + rate = np.pi / 180 + rho = np.sqrt(x**2 + y**2) + phi = np.arctan2(y, x) * rate + return [rho, phi] + + @pyaedt_function_handler() + def car2spherical(self): + """Convert cartesian data to spherical and assigns to property spherical data.""" + x = self._cartesian_data[0] + y = self._cartesian_data[1] + z = self._cartesian_data[2] + r = np.sqrt(x * x + y * y + z * z) + theta = np.arccos(z / r) * 180 / math.pi # to degrees + phi = np.arctan2(y, x) * 180 / math.pi + self._spherical_data = [r, theta, phi] + + @pyaedt_function_handler() + def spherical2car(self): + """Convert sherical data to cartesian data and assign to cartesian data property.""" + r = self._spherical_data[0] + theta = self._spherical_data[1] * math.pi / 180 # to radian + phi = self._spherical_data[2] * math.pi / 180 + x = r * np.sin(theta) * np.cos(phi) + y = r * np.sin(theta) * np.sin(phi) + z = r * np.cos(theta) + self._cartesian_data = [x, y, z] + + @pyaedt_function_handler() + def polar2car(self, r, theta): + """Convert polar data to cartesian data. + + Parameters + ---------- + r : list + theta : list + + Returns + ------- + list + List of [x,y]. + """ + x = r * np.cos(np.radians(theta)) + y = r * np.sin(np.radians(theta)) + return [x, y] + + +class LimitLine(Trace): + """Limit Line class.""" + + def __init__(self): + Trace.__init__(self) + self.hatch_above = True + + +class ReportPlotter: + """Matplotlib Report manager.""" + + def __init__(self): + rc_params = { + "axes.titlesize": 26, # Use these default settings for Matplotlb axes. + "axes.labelsize": 20, # Apply the settings only in this module. + "xtick.labelsize": 18, + "ytick.labelsize": 18, + } + self.block = settings.block_figure_plot + self._traces = {} + self._limit_lines = {} + self._notes = [] + self.plt_params = plt.rcParams + self.plt_params.update(rc_params) + self.show_legend = True + self.title = "" + self.fig = None + self.ax = None + self.__x_scale = "linear" + self.__y_scale = "linear" + self.__grid_color = (0.8, 0.8, 0.8) + self.__grid_enable_major_x = True + self.__grid_enable_major_y = True + self.__grid_enable_minor_x = False + self.__grid_enable_minor_y = True + self.__grid_style = "-" + self.__general_back_color = (1, 1, 1) + self.__general_plot_color = (1, 1, 1) + self._style = None + self.logo = None + self.show_logo = True + + @property + def traces(self): + """Traces. + + Returns + ------- + Dict[str, :class:`ansys.aedt.core.visualization.plot.matplotlib.Trace`] + """ + return self._traces + + @property + def traces_by_index(self): + """Traces. + + Returns + ------- + List[:class:`ansys.aedt.core.visualization.plot.matplotlib.Trace`] + """ + return list(self._traces.values()) + + @property + def trace_names(self): + """Trace names. + + Returns + ------- + list + """ + return list(self._traces.keys()) + + @property + def limit_lines(self): + """Limit Lines. + + Returns + ------- + Dict[str, :class:`ansys.aedt.core.visualization.plot.matplotlib.LimitLine`] + """ + return self._limit_lines + + @pyaedt_function_handler() + def apply_style(self, style_name): + """Apply a custom matplotlib style (eg. background_dark). + + Parameters + ---------- + style_name : str + Matplotlib style name. + + Returns + ------- + bool + """ + if style_name in plt.style.available: + plt.style.use(style_name) + self._style = style_name + return True + + @property + def grid_style(self): + """Grid style. + + Returns + ------- + str + """ + return self.__grid_style + + @grid_style.setter + def grid_style(self, val): + self.__grid_style = val + + @property + def grid_enable_major_x(self): + """Enable the major grid on x axis. + + Returns + ------- + bool + """ + return self.__grid_enable_major_x + + @grid_enable_major_x.setter + def grid_enable_major_x(self, val): + self.__grid_enable_major_x = val + + @property + def grid_enable_major_y(self): + """Enable the major grid on y axis. + + Returns + ------- + bool + """ + return self.__grid_enable_major_y + + @grid_enable_major_y.setter + def grid_enable_major_y(self, val): + self.__grid_enable_major_y = val + + @property + def grid_enable_minor_x(self): + """Enable the minor grid on x axis. + + Returns + ------- + bool + """ + return self.__grid_enable_minor_x + + @grid_enable_minor_x.setter + def grid_enable_minor_x(self, val): + self.__grid_enable_minor_x = val + + @property + def grid_enable_minor_y(self): + """Enable the minor grid on y axis. + + Returns + ------- + bool + """ + return self.__grid_enable_minor_y + + @grid_enable_minor_y.setter + def grid_enable_minor_y(self, val): + self.__grid_enable_minor_y = val + + @property + def grid_color(self): + """Grid color. + + Returns + ------- + str, list + Grid color tuple. + """ + return self.__grid_color + + @grid_color.setter + def grid_color(self, val): + if isinstance(val, (list, tuple)): + if any([i for i in val if i > 1]): + val = [i / 255 for i in val] + self.__grid_color = val + + @property + def general_back_color(self): + """General background color. + + Returns + ------- + str, list + """ + return self.__general_back_color + + @general_back_color.setter + def general_back_color(self, val): + if isinstance(val, (list, tuple)): + if any([i for i in val if i > 1]): + val = [i / 255 for i in val] + self.__general_back_color = val + + @property + def general_plot_color(self): + """General plot color. + + Returns + ------- + str, list + """ + return self.__general_plot_color + + @general_plot_color.setter + def general_plot_color(self, val): + if isinstance(val, (list, tuple)): + if any([i for i in val if i > 1]): + val = [i / 255 for i in val] + self.__general_plot_color = val + + @property + def _has_grid(self): + return True if self._has_x_axis or self._has_y_axis else False + + @property + def _has_x_axis(self): + return True if self.__grid_enable_major_x or self.__grid_enable_minor_x else False + + @property + def _has_y_axis(self): + return True if self.__grid_enable_major_y or self.__grid_enable_minor_y else False + + @property + def _has_major_axis(self): + return True if self.__grid_enable_major_x or self.__grid_enable_major_y else False + + @property + def _has_minor_axis(self): + return True if self.__grid_enable_minor_x or self.__grid_enable_minor_y else False + + # Open an image from a computer + @pyaedt_function_handler() + def _open_image_local(self): + + from PIL import Image + + if not self.logo: + self.logo = os.path.join(os.path.dirname(__file__), "../../generic/Ansys.png") + image = Image.open(self.logo) # Open the image + image_array = np.array(image) # Convert to a numpy array + return image_array # Output + + @pyaedt_function_handler() + def _update_grid(self): + if self._has_x_axis and self._has_y_axis: + axis = "both" + elif self._has_y_axis: + axis = "y" + elif self._has_x_axis: + axis = "x" + else: + axis = "both" + if self._has_minor_axis and self._has_major_axis: + which = "both" + elif self._has_minor_axis: + which = "minor" + elif self._has_major_axis: + which = "major" + else: + which = None + props = { + "axes.grid": True if self._has_grid else False, + "axes.grid.axis": axis, + "axes.grid.which": which, + "grid.linestyle": self.__grid_style, + } + if not self._style: + props["figure.facecolor"] = self.__general_back_color + props["axes.facecolor"] = self.__general_plot_color + props["grid.color"] = self.__grid_color + + self.plt_params.update(props) + if self.ax: + self.ax.grid(which=which) + if self._has_major_axis: + self.ax.grid(which="major", color=self.__grid_color) + if self._has_major_axis: + self.ax.grid(which="minor", color=self.__grid_color) + if self._has_minor_axis: + if self.__grid_enable_minor_x: + self.ax.xaxis.minorticks_on() + if self.__grid_enable_minor_y: + self.ax.yaxis.minorticks_on() + self.ax.tick_params(which="minor", grid_linestyle="--") + + @property + def y_scale(self): + """Y axis scale. It can be linear or log. + + Returns + ------- + str + """ + return self.__y_scale + + @y_scale.setter + def y_scale(self, val): + self.__y_scale = val + + @property + def x_scale(self): + """X axis scale. It can be linear or log. + + Returns + ------- + str + """ + return self.__x_scale + + @x_scale.setter + def x_scale(self, val): + self.__x_scale = val + + @property + def interactive(self): + """Enable interactive mode. + + Returns + ------- + bool + """ + return plt.isinteractive() + + @interactive.setter + def interactive(self, val): + if val: + plt.ion() + else: + plt.ioff() + + def add_note( + self, + text, + position=(0, 1), + back_color=None, + background_visibility=None, + border_width=None, + font="Arial", + font_size=12, + italic=False, + bold=False, + color=(0.2, 0.2, 0.2), + ): + """Add a note to the report. + + Parameters + ---------- + text : str + position : list, optional + back_color : list, optional + background_visibility : bool, optional + border_width : float, optional + font : str, optional + font_size : float, optional + italic : bool, optional + bold : bool, optional + color : list, optional + + Returns + ------- + bool + """ + note = Note() + note.text = text + note.position = position + note.background_color = back_color + note.background_visibility = background_visibility + note.border_width = border_width + note.font = font + note.font_size = font_size + note.italic = italic + note.bold = bold + note.color = color + self._notes.append(note) + + @pyaedt_function_handler() + def add_limit_line(self, plot_data, hatch_above=True, properties=None, name=""): + """Add a new limit_line to the chart. + + Parameters + ---------- + plot_data : list + Data to be inserted. Data has to be cartesian with x and y. + properties : dict, optional + Properties of the trace. + {x_label:prop, + y_label:prop, + trace_style : "-", + trace_width : 1.5, + trace_color : None, + symbol_style : 'v', + fill_symbol : None, + symbol_color : "C0" + } + name : str + Line name. + + Returns + ------- + bool + """ + nt = LimitLine() + nt.hatch_above = hatch_above + nt.name = name if name else f"Trace_{len(self.traces)}" + nt.x_label = properties.get("x_label", "") + nt.y_label = properties.get("y_label", "") + nt.trace_color = properties.get("trace_color", None) + nt.trace_style = properties.get("trace_style", "-") + nt.trace_width = properties.get("trace_width", 1.5) + nt.symbol_style = properties.get("symbol_style", "") + nt.fill_symbol = properties.get("fill_symbol", False) + nt.symbol_color = properties.get("symbol_color", None) + nt.cartesian_data = plot_data + self._limit_lines[nt.name] = nt + return True + + @pyaedt_function_handler() + def add_trace(self, plot_data, data_type=0, properties=None, name=""): + """Add a new trace to the chart. + + Parameters + ---------- + plot_data : list + Data to be inserted. + data_type : int, optional + Data format. ``0`` for cartesian, ``1`` for spherical data. + properties : dict, optional + Properties of the trace. + {x_label:prop, + y_label:prop, + z_label:prop, + trace_style : "-", + trace_width : 1.5, + trace_color : None, + symbol_style : 'v', + fill_symbol : None, + symbol_color : "C0" + } + name : str + Trace name. + + Returns + ------- + bool + """ + nt = Trace() + nt.name = name if name else f"Trace_{len(self.traces)}" + nt.x_label = properties.get("x_label", "") + nt.y_label = properties.get("y_label", "") + nt.z_label = properties.get("z_label", "") + nt.trace_color = properties.get("trace_color", None) + nt.trace_style = properties.get("trace_style", "-") + nt.trace_width = properties.get("trace_width", 1.5) + nt.symbol_style = properties.get("symbol_style", "") + nt.fill_symbol = properties.get("fill_symbol", False) + nt.symbol_color = properties.get("symbol_color", None) + if data_type == 0: + nt.cartesian_data = plot_data + else: + nt.spherical_data = plot_data + self._traces[nt.name] = nt + return True + + @property + def size(self): + """Figure size. + + Returns + ------- + list + """ + px = self.plt_params["figure.dpi"] # pixel in inches + return [i * px for i in self.plt_params["figure.figsize"]] + + @size.setter + def size(self, val, is_pixel=True): + if is_pixel: + px = 1 / self.plt_params["figure.dpi"] # pixel in inches + self.plt_params["figure.figsize"] = [i * px for i in val] + else: + self.plt_params["figure.figsize"] = val + + @pyaedt_function_handler() + def _plot(self, snapshot_path, show): + self.fig.set_size_inches( + self.size[0] / self.plt_params["figure.dpi"], self.size[1] / self.plt_params["figure.dpi"] + ) + + self._update_grid() + if self.show_logo: + image_xaxis = 0.9 + image_yaxis = 0.95 + image_width = 0.1 + image_height = 0.05 + ax_image = self.fig.add_axes([image_xaxis, image_yaxis, image_width, image_height]) + # Display the image + ax_image.imshow(self._open_image_local()) + ax_image.axis("off") # Remove axis of the image + + if snapshot_path: + self.fig.savefig(snapshot_path) + if show: # pragma: no cover + if is_notebook(): + pass + elif is_ipython() or "PYTEST_CURRENT_TEST" in os.environ: + self.fig.show() + else: + plt.show(block=self.block) + return self.fig + + def _set_scale(self, x, y): + min_y = np.amin(y) + max_y = np.amax(y) + min_x = np.amin(x) + max_x = np.amax(x) + if self.y_scale: + if not (self.y_scale == "log" and min_y < 0 or max_y < 0): + self.ax.set_yscale(self.y_scale) + if self.x_scale: + if not (self.x_scale == "log" and min_x < 0 or max_x < 0): + self.ax.set_xscale(self.x_scale) + y_range = max_y - min_y + x_range = max_x - min_x + if self.y_scale == "log": + y_range = -1e-12 + if self.x_scale == "log": + x_range = -1e-12 + self.ax.set_ylim(min_y - y_range * 0.2, max_y + y_range * 0.2) + self.ax.set_xlim(min_x - x_range * 0.2, max_x + x_range * 0.2) + + @pyaedt_function_handler() + def _retrieve_traces(self, traces): + if traces is None: + return self.traces_by_index + traces_to_plot = [] + if isinstance(traces, (int, str)): + traces = [traces] + try: + for tr in traces: + if isinstance(tr, int): + traces_to_plot.append(list(self.traces.values())[tr]) + elif isinstance(tr, str): + traces_to_plot.append(self.traces[tr]) + except (KeyError, IndexError): + settings.logger.error("Failed to retrieve traces") + return False + return traces_to_plot + + @pyaedt_function_handler() + def plot_polar(self, traces=None, to_polar=False, snapshot_path=None, show=True, is_degree=True): + """Create a Matplotlib polar plot based on a list of data. + + Parameters + ---------- + traces : int, str, list, optional + Trace or traces to be plotted. It can be the trace name, the trace id or a list of those. + to_polar : bool, optional + Whether if cartesian data has to be converted to polar before the plot or can be used as is. + snapshot_path : str + Full path to the image file if a snapshot is needed. + show : bool, optional + Whether to render the figure. The default is ``True``. If ``False``, the + figure is not drawn. + is_degree : bool, optional + Whether if data source are in degree or not. Default is ``True``. + + Returns + ------- + :class:`matplotlib.pyplot.Figure` + Matplotlib figure object. + """ + traces_to_plot = self._retrieve_traces(traces) + if not traces_to_plot: + return False + + self.fig, self.ax = plt.subplots(subplot_kw={"projection": "polar"}) + + legend = [] + i = 0 + rate = 1 + if is_degree: + rate = np.pi / 180 + for trace in traces_to_plot: + if not to_polar: + theta = trace._cartesian_data[0] * rate + r = trace._cartesian_data[1] + else: + theta, r = trace.car2polar(trace._cartesian_data[0], trace._cartesian_data[1], is_degree=is_degree) + self.ax.plot(theta, r) + self.ax.grid(True) + self.ax.set_theta_zero_location("N") + self.ax.set_theta_direction(-1) + legend.append(trace.name) + if i == 0: + self.ax.set(xlabel=trace.x_label, ylabel=trace.y_label, title=self.title) + i += 1 + + if self.show_legend: + self.ax.legend(legend, loc="upper right") + self._plot(snapshot_path, show) + return self.fig + + @pyaedt_function_handler() + def plot_3d(self, trace=0, snapshot_path=None, show=True, color_map_limits=[0, 1], is_polar=True): + """Create a Matplotlib 3D plot based on a list of data. + + Parameters + ---------- + trace : int, str optional + Trace index or name on which create the 3D Plot. + snapshot_path : str, optional + Full path to image file if a snapshot is needed. + show : bool, optional + Whether to show the plot or return the matplotlib object. Default is `True`. + color_map_limits : list, optional + Color map minimum and maximum values. + is_polar : bool, optional + Whether if the plot will be polar or not. Polar plot will hide axes and grids. Default is ``True``. + + Returns + ------- + :class:`matplotlib.pyplot.Figure` + Matplotlib figure object. + """ + trace_number = self._retrieve_traces(trace) + if not trace_number: + return False + self.fig, self.ax = plt.subplots(subplot_kw={"projection": "3d"}) + tr = trace_number[0] + if not is_polar: + self.ax.set_xlabel(tr.x_label, labelpad=20) + self.ax.set_ylabel(tr.y_label, labelpad=20) + self.ax.set_title(self.title) + cmap = plt.get_cmap("jet") + self.ax.plot_surface( + tr._cartesian_data[0], + tr._cartesian_data[1], + tr._cartesian_data[2], + rstride=1, + cstride=1, + cmap=cmap, + linewidth=0, + antialiased=True, + alpha=0.65, + ) + if is_polar: + step = (color_map_limits[1] - color_map_limits[0]) / 10 + ticks = np.arange(color_map_limits[0], color_map_limits[1] + step, step) + self.fig.colorbar( + plt.cm.ScalarMappable(norm=Normalize(color_map_limits[0], color_map_limits[1]), cmap=cmap), + ax=self.ax, + ticks=ticks, + shrink=0.7, + ) + radius = tr._spherical_data[0].max() - tr._spherical_data[0].min() + + X = np.cos(np.arange(-3.14, 3.14, 0.01)) * radius + Y = np.sin(np.arange(-3.14, 3.14, 0.01)) * radius + Z = np.zeros(len(Y)) + self.ax.plot(X, Y, Z, color=(0, 0, 0), linewidth=2.5) + + X = np.cos(np.arange(-3.14, 3.14, 0.01)) * 0.6 * radius + Y = np.sin(np.arange(-3.14, 3.14, 0.01)) * 0.6 * radius + self.ax.plot(X, Y, Z, linestyle=":", color=(0, 0, 0)) + self.ax.text( + 0, + radius, + 0.0, + "Phi", + fontweight="bold", + fontsize=15, + ) + + X = np.arange(0, radius, 0.01) + Y = np.zeros(len(X)) + Z = np.zeros(len(X)) + + self.ax.plot(X, Y, Z, linestyle=":", color=(0, 0, 0)) + Y = np.arange(0, radius, 0.01) + X = np.zeros(len(Y)) + self.ax.plot(X, Y, Z, linestyle=":", color=(0, 0, 0)) + + Y = np.sin(np.arange(-3.14, 3.14, 0.01)) * radius + Z = np.cos(np.arange(-3.14, 3.14, 0.01)) * radius + X = np.zeros(len(Z)) + self.ax.plot(X, Y, Z, color=(0, 0, 0), linewidth=2.5) + self.ax.text( + 0, + 0, + radius, + "Theta", + fontweight="bold", + fontsize=20, + ) + + Y = np.cos(np.arange(-3.14, 3.14, 0.01)) * 0.6 * radius + Z = np.sin(np.arange(-3.14, 3.14, 0.01)) * 0.6 * radius + self.ax.plot(X, Y, Z, linestyle=":", color=(0, 0, 0)) + self.ax.set_aspect("equal") + self.ax.grid(False) + + # Hide axes ticks + self.ax.set_xticks([]) + self.ax.set_yticks([]) + self.ax.set_zticks([]) + self.ax.set_axis_off() + + self._plot(snapshot_path, show) + return self.fig + + @pyaedt_function_handler() + def plot_2d(self, traces=None, snapshot_path=None, show=True): + """Create a Matplotlib figure based on a list of data. + + Parameters + ---------- + traces : int, str, list, optional + Trace or traces to be plotted. It can be the trace name, the trace id or a list of those. + snapshot_path : str, optional + Full path to image file if a snapshot is needed. + The default value is ``None``. + show : bool, optional + Whether to show the plot or return the matplotlib object. Default is `True`. + + Returns + ------- + :class:`matplotlib.pyplot.Figure` + Matplotlib figure object. + """ + traces_to_plot = self._retrieve_traces(traces) + if not traces_to_plot: + return False + self.fig, self.ax = plt.subplots() + legend_names = [] + + for trace in traces_to_plot: + self.ax.plot( + trace._cartesian_data[0], + trace._cartesian_data[1], + f"{trace.symbol_style}{trace.trace_style}", + fillstyle="full" if trace.fill_symbol else "none", + markeredgecolor=trace.symbol_color, + label=trace.name, + color=trace.trace_color, + ) + self.ax.set(xlabel=trace.x_label, ylabel=trace.y_label, title=self.title) + self._set_scale(trace._cartesian_data[0], trace._cartesian_data[1]) + + legend_names.append(trace.name) + self._plot_limit_lines() + self._plot_notes() + if self.show_legend: + self.ax.legend(legend_names, loc="upper right") + + self._plot(snapshot_path, show) + return self.fig + + @pyaedt_function_handler() + def _plot_notes(self): + for note in self._notes: + t = self.ax.text( + note.position[0], + note.position[1], + note.text, + style="italic" if note.italic else "normal", + fontweight="bold" if note.bold else "normal", + color=note.color if note.color else (0, 0, 0), + fontsize=note.font_size if note.font_size else 10, + fontfamily=note.font.lower(), + ) + if note.background_color and note.background_visibility: + bbox = { + "facecolor": note.background_color, + "alpha": 0.5, + "pad": note.border_width if note.border_width else 1, + } + t.set_bbox(bbox) + + @pyaedt_function_handler() + def _plot_limit_lines(self, convert_to_radians=False): + rate = 1 + if convert_to_radians: + rate = np.pi / 180 + for _, trace in self.limit_lines.items(): + min_y = np.amin(trace._cartesian_data[1]) * rate + max_y = np.amax(trace._cartesian_data[1]) * rate + delta = (max_y - min_y) / 5 + self.ax.plot( + trace._cartesian_data[0] * rate, + trace._cartesian_data[1], + f"{trace.symbol_style}{trace.trace_style}", + fillstyle="full" if trace.fill_symbol else "none", + markeredgecolor=trace.symbol_color, + label=trace.name, + color=trace.trace_color, + ) + if trace.hatch_above: + y_data = [i + delta for i in trace._cartesian_data[1]] + self.ax.fill_between( + trace._cartesian_data[0] * rate, + trace._cartesian_data[1], + y_data, + alpha=0.3, + color=trace.trace_color, + hatch="/", + ) + else: + y_data = [i - delta for i in trace._cartesian_data[1]] + self.ax.fill_between( + trace._cartesian_data[0] * rate, + y_data, + trace._cartesian_data[1], + alpha=0.3, + color=trace.trace_color, + hatch="/", + ) + + @pyaedt_function_handler() + def plot_contour( + self, + trace=0, + polar=False, + levels=64, + max_theta=180, + color_bar=None, + snapshot_path=None, + show=True, + ): + """Create a Matplotlib figure contour based on a list of data. + + Parameters + ---------- + trace : int, str, optional + Trace index on which create the 3D Plot. + polar : bool, optional + Generate the plot in polar coordinates. The default is ``True``. If ``False``, the plot + generated is rectangular. + levels : int, optional + Color map levels. The default is ``64``. + max_theta : float or int, optional + Maximum theta angle for plotting. It applies only for polar plots. + The default is ``180``, which plots the data for all angles. + Setting ``max_theta`` to 90 limits the displayed data to the upper + hemisphere, that is (0 < theta < 90). + color_bar : str, optional + Color bar title. The default is ``None`` in which case the color bar is not included. + snapshot_path : str, optional + Full path to image file if a snapshot is needed. + The default value is ``None``. + show : bool, optional + Whether to show the plot or return the matplotlib object. Default is ``True``. + + Returns + ------- + :class:`matplotlib.pyplot.Figure` + Matplotlib figure object. + """ + tr = self._retrieve_traces(trace) + if not tr: + return False + else: + tr = tr[0] + projection = "polar" if polar else "rectilinear" + self.fig, self.ax = plt.subplots(subplot_kw={"projection": projection}) + self.ax.set_xlabel(tr.x_label) + if polar: + self.ax.set_rticks(np.linspace(0, max_theta, 3)) + else: + self.ax.set_ylabel(tr.y_label) + + self.ax.set(title=self.title) + ph = tr._spherical_data[2] + th = tr._spherical_data[1] + data_to_plot = tr._spherical_data[0] + plt.contourf( + ph, + th, + data_to_plot, + levels=levels, + cmap="jet", + ) + if color_bar: + cbar = plt.colorbar() + cbar.set_label(color_bar, rotation=270, labelpad=20) + + self.ax = plt.gca() + self.ax.yaxis.set_label_coords(-0.1, 0.5) + self._plot(snapshot_path, show) + return self.fig + + @pyaedt_function_handler() def plot_polar_chart( - plot_data, size=(2000, 1000), show_legend=True, xlabel="", ylabel="", title="", snapshot_path=None, show=True + plot_data, size=(1920, 1440), show_legend=True, xlabel="", ylabel="", title="", snapshot_path=None, show=True ): """Create a Matplotlib polar plot based on a list of data. + .. deprecated:: 0.11.1 + Use :class:`ReportPlotter` instead. + Parameters ---------- plot_data : list of list @@ -107,46 +1373,28 @@ def plot_polar_chart( Returns ------- - :class:`matplotlib.pyplot.Figure` - Matplotlib figure object. + :class:`ansys.aedt.core.visualization.plot.matplotlib.ReportPlotter` + Matplotlib class object. """ - dpi = 100.0 - fig, ax = plt.subplots(subplot_kw={"projection": "polar"}) - - label_id = 1 - legend = [] - for plot_object in plot_data: - if len(plot_object) == 3: - label = plot_object[2] - else: - label = "Trace " + str(label_id) - theta = np.array(plot_object[0]) - r = np.array(plot_object[1]) - ax.plot(theta, r) - ax.grid(True) - ax.set_theta_zero_location("N") - ax.set_theta_direction(-1) - legend.append(label) - label_id += 1 - - ax.set(xlabel=xlabel, ylabel=ylabel, title=title) - if show_legend: - ax.legend(legend) - - # fig = plt.gcf() - fig.set_size_inches(size[0] / dpi, size[1] / dpi) - if snapshot_path: - fig.savefig(snapshot_path) - if show: # pragma: no cover - fig.show() - plt.rcParams.update(default_rc_params) - return fig + new = ReportPlotter() + new.size = size + new.show_legend = show_legend + new.title = title + props = {"x_label": xlabel, "y_label": ylabel} + for pdata in plot_data: + name = pdata[2] if len(pdata) > 2 else "Trace" + new.add_trace(pdata[:2], 0, props, name=name) + _ = new.plot_polar(traces=None, snapshot_path=snapshot_path, show=show) + return new @pyaedt_function_handler() -def plot_3d_chart(plot_data, size=(2000, 1000), xlabel="", ylabel="", title="", snapshot_path=None, show=True): +def plot_3d_chart(plot_data, size=(1920, 1440), xlabel="", ylabel="", title="", snapshot_path=None, show=True): """Create a Matplotlib 3D plot based on a list of data. + .. deprecated:: 0.11.1 + Use :class:`ReportPlotter` instead. + Parameters ---------- plot_data : list of list @@ -167,52 +1415,40 @@ def plot_3d_chart(plot_data, size=(2000, 1000), xlabel="", ylabel="", title="", Returns ------- - :class:`matplotlib.pyplot.Figure` - Matplotlib figure object. + :class:`ansys.aedt.core.visualization.plot.matplotlib.ReportPlotter` + Matplotlib class object. """ - dpi = 100.0 - fig, ax = plt.subplots(subplot_kw={"projection": "3d"}) - fig.set_size_inches(size[0] / dpi, size[1] / dpi) - if isinstance(plot_data[0], np.ndarray): - x = plot_data[0] - y = plot_data[1] - z = plot_data[2] - else: - theta_grid, phi_grid = np.meshgrid(plot_data[0], plot_data[1]) - if isinstance(plot_data[2], list): - r = np.array(plot_data[2]) - else: - r = plot_data[2] - x = r * np.sin(theta_grid) * np.cos(phi_grid) - y = r * np.sin(theta_grid) * np.sin(phi_grid) - z = r * np.cos(theta_grid) - - ax.set_xlabel(xlabel, labelpad=20) - ax.set_ylabel(ylabel, labelpad=20) - ax.set_title(title) - ax.plot_surface(x, y, z, rstride=1, cstride=1, cmap=plt.get_cmap("jet"), linewidth=0, antialiased=True, alpha=0.8) - - if snapshot_path: - fig.savefig(snapshot_path) - if show: # pragma: no cover - fig.show() - plt.rcParams.update(default_rc_params) - return fig + warnings.warn( + "`plot_3d_chart` is deprecated. Use class `ReportPlotter` to initialize and `plot_3d` instead.", + DeprecationWarning, + ) + new = ReportPlotter() + new.size = size + new.show_legend = False + new.title = title + props = {"x_label": xlabel, "y_label": ylabel} + name = plot_data[3] if len(plot_data) > 3 else "Trace" + new.add_trace(plot_data[:3], 2, props, name) + _ = new.plot_3d(trace=0, snapshot_path=snapshot_path, show=show) + return new @pyaedt_function_handler() def plot_2d_chart( - plot_data, size=(2000, 1000), show_legend=True, xlabel="", ylabel="", title="", snapshot_path=None, show=True + plot_data, size=(1920, 1440), show_legend=True, xlabel="", ylabel="", title="", snapshot_path=None, show=True ): """Create a Matplotlib figure based on a list of data. + .. deprecated:: 0.11.1 + Use :class:`ReportPlotter` instead. + Parameters ---------- plot_data : list of list List of plot data. Every item has to be in the following format `[x points, y points, label]`. size : tuple, optional - Image size in pixel (width, height). The default is `(2000,1600)`. + Image size in pixel (width, height). The default is `(1920,1440)`. show_legend : bool, optional Either to show legend or not. The default value is ``True``. xlabel : str, optional @@ -229,42 +1465,36 @@ def plot_2d_chart( Returns ------- - :class:`matplotlib.pyplot.Figure` - Matplotlib figure object. + :class:`ansys.aedt.core.visualization.plot.matplotlib.ReportPlotter` + Matplotlib class object. """ - dpi = 100.0 - fig, ax = plt.subplots() - fig.set_size_inches(size[0] / dpi, size[1] / dpi) - label_id = 1 - for plo_obj in plot_data: - if isinstance(plo_obj[0], np.ndarray): - x = plo_obj[0] - y = plo_obj[1] - else: - x = np.array([i for i, j in zip(plo_obj[0], plo_obj[1]) if j]) - y = np.array([i for i in plo_obj[1] if i]) - label = "Plot {}".format(str(label_id)) - if len(plo_obj) > 2: - label = plo_obj[2] - ax.plot(x, y, label=label) - label_id += 1 - - ax.set(xlabel=xlabel, ylabel=ylabel, title=title) - if show_legend: - ax.legend() + warnings.warn( + "`plot_2d_chart` is deprecated. Use class `ReportPlotter` to initialize and `plot_2d` instead.", + DeprecationWarning, + ) + new = ReportPlotter() + new.size = size + new.show_legend = show_legend + new.title = title + from ansys.aedt.core.generic.constants import CSS4_COLORS - if snapshot_path: - fig.savefig(snapshot_path) - elif show and not is_notebook(): # pragma: no cover - fig.show() - plt.rcParams.update(default_rc_params) - return fig + k = 0 + for data in plot_data: + label = f"{xlabel}_{data[2]}" if len(data) == 3 else xlabel + props = {"x_label": label, "y_label": ylabel, "line_color": list(CSS4_COLORS.keys())[k]} + k += 1 + if k == len(list(CSS4_COLORS.keys())): + k = 0 + name = data[2] if len(data) > 2 else "Trace" + new.add_trace(data[:2], 0, props, name) + _ = new.plot_2d(None, snapshot_path, show) + return new @pyaedt_function_handler() def plot_matplotlib( plot_data, - size=(2000, 1000), + size=(1920, 1440), show_legend=True, xlabel="", ylabel="", @@ -286,7 +1516,7 @@ def plot_matplotlib( For type ``path``: `[vertices, codes, color, label, alpha, type=="path"]`. For type ``contour``: `[vertices, codes, color, label, alpha, line_width, type=="contour"]`. size : tuple, optional - Image size in pixel (width, height). Default is `(2000, 1000)`. + Image size in pixel (width, height). Default is `(1920, 1440)`. show_legend : bool, optional Either to show legend or not. Default is `True`. xlabel : str, optional @@ -372,7 +1602,6 @@ def plot_matplotlib( plt.savefig(snapshot_path) if show: # pragma: no cover plt.show() - plt.rcParams.update(default_rc_params) return fig @@ -392,6 +1621,9 @@ def plot_contour( ): """Create a Matplotlib figure contour based on a list of data. + .. deprecated:: 0.11.1 + Use :class:`ReportPlotter` instead. + Parameters ---------- plot_data : list of np.ndarray @@ -425,47 +1657,30 @@ def plot_contour( Returns ------- - :class:`matplotlib.pyplot.Figure` - Matplotlib figure object. + :class:`ansys.aedt.core.visualization.plot.matplotlib.ReportPlotter` + Matplotlib class object. """ + warnings.warn( + "`plot_contour` is deprecated. Use class `ReportPlotter` to initialize and `plot_contour` instead.", + DeprecationWarning, + ) + new = ReportPlotter() + new.size = size + new.show_legend = False + new.title = title + props = { + "x_label": xlabel, + "y_label": ylabel, + } - dpi = 100.0 - figsize = (size[0] / dpi, size[1] / dpi) - - projection = "polar" if polar else "rectilinear" - fig, ax = plt.subplots(figsize=figsize, subplot_kw={"projection": projection}) - - ax.set_xlabel(xlabel) - if polar: - ax.set_rticks(np.linspace(0, max_theta, 3)) - else: - ax.set_ylabel(ylabel) - - ax.set(xlabel=xlabel, ylabel=ylabel, title=title) - if len(plot_data) != 3: # pragma: no cover - pyaedt_logger.error("Input should contain 3 numpy arrays.") - return False - ph = plot_data[2] - th = plot_data[1] - data_to_plot = plot_data[0] - plt.contourf( - ph, - th, - data_to_plot, + new.add_trace(plot_data, 2, props) + _ = new.plot_contour( + trace=0, + polar=polar, levels=levels, - cmap="jet", + max_theta=max_theta, + color_bar=color_bar, + snapshot_path=snapshot_path, + show=show, ) - - if color_bar: - cbar = plt.colorbar() - cbar.set_label(color_bar, rotation=270, labelpad=20) - - ax = plt.gca() - ax.yaxis.set_label_coords(-0.1, 0.5) - - if snapshot_path: - fig.savefig(snapshot_path) - if show: # pragma: no cover - fig.show() - plt.rcParams.update(default_rc_params) - return fig + return new diff --git a/src/ansys/aedt/core/visualization/plot/pyvista.py b/src/ansys/aedt/core/visualization/plot/pyvista.py index 5a6f3623e47..ba81ab58ec3 100644 --- a/src/ansys/aedt/core/visualization/plot/pyvista.py +++ b/src/ansys/aedt/core/visualization/plot/pyvista.py @@ -1019,7 +1019,7 @@ def _read_mesh_files(self, read_frames=False): else: field.scalar_name = field._cached_polydata.point_data.active_scalars_name - elif ".aedtplt" in field.path: + elif ".aedtplt" in field.path: # pragma no cover vertices, faces, scalars, log1 = _parse_aedtplt(field.path) if self.convert_fields_in_db: scalars = [np.multiply(np.log10(i), self.log_multiplier) for i in scalars] @@ -1030,14 +1030,16 @@ def _read_mesh_files(self, read_frames=False): 50 * (np.vstack(scalars[0]).max() - np.vstack(scalars[0]).min()) ) - field._cached_polydata["vectors"] = np.vstack(scalars[0]).T * vector_scale + field._cached_polydata["vectors"] = np.vstack(scalars).T * vector_scale field.label = "Vector " + field.label field._cached_polydata.point_data[field.label] = np.array( [np.linalg.norm(x) for x in np.vstack(scalars[0]).T] ) - field.scalar_name = field.field._cached_polydata.point_data.active_scalars_name - - field.is_vector = True + try: + field.scalar_name = field._cached_polydata.point_data.active_scalars_name + " Magnitude" + field.is_vector = True + except Exception: + field.is_vector = False else: field._cached_polydata.point_data[field.label] = scalars[0] field.scalar_name = field._cached_polydata.point_data.active_scalars_name @@ -1095,7 +1097,6 @@ def _read_mesh_files(self, read_frames=False): filedata.point_data[field.label] = np.array([np.linalg.norm(x) for x in np.vstack(values)]) field.scalar_name = field._cached_polydata.point_data.active_scalars_name field.is_vector = True - field.is_vector = True else: filedata = filedata.delaunay_2d(tol=field.surface_mapping_tolerance) filedata.point_data[field.label] = np.array(values) diff --git a/src/ansys/aedt/core/visualization/post/common.py b/src/ansys/aedt/core/visualization/post/common.py index 16f4f249438..f01fac346d5 100644 --- a/src/ansys/aedt/core/visualization/post/common.py +++ b/src/ansys/aedt/core/visualization/post/common.py @@ -38,6 +38,7 @@ from ansys.aedt.core.generic.general_methods import generate_unique_name from ansys.aedt.core.generic.general_methods import pyaedt_function_handler from ansys.aedt.core.generic.general_methods import read_configuration_file +from ansys.aedt.core.visualization.plot.matplotlib import ReportPlotter from ansys.aedt.core.visualization.post.solution_data import SolutionData from ansys.aedt.core.visualization.report.constants import TEMPLATES_BY_DESIGN import ansys.aedt.core.visualization.report.emi @@ -1571,7 +1572,9 @@ def get_solution_data( return solution_data @pyaedt_function_handler(input_dict="report_settings") - def create_report_from_configuration(self, input_file=None, report_settings=None, solution_name=None, name=None): + def create_report_from_configuration( + self, input_file=None, report_settings=None, solution_name=None, name=None, matplotlib=False + ): """Create a report based on a JSON file, TOML file, RPT file, or dictionary of properties. Parameters @@ -1582,6 +1585,8 @@ def create_report_from_configuration(self, input_file=None, report_settings=None Dictionary containing report settings. solution_name : str, optional Setup name to use. + matplotlib : bool, optional + Whether if use AEDT or ReportPlotter to generate the plot. Eye diagrams are not supported. Returns ------- @@ -1687,6 +1692,11 @@ def _update_props(prop_in, props_out): ): report._props["context"]["variations"][el] = k _ = report.expressions + if matplotlib: + if props.get("report_type", "").lower() in ["eye diagram", "statistical eye"]: # pragma: no cover + self.logger.warning("Eye Diagrams are not supported by Matplotlib.") + else: + return self._report_plotter(report) report.create(name) if report.report_type != "Data Table": report._update_traces() @@ -1696,6 +1706,116 @@ def _update_props(prop_in, props_out): self.logger.error("Failed to create report.") return False # pragma: no cover + @pyaedt_function_handler() + def _report_plotter(self, report): + sols = report.get_solution_data() + report_plotter = ReportPlotter() + report_plotter.title = report._props.get("plot_name", "PyAEDT Report") + try: + report_plotter.general_back_color = [ + i / 255 for i in report._props["general"]["appearance"]["background_color"] + ] + except KeyError: + pass + try: + + report_plotter.general_plot_color = [i / 255 for i in report._props["general"]["appearance"]["plot_color"]] + except KeyError: + pass + try: + report_plotter.grid_enable_major_x = report._props["general"]["grid"]["major_x"] + except KeyError: + pass + try: + report_plotter.grid_enable_minor_x = report._props["general"]["grid"]["minor_x"] + except KeyError: + pass + try: + report_plotter.grid_enable_major_y = report._props["general"]["grid"]["major_y"] + except KeyError: + pass + try: + report_plotter.grid_enable_minor_yi = report._props["general"]["grid"]["minor_y"] + except KeyError: + pass + try: + report_plotter.grid_color = [i / 255 for i in report._props["general"]["grid"]["major_color"]] + except KeyError: + pass + try: + report_plotter.show_legend = True if report._props["general"]["legend"] else False + except KeyError: + pass + sw = sols.primary_sweep_values + for curve in sols.expressions: + props = { + "x_label": sols.primary_sweep, + "y_label": curve, + } + pp = [i for i in report._props["expressions"] if i["name"] == curve] + if pp: + pp = pp[0] + try: + props["trace_width"] = pp["width"] + except KeyError: + pass + try: + props["trace_color"] = [i / 255 for i in pp["color"]] + except KeyError: + pass + try: + props["fill_symbol"] = pp["fill_symbol"] + except KeyError: + pass + try: + props["symbol_color"] = [i / 255 for i in pp["symbol_color"]] + except KeyError: + pass + try: + styles = {"Solid": "-", "Dash": "--", "DotDash": "-.", "DotDot": ":"} + props["trace_style"] = styles[pp["trace_style"]] + except KeyError: + pass + try: + markers = { + "Box": ",", + "Circle": "o", + "VerticalUpTriangle": "^", + "VerticalDownTriangle": "v", + "HorizontalLeftTriangle": "<", + "HorizontalRightTriangle": ">", + } + props["symbol_style"] = markers[pp["symbol_style"]] + except KeyError: + pass + report_plotter.add_trace([sw, sols.data_real(curve)], 0, properties=props, name=curve) + for name, line in report._props.get("limitLines", {}).items(): + props = {} + try: + props["trace_width"] = line["width"] + except KeyError: + pass + try: + props["trace_color"] = [i / 255 for i in line["color"]] + except KeyError: + pass + try: + report_plotter.add_limit_line([line["xpoints"], line["ypoints"]], 0, properties=props, name=name) + except KeyError: + self.logger.warning("Equation lines not supported yet.") + if report._props.get("report_type", "Rectangular Plot") == "Rectangular Plot": + _ = report_plotter.plot_2d() + return report_plotter + elif report._props.get("report_type", "Rectangular Plot") == "Polar Plot": + _ = report_plotter.plot_polar() + return report_plotter + elif report._props.get("report_type", "Rectangular Plot") == "Rectangular Contour Plot": + _ = report_plotter.plot_contour() + return report_plotter + elif report._props.get("report_type", "Rectangular Plot") in ["3D Polar Plot", "3D Spherical Plot"]: + _ = report_plotter.plot_3d() + return report_plotter + @staticmethod @pyaedt_function_handler() def __convert_dict_to_report_sel(sweeps): diff --git a/src/ansys/aedt/core/visualization/post/solution_data.py b/src/ansys/aedt/core/visualization/post/solution_data.py index 3443fe3885a..aedec767547 100644 --- a/src/ansys/aedt/core/visualization/post/solution_data.py +++ b/src/ansys/aedt/core/visualization/post/solution_data.py @@ -34,9 +34,7 @@ from ansys.aedt.core.generic.general_methods import pyaedt_function_handler from ansys.aedt.core.generic.general_methods import write_csv from ansys.aedt.core.generic.settings import settings -from ansys.aedt.core.visualization.plot.matplotlib import plot_2d_chart -from ansys.aedt.core.visualization.plot.matplotlib import plot_3d_chart -from ansys.aedt.core.visualization.plot.matplotlib import plot_polar_chart +from ansys.aedt.core.visualization.plot.matplotlib import ReportPlotter np = None pd = None @@ -269,7 +267,10 @@ def _init_solution_data_mag(self): _solutions_mag[expr] = {} self.units_data[expr] = self.nominal_variation.GetDataUnits(expr) if self.enable_pandas_output: - _solutions_mag[expr] = np.sqrt(self._solutions_real[expr]) + _solutions_mag[expr] = np.sqrt( + self._solutions_real[expr] * self._solutions_real[expr] + + self._solutions_imag[expr] * self._solutions_imag[expr] + ) else: for i in self._solutions_real[expr]: _solutions_mag[expr][i] = abs(complex(self._solutions_real[expr][i], self._solutions_imag[expr][i])) @@ -735,20 +736,20 @@ def export_data_to_csv(self, output, delimiter=";"): elif el in sweep_var: unit = self._original_data[0].GetSweepUnits(el) if unit == "": - header.append("{}".format(el)) + header.append(f"{el}") else: - header.append("{} [{}]".format(el, unit)) + header.append(f"{el} [{unit}]") # header = [el for el in self._sweeps_names] for el in self.expressions: data_unit = self._original_data[0].GetDataUnits(el) if data_unit: - data_unit = " [{}]".format(data_unit) + data_unit = f" [{data_unit}]" if not self.is_real_only(el): - header.append(el + " (Real){}".format(data_unit)) - header.append(el + " (Imag){}".format(data_unit)) + header.append(el + f" (Real){data_unit}") + header.append(el + f" (Imag){data_unit}") else: - header.append(el + "{}".format(data_unit)) + header.append(el + f"{data_unit}") list_full = [header] for e, v in self._solutions_real[self.active_expression].items(): @@ -766,12 +767,66 @@ def export_data_to_csv(self, output, delimiter=";"): return write_csv(output, list_full, delimiter=delimiter) + @pyaedt_function_handler() + def _get_data_formula(self, curve, formula=None): + if not formula or formula == "re": + return self.data_real(curve) + elif formula == "im": + return self.data_imag(curve) + elif formula == "db20": + return self.data_db20(curve) + elif formula == "db10": + return self.data_db10(curve) + elif formula == "mag": + return self.data_magnitude(curve) + elif formula == "phasedeg": + return curve + elif formula == "phaserad": + return self.data_phase(curve, True) + + @pyaedt_function_handler() + def get_report_plotter(self, curves=None, formula=None, to_radians=False, props=None): + """Get the `ReportPlotter` on the specified curves. + + Parameters + ---------- + curves : list, str, optional + Trace names. + formula : str, optional + Trace formula. Default is `None` which takes the real part of the trace. + to_radians : bool, optional + Whether is data has to be converted to radians or not. Default is ``False``. + + Returns + ------- + :class:`ansys.aedt.core.visualization.plot.matplotlib.ReportPlotter` + Report plotter class. + """ + if not curves: + curves = self.expressions + if isinstance(curves, str): + curves = [curves] + if not formula: + formula = "mag" + if to_radians: + sw = self.to_radians(self.primary_sweep_values) + else: + sw = self.primary_sweep_values + new = ReportPlotter() + for curve in curves: + if props is None: + props = {"x_label": self.primary_sweep, "y_label": ""} + active_intr = ",".join([f"{i}={k}" for i, k in self.active_intrinsic.items() if i != self.primary_sweep]) + name = f"{formula}({curve})_{active_intr}" + new.add_trace([sw, self._get_data_formula(curve, formula)], name=name, properties=props) + return new + @pyaedt_function_handler(math_formula="formula", xlabel="x_label", ylabel="y_label") def plot( self, curves=None, formula=None, - size=(2000, 1000), + size=(1920, 1440), show_legend=True, x_label="", y_label="", @@ -813,48 +868,18 @@ def plot( Returns ------- - :class:`matplotlib.pyplot.Figure` - Matplotlib figure object. + :class:`ansys.aedt.core.visualization.plot.matplotlib.ReportPlotter` + Matplotlib class object. """ - if not curves: - curves = [self.active_expression] - if isinstance(curves, str): - curves = [curves] - data_plot = [] - sweep_name = self.primary_sweep - if is_polar: - sw = self.to_radians(self.primary_sweep_values) - else: - sw = self.primary_sweep_values - for curve in curves: - if not formula: - data_plot.append([sw, self.data_real(curve), curve]) - elif formula == "re": - data_plot.append([sw, self.data_real(curve), "{}({})".format(formula, curve)]) - elif formula == "im": - data_plot.append([sw, self.data_imag(curve), "{}({})".format(formula, curve)]) - elif formula == "db20": - data_plot.append([sw, self.data_db20(curve), "{}({})".format(formula, curve)]) - elif formula == "db10": - data_plot.append([sw, self.data_db10(curve), "{}({})".format(formula, curve)]) - elif formula == "mag": - data_plot.append([sw, self.data_magnitude(curve), "{}({})".format(formula, curve)]) - elif formula == "phasedeg": - data_plot.append([sw, self.data_phase(curve, False), "{}({})".format(formula, curve)]) - elif formula == "phaserad": - data_plot.append([sw, self.data_phase(curve, True), "{}({})".format(formula, curve)]) - if not x_label: - x_label = sweep_name - if not y_label: - y_label = formula - if not title: - title = "Simulation Results Plot" - if len(data_plot) > 15: - show_legend = False + props = {"x_label": x_label, "y_label": y_label} + report_plotter = self.get_report_plotter(curves=curves, formula=formula, to_radians=is_polar, props=props) + report_plotter.show_legend = show_legend + report_plotter.title = title + report_plotter.size = size if is_polar: - return plot_polar_chart(data_plot, size, show_legend, x_label, y_label, title, snapshot_path, show=show) + return report_plotter.plot_polar(snapshot_path=snapshot_path, show=show) else: - return plot_2d_chart(data_plot, size, show_legend, x_label, y_label, title, snapshot_path, show=show) + return report_plotter.plot_2d(snapshot_path=snapshot_path, show=show) @pyaedt_function_handler(xlabel="x_label", ylabel="y_label", math_formula="formula") def plot_3d( @@ -866,7 +891,7 @@ def plot_3d( y_label="", title="", formula=None, - size=(2000, 1000), + size=(1920, 1440), snapshot_path=None, show=True, ): @@ -908,13 +933,16 @@ def plot_3d( if not formula: formula = "mag" - theta = self.variation_values(x_axis) + theta = [i * math.pi / 180 for i in self.variation_values(x_axis)] y_axis_val = self.variation_values(y_axis) phi = [] r = [] for el in y_axis_val: - self.active_variation[y_axis] = el + if y_axis in self.active_intrinsic: + self.active_intrinsic[y_axis] = el + else: + self.active_variation[y_axis] = el phi.append(el * math.pi / 180) if formula == "re": @@ -931,21 +959,36 @@ def plot_3d( r.append(self.data_phase(curve, False)) elif formula == "phaserad": r.append(self.data_phase(curve, True)) - active_sweep = self.active_intrinsic[self.primary_sweep] - position = self.variation_values(self.primary_sweep).index(active_sweep) - if len(self.variation_values(self.primary_sweep)) > 1: - new_r = [] - for el in r: - new_r.append([el[position]]) - r = new_r - data_plot = [theta, phi, r] + + min_r = 1e12 + max_r = -1e12 + for el in r: + min_r = min(min_r, el.values.min()) + max_r = max(max_r, el.values.max()) + if min_r < 0: + r = [i + np.abs(min_r) for i in r] + theta_grid, phi_grid = np.meshgrid(theta, phi) + r_grid = np.reshape(r, (len(phi), len(theta))) + + x = r_grid * np.sin(theta_grid) * np.cos(phi_grid) + y = r_grid * np.sin(theta_grid) * np.sin(phi_grid) + z = r_grid * np.cos(theta_grid) + data_plot = [x, y, z] if not x_label: x_label = x_axis if not y_label: y_label = y_axis if not title: title = "Simulation Results Plot" - return plot_3d_chart(data_plot, size, x_label, y_label, title, snapshot_path, show=show) + new = ReportPlotter() + new.size = size + new.show_legend = False + new.title = title + props = {"x_label": x_label, "y_label": y_label} + new.add_trace(data_plot, 0, props, curve) + + _ = new.plot_3d(trace=0, snapshot_path=snapshot_path, show=show, color_map_limits=[min_r, max_r]) + return new @pyaedt_function_handler() def ifft(self, curve_header="NearE", u_axis="_u", v_axis="_v", window=False): From fc37ef5d9479c668444d77f2abe008f600e91a50 Mon Sep 17 00:00:00 2001 From: Ramin Aghajafari <153928265+ramin4667@users.noreply.github.com> Date: Fri, 18 Oct 2024 17:55:01 -0400 Subject: [PATCH 2/4] FIX: Replace a function from wrong position (#5312) --- .../aedt/core/filtersolutions_core/export_to_aedt.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/ansys/aedt/core/filtersolutions_core/export_to_aedt.py b/src/ansys/aedt/core/filtersolutions_core/export_to_aedt.py index 31241b1a7dc..d093179669c 100644 --- a/src/ansys/aedt/core/filtersolutions_core/export_to_aedt.py +++ b/src/ansys/aedt/core/filtersolutions_core/export_to_aedt.py @@ -282,10 +282,6 @@ def _define_export_to_desktop_dll_functions(self): self._dll.importTunedVariables.argtypes = [c_char_p, c_int] self._dll.importTunedVariables.restype = c_int - def _open_aedt_export(self): - """Open export page to accept manipulate export parameters""" - status = self._dll.openLumpedExportPage() - ansys.aedt.core.filtersolutions_core._dll_interface().raise_error(status) self._dll.setPartLibraries.argtype = c_int self._dll.setPartLibraries.restype = c_int self._dll.getPartLibraries.argtype = POINTER(c_int) @@ -494,6 +490,11 @@ def _open_aedt_export(self): self._dll.removeModelithicsResistorsFamily.argtype = c_char_p self._dll.removeModelithicsResistorsFamily.restype = c_int + def _open_aedt_export(self): + """Open export page to accept manipulate export parameters""" + status = self._dll.openLumpedExportPage() + ansys.aedt.core.filtersolutions_core._dll_interface().raise_error(status) + @property def schematic_name(self) -> str: """Name of the exported schematic in ``AEDT``, displayed as the project and design names. From 63f3d3fddb5ee9b9f3a8625a3e05d7a252fae94f Mon Sep 17 00:00:00 2001 From: Hui Zhou Date: Mon, 21 Oct 2024 12:39:45 +0200 Subject: [PATCH 3/4] FIX: cutout extension (#5311) Co-authored-by: ring630 <@gmail.com> --- .../aedt/core/workflows/hfss3dlayout/cutout.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/ansys/aedt/core/workflows/hfss3dlayout/cutout.py b/src/ansys/aedt/core/workflows/hfss3dlayout/cutout.py index fb76591d1ea..bbd0d4044ed 100644 --- a/src/ansys/aedt/core/workflows/hfss3dlayout/cutout.py +++ b/src/ansys/aedt/core/workflows/hfss3dlayout/cutout.py @@ -58,7 +58,20 @@ def frontend(): # pragma: no cover aedt_process_id=aedt_process_id, student_version=is_student, ) - h3d = Hfss3dLayout() + + active_project = app.active_project() + active_design = app.active_design() + + if active_design.GetDesignType() in ["HFSS 3D Layout Design"]: + design_name = active_design.GetName().split(";")[1] + else: # pragma: no cover + app.logger.debug("HFSS 3D Layout project is needed.") + app.release_desktop(False, False) + raise Exception("HFSS 3D Layout designs needed.") + + project_name = active_project.GetName() + h3d = ansys.aedt.core.Hfss3dLayout(project=project_name, design=design_name) + objs_net = {} for net in h3d.oeditor.GetNets(): objs_net[net] = h3d.modeler.objects_by_net(net) From 6853d056b48056a210444d5dd50ea986681e4872 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Morais?= <146729917+SMoraisAnsys@users.noreply.github.com> Date: Mon, 21 Oct 2024 14:56:11 +0200 Subject: [PATCH 4/4] CI: Bump ansys actions version (#5315) --- .github/workflows/ci_cd.yml | 107 ++++------------------------- .github/workflows/nightly-docs.yml | 100 ++------------------------- doc/Makefile | 2 +- doc/source/conf.py | 24 +------ 4 files changed, 23 insertions(+), 210 deletions(-) diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index b089a550e06..ceeb18e83bf 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -33,7 +33,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Check commit name - uses: ansys/actions/commit-style@v6 + uses: ansys/actions/check-pr-title@v8 with: token: ${{ secrets.GITHUB_TOKEN }} use-upper-case: true @@ -57,10 +57,9 @@ jobs: os: [ubuntu-latest, windows-latest] python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] target: ['all', 'installer'] - steps: - name: Build wheelhouse and perform smoke test - uses: ansys/actions/build-wheelhouse@v4 + uses: ansys/actions/build-wheelhouse@v8 with: library-name: ${{ env.PACKAGE_NAME }} operating-system: ${{ matrix.os }} @@ -71,67 +70,18 @@ jobs: run: | python -c "import ansys.aedt.core; from ansys.aedt.core import __version__" - # TODO: Update to ansys/actions/doc-build@v6 once we remove examples doc-build: name: Documentation build runs-on: ubuntu-latest needs: [doc-style] steps: - - name: Install Git and checkout project - uses: actions/checkout@v4 - - - name: Setup Python - uses: actions/setup-python@v5 + - name: Documentation build + uses: ansys/actions/doc-build@v8 with: + dependencies: "graphviz texlive-latex-extra latexmk texlive-xetex texlive-fonts-extra" python-version: ${{ env.MAIN_PYTHON_VERSION }} - - - name: Update pip - run: | - pip install --upgrade pip - - - name: Install pyaedt and documentation dependencies - run: | - pip install .[doc] - - - name: Retrieve PyAEDT version - id: version - run: | - echo "PYAEDT_VERSION=$(python -c 'from ansys.aedt.core import __version__; print(__version__)')" >> $GITHUB_OUTPUT - echo "PyAEDT version is: $(python -c "from ansys.aedt.core import __version__; print(__version__)")" - - - name: Install doc build requirements - run: | - sudo apt update - sudo apt install graphviz texlive-latex-extra latexmk texlive-xetex texlive-fonts-extra -y - - # TODO: Update this step once pyaedt-examples is ready - - name: Build HTML documentation - run: | - make -C doc clean - make -C doc html - - # Verify that sphinx generates no warnings - - name: Check for warnings - run: | - python doc/print_errors.py - - - name: Upload HTML documentation - uses: actions/upload-artifact@v4 - with: - name: documentation-html - path: doc/_build/html - retention-days: 7 - - - name: Build PDF documentation - run: | - make -C doc pdf - - - name: Upload PDF documentation - uses: actions/upload-artifact@v4 - with: - name: documentation-pdf - path: doc/_build/latex/PyAEDT-Documentation-*.pdf - retention-days: 7 + sphinxopts: '-j auto --color -w build_errors.txt' + check-links: false # # ================================================================================================= # # vvvvvvvvvvvvvvvvvvvvvvvvvvvvvv RUNNING ON SELF-HOSTED RUNNER vvvvvvvvvvvvvvvvvvvvvvvvvvvvvv @@ -392,7 +342,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Build library source and wheel artifacts - uses: ansys/actions/build-library@v4 + uses: ansys/actions/build-library@v8 with: library-name: ${{ env.PACKAGE_NAME }} python-version: ${{ env.MAIN_PYTHON_VERSION }} @@ -405,14 +355,14 @@ jobs: runs-on: ubuntu-latest steps: - name: Release to the public PyPI repository - uses: ansys/actions/release-pypi-public@v4 + uses: ansys/actions/release-pypi-public@v8 with: library-name: ${{ env.PACKAGE_NAME }} twine-username: "__token__" twine-token: ${{ secrets.PYPI_TOKEN }} - name: Release to GitHub - uses: ansys/actions/release-github@v4 + uses: ansys/actions/release-github@v8 with: library-name: ${{ env.PACKAGE_NAME }} @@ -423,40 +373,9 @@ jobs: needs: [release] steps: - name: Deploy the stable documentation - uses: ansys/actions/doc-deploy-stable@v5 + uses: ansys/actions/doc-deploy-stable@v8 with: cname: ${{ env.DOCUMENTATION_CNAME }} token: ${{ secrets.GITHUB_TOKEN }} - doc-artifact-name: 'documentation-html' - - doc-index-stable: - name: Deploy stable docs index - if: github.event_name == 'push' && contains(github.ref, 'refs/tags') - runs-on: ubuntu-latest - needs: upload-release-doc - steps: - - name: Install Git and clone project - uses: actions/checkout@v4 - - - name: Install the package requirements - run: | - python -m pip install --upgrade pip - pip install -e . - - - name: Get the version to PyMeilisearch - run: | - VERSION=$(python -c "from ansys.aedt.core import __version__; print('.'.join(__version__.split('.')[:2]))") - VERSION_MEILI=$(python -c "from ansys.aedt.core import __version__; print('-'.join(__version__.split('.')[:2]))") - echo "Calculated VERSION: $VERSION" - echo "Calculated VERSION_MEILI: $VERSION_MEILI" - echo "VERSION=$VERSION" >> $GITHUB_ENV - echo "VERSION_MEILI=$VERSION_MEILI" >> $GITHUB_ENV - - - name: Deploy the latest documentation index - uses: ansys/actions/doc-deploy-index@v5 - with: - cname: ${{ env.DOCUMENTATION_CNAME }}/version/${{ env.VERSION }} - index-name: pyaedt-v${{ env.VERSION_MEILI }} - host-url: ${{ env.MEILISEARCH_HOST_URL }} - api-key: ${{ env.MEILISEARCH_API_KEY }} - python-version: ${{ env.MAIN_PYTHON_VERSION }} + bot-user: ${{ secrets.PYANSYS_CI_BOT_USERNAME }} + bot-email: ${{ secrets.PYANSYS_CI_BOT_EMAIL }} diff --git a/.github/workflows/nightly-docs.yml b/.github/workflows/nightly-docs.yml index edd24011d09..ffb37e948e7 100644 --- a/.github/workflows/nightly-docs.yml +++ b/.github/workflows/nightly-docs.yml @@ -22,110 +22,22 @@ jobs: doc-build: name: Documentation build runs-on: [ self-hosted, Windows, pyaedt ] - timeout-minutes: 720 + needs: [doc-style] steps: - - name: Install Git and checkout project - uses: actions/checkout@v4 - - - name: Setup Python - uses: actions/setup-python@v5 + - name: Documentation build + uses: ansys/actions/doc-build@v8 with: python-version: ${{ env.MAIN_PYTHON_VERSION }} - - name: Create virtual environment - run: | - python -m venv .venv - .venv\Scripts\Activate.ps1 - python -m pip install pip -U - python -m pip install wheel setuptools -U - python -c "import sys; print(sys.executable)" - - - name: Install pyaedt and documentation dependencies - run: | - .venv\Scripts\Activate.ps1 - pip install .[doc] - - - name: Retrieve PyAEDT version - id: version - run: | - .venv\Scripts\Activate.ps1 - echo "PYAEDT_VERSION=$(python -c 'from ansys.aedt.core import __version__; print(__version__)')" >> $GITHUB_OUTPUT - echo "PyAEDT version is: $(python -c "from ansys.aedt.core import __version__; print(__version__)")" - - - name: Install CI dependencies (e.g. vtk-osmesa) - run: | - .venv\Scripts\Activate.ps1 - # Uninstall conflicting dependencies - pip uninstall --yes vtk - pip install --extra-index-url https://wheels.vtk.org vtk-osmesa==9.2.20230527.dev0 - - # TODO: Update this step once pyaedt-examples is ready - # NOTE: Use environment variable to keep the doctree and avoid redundant build for PDF pages - - name: Build HTML documentation with examples - env: - SPHINXBUILD_KEEP_DOCTREEDIR: "1" - run: | - .venv\Scripts\Activate.ps1 - .\doc\make.bat clean - .\doc\make.bat html - - # TODO: Keeping this commented as reminder of https://github.com/ansys/pyaedt/issues/4296 - # # Verify that sphinx generates no warnings - # - name: Check for warnings - # run: | - # .venv\Scripts\Activate.ps1 - # python doc/print_errors.py - - # Use environment variable to remove the doctree after the build of PDF pages - - name: Build PDF documentation with examples - env: - SPHINXBUILD_KEEP_DOCTREEDIR: "0" - run: | - .venv\Scripts\Activate.ps1 - .\doc\make.bat pdf - - # - name: Add assets to HTML docs - # run: | - # zip -r documentation-html.zip ./doc/_build/html - # mv documentation-html.zip ./doc/_build/html/_static/assets/download/ - # cp doc/_build/latex/PyAEDT-Documentation-*.pdf ./doc/_build/html/_static/assets/download/pyaedt.pdf - - - name: Upload HTML documentation with examples artifact - uses: actions/upload-artifact@v4 - with: - name: documentation-html - path: doc/_build/html - retention-days: 7 - - - name: Upload PDF documentation without examples artifact - uses: actions/upload-artifact@v4 - with: - name: documentation-pdf - path: doc/_build/latex/PyAEDT-Documentation-*.pdf - retention-days: 7 - upload-dev-doc: name: Upload dev documentation runs-on: ubuntu-latest needs: doc-build steps: - name: Upload development documentation - uses: ansys/actions/doc-deploy-dev@v5 + uses: ansys/actions/doc-deploy-dev@v8 with: cname: ${{ env.DOCUMENTATION_CNAME }} token: ${{ secrets.GITHUB_TOKEN }} - doc-artifact-name: 'documentation-html' - - doc-index-dev: - name: Deploy dev index docs - runs-on: ubuntu-latest - needs: upload-dev-doc - steps: - - name: Deploy the latest documentation index - uses: ansys/actions/doc-deploy-index@v5 - with: - cname: ${{ env.DOCUMENTATION_CNAME }}/version/dev - index-name: pyaedt-vdev - host-url: ${{ env.MEILISEARCH_HOST_URL }} - api-key: ${{ env.MEILISEARCH_API_KEY }} - python-version: ${{ env.MAIN_PYTHON_VERSION }} + bot-user: ${{ secrets.PYANSYS_CI_BOT_USERNAME }} + bot-email: ${{ secrets.PYANSYS_CI_BOT_EMAIL }} diff --git a/doc/Makefile b/doc/Makefile index 8fefd56a79d..4dbd34de3a2 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -55,5 +55,5 @@ pdf: .install-deps @echo "Building PDF pages." @$(SPHINXBUILD) -M latex "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) cd $(BUILDDIR)/latex && latexmk -r latexmkrc -pdf *.tex -interaction=nonstopmode || true - (test -f $(BUILDDIR)/latex/PyAEDT-Documentation-*.pdf && echo pdf exists) || exit 1 + (test -f $(BUILDDIR)/latex/pyaedt.pdf && echo pdf exists) || exit 1 @echo "Build finished. The PDF pages are in $(BUILDDIR)." diff --git a/doc/source/conf.py b/doc/source/conf.py index ba352a4086c..6a4e7c92bbc 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -318,29 +318,11 @@ def setup(app): 'custom.css', ] - -# -- Options for HTMLHelp output --------------------------------------------- - -# Output file base name for HTML help builder. -htmlhelp_basename = "pyaedtdoc" - # -- Options for LaTeX output ------------------------------------------------ -# additional logos for the latex coverpage + +# Additional logos for the latex coverpage latex_additional_files = [watermark, ansys_logo_white, ansys_logo_white_cropped] -# change the preamble of latex with customized title page +# Change the preamble of latex with customized title page # variables are the title of pdf, watermark latex_elements = {"preamble": latex.generate_preamble(html_title)} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, -# author, documentclass [howto, manual, or own class]). -latex_documents = [ - ( - master_doc, - f"{project}-Documentation-{__version__}.tex", - f"{project} Documentation", - author, - "manual", - ), -]