From 5b3668c9cd264c23d454d97a8a340949b235a55b Mon Sep 17 00:00:00 2001 From: DanicaSTFC <138598662+DanicaSTFC@users.noreply.github.com> Date: Wed, 9 Oct 2024 12:41:02 +0100 Subject: [PATCH] Improve graph window (#335) - Adds pandas - Creates classes to generate the tabs in the graphs window. In particular, the following tabs are created: SingleRunResultsWidget, BulkRunResultsWidget, StatisticsResultsWidget. - Edits the results reader to be a numpy reader. - Edits help text for the graphs. - Adds docscrings to GraphsWindow. - Improve way labels look, text style in the plots in the graphs window. - Adds option in settings to edit the app fontsize to be user defined. Default is 12 --- CHANGES.md | 7 + docs/source/results.rst | 4 +- recipe/dev_environment.yml | 1 + recipe/meta.yaml | 1 + src/idvc/dvc_interface.py | 25 +- src/idvc/idvc.py | 7 +- src/idvc/ui/dialogs.py | 11 +- src/idvc/ui/widgets.py | 952 ++++++++++++++++------ src/idvc/ui/windows.py | 96 ++- src/idvc/utilities.py | 18 +- src/idvc/utils/manipulate_result_files.py | 113 +++ 11 files changed, 924 insertions(+), 311 deletions(-) create mode 100644 src/idvc/utils/manipulate_result_files.py diff --git a/CHANGES.md b/CHANGES.md index 26b0f022..44b6ccae 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,9 +1,16 @@ # ChangeLog ## v24.1.0 +Enhancements: +* Improve graphs window #335 +* Option to edit the app fontsize in settings #335 + Bug fixes: * use eqt FormDialog in settings #318 +Dependencies: +* Add pandas #335 + ## v24.0.1 Bug fixes: * Use RawInputDialog from the viewer package #314 diff --git a/docs/source/results.rst b/docs/source/results.rst index 9bb20120..1254e58f 100644 --- a/docs/source/results.rst +++ b/docs/source/results.rst @@ -10,14 +10,14 @@ Graphs of the Results ===================== Then click on **Display Graphs**. Another window will open (once you are done looking at the graphs you can either close or minimize this window and it will take you back to the main app just fine). -It will start you off on the **Summary** tab. +It will start you off on the **Bulk** tab. This isn’t so useful if you only performed one run. For each run that you performed, there will be a separate tab. If you navigate to one of these it will show you graphs for the objective minimum, and displacements in x, y, z as well as changes in φ, θ, ψ for that run. The title of the tab also gives the number of sampling points in the subvolume and the subvolume size. This will automatically show the displacements including the translation that you set in the manual registration. You can adjust the displacements to exclude this translation by going to Settings and selecting **Show displacement relative to reference point 0**. -Now, coming back to the summary tab, this shows the settings for the runs including the subvolume geometry, maximum displacement etc., +Now, coming back to the bulk tab, this shows the settings for the runs including the subvolume geometry, maximum displacement etc., and if you have done a bulk run then you can select a particular variable (such as the objective minimum) and then compare the graphs for this variable in each of the runs. You can select to just compare for a certain subvolume size or number of sampling points, or you can choose to compare them all (which is what is chosen in the image below). diff --git a/recipe/dev_environment.yml b/recipe/dev_environment.yml index 48fc875f..0bbdca56 100644 --- a/recipe/dev_environment.yml +++ b/recipe/dev_environment.yml @@ -6,6 +6,7 @@ dependencies: - openpyxl - python - numpy + - pandas - scipy - ccpi::ccpi-viewer >=24.0.1 - ccpi::ccpi-dvc >=22.0.0 diff --git a/recipe/meta.yaml b/recipe/meta.yaml index be7d96b0..8a4a6782 100644 --- a/recipe/meta.yaml +++ b/recipe/meta.yaml @@ -37,6 +37,7 @@ requirements: - openpyxl - python - numpy + - pandas - scipy - ccpi-viewer >=24.0.1 - ccpi-dvc >=22.0.0 diff --git a/src/idvc/dvc_interface.py b/src/idvc/dvc_interface.py index d89b825f..e1d8ccf1 100644 --- a/src/idvc/dvc_interface.py +++ b/src/idvc/dvc_interface.py @@ -487,13 +487,21 @@ def CreateHelpPanel(self): "4. Limit the range of the vectors viewed by changing the 'Vector Range Min' and Vector Range Max'. Then, click 'View Pointcloud/Vectors' to apply the changes.\n" "5. On the 2D viewer, the vectors are shown as 2D arrows, showing the displacements in the current plane. If the 'x', 'y' or 'z' keys are pressed click 'View Pointcloud/Vectors' to apply the changes.\n\n" "Display Graphs:\n" - "Graphs are displayed in a new window (once you are done looking at the graphs you can either close or minimize this window). A tab is created for each run, showing a summary of the parameters.\n" - "1. Select an option from the list for the variable to compare.\n" - "2. Select the parameters to compare from the list.\n" - "3. Click on 'Plot Histograms'.\n" - "Note: This will automatically show the displacements including the translation that you set in the manual registration.\n" - "4. Optionally, go to 'Settings' and select 'Show displacement relative to reference point 0' to adjust the displacements to exclude the initial registration translation.\n" - "5. In the case of a bulk run, a particular variable can be selected and the graphs for this variable in each of the runs can be compared.\n\n" + "Graphs are displayed in a new window (once you are done looking at the graphs you can either close or minimize this window).\n" + "A tab is created to visualise a single run. Information on the run is displayed on the left.\n" + "1. Select an option for the result to plot.\n" + "2. Select the parameter to fix and its value.\n" + "3. Click on 'Plot'.\n" + "In the case of a bulk run, an additional tab enables comparison of the results. Information on the bulk run is displayed on the left.\n" + "1. Select an option for the result to plot.\n" + "2. Select the parameter to fix and its value. Alternatively, select 'None' to plot all values.\n" + "3. Click on 'Plot histograms'.\n" + "An additional tab includes quantatitative statistical analysis of the bulk run.\n" + "1. Select an option for the result to plot. Alternatively, select 'All'.\n" + "2. Select the parameter to fix and its value. Alternatively, select 'All' to plot all values. Optionally, collapse the plots.\n" + "3. Click on 'Plot'.\n" + "Note: As a default, the displacements include the translation set in the manual registration.\n" + "4. Optionally, go to 'Settings' and select 'Show displacement relative to reference point 0' to adjust the displacements to exclude the initial registration translation.\n\n" "Results Files:\n" "Select a folder and export a session to access the result files. Two tab-delimited text files are generated for each run at location \Results\\dvc_result_*.\n" "1. The status file (dvc_result_*.stat) contains an echo of the input file used for the analysis, information about the point cloud, dvc program version, run date/time, search statistics and timing.\n" @@ -4959,8 +4967,7 @@ def show_run_pcs(self): points_list = [] subvol_list = [] for folder in glob.glob(os.path.join(directory, "dvc_result_*")): - file_path = os.path.join(folder, os.path.basename(folder)) - result = RunResults(file_path) + result = RunResults(folder) self.result_list.append(result) el = str(result.subvol_points) if el not in points_list: diff --git a/src/idvc/idvc.py b/src/idvc/idvc.py index 4c816761..ee9f168c 100644 --- a/src/idvc/idvc.py +++ b/src/idvc/idvc.py @@ -1,4 +1,3 @@ -import PySide2 from PySide2 import QtWidgets, QtGui import os, sys import logging @@ -16,8 +15,12 @@ def main(): level = eval(f'logging.{args.debug.upper()}') logging.basicConfig(level=level) logging.info(f"iDVC: Setting debugging level to {args.debug.upper()}") - app = QtWidgets.QApplication([]) + # Set a global font for the application + default_font_family = app.font().family() + font = QtGui.QFont(default_font_family, 12) # Replace with your preferred font and size + QtWidgets.QApplication.setFont(font) + file_dir = os.path.dirname(__file__) owl_file = os.path.join(file_dir, "DVCIconSquare.png") diff --git a/src/idvc/ui/dialogs.py b/src/idvc/ui/dialogs.py index 384a4aad..1fdb1563 100644 --- a/src/idvc/ui/dialogs.py +++ b/src/idvc/ui/dialogs.py @@ -17,7 +17,13 @@ def __init__(self, parent, title="Settings"): self.parent = parent - + self.fontsize_label = QLabel("Fontsize: ") + self.fontsize_widget = QSpinBox() + self.fontsize_widget.setMaximum(25) + self.fontsize_widget.setMinimum(5) + self.fontsize_widget.setSingleStep(1) + self.fontsize_widget.setValue(12) + self.addWidget(self.fontsize_widget, self.fontsize_label, 'fontsize') self.dark_checkbox = QCheckBox("Dark Mode") # populate from settings if self.parent.settings.value("dark_mode") is not None: @@ -105,6 +111,9 @@ def __init__(self, parent, title="Settings"): def onOk(self): + default_font_family = PySide2.QtWidgets.QApplication.font().family() + font = PySide2.QtGui.QFont(default_font_family, self.fontsize_widget.value()) + PySide2.QtWidgets.QApplication.setFont(font) #self.parent.settings.setValue("settings_chosen", 1) if self.dark_checkbox.isChecked(): self.parent.settings.setValue("dark_mode", True) diff --git a/src/idvc/ui/widgets.py b/src/idvc/ui/widgets.py index 8cf37f41..a38d2c4c 100644 --- a/src/idvc/ui/widgets.py +++ b/src/idvc/ui/widgets.py @@ -1,271 +1,725 @@ -import PySide2 from PySide2 import QtWidgets, QtCore from PySide2.QtWidgets import * from PySide2.QtCore import * from PySide2.QtGui import * import numpy as np -import matplotlib.pyplot as plt +import matplotlib +from matplotlib.figure import Figure from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar -from idvc.pointcloud_conversion import PointCloudConverter from functools import partial import shutil import os import tempfile from eqt.threading import Worker +from scipy.stats import norm +from eqt.ui.NoBorderScrollArea import NoBorderScrollArea - - -class SingleRunResultsWidget(QtWidgets.QWidget): - '''creates a dockable widget which will display results from a single run of the DVC code +class BaseResultsWidget(QtWidgets.QWidget): ''' - def __init__(self, parent, plot_data, displ_wrt_point0 = False): - super().__init__() - self.parent = parent - - self.figure = plt.figure() - self.canvas = FigureCanvas(self.figure) - self.toolbar = NavigationToolbar(self.canvas, self) - - #Layout + Creates a widget which can be set in a QDockWidget. + ''' + def __init__(self, parent, result_data_frame): + ''' + Creates the attributes, including a list of fontsizes and linewidth for the plots, and + a list of colour-blind friendly colours taken from matplotlib + (https://matplotlib.org/stable/users/explain/colors/colors.html#colors-def). + Initialises the Qwidget, adds a vertical layout to it. + Adds a grid layout containing information about the results to the vertical layout. + Creates a figure to be added in a canvas. The canvas is added in a scrool bar wherby + its size must be set to a minimum. The figure size and dpi are fixed + for consistency among screens. The canvas size policy must be set to expandable + to occupy all of the available space in both directions. + A toolbar and the scroll bar are added to the vertical layout. + + Parameters + ---------- + parent : QWidget + result_data_frame : pandas.DataFrame + Data frame containing the results, with columns: 'subvol_size', 'subvol_points', + 'result', 'result_arrays', 'mean_array', 'std_array'. + ''' + self.result_data_frame = result_data_frame + single_result = result_data_frame.iloc[0]['result'] + self.run_name = single_result.run_name + self.data_label = single_result.data_label + self.subvol_sizes = result_data_frame['subvol_size'].unique() + self.subvol_points = result_data_frame['subvol_points'].unique() + self.color_list = [ + '#1f77b4', # Blue + '#ff7f0e', # Orange + '#2ca02c', # Green + '#d62728', # Red + '#9467bd', # Purple + '#8c564b', # Brown + '#e377c2', # Pink + '#7f7f7f', # Gray + '#bcbd22', # Olive + '#17becf' # Cyan + ] + self.linewidth = 3 #pixels? + self.fontsizes = {'figure_title':18, 'subplot_title':14, 'label':10} + super().__init__(parent = parent) self.layout = QtWidgets.QVBoxLayout() - self.layout.addWidget(self.toolbar) - self.layout.addWidget(self.canvas) self.setLayout(self.layout) - - self.CreateHistogram(plot_data, displ_wrt_point0) - - def CreateHistogram(self, result, displ_wrt_point0): - displ = np.asarray( - PointCloudConverter.loadPointCloudFromCSV(result.disp_file,'\t')[:] - ) - if displ_wrt_point0: - point0_disp = [displ[0][6],displ[0][7], displ[0][8]] - for count in range(len(displ)): - for i in range(3): - displ[count][i+6] = displ[count][i+6] - point0_disp[i] - - plot_data = [displ[:,i] for i in range(5, displ.shape[1])] - - numGraphs = len(plot_data) - if numGraphs <= 3: - numRows = 1 - else: - numRows = np.round(np.sqrt(numGraphs)) - numColumns = np.ceil(numGraphs/numRows) - - plotNum = 0 - for array in plot_data: - plotNum = plotNum + 1 - ax = self.figure.add_subplot(int(numRows), int(numColumns), int(plotNum)) - ax.set_ylabel("") - #ax.set_xlabel(plot_titles[plotNum-1]) - ax.set_title(result.plot_titles[plotNum-1]) - ax.hist(array,20) - - plt.tight_layout() # Provides proper spacing between figures - - self.canvas.draw() - -class SummaryGraphsWidget(QtWidgets.QWidget): - '''creates a dockable widget which will display results from all runs in a bulk run - ''' - def __init__(self, parent, result_list, displ_wrt_point0 = False): - super().__init__() - self.parent = parent - - #Layout - self.layout = QtWidgets.QGridLayout() - #self.layout.setSpacing(1) - self.layout.setAlignment(Qt.AlignTop) - - widgetno=0 - - if len(result_list) >=1: - result = result_list[0] #These options were the same for all runs: - - self.results_details_label = QLabel(self) - self.results_details_label.setText("Subvolume Geometry: {subvol_geom}\n\ + self.grid_layout = QtWidgets.QGridLayout() + self.grid_layout.setAlignment(Qt.AlignTop) + self.layout.addLayout(self.grid_layout,0) + self.addInfotoGridLayout(single_result) + self.figsize = (8, 4) # Size in inches + self.dpi = 100 + self.fig = Figure(figsize=self.figsize, dpi=self.dpi) + self.canvas = FigureCanvas(self.fig) + self.canvas.setMinimumSize(800, 400) #needed for scrollbar + self.canvas.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + self.toolbar = NavigationToolbar(self.canvas, self) + self.layout.addWidget(self.toolbar) + scroll_area_widget = NoBorderScrollArea(self.canvas) + self.layout.addWidget(scroll_area_widget,1) + + def addInfotoGridLayout(self, result): + '''Adds a QLabel widget containing information about the results to the grid layout.''' + self.results_details_label = QLabel(self) + self.results_details_label.setText("Subvolume Geometry: {subvol_geom}\n\ Maximum Displacement: {disp_max}\n\ Degrees of Freedom: {num_srch_dof}\n\ Objective Function: {obj_function}\n\ Interpolation Type: {interp_type}\n\ Rigid Body Offset: {rigid_trans}".format(subvol_geom=result.subvol_geom, \ - disp_max=result.disp_max, num_srch_dof=str(result.num_srch_dof), obj_function=result.obj_function, \ - interp_type=result.interp_type, rigid_trans=str(result.rigid_trans))) - self.layout.addWidget(self.results_details_label,widgetno,0,5,1) - self.results_details_label.setAlignment(Qt.AlignTop) - widgetno+=1 - - - self.label = QLabel(self) - self.label.setText("Select which variable would like to compare: ") - self.layout.addWidget(self.label,widgetno,1) - - self.combo = QComboBox(self) - self.combo.addItems(result.plot_titles) - self.layout.addWidget(self.combo,widgetno,2) - widgetno+=1 - - self.label1 = QLabel(self) - self.label1.setText("Select which parameter you would like to compare: ") - self.layout.addWidget(self.label1,widgetno,1) + disp_max=result.disp_max, num_srch_dof=str(result.num_srch_dof), obj_function=result.obj_function, \ + interp_type=result.interp_type, rigid_trans=str(result.rigid_trans))) + self.grid_layout.addWidget(self.results_details_label,0,0,5,1) + self.results_details_label.setAlignment(Qt.AlignTop) + + def addSubplotsToFigure(self): + '''To be defined in the child classes.''' + pass + + def addWidgetsToGridLayout(self): + '''To be defined in the child classes.''' + pass + + def _selectRow(self, result_data_frame, selected_subvol_points, selected_subvol_size): + '''Given a dataframe, returns the whole row whose 'subvol_points' and 'subvol_size' columns + are associated with the selected values. + + Parameters + ---------- + result_data_frame : pandas.DataFrame + Data frame containing the results. Includes the 'subvol_points' and 'subvol_size' columns. + selected_subvol_points : str of an integer + Selected points in subvolume. + selected_subvol_size : str of an integer + Selected subvolume size. + ''' + df = result_data_frame + if len(df) > 1: + df = df[(df['subvol_points'].astype(str) == selected_subvol_points) & (df['subvol_size'].astype(str) == selected_subvol_size)] + elif len(df) == 1: + df = self.result_data_frame + row = df.iloc[0] + return row + + def _selectOneParameter(self, result_data_frame, parameter, selected_parameter): + '''Given a dataframe, returns a filtered dataframe whose rows are associated with + the selected value of a parameter. + + Parameters + ---------- + result_data_frame : pandas.DataFrame + Data frame containing the results. Includes the 'subvol_points' and 'subvol_size' columns. + parameter : str + 'subvol_points' or 'subvol_size' + selected_parameter : str of an integer + Selected value of the parameter. + ''' + df = result_data_frame + df = df[(df[parameter].astype(str) == selected_parameter)] + return df + + def _addHistogramSubplot(self, subplot, array, xlabel, mean, std): + '''Given an array, calculates the relative counts by using the matplotlib + histogram functionality. It clears the plot and plots the relative frequency histogram + as a bar plot. Sets the x an y labels. Adds the mean and std values as vertical lines. + Plots the gaussian fit. Adds the legend to the plot. + + Parameters + ---------- + subplot : matplotlib subplot of a figure + array : numpyarray + xlabel : str + mean : float + std : float + ''' + counts, bins = subplot.hist(array, bins=20)[0:2] + relative_counts = counts*100/ len(array) + subplot.cla() + bin_widths = np.diff(bins) + subplot.bar(bins[:-1], relative_counts, width=bin_widths, align='edge',color='lightgrey') + subplot.set_ylabel("Relative frequency (% points in run)", fontsize=self.fontsizes['label']) + subplot.set_xlabel(xlabel, fontsize=self.fontsizes['label']) + subplot.axvline(mean, color=self.color_list[0], linestyle='--', linewidth=self.linewidth, label=f'mean = {mean:.3f}') + subplot.axvline(mean-std, color=self.color_list[1], linestyle='--', linewidth=self.linewidth, label=f'std = {std:.3f}') + subplot.axvline(mean+std, color=self.color_list[1], linestyle='--', linewidth=self.linewidth) + x = np.linspace(min(array), max(array), 1000) + gaussian = norm.pdf(x, mean, std) * (bins[1] - bins[0]) *100 + subplot.plot(x, gaussian, self.color_list[2],linestyle='--', linewidth=self.linewidth, label='gaussian fit') + subplot.legend(loc='upper right') + + def _addStatisticalAnalysisPlot(self, subplot, xlabel, ylabel, xpoints, ypoints, color, label, linestyle): + "Draws a line plot in 'subplot'. Adds labels and sets user-defined properties." + subplot.plot(xpoints, ypoints, color=color, linestyle=linestyle, linewidth=self.linewidth, label=label) + subplot.set_ylabel(ylabel + " (pixels)", fontsize=self.fontsizes['label']) + subplot.set_xlabel(xlabel, fontsize=self.fontsizes['label']) + + +class SingleRunResultsWidget(BaseResultsWidget): + ''' + Creates a widget which can be set in a QDockWidget. + This will display results from a single run of the DVC code. + ''' + def __init__(self, parent, result_data_frame): + ''' + Initialises the SingleRunResultsWidget. + + Parameters + ---------- + parent : QWidget + The parent widget. + result_data_frame : pandas.DataFrame + Dataframe containing the result data. + ''' + super().__init__(parent, result_data_frame) + if len(result_data_frame) > 1: + self.addWidgetsToGridLayout() + self.addSubplotsToFigure() + + def addWidgetsToGridLayout(self): + """ + Initialises and adds the following widgets to the grid layout: + - a QLabel and QComboBox for selecting points in a subvolume. + - a QLabel and QComboBox for selecting the size of a subvolume. + - a QPushButton for plotting histograms, which is connected to the `addSubplotsToFigure` method. + """ + widgetno=1 + + self.subvol_points_label = QLabel(self) + self.subvol_points_label.setText("Select points in subvolume: ") + self.grid_layout.addWidget(self.subvol_points_label,widgetno,1) - self.combo1 = QComboBox(self) - self.param_list = ["All","Sampling Points in Subvolume", "Subvolume Size"] - self.combo1.addItems(self.param_list) - self.layout.addWidget(self.combo1,widgetno,2) + self.subvol_points_widget = QComboBox(self) + self.subvol_points_widget.addItems(self.subvol_points) + self.grid_layout.addWidget(self.subvol_points_widget,widgetno,2) widgetno+=1 - self.subvol_points=[] - self.subvol_sizes=[] - - for result in result_list: - if result.subvol_points not in self.subvol_points: - self.subvol_points.append(result.subvol_points) - if result.subvol_size not in self.subvol_sizes: - self.subvol_sizes.append(result.subvol_size) - self.subvol_points.sort() - self.subvol_sizes.sort() - - self.secondParamLabel = QLabel(self) - self.secondParamLabel.setText("Subvolume size:") - self.layout.addWidget(self.secondParamLabel,widgetno,1) + self.subvol_size_label = QLabel(self) + self.subvol_size_label.setText("Select subvolume size: ") + self.grid_layout.addWidget(self.subvol_size_label,widgetno,1) - self.secondParamCombo = QComboBox(self) - self.secondParamList = [str(i) for i in self.subvol_sizes] - self.secondParamCombo.addItems(self.secondParamList) - self.layout.addWidget(self.secondParamCombo,widgetno,2) + self.subvol_size_widget = QComboBox(self) + self.subvol_size_widget.addItems(self.subvol_sizes) + self.grid_layout.addWidget(self.subvol_size_widget,widgetno,2) widgetno+=1 - self.combo1.currentIndexChanged.connect(self.showSecondParam) - self.secondParamLabel.hide() - self.secondParamCombo.hide() - - self.button = QtWidgets.QPushButton("Plot Histograms") - self.button.clicked.connect(partial(self.CreateHistogram,result_list, displ_wrt_point0)) - self.layout.addWidget(self.button,widgetno,2) + self.button = QtWidgets.QPushButton("Plot histograms") + self.button.clicked.connect(partial(self.addSubplotsToFigure)) + self.grid_layout.addWidget(self.button,widgetno,2) widgetno+=1 - self.figure = plt.figure() + def addSubplotsToFigure(self): + ''' + Clears the current figure. Determines the number of rows and + columns for the figure layout. Selects the appropriate row + from the result dataframe based on the user selected subvolume points and size. + Extracts result arrays, mean array, and standard deviation array from the selected row. + Sets the figure title with details about the run and subvolume. + Iterates over the result arrays to create histograms and adds them as subplots. + Adjusts the figure layout and redraws the canvas. - self.canvas = FigureCanvas(self.figure) - self.toolbar = NavigationToolbar(self.canvas, self) - self.layout.addWidget(self.toolbar,widgetno,0,1,3) - widgetno+=1 - self.layout.addWidget(self.canvas,widgetno,0,3,3) - widgetno+=1 - - self.setLayout(self.layout) + Attributes + ---------- + result_data_frame : DataFrame + Data frame containing the results. + subvol_points_widget : QWidget + Widget for selecting subvolume points. + subvol_size_widget : QWidget + Widget for selecting subvolume size. + fig : Figure + Matplotlib figure object. + run_name : str + Name of the current run. + data_label : list + List of labels for the result data. + fontsizes : dict + Dictionary containing font sizes for various elements. + canvas : FigureCanvas + Matplotlib canvas object for rendering the figure. + ''' + self.fig.clf() + numRows = 2 + numColumns = 2 + if len(self.result_data_frame) > 1: + current_subvol_points = self.subvol_points_widget.currentText() + current_subvol_size = self.subvol_size_widget.currentText() + row = self._selectRow(self.result_data_frame, current_subvol_points, current_subvol_size) + elif len(self.result_data_frame) == 1: + row = self._selectRow(self.result_data_frame, None, None) + result_arrays = row.result_arrays + mean_array = row.mean_array + std_array = row.std_array + self.fig.suptitle(f"Run '{self.run_name}': points in subvolume {row.subvol_points}, subvolume size {row.subvol_size}",fontsize=self.fontsizes['figure_title']) + for plotNum, array in enumerate(result_arrays): + x_label = self.data_label[plotNum] + if 0 1: + bulk_run_results_widget = BulkRunResultsWidget(self, result_data_frame) + dock2 = QDockWidget("Bulk",self) + dock2.setFeatures(QDockWidget.NoDockWidgetFeatures) + dock2.setAllowedAreas(QtCore.Qt.RightDockWidgetArea) + dock2.setWidget(bulk_run_results_widget) + self.addDockWidget(QtCore.Qt.RightDockWidgetArea,dock2) + self.tabifyDockWidget(prev,dock2) + + dock2.raise_() # makes bulk panel the one that is open by default. + + # add statistial analysis tab + statistical_analisis_widget = StatisticsResultsWidget(self, result_data_frame) + dock3 = QDockWidget("Statistical analysis",self) + dock3.setFeatures(QDockWidget.NoDockWidgetFeatures) + dock3.setAllowedAreas(QtCore.Qt.RightDockWidgetArea) + dock3.setWidget(statistical_analisis_widget) + self.addDockWidget(QtCore.Qt.RightDockWidgetArea,dock3) + self.tabifyDockWidget(dock2,dock3) # Stop the widgets in the tab to be moved around for wdg in self.findChildren(QTabBar): wdg.setMovable(False) + diff --git a/src/idvc/utilities.py b/src/idvc/utilities.py index 3449c7b1..270064bc 100644 --- a/src/idvc/utilities.py +++ b/src/idvc/utilities.py @@ -4,14 +4,14 @@ from PySide2.QtCore import * from PySide2.QtGui import * import numpy as np +import os class RunResults(object): - def __init__(self, file_name): - + def __init__(self, folder): + file_name = os.path.join(folder, os.path.basename(folder)) self.points = None - disp_file_name = file_name + ".disp" stat_file_name = file_name + ".stat" @@ -46,19 +46,19 @@ def __init__(self, file_name): # self.subvol_aspect = [int(line.split('\t')[1]),int(line.split('\t')[2]), int(line.split('\t')[3])] count+=1 - plot_titles_dict = { - 'objmin': "Objective Minimum", 'u': "Displacement in x", 'v':"Displacement in y", 'w':"Displacement in z", + data_label_dict = { + 'objmin': "Objective minimum", 'u': "Displacement x component", 'v':"Displacement y component", 'w':"Displacement z component", 'phi':"Change in phi",'the':"Change in theta", 'psi':"Change in psi"} with open(disp_file_name) as f: # first 4 columns are: n, x, y, z, status - we don't want these - self.plot_titles = f.readline().split()[5:] - self.plot_titles = [plot_titles_dict.get(text, text) for text in self.plot_titles] + self.data_label = f.readline().split()[5:] + self.data_label = [data_label_dict.get(text, text) for text in self.data_label] - self.disp_file = disp_file_name + self.run_name = os.path.basename(os.path.dirname(folder)) - self.title = str(self.subvol_points) + " Points in Subvolume," + " Subvolume Size: " + str(self.subvol_size) + self.title = str(self.subvol_points) + "," + str(self.subvol_size) def __str__(self): diff --git a/src/idvc/utils/manipulate_result_files.py b/src/idvc/utils/manipulate_result_files.py new file mode 100644 index 00000000..0a56c0c9 --- /dev/null +++ b/src/idvc/utils/manipulate_result_files.py @@ -0,0 +1,113 @@ +import numpy as np +import pandas as pd +from idvc.pointcloud_conversion import PointCloudConverter +from idvc.utilities import RunResults +import glob, os + +def _extractDataFromDispResultFile(result, displ_wrt_point0): + """ + Extracts objective minimum and displacement vectors from a result file. + Optionally, adjusts the displacement vectors relative to the displacement of the first point. + + The objective function minimum is located at index 5. + The displacement vector is extracted from columns indexed 6 to 8 (inclusive). + If `displ_wrt_point0` is True, the displacement values will be adjusted by + subtracting the displacement of the first point. + + Parameters + ---------- + result : RunResults + An object containing the filepath to the displacement result file (`result.disp_file`). + The displacement result file is expected to be tab-delimited and have a header row that will be skipped. + displ_wrt_point0 : bool + If True, the displacement vectors will be adjusted relative to the displacement vector of the first point + (point zero). + + Returns + ------- + numpy.ndarray + 2D array where each row corresponds to a column of data from the file, + including the objective function minimum and displacement vectors. + """ + data = np.genfromtxt(result.disp_file, delimiter='\t', skip_header=1) + data_shape = data.shape + index_objmin = 5 + index_disp = [6, 9] + if displ_wrt_point0: + point0_disp_array = data[0, index_disp[0]:index_disp[1]] + data[:, index_disp[0]:index_disp[1]] -= point0_disp_array + result_arrays = np.transpose(data[:, index_objmin:data_shape[1]]) + return result_arrays + +def createResultsDataFrame(results_folder, displ_wrt_point0): + """ + Creates a pandas DataFrame containing results from DVC result files. + The folder is scanned for subfolders matching the pattern + "dvc_result_*". The data for a result is extracted from each result file, and compiles + the data into a pandas DataFrame. + + Parameters + ---------- + results_folder : str + The path to the folder containing the DVC result subfolders. + displ_wrt_point0 :bool + A flag indicating whether to extract displacement data with respect to point 0. + + Returns + ------- + pd.DataFrame: A DataFrame with the following columns: + - 'subvol_size': List of subvolume sizes as strings. + - 'subvol_points': List of subvolume points as strings. + - 'result': List of RunResults objects. + - 'result_arrays': List of 4 arrays containing extracted data. + """ + subvol_size_list = [] + subvol_points_list = [] + result_list = [] + result_arrays_list = [] + + for folder in glob.glob(os.path.join(results_folder, "dvc_result_*")): + result = RunResults(folder) + result_arrays = _extractDataFromDispResultFile(result, displ_wrt_point0) + subvol_size_list.append(str(result.subvol_size)) + subvol_points_list.append(str(result.subvol_points)) + result_list.append(result) + + result_arrays_list.append(result_arrays) + result_data_frame = pd.DataFrame({ +'subvol_size': subvol_size_list, +'subvol_points': subvol_points_list, +'result': result_list, +'result_arrays': result_arrays_list}) + return result_data_frame + +def addMeanAndStdToResultDataFrame(result_data_frame): + """ + Adds mean and standard deviation arrays to the result DataFrame. + + In particular, iterates over each row in the DataFrame, calculates the mean and + standard deviation for each array in the 'result_arrays' column, and appends + the new columns 'mean_array' and 'std_array'. + + Parameters + ---------- + result_data_frame : pd.DataFrame + A pandas DataFrame. + + Returns + ------- + pd.DataFrame: The modified DataFrame with additional columns 'mean_array' and 'std_array'. + """ + mean_array_list = [] + std_array_list = [] + for row in result_data_frame.itertuples(): + mean_array = [] + std_array = [] + for array in row.result_arrays: + mean_array.append(array.mean()) + std_array.append(array.std()) + mean_array_list.append(mean_array) + std_array_list.append(std_array) + result_data_frame['mean_array'] = mean_array_list + result_data_frame['std_array'] = std_array_list + return result_data_frame \ No newline at end of file