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
+ 1520
@@ -93,7 +93,7 @@
Selected parent folder
- Full Path:
+ Folder Path:
@@ -111,7 +111,7 @@
- 5
+ 1520
@@ -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.