diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 462051a6..c23ee035 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -68,7 +68,7 @@ jobs: name: mdaviz-docs path: docs/build/html # ${{ steps.deployment.outputs.artifact }} - + - name: Publish (push gh-pages branch) only on demand uses: peaceiris/actions-gh-pages@v4 if: ${{ github.event.inputs.deploy }} diff --git a/.gitignore b/.gitignore index f8349f13..6be79607 100644 --- a/.gitignore +++ b/.gitignore @@ -172,4 +172,5 @@ dev_*.py mdaviz/resources/*.py # temporary files from Microsoft -~* \ No newline at end of file +~* +Untitled.ipynb diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..ca4cbce7 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,24 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + - id: check-toml + - id: check-ast + - id: check-merge-conflict + - id: check-added-large-files + - id: mixed-line-ending + - id: end-of-file-fixer + - id: trailing-whitespace + - repo: https://github.com/kynan/nbstripout + rev: 0.7.1 + hooks: + - id: nbstripout + - repo: https://github.com/astral-sh/ruff-pre-commit + # Ruff version. + rev: v0.3.2 + hooks: + # Run the linter. + - id: ruff + args: [--fix] + # Run the formatter. + - id: ruff-format diff --git a/README.md b/README.md index 4ab26050..d96a3f5f 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ Python Qt5 application to visualize mda data. GH tag | GH release | PyPI ---- | --- | --- +--- | --- | --- [![tag](https://img.shields.io/github/tag/BCDA-APS/mdaviz.svg)](https://github.com/BCDA-APS/mdaviz/tags) | [![release](https://img.shields.io/github/release/BCDA-APS/mdaviz.svg)](https://github.com/BCDA-APS/mdaviz/releases) | [![PyPi](https://img.shields.io/pypi/v/mdaviz.svg)](https://pypi.python.org/pypi/mdaviz) Python version(s) | Unit Tests | Code Coverage | License @@ -20,5 +20,5 @@ For complete installation guide, see [https://bcda-aps.github.io/mdaviz/](https: ## Acknowledgements -"This product includes software produced by UChicago Argonne, LLC +"This product includes software produced by UChicago Argonne, LLC under Contract No. DE-AC02-06CH11357 with the Department of Energy." diff --git a/docs/source/changes.rst b/docs/source/changes.rst index 6fa805e2..6096c38f 100644 --- a/docs/source/changes.rst +++ b/docs/source/changes.rst @@ -3,4 +3,4 @@ Changes ======= -TODO \ No newline at end of file +TODO diff --git a/docs/source/index.rst b/docs/source/index.rst index a19bab61..5f8c3723 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -36,8 +36,8 @@ Documentation :align: center :figclass: align-center :figwidth: 100% - - Screenshot of the mdaviz GUI displaying sample data. + + Screenshot of the mdaviz GUI displaying sample data. .. toctree:: :maxdepth: 1 @@ -68,5 +68,5 @@ About Acknowledgements ================ -"This product includes software produced by UChicago Argonne, LLC +"This product includes software produced by UChicago Argonne, LLC under Contract No. DE-AC02-06CH11357 with the Department of Energy." diff --git a/docs/source/license.rst b/docs/source/license.rst index 36252c4e..eaa685c3 100644 --- a/docs/source/license.rst +++ b/docs/source/license.rst @@ -4,4 +4,4 @@ License ======= .. literalinclude:: ../../mdaviz/LICENSE - :language: text \ No newline at end of file + :language: text diff --git a/mdaviz/LICENSE b/mdaviz/LICENSE index 570ab243..1720f7fa 100644 --- a/mdaviz/LICENSE +++ b/mdaviz/LICENSE @@ -9,27 +9,27 @@ BCDA, Advanced Photon Source, Argonne National Laboratory OPEN SOURCE LICENSE -Redistribution and use in source and binary forms, with or without +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: -1. Redistributions of source code must retain the above copyright notice, - this list of conditions and the following disclaimer. Software changes, - modifications, or derivative works, should be noted with comments and +1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. Software changes, + modifications, or derivative works, should be noted with comments and the author and organization's name. 2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation + this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. -3. Neither the names of UChicago Argonne, LLC or the Department of Energy - nor the names of its contributors may be used to endorse or promote - products derived from this software without specific prior written +3. Neither the names of UChicago Argonne, LLC or the Department of Energy + nor the names of its contributors may be used to endorse or promote + products derived from this software without specific prior written permission. -4. The software and the end-user documentation included with the +4. The software and the end-user documentation included with the redistribution, if any, must include the following acknowledgment: - "This product includes software produced by UChicago Argonne, LLC + "This product includes software produced by UChicago Argonne, LLC under Contract No. DE-AC02-06CH11357 with the Department of Energy." **************************************************************************** @@ -38,11 +38,11 @@ DISCLAIMER THE SOFTWARE IS SUPPLIED "AS IS" WITHOUT WARRANTY OF ANY KIND. -Neither the United States GOVERNMENT, nor the United States Department -of Energy, NOR uchicago argonne, LLC, nor any of their employees, makes -any warranty, express or implied, or assumes any legal liability or -responsibility for the accuracy, completeness, or usefulness of any -information, data, apparatus, product, or process disclosed, or +Neither the United States GOVERNMENT, nor the United States Department +of Energy, NOR uchicago argonne, LLC, nor any of their employees, makes +any warranty, express or implied, or assumes any legal liability or +responsibility for the accuracy, completeness, or usefulness of any +information, data, apparatus, product, or process disclosed, or represents that its use would not infringe privately owned rights. **************************************************************************** diff --git a/mdaviz/app.py b/mdaviz/app.py index 10636402..eebbd00d 100644 --- a/mdaviz/app.py +++ b/mdaviz/app.py @@ -23,9 +23,7 @@ def gui(directory): app = QtWidgets.QApplication(sys.argv) main_window = MainWindow(directory=directory) - main_window.setStatus( - f"Application started, loading {pathlib.Path(directory).absolute()} ..." - ) + main_window.setStatus(f"Application started, loading {pathlib.Path(directory).absolute()} ...") main_window.show() sys.exit(app.exec()) @@ -74,16 +72,12 @@ def main(): # for future command-line options # Ensure the path is absolute (starts with a "/") if not directory.startswith("/"): - print( - f"\n\nERROR: The specified directory is not an absolute path:\n\t{directory}\n" - ) + print(f"\n\nERROR: The specified directory is not an absolute path:\n\t{directory}\n") sys.exit(1) # Check if the directory exists if not directory_path.exists() or not directory_path.is_dir(): - print( - f"\n\nERROR: The specified directory does not exist or is not a directory:\n\t{directory}\n" - ) + print(f"\n\nERROR: The specified directory does not exist or is not a directory:\n\t{directory}\n") sys.exit(1) logging.basicConfig(level=options.log.upper()) diff --git a/mdaviz/chartview.py b/mdaviz/chartview.py index 32204608..caffc2a1 100644 --- a/mdaviz/chartview.py +++ b/mdaviz/chartview.py @@ -6,7 +6,6 @@ from functools import partial from itertools import cycle import numpy -from pathlib import Path from PyQt5 import QtCore, QtWidgets from . import utils @@ -414,11 +413,7 @@ def calculateBasicMath(self, x_data, y_data): x_at_y_min = x_array[y_min_index] x_at_y_max = x_array[y_max_index] # Calculate x_com and y_mean - x_com = ( - numpy.sum(x_array * y_array) / numpy.sum(y_array) - if numpy.sum(y_array) != 0 - else None - ) + x_com = numpy.sum(x_array * y_array) / numpy.sum(y_array) if numpy.sum(y_array) != 0 else None y_mean = numpy.mean(y_array) return (x_at_y_min, y_min), (x_at_y_max, y_max), x_com, y_mean @@ -430,9 +425,7 @@ def onRemoveCursor(self, cursor_num): cross.remove() self.cursors[cursor_num] = None self.cursors[f"pos{cursor_num}"] = None - self.cursors[f"text{cursor_num}"] = ( - "middle click" if cursor_num == 1 else "right click" - ) + self.cursors[f"text{cursor_num}"] = "middle click" if cursor_num == 1 else "right click" self.cursors["diff"] = "n/a" self.cursors["midpoint"] = "n/a" self.updateCursorInfo() @@ -493,12 +486,8 @@ def calculateCursors(self): delta_y = y2 - y1 midpoint_x = (x1 + x2) / 2 midpoint_y = (y1 + y2) / 2 - self.cursors["diff"] = ( - f"({utils.num2fstr(delta_x)}, {utils.num2fstr(delta_y)})" - ) - self.cursors["midpoint"] = ( - f"({utils.num2fstr(midpoint_x)}, {utils.num2fstr(midpoint_y)})" - ) + self.cursors["diff"] = f"({utils.num2fstr(delta_x)}, {utils.num2fstr(delta_y)})" + self.cursors["midpoint"] = f"({utils.num2fstr(midpoint_x)}, {utils.num2fstr(midpoint_y)})" self.updateCursorInfo() def updateCursorInfo(self): @@ -525,9 +514,7 @@ class CurveManager(QtCore.QObject): curveUpdated = QtCore.pyqtSignal( str, bool, bool ) # Emit curveID, recompute_y (bool) & update_x (bool) when a curve is updated - allCurvesRemoved = QtCore.pyqtSignal( - bool - ) # Emit a doNotClearCheckboxes bool when all curve are removed + allCurvesRemoved = QtCore.pyqtSignal(bool) # Emit a doNotClearCheckboxes bool when all curve are removed def __init__(self, parent=None): super().__init__(parent) diff --git a/mdaviz/data_table_view.py b/mdaviz/data_table_view.py index 2cb7c08e..f0ed856a 100644 --- a/mdaviz/data_table_view.py +++ b/mdaviz/data_table_view.py @@ -1,11 +1,9 @@ -from PyQt5 import QtCore, QtWidgets -from . import utils +from PyQt5 import QtWidgets HEADERS = ["X", "Y"] class DataTableView(QtWidgets.QWidget): - def __init__(self, data, parent): """ This class is responsible for setting up a table view widget, managing the data displayed within it, diff --git a/mdaviz/mainwindow.py b/mdaviz/mainwindow.py index 1dc546ac..7c1161aa 100644 --- a/mdaviz/mainwindow.py +++ b/mdaviz/mainwindow.py @@ -8,7 +8,6 @@ from pathlib import Path from PyQt5 import QtWidgets -from PyQt5.QtGui import QIcon from . import APP_TITLE from .mda_folder import MDA_MVC @@ -17,9 +16,8 @@ from .opendialog import DIR_SETTINGS_KEY UI_FILE = utils.getUiFileName(__file__) +MAX_FILES = 100 MAX_RECENT_DIRS = 10 -MAX_DEPTH = 4 -MAX_SUBFOLDERS_PER_DEPTH = 10 class MainWindow(QtWidgets.QMainWindow): @@ -27,61 +25,53 @@ class MainWindow(QtWidgets.QMainWindow): .. autosummary:: - ~setup + ~connect ~status ~setStatus ~doAboutDialog ~closeEvent ~doClose ~doOpen + ~reset_mainwindow ~dataPath - ~subFolderPath - ~folderPath - ~folderList - ~subFolderList + ~setDataPath ~mdaFileList - ~mdaFileCount ~setMdaFileList - ~setSubFolderName - ~setSubfolderList - ~setFolderPath - ~setSubFolderPath - ~cleanFolderList + ~mdaInfoList + ~setMdaInfoList + ~folderList ~setFolderList - ~updateRecentFolders + ~onFolderSelected + ~onRefresh + ~_buildFolderList + ~_updateRecentFolders """ def __init__(self, directory): super().__init__() - self.directory = directory utils.myLoadUi(UI_FILE, baseinstance=self) - self.setup() - - def setup(self): - self._dataPath = None # the combined data path obj (folder.parent + subfolder) - self._folderPath = None # the path obj from pull down 1 - self._folderList = [] # the list of folder in pull down 1 - self._subFolderPath = None # the subfolder path obj selected in pull down 2 - self._subFolderList = [] # the list of subfolder in pull down 2 - self._mdaFileList = [] # the list of mda file NAME str (name only) - self._mdaFileCount = 0 # the number of mda files in the list + self.setWindowTitle(APP_TITLE) + + self.directory = directory self.mvc_folder = None + self.setDataPath() # the combined data path obj + self.setFolderList() # the list of recent folders in folder QCombobox + self.setMdaFileList() # the list of mda file NAME str (name only) + self.setMdaInfoList() # the list of mda file Info (all the data necessary to fill the table view) - self.setWindowTitle(APP_TITLE) - self.setFolderList(None) + self.connect() + self.onFolderSelected(directory) + settings.restoreWindowGeometry(self, "mainwindow_geometry") + print("Settings are saved in:", settings.fileName()) + + def connect(self): self.actionOpen.triggered.connect(self.doOpen) self.actionAbout.triggered.connect(self.doAboutDialog) self.actionExit.triggered.connect(self.doClose) utils.reconnect(self.open.released, self.doOpen) - self.open.released.connect(self.doOpen) - - self.folder.currentTextChanged.connect(self.setFolderPath) - self.subfolder.currentTextChanged.connect(self.setSubFolderPath) - - settings.restoreWindowGeometry(self, "mainwindow_geometry") - print("Settings are saved in:", settings.fileName()) - self.setFolderPath(self.directory) + utils.reconnect(self.refresh.released, self.onRefresh) + self.folder.currentTextChanged.connect(self.onFolderSelected) @property def status(self): @@ -113,9 +103,7 @@ def doClose(self, *args, **kw): User chose exit (or quit), or closeEvent() was called. """ self.setStatus("Application quitting ...") - settings.saveWindowGeometry(self, "mainwindow_geometry") - self.close() def doOpen(self, *args, **kw): @@ -129,185 +117,173 @@ def doOpen(self, *args, **kw): dir_name = open_dialog.getExistingDirectory(self, "Select a Directory") if dir_name: folder_list = self.folderList() - if folder_list[0] == "": - folder_list[0] = dir_name - else: - folder_list.insert(0, dir_name) + folder_list.insert(0, dir_name) self.setFolderList(folder_list) + def reset_mainwindow(self): + self.setDataPath() + self.setMdaInfoList() + self.setMdaFileList() + if self.mvc_folder is not None: + self.mvc_folder.mda_folder_tableview.clearContents() + def dataPath(self): """ - Full path object for the displayed data: - dataPath = folderPath.parent + subFolderPath + Full path object for the selected folder """ return self._dataPath - def subFolderPath(self): - """Subfolder name (str) of the selected subfolder.""" - return self._subFolderPath - - def folderPath(self): - """Full path (obj) of the selected folder.""" - return self._folderPath - - def folderList(self): - """Folder path (str) list in the pull down menu.""" - return self._folderList - - def subFolderList(self): - """Subfolder path (str) list in the pull down menu.""" - return self._subFolderList + def setDataPath(self, path=None): + self._dataPath = path def mdaFileList(self): """List of mda file (name only) in the selected folder.""" return self._mdaFileList - def mdaFileCount(self): - """Number of mda files in the selected folder.""" - return self._mdaFileCount + def setMdaFileList(self, mda_file_list=None): + self._mdaFileList = mda_file_list if mda_file_list else [] - def setMdaFileList(self, data_path): - if data_path: - self._mdaFileList = sorted([file.name for file in data_path.glob("*.mda")]) - else: - self._mdaFileList = [] + def mdaInfoList(self): + return self._mdaInfoList + + def setMdaInfoList(self, infoList=None): + self._mdaInfoList = infoList if infoList else [] + + def folderList(self): + return self._folderList - def setSubFolderName(self): - self._subFolderPath = Path(self.subfolder.currentText()) + def setFolderList(self, folder_list=None): + """Set the folder path list & populating the folder QComboBox. - def setSubfolderList(self, subfolder_list): - """Set the subfolders path list in the pop-up list.""" - self._subFolderList = subfolder_list - self.subfolder.clear() - self.subfolder.addItems(subfolder_list) + - If folder_list is not None, it will remove its duplicates. + - If folder_list is None, the call to buildFolderList will take care of building the list + based on the recent list of folder saved in the app settings. - def setFolderPath(self, folder_name): - """A folder was selected (from the open dialog).""" - if folder_name == "Other...": + Args: + folder_list (list, optional): the current list of recent folders. Defaults to None. + """ + if folder_list != "": + folder_list = self._buildFolderList(folder_list) + self._fillFolderBox(folder_list) + self._folderList = folder_list + + def onFolderSelected(self, folder_name): + """A folder was selected (from the open dialog or pull down menu).""" + if folder_name == "Open...": self.doOpen() + elif folder_name == "Clear Recently Open...": + settings.setKey(DIR_SETTINGS_KEY, "") + folder_list = [str(self.dataPath())] if self.dataPath() else [""] + self.setFolderList(folder_list) else: folder_path = Path(folder_name) if folder_path.exists() and folder_path.is_dir(): # folder exists - self._folderPath = folder_path - - def get_all_subfolders( - folder_path, - parent_path="", - current_depth=0, - max_depth=MAX_DEPTH, - max_subfolders_per_depth=MAX_SUBFOLDERS_PER_DEPTH, - depth_counter=None, - ): - if depth_counter is None: - depth_counter = {depth: 0 for depth in range(1, max_depth + 1)} - - subfolder_list = [] - if parent_path: # Don't add the root parent folder - subfolder_list.append(parent_path) - - if current_depth >= max_depth: - print(f"{current_depth=}") - return subfolder_list - - try: - for item in folder_path.iterdir(): - # Check if we have collected enough subfolders for the current depth - if ( - depth_counter[current_depth + 1] - >= max_subfolders_per_depth - ): - break - - if item.is_dir() and not item.name.startswith("."): - full_path = ( - f"{parent_path}/{item.name}" - if parent_path - else item.name - ) - subfolder_list.append(full_path) - # Addition of a subfolder that exists at one level deeper than the current level - depth_counter[current_depth + 1] += 1 - # Recursively collect subfolders, passing the updated depth_counter - subfolder_list.extend( - get_all_subfolders( - item, - full_path, - current_depth + 1, - max_depth, - max_subfolders_per_depth, - depth_counter, - ) - ) - except PermissionError: - print(f"Permission denied for folder: {folder_path}") - subfolder_list = list(dict.fromkeys(sorted(subfolder_list))) - - return subfolder_list - - self.setSubfolderList(get_all_subfolders(folder_path, folder_path.name)) - self.updateRecentFolders(str(folder_path)) - else: - self._folderPath = None - self._dataPath = None - self._mdaFileList = [] - self._mdaFileCount = 0 - self.setSubfolderList([]) - self.setStatus(f"\n{str(folder_path)!r} - invalid path.") - if self.mvc_folder is not None: - # If MVC exists, display empty table views - self.mvc_folder.mda_folder_tableview.clearContents() - - def setSubFolderPath(self, subfolder_name): - if subfolder_name: - data_path = self.folderPath().parent / Path(subfolder_name) - self._dataPath = data_path - layout = self.groupbox.layout() - mda_files_path = list(data_path.glob("*.mda")) - self._mdaFileCount = len(mda_files_path) - self.setMdaFileList(data_path) - self.info.setText(f"{self._mdaFileCount} mda files") - if self.mvc_folder is None: - self.mvc_folder = MDA_MVC(self) - layout.addWidget(self.mvc_folder) + mda_list = [utils.get_file_info(f) for f in folder_path.glob("*.mda")] + if mda_list: + self.setDataPath(folder_path) + mda_list = sorted(mda_list, key=lambda x: x["Name"]) + mda_name_list = [entry["Name"] for entry in mda_list] + self.setMdaInfoList(mda_list) + self.setMdaFileList(mda_name_list) + self._addToRecentFolders(str(folder_path)) + self.info.setText(f"{len(mda_list)} mda files") + layout = self.groupbox.layout() + if self.mvc_folder is None: + self.mvc_folder = MDA_MVC(self) + layout.addWidget(self.mvc_folder) + else: + # Always update the folder view since it is a new folder + self.mvc_folder.updateFolderView() + else: + self.info.setText("No mda files") + self.reset_mainwindow() + self.setStatus(f"\n{str(folder_path)!r} - No MDA files found.") else: - # Always update the folder view since it is a new subfolder - self.mvc_folder.updateFolderView() - if mda_files_path == []: - # If there are no MDA files, pass None to display empty table - self.mvc_folder.updateFolderView() - self.setStatus("No MDA files found in the selected folder.") - - def cleanFolderList(self, folder_list=None): - """Check the list of recent folder and remove duplicate""" + self.reset_mainwindow() + self.setStatus(f"\n{str(folder_path)!r} - Path does not exist.") + + def onRefresh(self): + """ + Refreshes the file list in the currently selected folder + - Re-fetch the list of MDA files in the current folder. + - Display the updated file list in the MDA folder table view. + """ + # TODO: could be more efficient (i.e. ignore mda files already loaded) + self.setStatus("Refreshing folder...") + current_folder = self.dataPath() + if current_folder: + current_mdaFileList = self.mdaFileList() + self.onFolderSelected(current_folder) + new_mdaFileList = self.mdaFileList() + if new_mdaFileList: + difference = [ + item for item in new_mdaFileList if item not in current_mdaFileList + ] + if difference: + self.setStatus(f"Loading new files: {difference}") + else: + self.setStatus("No new files.") + else: + self.setStatus("Nothing to update.") + + def _buildFolderList(self, folder_list=None): + """Build the list of recent folders and remove duplicates from the folder list. + + - If folder_list arg is not None (after a doOpen call), it just removes duplicates. + - If folder_list arg is None, it grabs the list of recent folder from the app settings. + The directory loaded at start-up will be added at index 0. + + Args: + folder_list (list, optional): a list folders. Defaults to None. + + Returns: + list: list of folders to be populated in the QComboBox + """ unique_paths = set() - new_path_list = [] - candidate_paths = [self.directory, "Other..."] + candidate_paths = [self.directory] if not folder_list: - recent_dirs_str = settings.getKey(DIR_SETTINGS_KEY) - recent_dirs = recent_dirs_str.split(",") if recent_dirs_str else [] + recent_dirs = self._getRecentFolders() if recent_dirs: candidate_paths[1:1] = recent_dirs else: candidate_paths = folder_list - for p in candidate_paths: - if p not in unique_paths: - unique_paths.add(p) - new_path_list.append(p) + new_path_list = [ + p + for p in candidate_paths + if p not in unique_paths and (unique_paths.add(p) or True) + ] return new_path_list - def setFolderList(self, folder_list): - """Sets the folder list, updating the internal folder list & populate the QComboBox""" - folder_list = self.cleanFolderList(folder_list) - self.folder.clear() - self.folder.addItems(folder_list) - self._folderList = folder_list + def _getRecentFolders(self): + recent_dirs = ( + settings.getKey(DIR_SETTINGS_KEY).split(",") + if settings.getKey(DIR_SETTINGS_KEY) + else [] + ) + return recent_dirs + + def _addToRecentFolders(self, folder_path): + """Add a new folder path to the list of recent folders in the app settings. - def updateRecentFolders(self, folder_name): - recent_dirs_str = settings.getKey(DIR_SETTINGS_KEY) - recent_dirs = recent_dirs_str.split(",") if recent_dirs_str else [] - if folder_name in recent_dirs: - recent_dirs.remove(folder_name) - recent_dirs.insert(0, str(folder_name)) - recent_dirs = recent_dirs[:MAX_RECENT_DIRS] + Args: + folder_path (str): The path of the folder to be added. + """ + recent_dirs = self._getRecentFolders() + if folder_path in recent_dirs: + recent_dirs.remove(folder_path) + recent_dirs.insert(0, str(folder_path)) recent_dirs = [dir for dir in recent_dirs if dir != "."] - settings.setKey(DIR_SETTINGS_KEY, ",".join(recent_dirs)) + settings.setKey(DIR_SETTINGS_KEY, ",".join(recent_dirs[:MAX_RECENT_DIRS])) + + def _fillFolderBox(self, folder_list=[]): + """Fill the Folder ComboBox; Open... and Clear Recently Open... are added at the end by default. + + Args: + folder_list (list, optional): The list of folders to be displayed in the ComboBox. Defaults to []. + """ + self.folder.clear() + self.folder.addItems(folder_list) + self.folder.addItems(["Open...", "Clear Recently Open..."]) + count = self.folder.count() + self.folder.insertSeparator(count - 1) + self.folder.insertSeparator(count - 2) diff --git a/mdaviz/mda_file.py b/mdaviz/mda_file.py index d885f120..cb8349d2 100644 --- a/mdaviz/mda_file.py +++ b/mdaviz/mda_file.py @@ -5,12 +5,12 @@ ~MDAFile ~tabManager - + User: tabCloseRequested.connect (emit: index) --> onTabCloseRequested(index --> file_path) - --> tabManager.removeTab(file_path) - --> tabManager.tabRemoved.emit(file_path) - --> onTabRemoved(file_path --> index) + --> tabManager.removeTab(file_path) + --> tabManager.tabRemoved.emit(file_path) + --> onTabRemoved(file_path --> index) User: clearButton.clicked (emit: no data) --> onClearAllTabsRequested() @@ -20,7 +20,7 @@ --> removeAllFileTabs() - + """ from .synApps_mdalib.mda import readMDA @@ -30,7 +30,6 @@ import yaml from . import utils -from .chartview import ChartView from .mda_file_table_view import MDAFileTableView @@ -61,12 +60,8 @@ def setup(self): self.setData() self.tabManager = TabManager() # Instantiate TabManager self.currentHighlightedRow = None # To store the current highlighted row - self.currentHighlightedFilePath = ( - None # To store the current highlighted row's file path - ) - self.currentHighlightedModel = ( - None # To store the current highlighted's row model - ) + self.currentHighlightedFilePath = None # To store the current highlighted row's file path + self.currentHighlightedModel = None # To store the current highlighted's row model # Buttons handling: self.addButton.hide() @@ -166,9 +161,7 @@ def setData(self, index=None): file_path = self.dataPath() / file_name file_metadata, file_data_dim1, *_ = readMDA(file_path) if file_metadata["rank"] > 1: - self.setStatus( - "WARNING: Multidimensional data not supported - ignoring ranks > 1." - ) + self.setStatus("WARNING: Multidimensional data not supported - ignoring ranks > 1.") scanDict, first_pos, first_det = utils.get_scan(file_data_dim1) pvList = [v["name"] for v in scanDict.values()] self._data = { @@ -383,9 +376,7 @@ def updateCurrentTabInfo(self, new_tab_index): new_selection_field = new_tab_tableview.tableView.model().plotFields() else: new_selection_field = {} - self.tabChanged.emit( - new_tab_index, new_file_path, new_tab_data, new_selection_field - ) + self.tabChanged.emit(new_tab_index, new_file_path, new_tab_data, new_selection_field) def highlightRowInTab(self, file_path, row): """ @@ -401,10 +392,7 @@ def highlightRowInTab(self, file_path, row): model = tableview.tableView.model() if model is not None: # Unhighlight the previous row if it exists - if ( - self.currentHighlightedRow is not None - and self.currentHighlightedModel is not None - ): + if self.currentHighlightedRow is not None and self.currentHighlightedModel is not None: self.currentHighlightedModel.unhighlightRow(self.currentHighlightedRow) # Highlight the new row diff --git a/mdaviz/mda_file_table_model.py b/mdaviz/mda_file_table_model.py index 8a1befc4..6f8e34d4 100644 --- a/mdaviz/mda_file_table_model.py +++ b/mdaviz/mda_file_table_model.py @@ -222,9 +222,7 @@ def setCheckbox(self, index, state): self.checkboxStateChanged.emit(self.plotFields(), det_removed) def checkCheckBox(self, row, column_name): - self.selections[row] = ( - column_name # Mark the checkbox as checked by updating 'selections' - ) + self.selections[row] = column_name # Mark the checkbox as checked by updating 'selections' col = self.columnNumber(column_name) # Translate column name to its index index = self.index(row, col) self.dataChanged.emit(index, index, [QtCore.Qt.CheckStateRole]) # Update view @@ -251,9 +249,7 @@ def clearAllCheckboxes(self): self.selections.clear() topLeftIndex = self.index(0, 0) bottomRightIndex = self.index(self.rowCount() - 1, self.columnCount() - 1) - self.dataChanged.emit( - topLeftIndex, bottomRightIndex, [QtCore.Qt.CheckStateRole] - ) + self.dataChanged.emit(topLeftIndex, bottomRightIndex, [QtCore.Qt.CheckStateRole]) # Update the mda_mvc selection self.mda_mvc.setSelectionField() @@ -269,9 +265,7 @@ def applySelectionRules(self, index, changes=False): changes = True return changes - def updateCheckboxes( - self, new_selection=None, old_selection=None, update_mda_mvc=True - ): + def updateCheckboxes(self, new_selection=None, old_selection=None, update_mda_mvc=True): """Update checkboxes to agree with self.selections.""" if new_selection is None: new_selection = self.selections diff --git a/mdaviz/mda_file_table_view.py b/mdaviz/mda_file_table_view.py index 4632dbc7..0c924db4 100644 --- a/mdaviz/mda_file_table_view.py +++ b/mdaviz/mda_file_table_view.py @@ -8,7 +8,6 @@ ~MDAFileTableView """ -from PyQt5 import QtCore from PyQt5 import QtWidgets @@ -104,9 +103,7 @@ def displayTable(self, selection_field): if self.data() is not None: fields = self.data()["fields"] - data_model = MDAFileTableModel( - COLUMNS, fields, selection_field, self.mda_file.mda_mvc - ) + data_model = MDAFileTableModel(COLUMNS, fields, selection_field, self.mda_file.mda_mvc) self.tableView.setModel(data_model) # Hide Field/Mon/Norm columns (Field = vertical header, Mon & Norm not yet implemented) for i in [0, 3, 4]: @@ -146,7 +143,7 @@ def data2Plot(self, selections): scanDict = self.data()["fileInfo"]["scanDict"] # ------ extract x data: x_index = selections.get("X") - x_data = scanDict[x_index].get("data") if x_index in scanDict else None + x_data = scanDict[x_index].get("data") if x_index in scanDict else None # ------ extract y(s) data: y_index = selections.get("Y", []) y_first_unit = y_first_name = "" @@ -169,9 +166,7 @@ def data2Plot(self, selections): # scanDict = {index: {'object': scanObject, 'data': [...], 'unit': '...', 'name': '...','type':...}} plot_options = { "x": scanDict[x_index].get("name", "") if x_index in scanDict else "", - "x_unit": ( - scanDict[x_index].get("unit", "") if x_index in scanDict else "" - ), + "x_unit": (scanDict[x_index].get("unit", "") if x_index in scanDict else ""), "y": y_first_name, "y_unit": y_first_unit, "title": "", diff --git a/mdaviz/mda_file_viz.py b/mdaviz/mda_file_viz.py index ea44b73f..4da91eb1 100644 --- a/mdaviz/mda_file_viz.py +++ b/mdaviz/mda_file_viz.py @@ -5,7 +5,7 @@ from .chartview import ChartView from .data_table_view import DataTableView -MD_FONT = "Monospace" +MD_FONT = "Arial" MD_FONT_SIZE = 12 diff --git a/mdaviz/mda_folder.py b/mdaviz/mda_folder.py index a3df6d05..9701773b 100644 --- a/mdaviz/mda_folder.py +++ b/mdaviz/mda_folder.py @@ -6,46 +6,46 @@ .. Summary:: ~MDA_MVC - + General initialization and setup methods: - __init__: Initializes the MDA_MVC instance, linking it with the main application window. - - setup: Sets up folder and file table views, data visualization components, and establishes + - setup: Sets up folder and file table views, data visualization components, and establishes signal-slot connections. - + Data Access and Management: - dataPath: Provides the path to the folder containing MDA files. - mdaFileList: Fetches names of MDA files in the selected folder. - + User interaction handling methods: - doRefresh: Refreshes the view to display updated MDA files from the selected folder. - onFileSelected: Handles user selection of MDA files, updating UI and initiating data plotting. - doPlot: Initiates data plotting based on user selections and current plot mode. It checks for the selected positioner and detectors, retrieves the corresponding data, and plots it in the visualization panel. - - onCheckboxStateChanged: Responds to user changes in checkbox states within the MDA file table, + - onCheckboxStateChanged: Responds to user changes in checkbox states within the MDA file table, triggering a re-plot of selected data. - handlePlotBasedOnMode: Determines plot updates based on the user-selected mode, e.g. Auto-add or Auto-replace. - - + + Navigation and UI state management: - goToFirst, goToLast, goToNext, goToPrevious: Methods for navigating through the list of MDA files. - selectAndShowIndex: Selects and highlights a file in the folder view based on index. - - selectionModel, setSelectionModel: Get and set methods for the current selection model, managing + - selectionModel, setSelectionModel: Get and set methods for the current selection model, managing item selections within the view. - setCurrentFileTableview, currentFileTableview: Manages the table view for the currently active file. - + Selection Configuration: - - updateDetectorSelection: Maps user-selected detectors across file changes to ensure consistency + - updateDetectorSelection: Maps user-selected detectors across file changes to ensure consistency despite changes in detector ordering or availability - updateSelectionForNewPVs: Updates to both detector and positioner selections for a new file. - applySelectionChanges: Updates the UI with new selections after a file change. - setSelectionField, selectionField: Sets and retrieves field selections for plotting. - + Splitter position management: - - setSplitterSettingsName, setSplitterMoved, setSplitterWaitChanges: Methods for managing user-adjusted + - setSplitterSettingsName, setSplitterMoved, setSplitterWaitChanges: Methods for managing user-adjusted splitter positions and saving these settings for future sessions. - + Flow Chart: - + Refresh Button Press |___> doRefresh |___> mda_folder_tableview.displayTable() (to reload folder content) @@ -55,11 +55,11 @@ |___> Update UI (Tabs, Metadata, Data Display) |___> doPlot (Based on current mode and selections) |___> Retrieve and plot data based on selected positioner and detectors - + Checkbox State Change in File View |___> onCheckboxStateChanged |___> doPlot (Replot based on new selections) - |___> Retrieve and plot data based on selected positioner and detectors + |___> Retrieve and plot data based on selected positioner and detectors """ import time @@ -111,7 +111,6 @@ def setup(self): layout = self.folder_groupbox.layout() layout.addWidget(self.mda_folder_tableview) self.mda_folder_tableview.displayTable() - utils.reconnect(self.mainWindow.refresh.released, self.doRefresh) # File table view: self.mda_file = MDAFile(self) @@ -194,6 +193,15 @@ def mdaFileList(self): """ return self.mainWindow.mdaFileList() + def mdaInfoList(self): + """ + Fetches a list of MDA file info from the currently selected folder. + + Returns: + list: A list of dictionary containing the high level info for the MDA files in the selected folder. + """ + return self.mainWindow.mdaInfoList() + def currentFileTableview(self): """ Gets the current file TableView being displayed in the active tab. @@ -247,29 +255,6 @@ def updateFolderView(self): self.setSelectionModel(selection_model) utils.reconnect(self.selectionModel().currentChanged, self.onFileSelected) - def doRefresh(self): - """ - Refreshes the file list in the currently selected folder - - Re-fetch the list of MDA files in the current folder. - - Display the updated file list in the MDA folder table view. - """ - self.setStatus("Refreshing folder...") - current_folder = self.dataPath() - current_mdaFileList = self.mdaFileList() - self.mainWindow.setMdaFileList(current_folder) - new_mdaFileList = self.mdaFileList() - if new_mdaFileList: - self.mda_folder_tableview.displayTable() - difference = [ - item for item in new_mdaFileList if item not in current_mdaFileList - ] - if difference: - self.setStatus(f"Loading new files: {difference}") - else: - self.setStatus("No new files.") - else: - self.setStatus("Nothing to update.") - # # ------------ Fields selection methods: def selectionField(self): @@ -350,7 +335,7 @@ def updateSelectionForNewPVs( print(f"----- Selection After clean up: {self.selectionField()}\n") def updateDetectorSelection( - self, oldPvList, old_selection, newPvList, new_selection, verbose + self, oldPvList, old_selection, newPvList, new_selection, verbose=False ): """ Helper function to update detector selections in the new selection field. @@ -379,7 +364,7 @@ def updateDetectorSelection( # # ------------ File selection methods: - def onFileSelected(self, index, verbose=True): + def onFileSelected(self, index, verbose=False): """ - Handles the selection of a new file in the folder table view. - Updates the UI to: @@ -407,7 +392,7 @@ def onFileSelected(self, index, verbose=True): """ selected_file = self.mdaFileList()[index.row()] - self.setStatus(f"\n\n========= {selected_file} in {str(self.dataPath())}") + self.setStatus(f"\nLoading {str(self.dataPath())}/{selected_file}") # Ensures the table view scrolls to the selected item. if isinstance(index, QtCore.QModelIndex): diff --git a/mdaviz/mda_folder_table_model.py b/mdaviz/mda_folder_table_model.py index be509ab8..1bf66420 100644 --- a/mdaviz/mda_folder_table_model.py +++ b/mdaviz/mda_folder_table_model.py @@ -6,16 +6,11 @@ ~MDAFolderTableModel """ -from .synApps_mdalib.mda import readMDA -import re from PyQt5 import QtCore -from . import utils - -HEADERS = "Prefix", "Scan #", "Points", "Dim", "Positioner", "Date", "Size" +from .utils import HEADERS class MDAFolderTableModel(QtCore.QAbstractTableModel): - def __init__(self, data, parent): """ Create the model and connect with its parent. @@ -30,13 +25,13 @@ def __init__(self, data, parent): super().__init__() self.columnLabels = HEADERS - self.setFileList(data) + self.setFileInfoList(data) # ------------ methods required by Qt's view def rowCount(self, parent=None): # Want it to return the number of rows to be shown at a given time - value = len(self.fileList()) + value = len(self.fileInfoList()) return value def columnCount(self, parent=None): @@ -47,9 +42,8 @@ def columnCount(self, parent=None): def data(self, index, role=None): # display data if role == QtCore.Qt.DisplayRole: - file = self.fileList()[index.row()] label = self.columnLabels[index.column()] - file_info = self.get_file_info(file) + file_info = self.fileInfoList()[index.row()] value = file_info[label] return value @@ -60,59 +54,16 @@ def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole): else: return str(section + 1) # may want to alter at some point - # ------------ local methods - - def get_file_info(self, file): - - def extract_prefix(filename, scan_number): - """Create a pattern that matches the prefix followed by an optional separator and the scan number with possible leading zeros - The separators considered here are underscore (_), hyphen (-), dot (.), and space ( ) - """ - scan_number = str(scan_number) - pattern = rf"^(.*?)[_\-\. ]?0*{scan_number}\.mda$" - match = re.match(pattern, filename) - if match: - return match.group(1) - return None - - file_path = self.mda_mvc.dataPath() / file - file_data = readMDA(str(file_path))[1] - file_metadata = readMDA(str(file_path))[0] - file_num = file_metadata.get("scan_number", None) - file_prefix = extract_prefix(file, file_num) - file_size = utils.human_readable_size(file_path.stat().st_size) - file_date = utils.byte2str(file_data.time).split(".")[0] - file_pts = file_data.curr_pt - file_dim = file_data.dim - pv = utils.byte2str(file_data.p[0].name) if len(file_data.p) else "index" - desc = utils.byte2str(file_data.p[0].desc) if len(file_data.p) else "index" - file_pos = desc if desc else pv - - fileInfo = {} - # HEADERS = "Prefix", "Scan #", "Points", "Dim", "Positioner", "Date", "Size" - values = [ - file_prefix, - file_num, - file_pts, - file_dim, - file_pos, - file_date, - file_size, - ] - for k, v in zip(HEADERS, values): - fileInfo[k] = v - return fileInfo - # # ------------ get & set methods - def fileList(self): - """Here fileList = data arg = self.mainWindow.mdaFileList() - ie the list of mda file NAME str (name only) + def fileInfoList(self): + """Here fileList = data arg = self.mainWindow.mdaInfoList() + ie the list of mda files info """ return self._data - def setFileList(self, data): - """Here data arg = self.mainWindow.mdaFileList() - ie the list of mda file NAME str (name only) + def setFileInfoList(self, data): + """Here data arg = self.mainWindow.mdaInfoList() + ie the list of mda files info """ self._data = data diff --git a/mdaviz/mda_folder_table_view.py b/mdaviz/mda_folder_table_view.py index 7105a0f9..9aa3f41f 100644 --- a/mdaviz/mda_folder_table_view.py +++ b/mdaviz/mda_folder_table_view.py @@ -42,7 +42,10 @@ def displayTable(self): from .mda_folder_table_model import MDAFolderTableModel from .empty_table_model import EmptyTableModel - data = self.mdaFileList() + data = self.mdaInfoList() + if data: + print(f"==== {data[0]=}") + if len(data) > 0: data_model = MDAFolderTableModel(data, self.mda_mvc) self.tableView.setModel(data_model) @@ -62,9 +65,9 @@ def centerColumn(label): empty_model = EmptyTableModel(HEADERS) self.tableView.setModel(empty_model) - def mdaFileList(self): + def mdaInfoList(self): """List of mda file (name only) in the selected folder.""" - return self.mda_mvc.mdaFileList() + return self.mda_mvc.mdaInfoList() def setStatus(self, text): self.mda_mvc.setStatus(text) diff --git a/mdaviz/opendialog.py b/mdaviz/opendialog.py index 3aa6c015..2c0037ce 100644 --- a/mdaviz/opendialog.py +++ b/mdaviz/opendialog.py @@ -1,7 +1,6 @@ from PyQt5.QtWidgets import QFileDialog from pathlib import Path -from . import utils from .user_settings import settings DIR_SETTINGS_KEY = "directory" diff --git a/mdaviz/resources/first.svg b/mdaviz/resources/first.svg index fd956f6c..3b923e8a 100644 --- a/mdaviz/resources/first.svg +++ b/mdaviz/resources/first.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/mdaviz/resources/last.svg b/mdaviz/resources/last.svg index 8a683d23..b05cf076 100644 --- a/mdaviz/resources/last.svg +++ b/mdaviz/resources/last.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/mdaviz/resources/mainwindow.ui b/mdaviz/resources/mainwindow.ui index 6f030349..b0ad5f3a 100644 --- a/mdaviz/resources/mainwindow.ui +++ b/mdaviz/resources/mainwindow.ui @@ -31,7 +31,7 @@ 1 - + 6 @@ -81,7 +81,7 @@ - 10 + 15 20 @@ -93,7 +93,7 @@ Selected parent folder - Full Path: + Folder Path: @@ -111,7 +111,7 @@ - 5 + 15 20 @@ -147,36 +147,6 @@ - - - - Select folder or subfolder from full path - - - Subfolder(s): - - - - - - - Select folder or subfolder from full path - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - diff --git a/mdaviz/resources/open.svg b/mdaviz/resources/open.svg index 30701373..1128524f 100644 --- a/mdaviz/resources/open.svg +++ b/mdaviz/resources/open.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/mdaviz/synApps_mdalib/LICENSE b/mdaviz/synApps_mdalib/LICENSE index a815d86a..9738f6c3 100644 --- a/mdaviz/synApps_mdalib/LICENSE +++ b/mdaviz/synApps_mdalib/LICENSE @@ -9,27 +9,27 @@ BCDA, Advanced Photon Source, Argonne National Laboratory OPEN SOURCE LICENSE -Redistribution and use in source and binary forms, with or without +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: -1. Redistributions of source code must retain the above copyright notice, - this list of conditions and the following disclaimer. Software changes, - modifications, or derivative works, should be noted with comments and +1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. Software changes, + modifications, or derivative works, should be noted with comments and the author and organization's name. 2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation + this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. -3. Neither the names of UChicago Argonne, LLC or the Department of Energy - nor the names of its contributors may be used to endorse or promote - products derived from this software without specific prior written +3. Neither the names of UChicago Argonne, LLC or the Department of Energy + nor the names of its contributors may be used to endorse or promote + products derived from this software without specific prior written permission. -4. The software and the end-user documentation included with the +4. The software and the end-user documentation included with the redistribution, if any, must include the following acknowledgment: - "This product includes software produced by UChicago Argonne, LLC + "This product includes software produced by UChicago Argonne, LLC under Contract No. DE-AC02-06CH11357 with the Department of Energy." **************************************************************************** @@ -38,11 +38,11 @@ DISCLAIMER THE SOFTWARE IS SUPPLIED "AS IS" WITHOUT WARRANTY OF ANY KIND. -Neither the United States GOVERNMENT, nor the United States Department -of Energy, NOR uchicago argonne, LLC, nor any of their employees, makes -any warranty, express or implied, or assumes any legal liability or -responsibility for the accuracy, completeness, or usefulness of any -information, data, apparatus, product, or process disclosed, or +Neither the United States GOVERNMENT, nor the United States Department +of Energy, NOR uchicago argonne, LLC, nor any of their employees, makes +any warranty, express or implied, or assumes any legal liability or +responsibility for the accuracy, completeness, or usefulness of any +information, data, apparatus, product, or process disclosed, or represents that its use would not infringe privately owned rights. **************************************************************************** diff --git a/mdaviz/synApps_mdalib/METADATA b/mdaviz/synApps_mdalib/METADATA index 8c8f7082..1c973f4b 100644 --- a/mdaviz/synApps_mdalib/METADATA +++ b/mdaviz/synApps_mdalib/METADATA @@ -10,13 +10,10 @@ Platform: UNKNOWN Python support library for EPICS synApps MDA-format data files - + Install the Python mda support library: - + cd synApps/support/utils/mdaPythonUtils python ./setup.py install # -or- pip install . - - - diff --git a/mdaviz/synApps_mdalib/f_xdrlib.py b/mdaviz/synApps_mdalib/f_xdrlib.py index 2f6e460a..b3a3f42d 100644 --- a/mdaviz/synApps_mdalib/f_xdrlib.py +++ b/mdaviz/synApps_mdalib/f_xdrlib.py @@ -254,7 +254,7 @@ def unpack_farray_float(self, n): i = self.__pos self.__pos = j = i+4*n return self.unpackFloatDict[n](self.__buf[i:j]) - + def unpack_farray_double(self, n): if not self.unpackDoubleDict.has_key(n): fmt = ">" + "d"*n diff --git a/mdaviz/synApps_mdalib/mda.py b/mdaviz/synApps_mdalib/mda.py index 018417ce..7fab66ba 100644 --- a/mdaviz/synApps_mdalib/mda.py +++ b/mdaviz/synApps_mdalib/mda.py @@ -199,7 +199,7 @@ def oldDetName(i): else: return "?" -# Given a positioner number, , return the name of the associated sscanRecord PV, "P1'-'P4' +# Given a positioner number, , return the name of the associated sscanRecord PV, "P1'-'P4' def posName(i): if i < 4: return "P%d" % (i+1) @@ -233,7 +233,7 @@ def readScan(scanFile, verbose=0, out=sys.stdout, unpacker=None): out.write("\nreadScan('0x%x'): entry\n" % (unpacker.get_position())) else: out.write("\nreadScan('%s'): entry\n" % (scanFile.name)) - + scan = scanDim() # data structure to hold scan info and data buf = scanFile.read(100000) # enough to read scan header and info @@ -349,7 +349,7 @@ def readScan(scanFile, verbose=0, out=sys.stdout, unpacker=None): scan.p[j].data = data[start:end] start = end end += scan.npts - + # detectors if have_fast_xdr: data = u.unpack_farray_float(scan.npts*scan.nd) @@ -801,7 +801,7 @@ def readMDA(fname=None, maxdim=4, verbose=0, showHelp=0, outFile=None, useNumpy= value = '' count = 0 if EPICS_type != 0: # not DBR_STRING; array is permitted - count = u.unpack_int() # + count = u.unpack_int() # if verbose: out.write("\tcount = %d\n" % count) n = u.unpack_int() # length of unit string if n: unit = u.unpack_string() @@ -1086,14 +1086,14 @@ def packScanData(scan, cpt): p = xdr.Packer() if (len(cpt) == 0): # 1D array for i in range(scan.np): - p.pack_farray(scan.npts, scan.p[i].data, p.pack_double) + p.pack_farray(scan.npts, scan.p[i].data, p.pack_double) for i in range(scan.nd): p.pack_farray(scan.npts, scan.d[i].data, p.pack_float) - + elif (len(cpt) == 1): # 2D array j = cpt[0] for i in range(scan.np): - p.pack_farray(scan.npts, scan.p[i].data[j], p.pack_double) + p.pack_farray(scan.npts, scan.p[i].data[j], p.pack_double) for i in range(scan.nd): p.pack_farray(scan.npts, scan.d[i].data[j], p.pack_float) @@ -1101,7 +1101,7 @@ def packScanData(scan, cpt): j = cpt[0] k = cpt[1] for i in range(scan.np): - p.pack_farray(scan.npts, scan.p[i].data[j][k], p.pack_double) + p.pack_farray(scan.npts, scan.p[i].data[j][k], p.pack_double) for i in range(scan.nd): p.pack_farray(scan.npts, scan.d[i].data[j][k], p.pack_float) @@ -1544,7 +1544,7 @@ def opMDA_scalar(op, d1, scalar): for k in range(s[2].npts): for l in range(s[3].npts): s[3].d[i].data[j][k][l] = op(s[3].d[i].data[j][k][l], scalar) - + if (len(s) == 4): return s # 4D op for i in range(s[4].nd): @@ -1620,7 +1620,7 @@ def opMDA(op, d1, d2): for j in range(s[1].npts): for k in range(s[2].npts): s[3].d[i].data[j][k] = map(op, s[3].d[i].data[j][k], d2[3].d[i].data[j][k]) - + if (len(s) == 4): return s # 3D op if s[4].nd != d2[4].nd: @@ -1669,4 +1669,3 @@ def main(): if __name__ == "__main__": main() - diff --git a/mdaviz/user_settings.py b/mdaviz/user_settings.py index 907b62bd..3755a57d 100644 --- a/mdaviz/user_settings.py +++ b/mdaviz/user_settings.py @@ -198,17 +198,9 @@ def restoreWindowGeometry(self, window, label): # find the "available" screen dimensions # (excludes docks, menu bars, ...) available_rect = qdw.availableGeometry(screen_num) - if ( - available_rect.x() - <= int(x) - < available_rect.x() + available_rect.width() - ): + if available_rect.x() <= int(x) < available_rect.x() + available_rect.width(): x_onscreen = True - if ( - available_rect.y() - <= int(y) - < available_rect.y() + available_rect.height() - ): + if available_rect.y() <= int(y) < available_rect.y() + available_rect.height(): y_onscreen = True # Move the window to the primary window if it would otherwise be drawn off screen diff --git a/mdaviz/utils.py b/mdaviz/utils.py index c42d75aa..145ea952 100644 --- a/mdaviz/utils.py +++ b/mdaviz/utils.py @@ -16,8 +16,11 @@ import datetime import pathlib +import re import threading -from .synApps_mdalib.mda import scanPositioner +from .synApps_mdalib.mda import readMDA, scanPositioner + +HEADERS = "Prefix", "Scan #", "Points", "Dim", "Positioner", "Date", "Size" def human_readable_size(size, decimal_places=2): @@ -63,11 +66,48 @@ def byte2str(byte_literal): Returns: - str | Any: The decoded string if the input is a byte literal, otherwise the original input. """ - return ( - byte_literal.decode("utf-8") - if isinstance(byte_literal, bytes) - else byte_literal - ) + return byte_literal.decode("utf-8") if isinstance(byte_literal, bytes) else byte_literal + + +def get_file_info(file_path): + file_name = file_path.name + file_data = readMDA(str(file_path))[1] + file_metadata = readMDA(str(file_path))[0] + file_num = file_metadata.get("scan_number", None) + file_prefix = extract_file_prefix(file_name, file_num) + file_size = human_readable_size(file_path.stat().st_size) + file_date = byte2str(file_data.time).split(".")[0] + file_pts = file_data.curr_pt + file_dim = file_data.dim + pv = byte2str(file_data.p[0].name) if len(file_data.p) else "index" + desc = byte2str(file_data.p[0].desc) if len(file_data.p) else "index" + file_pos = desc if desc else pv + + fileInfo = {"Name": file_name} + values = [ + file_prefix, + file_num, + file_pts, + file_dim, + file_pos, + file_date, + file_size, + ] + for k, v in zip(HEADERS, values): + fileInfo[k] = v + return fileInfo + + +def extract_file_prefix(file_name, scan_number): + """Create a pattern that matches the prefix followed by an optional separator and the scan number with possible leading zeros + The separators considered here are underscore (_), hyphen (-), dot (.), and space ( ) + """ + scan_number = str(scan_number) + pattern = rf"^(.*?)[_\-\. ]?0*{scan_number}\.mda$" + match = re.match(pattern, file_name) + if match: + return match.group(1) + return None def get_det(mda_file_data): @@ -109,7 +149,7 @@ def get_det(mda_file_data): # Defining a default scanPositioner Object for "Index" at for key=0: p0 = scanPositioner() p0.number = 0 # positioner number in sscan record - p0.fieldName = "p0" # name of sscanRecord PV + p0.fieldName = "P0" # name of sscanRecord PV p0.name = "Index" # name of EPICS PV this positioner wrote to p0.desc = "Index" # description of 'name' PV p0.step_mode = "" # 'LINEAR', 'TABLE', or 'FLY' @@ -337,7 +377,7 @@ def reconnect(signal, new_slot): - signal: The signal to disconnect and then reconnect. - new_slot: The new slot to connect to the signal. - Note: + Note: - this function catches TypeError which occurs if the signal was not connected to any slots. """ try: diff --git a/pyproject.toml b/pyproject.toml index 5cc98866..4ac89d1c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,10 +11,14 @@ description = "Python Qt5 application to visualize MDA data." authors = [ { name="Fanny Rodolakis", email="rodolakis@anl.gov" }, { name="Pete Jemian", email="prjemian@gmail.com" }, + { name="Rafael Vescovi", email="ravescovi@anl.gov" }, + { name="Eric Codrea", email="ecodrea@anl.gov" }, ] maintainers = [ { name="Fanny Rodolakis", email="rodolakis@anl.gov" }, { name="Pete Jemian", email="prjemian@gmail.com" }, + { name="Rafael Vescovi", email="ravescovi@anl.gov" }, + { name="Eric Codrea", email="ecodrea@anl.gov" }, ] dynamic = ["version"] readme = "README.md" @@ -157,6 +161,7 @@ exclude = [ "node_modules", "site-packages", "venv", + "mdaviz/synApps_mdalib", ] # Same as Black.