From 18c3fbd0d72450ae5cc5a3976eced56c3974961a Mon Sep 17 00:00:00 2001 From: aletgn Date: Wed, 1 May 2024 16:31:37 +0200 Subject: [PATCH] implement method and classes for post-proc Changes to be committed: modified: src/pytom3d/scan.py modified: src/pytom3d/stats.py modified: src/pytom3d/util.py modified: src/pytom3d/viewer.py -> add canvas contours and plots --- src/pytom3d/scan.py | 29 ++++- src/pytom3d/stats.py | 8 +- src/pytom3d/util.py | 64 ++++++++-- src/pytom3d/viewer.py | 272 ++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 348 insertions(+), 25 deletions(-) diff --git a/src/pytom3d/scan.py b/src/pytom3d/scan.py index 1c4636c..f67e318 100644 --- a/src/pytom3d/scan.py +++ b/src/pytom3d/scan.py @@ -18,6 +18,30 @@ def __init__(self, **kwargs: Dict['str', Any]) -> None: except KeyError: self.name = "Untitled" + try: + self.color = kwargs.pop("color") + except KeyError: + self.color = "k" + + try: + self.line = kwargs.pop("line") + except KeyError: + self.line = "-" + + try: + self.alpha = kwargs.pop("alpha") + except KeyError: + self.alpha = 0.3 + + try: + self.err_bar = kwargs.pop("err_bar") + except KeyError: + self.err_bar = False + + def config_aspect(self, color, line, alpha): + self.color = color + self.line = line + self.alpha = alpha def load_file(self, reader: callable, path: str, **kwargs: Dict['str', Any]) -> None: """ @@ -37,8 +61,7 @@ def load_file(self, reader: callable, path: str, **kwargs: Dict['str', Any]) -> self.y = data.iloc[:, 1].to_numpy() self.y_err = data.iloc[:, 2].to_numpy() - - def load_data(self, x: np.ndarray, y: np.ndarray, y_err) -> None: + def load_data(self, x: np.ndarray, y: np.ndarray, y_err: np.ndarray = None) -> None: """ Load data directly. @@ -53,7 +76,7 @@ def load_data(self, x: np.ndarray, y: np.ndarray, y_err) -> None: """ self.x = x self.y = y - self.y_err = y + self.y_err = y_err def export_line_scan(reader: callable, path: str, *scans: List, **kwargs: Dict['str', Any]) -> None: diff --git a/src/pytom3d/stats.py b/src/pytom3d/stats.py index f1347bf..b7c62f8 100644 --- a/src/pytom3d/stats.py +++ b/src/pytom3d/stats.py @@ -2,7 +2,7 @@ import numpy as np -def running_mean(inp: List[int], out: int, up_to_id = None, *list_path: List[str]) -> np.ndarray: +def running_mean(out: int, up_to_id = None, *list_path: List[str]) -> np.ndarray: """ Calculate the running mean of specified columns across multiple files. @@ -40,7 +40,7 @@ def running_mean(inp: List[int], out: int, up_to_id = None, *list_path: List[str return cumulative_sum/N -def running_std(inp: List[int], out: int, up_to_id = None, ddof: int = 1, *list_path: List[str]) -> np.ndarray: +def running_std(out: int, up_to_id = None, ddof: int = 1, *list_path: List[str]) -> np.ndarray: """ Calculate the running standard deviation of specified columns across multiple files. @@ -56,7 +56,7 @@ def running_std(inp: List[int], out: int, up_to_id = None, ddof: int = 1, *list_ Index of the last file to include in the calculation. If None, all files are included. Default is None. ddof : int, optional - Delta degrees of freedom. Default is 1. + Degrees of freedom. Default is 1 (unbiased variance estimator). list_path : List[str] Variable number of file paths containing the data. @@ -74,7 +74,7 @@ def running_std(inp: List[int], out: int, up_to_id = None, ddof: int = 1, *list_ else: max_id = up_to_id + 1 - mean = running_mean(inp, out, max_id - 1, *list_path) + mean = running_mean(out, max_id - 1, *list_path) for r in range(0, max_id): data = np.load(list_path[r]) cumulative_sum += (data[:, out] - mean)**2 diff --git a/src/pytom3d/util.py b/src/pytom3d/util.py index 9e5ed99..24c9274 100644 --- a/src/pytom3d/util.py +++ b/src/pytom3d/util.py @@ -7,17 +7,7 @@ import re from typing import Tuple, List, Any - -def summation(x, y): - return x+y - - -def distance(x, y): - return (x**2+y**2)**0.5 - - -def distance2(x, y): - return (abs(2*x)+y**2)**0.5 +from pytom3d.stats import running_mean, running_std def export_regressor(regressor, folder: str = "./", filename: str = "my_regressor", extension: str = ".rg", @@ -463,3 +453,55 @@ def wrapper(self, *args, **kwargs) -> None: self.history_.append(event) return wrapper + + +def contour_data_wrapper(path: str, match: str) -> Tuple[np.ndarray]: + """ + Wrapper function for generating contour data. + + Parameters + ---------- + path : str + Path to the directory containing data files. + match : str + A string used to match the desired data files. + + Returns + ------- + Tuple[np.ndarray] + A tuple containing the x-coordinates, y-coordinates, mean value, and standard deviation. + + """ + data = recursive_search(path, match=match, pop_first=True, take_first=False) + + mean = running_mean(3, None, *data) + std = running_std(3, None, 1, *data) + x, y = get_coordinates([0], *data), get_coordinates([1], *data) + + return x.reshape(-1), y.reshape(-1), mean, std + + +def scan_data_wrapper(path: str, match: str) -> Tuple[np.ndarray]: + """ + Wrapper function for generating scan data. + + Parameters + ---------- + path : str + Path to the directory containing data files. + match : str + A string used to match the desired data files. + + Returns + ------- + Tuple[np.ndarray] + A tuple containing the x-coordinates, mean value, and standard deviation. + + """ + data = recursive_search(path, match=match, pop_first=True, take_first=False) + + mean = running_mean(3, None, *data) + std = running_std(3, None, 1, *data) + x = get_coordinates([0], *data) + + return x.reshape(-1), mean, std diff --git a/src/pytom3d/viewer.py b/src/pytom3d/viewer.py index 5c5c02b..1b0588f 100644 --- a/src/pytom3d/viewer.py +++ b/src/pytom3d/viewer.py @@ -1,9 +1,12 @@ from pytom3d.core import Topography # from util import summation, distance, distance2 from matplotlib import pyplot as plt -from typing import List +from typing import List, Tuple import matplotlib from matplotlib import ticker +import matplotlib.cm as cm +from matplotlib.gridspec import GridSpec +from matplotlib.colors import BoundaryNorm #Normalize import numpy as np @@ -210,14 +213,215 @@ def scatter3DRegression(self, regression: Topography, reference: Topography = No plt.show() -def ScanViewer(*scan): - fig, ax = plt.subplots(dpi=300) - for s in scan: +class PostViewer: + + def __init__(self, **kwargs) -> None: + try: - ax.errorbar(s.x, s.y, s.y_err, fmt="-o", markersize=3, capsize=3, capthick=1, linewidth=0.8) + self.name = kwargs.pop("name") except KeyError: - ax.plot(s.x, s.y) - plt.show() + self.name = "Untitled" + + self.config_canvas() + + + def config_canvas(self, dpi: int = 300, cmap: str = "RdYlBu_r", levels: int = 8, + x_lim: List[float] = [-100, 100], y_lim_top: List[float] = [10,20], + y_lim_bot: List[float] = [-10,-20], y_lim_scan: str = [-140, 140], + cbar_lim: List[float] = [-140, 140], loc: str = "best", + bbox_to_anchor: List[float] = None) -> None: + """ + Configure canvas for plotting. + + Parameters + ---------- + dpi : int, optional + Dots per inch for the plot resolution. Default is 300. + cmap : str, optional + Colormap to use for plotting. Default is "RdYlBu_r". + levels : int, optional + Number of levels for the colorbar. Default is 8. + x_lim : List[float], optional + Limits for the x-axis. Default is [-100, 100]. + y_lim_top : List[float], optional + Limits for the top part of the y-axis. Default is [10, 20]. + y_lim_bot : List[float], optional + Limits for the bottom part of the y-axis. Default is [-10, -20]. + y_lim_scan : List[float], optional + Limits for the y-axis for scan data. Default is [-140, 140]. + cbar_lim : List[float], optional + Limits for the color bar. Default is [-140, 140]. + loc : str, optional + Location of the legend. Default is "best". + bbox_to_anchor : List[float], optional + Anchor point for the legend bounding box. Default is None. + + Returns + ------- + None + This function does not return anything. It only sets instance attributes. + """ + self.dpi = dpi + + self.cmap = cmap + self.levels = levels + + self.x_lim = x_lim + self.y_lim_top = y_lim_top + self.y_lim_bot = y_lim_bot + self.y_lim_scan = y_lim_scan + + self.mean_lim = cbar_lim + self.std_lim = cbar_lim + + self.loc = loc + self.bbox_to_anchor = bbox_to_anchor + + def config_colourbar(self, top, bot) -> None: + """ + Configure color bar limits based on top and bottom bounds. + + Parameters + ---------- + top : Tuple + Tuple containing data for the top bounds. It should contain the mean and standard deviation. + bot : Tuple + Tuple containing data for the bottom bounds. It should contain the mean and standard deviation. + + Returns + ------- + None + This function does not return anything. It only sets the instance attributes for color bar limits. + + """ + self.mean_lim = cbar_bounds(bot[2], top[2]) + self.std_lim = cbar_bounds(bot[3], top[3]) + + def scan_view(self, *scan: List) -> None: + """ + Visualize scan data. + + Parameters + ---------- + *scan : List + Variable number of scan objects. Each scan object should have attributes x, y, and y_err. + If y_err is not available, the function will plot only x and y. + + Returns + ------- + None + This function does not return anything. It simply displays the plot. + + """ + fig, ax = plt.subplots(dpi=300) + + for s in scan: + if s.err_bar: + try: + ax.errorbar(s.x, s.y, s.y_err, fmt="-o", markersize=3, capsize=3, capthick=1, linewidth=0.8) + except KeyError: + ax.plot(s.x, s.y) + else: + try: + ax.fill_between(s.x, s.y-s.y_err, s.y+s.y_err, alpha=0.5, edgecolor="none") + ax.plot(s.x, s.y) + except KeyError: + ax.plot(s.x, s.y) + + plt.show() + + def contour_and_scan_2(self, top_cnt, bot_cnt, top_scan = None, bot_scan = None) -> None: + """ + Plot contour and scan data. + + Parameters + ---------- + top_cnt : object + Object containing data for the top contours. + bot_cnt : object + Object containing data for the bottom contours. + top_scan : object, optional + Object containing data for the top scan. Default is None. + bot_scan : object, optional + Object containing data for the bottom scan. Default is None. + + Returns + ------- + None + This function does not return anything. It only displays the plot. + + """ + fig = plt.figure(dpi=self.dpi) + gs = GridSpec(6, 2, figure=fig, + width_ratios=[0.975, 0.025], + height_ratios=[0.1, 0.1, 0.1, 0.1, 0.3, 0.3]) + ax1 = fig.add_subplot(gs[0, 0]) + ax2 = fig.add_subplot(gs[1, 0]) + ax3 = fig.add_subplot(gs[0:2, 1]) + ax4 = fig.add_subplot(gs[2, 0]) + ax5 = fig.add_subplot(gs[3, 0]) + ax6 = fig.add_subplot(gs[2:4, 1]) + ax7 = fig.add_subplot(gs[4, 0]) + ax8 = fig.add_subplot(gs[5, 0]) + + top_ = [ax1, ax4] + bot_ = [ax2, ax5] + mean_cnt = [ax1, ax2] + std_cnt = [ax4, ax5] + scans = [ax7, ax8] + all_axs = [ax1, ax2, ax4, ax5, ax7, ax8] + + im12, norm12 = discrete_colorbar(self.cmap, self.mean_lim[0], self.mean_lim[1], self.levels) + a1 = ax1.tricontourf(top_cnt.P[:,0], top_cnt.P[:,1], top_cnt.P[:,2], levels=self.levels, cmap=self.cmap, norm=norm12) + a2 = ax2.tricontourf(bot_cnt.P[:,0], bot_cnt.P[:,1], bot_cnt.P[:,2], levels=self.levels, cmap=self.cmap, norm=norm12) + # ax1.tricontour(top_cnt.P[:,0], top_cnt.P[:,1], top_cnt.P[:,2], levels=levels, colors="k", linewidths=0.2, linestyles="-", alpha=0.5) + # ax2.tricontour(bot_cnt.P[:,0], bot_cnt.P[:,1], bot_cnt.P[:,2], levels=levels, colors="k", linewidths=0.2, linestyles="-", alpha=0.5) + cb1 = fig.colorbar(im12, ax=[a1,a2], cax=ax3, orientation='vertical', label="Expected Value", pad=0.1, fraction=0.5, + ticks=(np.linspace(self.mean_lim[0], self.mean_lim[1], self.levels))) + + im45, norm45 = discrete_colorbar(self.cmap, self.std_lim[0], self.std_lim[1], self.levels) + a4 = ax4.tricontourf(top_cnt.P[:,0], top_cnt.P[:,1], top_cnt.unc, levels=self.levels, cmap=self.cmap, norm=norm45) + a5 = ax5.tricontourf(bot_cnt.P[:,0], bot_cnt.P[:,1], bot_cnt.unc, levels=self.levels, cmap=self.cmap, norm=norm45) + # ax4.tricontour(top_cnt.P[:,0], top_cnt.P[:,1], top_cnt.unc, levels=levels, colors="k", linewidths=0.2, linestyles="-", alpha=0.5) + # ax5.tricontour(bot_cnt.P[:,0], bot_cnt.P[:,1], bot_cnt.unc, levels=levels, colors="k", linewidths=0.2, linestyles="-", alpha=0.5) + cb2 = fig.colorbar(im45, cax=ax6, orientation='vertical', label="Uncertainty", pad=0.1, + ticks=list(np.linspace(self.std_lim[0], self.std_lim[1], self.levels))) + + ax7.plot(top_scan.x, top_scan.y, top_scan.color, zorder=0, label=top_scan.name) + ax7.fill_between(top_scan.x, top_scan.y-top_scan.y_err, top_scan.y+top_scan.y_err, + color=top_scan.color, alpha=top_scan.alpha, zorder=-1, edgecolor="none") + + ax8.plot(bot_scan.x, bot_scan.y, bot_scan.color, zorder=0, label=bot_scan.name) + ax8.fill_between(bot_scan.x, bot_scan.y-bot_scan.y_err, bot_scan.y+bot_scan.y_err, + color=bot_scan.color, alpha=bot_scan.alpha, zorder=-1, edgecolor="none") + + for a in all_axs: + a.tick_params(direction="in", top=1, right=1, color="k") # pad=5 + a.set_xlim(self.x_lim) + a.set_xticks(np.linspace(self.x_lim[0], self.x_lim[1], 10)) + + for t in top_: + t.set_ylim(self.y_lim_top) + t.set_yticks(self.y_lim_top) + + for b in bot_: + b.set_ylim(self.y_lim_bot) + b.set_yticks(self.y_lim_bot) + + for s in scans: + s.set_yticks(np.linspace(self.y_lim_scan[0], self.y_lim_scan[1], 5)) + s.set_ylim(self.y_lim_scan) + + for c in [cb1, cb2]: + c.ax.tick_params(direction='in', right=1, left=1, size=1.5, labelsize=8) + + if self.bbox_to_anchor is None: + fig.legend(loc=self.loc) + else: + fig.legend(loc=self.loc, bbox_to_anchor=self.bbox_to_anchor) + plt.tight_layout() + plt.show() + # plt.savefig("/home/ale/Desktop/exp/test.png", dpi=300, format="png") def cfg_matplotlib(font_size: int = 12, font_family: str = 'sans-serif', use_latex: bool = False, interactive: bool = False) -> None: @@ -235,6 +439,9 @@ def cfg_matplotlib(font_size: int = 12, font_family: str = 'sans-serif', use_lat use_latex : bool, optional Enable LaTeX text rendering. The default is False. + interactive: bool, optional + Whether to keep matplotlib windows open. + Returns ------- None @@ -243,3 +450,54 @@ def cfg_matplotlib(font_size: int = 12, font_family: str = 'sans-serif', use_lat matplotlib.rcParams['font.size'] = font_size matplotlib.rcParams['font.family'] = font_family matplotlib.rcParams['text.usetex'] = use_latex + + +def cbar_bounds(v: np.ndarray, w: np.ndarray) -> Tuple[float]: + """ + Calculate the lower and upper bounds for color bar. + + Parameters + ---------- + v : np.ndarray + First array for comparison. + w : np.ndarray + Second array for comparison. + + Returns + ------- + Tuple[float] + A tuple containing the lower and upper bounds for the color bar. + + """ + return min(v.min(), w.min()), max(v.max(), w.max()) + + +def discrete_colorbar(cmap: str, lower_bound: float, upper_bound: float, levels: int) -> Tuple[cm.ScalarMappable, BoundaryNorm]: + """ + Create a discrete colorbar with custom colormap and bounds. + + Parameters + ---------- + cmap : str + Name of the colormap to use. + lower_bound : float + Lower bound for the colorbar. + upper_bound : float + Upper bound for the colorbar. + levels : int + Number of discrete levels for the colorbar. + + Returns + ------- + Tuple[plt.ScalarMappable, BoundaryNorm] + A tuple containing the ScalarMappable object for mapping scalar data to colors, + and the BoundaryNorm object for normalizing scalar data to the colormap's range. + + """ + cmap = getattr(plt.cm, cmap) # get cmap + cmaplist = [cmap(i) for i in range(cmap.N)] # get cmap colours + cmap = matplotlib.colors.LinearSegmentedColormap.from_list('Custom cmap', cmaplist, cmap.N) + bounds = np.linspace(lower_bound, upper_bound, levels) + norm = BoundaryNorm(bounds, cmap.N) + cbar = cm.ScalarMappable(norm=norm, cmap=cmap) + return cbar, norm \ No newline at end of file