diff --git a/ptychodus/__main__.py b/ptychodus/__main__.py index 8d2ea127..0c0f0266 100644 --- a/ptychodus/__main__.py +++ b/ptychodus/__main__.py @@ -4,7 +4,7 @@ import argparse import sys -from ptychodus.model import ModelArgs, ModelCore +from ptychodus.model import ModelCore import ptychodus @@ -17,17 +17,6 @@ def verifyAllArgumentsParsed(parser: argparse.ArgumentParser, argv: list[str]) - parser.error('unrecognized arguments: %s' % ' '.join(argv)) -class DirectoryType: - - def __call__(self, string: str) -> Path: - path = Path(string) - - if not path.is_dir(): - raise argparse.ArgumentTypeError(f'\"{string}\" is not a directory!') - - return path - - def main() -> int: parser = argparse.ArgumentParser( prog=ptychodus.__name__.lower(), @@ -46,25 +35,18 @@ def main() -> int: help=argparse.SUPPRESS, ) parser.add_argument( - '-f', - '--file-prefix', - help='replace file path prefix in settings', - ) - parser.add_argument( - # input data product file (batch mode) '-i', '--input', metavar='INPUT_FILE', type=argparse.FileType('r'), - help=argparse.SUPPRESS, + help='input file (batch mode)', ) parser.add_argument( - # output data product file (batch mode) '-o', '--output', metavar='OUTPUT_FILE', type=argparse.FileType('w'), - help=argparse.SUPPRESS, + help='output file (batch mode)', ) parser.add_argument( # preprocessed diffraction patterns file (batch mode) @@ -87,25 +69,14 @@ def main() -> int: action='version', version=versionString(), ) - parser.add_argument( - '-w', - '--write', - metavar='OUTPUT_DIR', - type=DirectoryType(), - help='stage reconstruction inputs to directory', - ) parsedArgs, unparsedArgs = parser.parse_known_args() + settingsFile = Path(parsedArgs.settings.name) if parsedArgs.settings else None - modelArgs = ModelArgs( - settingsFile=Path(parsedArgs.settings.name) if parsedArgs.settings else None, - patternsFile=Path(parsedArgs.patterns.name) if parsedArgs.patterns else None, - replacementPathPrefix=parsedArgs.file_prefix, - ) - - with ModelCore(modelArgs, isDeveloperModeEnabled=parsedArgs.dev) as model: - if parsedArgs.write is not None: - return model.stageReconstructionInputs(parsedArgs.write) + with ModelCore(settingsFile, isDeveloperModeEnabled=parsedArgs.dev) as model: + if parsedArgs.patterns is not None: + patternsFilePath = Path(parsedArgs.patterns.name) + model.workflowAPI.importProcessedPatterns(patternsFilePath) if parsedArgs.batch is not None: verifyAllArgumentsParsed(parser, unparsedArgs) @@ -114,16 +85,10 @@ def main() -> int: parser.error('Batch mode requires input and output arguments!') return -1 - inputPath = Path(parsedArgs.input.name) - outputPath = Path(parsedArgs.output.name) - - if parsedArgs.batch == 'reconstruct': - return model.batchModeReconstruct(inputPath, outputPath) - elif parsedArgs.batch == 'train': - return model.batchModeTrain(inputPath, outputPath) - else: - parser.error(f'Unknown batch mode action \"{parsedArgs.batch}\"!') - return -1 + action = parsedArgs.batch + inputFilePath = Path(parsedArgs.input.name) + outputFilePath = Path(parsedArgs.output.name) + return model.batchModeExecute(action, inputFilePath, outputFilePath) try: from PyQt5.QtWidgets import QApplication diff --git a/ptychodus/api/automation.py b/ptychodus/api/automation.py deleted file mode 100644 index eec3d138..00000000 --- a/ptychodus/api/automation.py +++ /dev/null @@ -1,65 +0,0 @@ -from abc import ABC, abstractmethod -from collections.abc import Mapping -from pathlib import Path -from typing import Any - - -class WorkflowProductAPI(ABC): - - @abstractmethod - def openScan(self, filePath: Path, fileType: str) -> None: - pass - - @abstractmethod - def buildScan(self, builderName: str, builderParameters: Mapping[str, Any] = {}) -> None: - pass - - @abstractmethod - def openProbe(self, filePath: Path, fileType: str) -> None: - pass - - @abstractmethod - def buildProbe(self, builderName: str, builderParameters: Mapping[str, Any] = {}) -> None: - pass - - @abstractmethod - def openObject(self, filePath: Path, fileType: str) -> None: - pass - - @abstractmethod - def buildObject(self, builderName: str, builderParameters: Mapping[str, Any] = {}) -> None: - pass - - @abstractmethod - def reconstruct(self) -> None: - pass - - @abstractmethod - def saveProduct(self, filePath: Path, fileType: str) -> None: - pass - - -class WorkflowAPI(ABC): - - @abstractmethod - def openPatterns(self, filePath: Path, fileType: str) -> None: - '''loads diffraction patterns from file''' - pass - - @abstractmethod - def createProduct(self, name: str) -> WorkflowProductAPI: - '''creates a new product''' - pass - - -class FileBasedWorkflow(ABC): - - @abstractmethod - def getFilePattern(self) -> str: - '''UNIX-style filename pattern. For rules see fnmatch from Python standard library.''' - pass - - @abstractmethod - def execute(self, api: WorkflowAPI, filePath: Path) -> None: - '''uses workflow API to execute the workflow''' - pass diff --git a/ptychodus/api/plugins.py b/ptychodus/api/plugins.py index cbdbdf34..d0c7805b 100644 --- a/ptychodus/api/plugins.py +++ b/ptychodus/api/plugins.py @@ -8,7 +8,6 @@ import pkgutil import re -from .automation import FileBasedWorkflow from .fluorescence import (DeconvolutionStrategy, FluorescenceFileReader, FluorescenceFileWriter, UpscalingStrategy) from .object import ObjectPhaseCenteringStrategy, ObjectFileReader, ObjectFileWriter @@ -17,6 +16,7 @@ from .probe import FresnelZonePlate, ProbeFileReader, ProbeFileWriter from .product import ProductFileReader, ProductFileWriter from .scan import ScanFileReader, ScanFileWriter +from .workflow import FileBasedWorkflow __all__ = [ 'PluginChooser', diff --git a/ptychodus/api/settings.py b/ptychodus/api/settings.py index b9cc7bda..b5b1213b 100644 --- a/ptychodus/api/settings.py +++ b/ptychodus/api/settings.py @@ -1,8 +1,9 @@ from __future__ import annotations from collections.abc import Iterator, Sequence +from dataclasses import dataclass from decimal import Decimal from pathlib import Path -from typing import Any, Callable, Final, Generic, TypeVar +from typing import Any, Callable, Generic, TypeVar from uuid import UUID import configparser import logging @@ -14,6 +15,12 @@ logger = logging.getLogger(__name__) +@dataclass(frozen=True) +class PathPrefixChange: + findPathPrefix: Path + replacementPathPrefix: Path + + class SettingsEntry(Generic[T], Observable): def __init__(self, name: str, defaultValue: T, stringConverter: Callable[[str], T]) -> None: @@ -115,11 +122,9 @@ def update(self, observable: Observable) -> None: class SettingsRegistry(Observable): - PREFIX_PLACEHOLDER_TEXT: Final[str] = 'PREFIX' - def __init__(self, replacementPathPrefix: str | None) -> None: + def __init__(self) -> None: super().__init__() - self._replacementPathPrefix = replacementPathPrefix self._groupList: list[SettingsGroup] = list() self._fileFilterList: list[str] = ['Initialization Files (*.ini)'] @@ -142,12 +147,6 @@ def __getitem__(self, index: int) -> SettingsGroup: def __len__(self) -> int: return len(self._groupList) - def getReplacementPathPrefix(self) -> str | None: - return self._replacementPathPrefix - - def setReplacementPathPrefix(self, replacementPathPrefix: str) -> None: - self._replacementPathPrefix = replacementPathPrefix - def getOpenFileFilterList(self) -> Sequence[str]: return self._fileFilterList @@ -166,13 +165,6 @@ def openSettings(self, filePath: Path) -> None: for settingsEntry in settingsGroup: if config.has_option(settingsGroup.name, settingsEntry.name): valueString = config.get(settingsGroup.name, settingsEntry.name) - - if self._replacementPathPrefix is not None \ - and isinstance(settingsEntry.value, Path): - if valueString.startswith(SettingsRegistry.PREFIX_PLACEHOLDER_TEXT): - valueString = self._replacementPathPrefix \ - + valueString[len(SettingsRegistry.PREFIX_PLACEHOLDER_TEXT):] - settingsEntry.setValueFromString(valueString) self.notifyObservers() @@ -183,7 +175,9 @@ def getSaveFileFilterList(self) -> Sequence[str]: def getSaveFileFilter(self) -> str: return self._fileFilterList[0] - def saveSettings(self, filePath: Path) -> None: + def saveSettings(self, + filePath: Path, + changePathPrefix: PathPrefixChange | None = None) -> None: config = configparser.ConfigParser(interpolation=None) setattr(config, 'optionxform', lambda option: option) @@ -193,10 +187,16 @@ def saveSettings(self, filePath: Path) -> None: for settingsEntry in settingsGroup: valueString = str(settingsEntry.value) - if self._replacementPathPrefix and isinstance(settingsEntry.value, Path): - if valueString.startswith(self._replacementPathPrefix): - valueString = SettingsRegistry.PREFIX_PLACEHOLDER_TEXT \ - + valueString[len(self._replacementPathPrefix):] + if changePathPrefix and isinstance(settingsEntry.value, Path): + try: + relativePath = settingsEntry.value.relative_to( + changePathPrefix.findPathPrefix) + except ValueError: + pass + else: + modifiedPath = changePathPrefix.replacementPathPrefix / relativePath + valueString = str(modifiedPath) + config.set(settingsGroup.name, settingsEntry.name, valueString) logger.debug(f'Writing settings to \"{filePath}\"') diff --git a/ptychodus/api/workflow.py b/ptychodus/api/workflow.py new file mode 100644 index 00000000..3d9f63f3 --- /dev/null +++ b/ptychodus/api/workflow.py @@ -0,0 +1,106 @@ +from abc import ABC, abstractmethod +from collections.abc import Mapping +from pathlib import Path +from typing import Any + +from ptychodus.api.geometry import ImageExtent +from ptychodus.api.patterns import CropCenter +from ptychodus.api.settings import PathPrefixChange + + +class WorkflowProductAPI(ABC): + + @abstractmethod + def openScan(self, filePath: Path, *, fileType: str | None = None) -> None: + pass + + @abstractmethod + def buildScan(self, builderName: str, builderParameters: Mapping[str, Any] = {}) -> None: + pass + + @abstractmethod + def openProbe(self, filePath: Path, *, fileType: str | None = None) -> None: + pass + + @abstractmethod + def buildProbe(self, builderName: str, builderParameters: Mapping[str, Any] = {}) -> None: + pass + + @abstractmethod + def openObject(self, filePath: Path, *, fileType: str | None = None) -> None: + pass + + @abstractmethod + def buildObject(self, builderName: str, builderParameters: Mapping[str, Any] = {}) -> None: + pass + + @abstractmethod + def reconstruct(self) -> None: + pass + + @abstractmethod + def saveProduct(self, filePath: Path, *, fileType: str | None = None) -> None: + pass + + +class WorkflowAPI(ABC): + + @abstractmethod + def openPatterns( + self, + filePath: Path, + *, + fileType: str | None = None, + cropCenter: CropCenter | None = None, + cropExtent: ImageExtent | None = None, + ) -> None: + '''opens diffraction patterns from file''' + pass + + @abstractmethod + def importProcessedPatterns(self, filePath: Path) -> None: + '''import processed patterns''' + pass + + @abstractmethod + def exportProcessedPatterns(self, filePath: Path) -> None: + '''export processed patterns''' + pass + + @abstractmethod + def openProduct(self, filePath: Path, *, fileType: str | None = None) -> WorkflowProductAPI: + '''opens product from file''' + pass + + @abstractmethod + def createProduct( + self, + name: str, + *, + comments: str = '', + detectorDistanceInMeters: float | None = None, + probeEnergyInElectronVolts: float | None = None, + probePhotonsPerSecond: float | None = None, + exposureTimeInSeconds: float | None = None, + ) -> WorkflowProductAPI: + '''creates a new product''' + pass + + @abstractmethod + def saveSettings(self, + filePath: Path, + changePathPrefix: PathPrefixChange | None = None) -> None: + pass + + +class FileBasedWorkflow(ABC): + + @abstractmethod + def getFilePattern(self) -> str: + '''UNIX-style filename pattern. For rules see fnmatch from Python standard library.''' + pass + + @abstractmethod + def execute(self, api: WorkflowAPI, filePath: Path) -> None: + '''uses workflow API to execute the workflow''' + pass diff --git a/ptychodus/controller/core.py b/ptychodus/controller/core.py index 2590c789..53b7f703 100644 --- a/ptychodus/controller/core.py +++ b/ptychodus/controller/core.py @@ -33,8 +33,8 @@ def __init__(self, model: ModelCore, view: ViewCore) -> None: model.ptychonnReconstructorLibrary, self._fileDialogFactory) self._tikeViewControllerFactory = TikeViewControllerFactory(model.tikeReconstructorLibrary) self._settingsController = SettingsController.createInstance(model.settingsRegistry, - view.settingsParametersView, - view.settingsEntryView, + view.settingsView, + view.settingsTableView, self._fileDialogFactory) self._patternsImageController = ImageController.createInstance( model.patternVisualizationEngine, view.patternsImageView, view.statusBar(), diff --git a/ptychodus/controller/object/core.py b/ptychodus/controller/object/core.py index 78e5de22..659a77ad 100644 --- a/ptychodus/controller/object/core.py +++ b/ptychodus/controller/object/core.py @@ -146,7 +146,7 @@ def _loadCurrentObjectFromFile(self) -> None: if filePath: try: - self._api.openObject(itemIndex, filePath, nameFilter) + self._api.openObject(itemIndex, filePath, fileType=nameFilter) except Exception as err: logger.exception(err) ExceptionDialog.showException('File Reader', err) diff --git a/ptychodus/controller/probe/core.py b/ptychodus/controller/probe/core.py index e44023b7..b6519182 100644 --- a/ptychodus/controller/probe/core.py +++ b/ptychodus/controller/probe/core.py @@ -111,7 +111,7 @@ def _loadCurrentProbeFromFile(self) -> None: if filePath: try: - self._api.openProbe(itemIndex, filePath, nameFilter) + self._api.openProbe(itemIndex, filePath, fileType=nameFilter) except Exception as err: logger.exception(err) ExceptionDialog.showException('File Reader', err) diff --git a/ptychodus/controller/product/core.py b/ptychodus/controller/product/core.py index 50e1fa1e..4fcd7bfa 100644 --- a/ptychodus/controller/product/core.py +++ b/ptychodus/controller/product/core.py @@ -201,7 +201,7 @@ def _openProduct(self) -> None: if filePath: try: - self._api.openProduct(filePath, nameFilter) + self._api.openProduct(filePath, fileType=nameFilter) except Exception as err: logger.exception(err) ExceptionDialog.showException('File Reader', err) @@ -221,7 +221,7 @@ def _saveCurrentProduct(self) -> None: if filePath: try: - self._api.saveProduct(current.row(), filePath, nameFilter) + self._api.saveProduct(current.row(), filePath, fileType=nameFilter) except Exception as err: logger.exception(err) ExceptionDialog.showException('File Writer', err) diff --git a/ptychodus/controller/scan/core.py b/ptychodus/controller/scan/core.py index 1f883aca..85d0955f 100644 --- a/ptychodus/controller/scan/core.py +++ b/ptychodus/controller/scan/core.py @@ -100,7 +100,7 @@ def _loadCurrentScanFromFile(self) -> None: if filePath: try: - self._api.openScan(itemIndex, filePath, nameFilter) + self._api.openScan(itemIndex, filePath, fileType=nameFilter) except Exception as err: logger.exception(err) ExceptionDialog.showException('File Reader', err) diff --git a/ptychodus/controller/settings.py b/ptychodus/controller/settings.py index 631bf2ed..8331be09 100644 --- a/ptychodus/controller/settings.py +++ b/ptychodus/controller/settings.py @@ -7,20 +7,16 @@ from ptychodus.api.observer import Observable, Observer from ptychodus.api.settings import SettingsGroup, SettingsRegistry -from ..view.settings import SettingsParametersView +from ..view.settings import SettingsView from .data import FileDialogFactory -class SettingsGroupListModel(QAbstractListModel): +class SettingsListModel(QAbstractListModel): def __init__(self, settingsRegistry: SettingsRegistry, parent: QObject | None = None) -> None: super().__init__(parent) self._settingsRegistry = settingsRegistry - def refresh(self) -> None: - self.beginResetModel() - self.endResetModel() - def data(self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole) -> Any: if index.isValid() and role == Qt.ItemDataRole.DisplayRole: settingsGroup = self._settingsRegistry[index.row()] @@ -30,7 +26,7 @@ def rowCount(self, parent: QModelIndex = QModelIndex()) -> int: return len(self._settingsRegistry) -class SettingsEntryTableModel(QAbstractTableModel): +class SettingsTableModel(QAbstractTableModel): def __init__(self, settingsGroup: SettingsGroup | None, parent: QObject | None = None) -> None: super().__init__(parent) @@ -67,43 +63,35 @@ def columnCount(self, parent: QModelIndex = QModelIndex()) -> int: class SettingsController(Observer): - def __init__(self, settingsRegistry: SettingsRegistry, parametersView: SettingsParametersView, - entryTableView: QTableView, fileDialogFactory: FileDialogFactory) -> None: + def __init__(self, settingsRegistry: SettingsRegistry, view: SettingsView, + tableView: QTableView, fileDialogFactory: FileDialogFactory) -> None: super().__init__() self._settingsRegistry = settingsRegistry - self._groupListModel = SettingsGroupListModel(settingsRegistry) - self._parametersView = parametersView - self._entryTableView = entryTableView + self._listModel = SettingsListModel(settingsRegistry) + self._view = view + self._tableView = tableView self._fileDialogFactory = fileDialogFactory @classmethod - def createInstance(cls, settingsRegistry: SettingsRegistry, - parametersView: SettingsParametersView, entryTableView: QTableView, + def createInstance(cls, settingsRegistry: SettingsRegistry, view: SettingsView, + tableView: QTableView, fileDialogFactory: FileDialogFactory) -> SettingsController: - controller = cls(settingsRegistry, parametersView, entryTableView, fileDialogFactory) + controller = cls(settingsRegistry, view, tableView, fileDialogFactory) settingsRegistry.addObserver(controller) - parametersView.settingsView.replacementPathPrefixLineEdit.editingFinished.connect( - controller._syncReplacementPathPrefixToModel) - - groupListView = parametersView.groupView.listView - groupListView.setModel(controller._groupListModel) - groupListView.selectionModel().currentChanged.connect(controller._updateEntryTable) + view.listView.setModel(controller._listModel) + view.listView.selectionModel().currentChanged.connect(controller._updateView) - parametersView.groupView.buttonBox.openButton.clicked.connect(controller._openSettings) - parametersView.groupView.buttonBox.saveButton.clicked.connect(controller._saveSettings) + view.buttonBox.openButton.clicked.connect(controller._openSettings) + view.buttonBox.saveButton.clicked.connect(controller._saveSettings) controller._syncModelToView() return controller - def _syncReplacementPathPrefixToModel(self) -> None: - self._settingsRegistry.setReplacementPathPrefix( - self._parametersView.settingsView.replacementPathPrefixLineEdit.text()) - def _openSettings(self) -> None: filePath, _ = self._fileDialogFactory.getOpenFilePath( - self._parametersView, + self._view, 'Open Settings', nameFilters=self._settingsRegistry.getOpenFileFilterList(), selectedNameFilter=self._settingsRegistry.getOpenFileFilter()) @@ -113,7 +101,7 @@ def _openSettings(self) -> None: def _saveSettings(self) -> None: filePath, _ = self._fileDialogFactory.getSaveFilePath( - self._parametersView, + self._view, 'Save Settings', nameFilters=self._settingsRegistry.getSaveFileFilterList(), selectedNameFilter=self._settingsRegistry.getSaveFileFilter()) @@ -121,23 +109,17 @@ def _saveSettings(self) -> None: if filePath: self._settingsRegistry.saveSettings(filePath) - def _updateEntryTable(self, current: QModelIndex, previous: QModelIndex) -> None: + def _updateView(self, current: QModelIndex, previous: QModelIndex) -> None: settingsGroup = self._settingsRegistry[current.row()] if current.isValid() else None - entryTableModel = SettingsEntryTableModel(settingsGroup) - self._entryTableView.setModel(entryTableModel) + tableModel = SettingsTableModel(settingsGroup) + self._tableView.setModel(tableModel) def _syncModelToView(self) -> None: - replacementPathPrefix = self._settingsRegistry.getReplacementPathPrefix() - - if replacementPathPrefix: - self._parametersView.settingsView.replacementPathPrefixLineEdit.setText( - replacementPathPrefix) - else: - self._parametersView.settingsView.replacementPathPrefixLineEdit.clear() + self._listModel.beginResetModel() + self._listModel.endResetModel() - self._groupListModel.refresh() - current = self._parametersView.groupView.listView.currentIndex() - self._updateEntryTable(current, QModelIndex()) + current = self._view.listView.currentIndex() + self._updateView(current, QModelIndex()) def update(self, observable: Observable) -> None: if observable is self._settingsRegistry: diff --git a/ptychodus/model/__init__.py b/ptychodus/model/__init__.py index 28c6b379..343e2722 100644 --- a/ptychodus/model/__init__.py +++ b/ptychodus/model/__init__.py @@ -1,6 +1,5 @@ -from .core import ModelArgs, ModelCore +from .core import ModelCore __all__ = [ - 'ModelArgs', 'ModelCore', ] diff --git a/ptychodus/model/automation/api.py b/ptychodus/model/automation/api.py deleted file mode 100644 index 70a66662..00000000 --- a/ptychodus/model/automation/api.py +++ /dev/null @@ -1,72 +0,0 @@ -from collections.abc import Mapping -from pathlib import Path -from typing import Any - -from ptychodus.api.automation import WorkflowAPI, WorkflowProductAPI - -from ..patterns import PatternsAPI -from ..product import ObjectAPI, ProbeAPI, ProductAPI, ScanAPI -from ..workflow import WorkflowCore - - -class ConcreteWorkflowProductAPI(WorkflowProductAPI): - - def __init__(self, productAPI: ProductAPI, scanAPI: ScanAPI, probeAPI: ProbeAPI, - objectAPI: ObjectAPI, workflowCore: WorkflowCore, productIndex: int) -> None: - self._productAPI = productAPI - self._scanAPI = scanAPI - self._probeAPI = probeAPI - self._objectAPI = objectAPI - self._workflowCore = workflowCore - self._productIndex = productIndex - - def openScan(self, filePath: Path, fileType: str) -> None: - self._scanAPI.openScan(self._productIndex, filePath, fileType) - - def buildScan(self, builderName: str, builderParameters: Mapping[str, Any] = {}) -> None: - self._scanAPI.buildScan(self._productIndex, builderName, builderParameters) - - def openProbe(self, filePath: Path, fileType: str) -> None: - self._probeAPI.openProbe(self._productIndex, filePath, fileType) - - def buildProbe(self, builderName: str, builderParameters: Mapping[str, Any] = {}) -> None: - self._probeAPI.buildProbe(self._productIndex, builderName, builderParameters) - - def openObject(self, filePath: Path, fileType: str) -> None: - self._objectAPI.openObject(self._productIndex, filePath, fileType) - - def buildObject(self, builderName: str, builderParameters: Mapping[str, Any] = {}) -> None: - self._objectAPI.buildObject(self._productIndex, builderName, builderParameters) - - def reconstruct(self) -> None: - self._workflowCore.executeWorkflow(self._productIndex) - - def saveProduct(self, filePath: Path, fileType: str) -> None: - self._productAPI.saveProduct(self._productIndex, filePath, fileType) - - -class ConcreteWorkflowAPI(WorkflowAPI): - - def __init__(self, patternsAPI: PatternsAPI, productAPI: ProductAPI, scanAPI: ScanAPI, - probeAPI: ProbeAPI, objectAPI: ObjectAPI, workflowCore: WorkflowCore) -> None: - self._patternsAPI = patternsAPI - self._productAPI = productAPI - self._scanAPI = scanAPI - self._probeAPI = probeAPI - self._objectAPI = objectAPI - self._workflowCore = workflowCore - - def _createProductAPI(self, productIndex: int) -> WorkflowProductAPI: - return ConcreteWorkflowProductAPI(self._productAPI, self._scanAPI, self._probeAPI, - self._objectAPI, self._workflowCore, productIndex) - - def openPatterns(self, filePath: Path, fileType: str) -> None: - self._patternsAPI.openPatterns(filePath, fileType) - - def openProduct(self, filePath: Path, fileType: str) -> WorkflowProductAPI: - productIndex = self._productAPI.openProduct(filePath, fileType) - return self._createProductAPI(productIndex) - - def createProduct(self, name: str) -> WorkflowProductAPI: - productIndex = self._productAPI.insertNewProduct(name) - return self._createProductAPI(productIndex) diff --git a/ptychodus/model/automation/core.py b/ptychodus/model/automation/core.py index b25c898b..c6b7efd8 100644 --- a/ptychodus/model/automation/core.py +++ b/ptychodus/model/automation/core.py @@ -3,16 +3,12 @@ from pathlib import Path import queue -from ptychodus.api.automation import FileBasedWorkflow from ptychodus.api.geometry import Interval from ptychodus.api.observer import Observable, Observer from ptychodus.api.plugins import PluginChooser from ptychodus.api.settings import SettingsRegistry +from ptychodus.api.workflow import FileBasedWorkflow, WorkflowAPI -from ..patterns import PatternsAPI -from ..product import ObjectAPI, ProbeAPI, ProductAPI, ScanAPI -from ..workflow import WorkflowCore -from .api import ConcreteWorkflowAPI from .buffer import AutomationDatasetBuffer from .processor import AutomationDatasetProcessor from .repository import AutomationDatasetRepository, AutomationDatasetState @@ -148,18 +144,14 @@ def update(self, observable: Observable) -> None: class AutomationCore: - def __init__(self, settingsRegistry: SettingsRegistry, patternsAPI: PatternsAPI, - productAPI: ProductAPI, scanAPI: ScanAPI, probeAPI: ProbeAPI, - objectAPI: ObjectAPI, workflowCore: WorkflowCore, + def __init__(self, settingsRegistry: SettingsRegistry, workflowAPI: WorkflowAPI, workflowChooser: PluginChooser[FileBasedWorkflow]) -> None: self._settings = AutomationSettings(settingsRegistry) self.repository = AutomationDatasetRepository(self._settings) self._workflow = CurrentFileBasedWorkflow(self._settings, workflowChooser) - self._workflowAPI = ConcreteWorkflowAPI(patternsAPI, productAPI, scanAPI, probeAPI, - objectAPI, workflowCore) self._processingQueue: queue.Queue[Path] = queue.Queue() self._processor = AutomationDatasetProcessor(self._settings, self.repository, - self._workflow, self._workflowAPI, + self._workflow, workflowAPI, self._processingQueue) self._datasetBuffer = AutomationDatasetBuffer(self._settings, self.repository, self._processor) diff --git a/ptychodus/model/automation/processor.py b/ptychodus/model/automation/processor.py index 96425d8a..928da491 100644 --- a/ptychodus/model/automation/processor.py +++ b/ptychodus/model/automation/processor.py @@ -4,7 +4,7 @@ import queue import threading -from ptychodus.api.automation import FileBasedWorkflow, WorkflowAPI +from ptychodus.api.workflow import FileBasedWorkflow, WorkflowAPI from .repository import AutomationDatasetRepository, AutomationDatasetState from .settings import AutomationSettings diff --git a/ptychodus/model/automation/watcher.py b/ptychodus/model/automation/watcher.py index 9c7c5a4e..235f06cb 100644 --- a/ptychodus/model/automation/watcher.py +++ b/ptychodus/model/automation/watcher.py @@ -6,8 +6,8 @@ from watchdog.observers.polling import PollingObserver import watchdog.observers -from ptychodus.api.automation import FileBasedWorkflow from ptychodus.api.observer import Observable, Observer +from ptychodus.api.workflow import FileBasedWorkflow from .buffer import AutomationDatasetBuffer from .settings import AutomationSettings diff --git a/ptychodus/model/automation/workflow.py b/ptychodus/model/automation/workflow.py index 0196626b..a04bf715 100644 --- a/ptychodus/model/automation/workflow.py +++ b/ptychodus/model/automation/workflow.py @@ -1,9 +1,9 @@ from collections.abc import Sequence from pathlib import Path -from ptychodus.api.automation import FileBasedWorkflow, WorkflowAPI -from ptychodus.api.plugins import PluginChooser from ptychodus.api.observer import Observable, Observer +from ptychodus.api.plugins import PluginChooser +from ptychodus.api.workflow import FileBasedWorkflow, WorkflowAPI from .settings import AutomationSettings diff --git a/ptychodus/model/core.py b/ptychodus/model/core.py index 21b497bf..bfdaeaa5 100644 --- a/ptychodus/model/core.py +++ b/ptychodus/model/core.py @@ -1,6 +1,5 @@ from __future__ import annotations from collections.abc import Sequence -from dataclasses import dataclass from importlib.metadata import version from pathlib import Path from types import TracebackType @@ -20,6 +19,7 @@ from ptychodus.api.patterns import DiffractionMetadata, DiffractionPatternArray from ptychodus.api.plugins import PluginRegistry from ptychodus.api.settings import SettingsRegistry +from ptychodus.api.workflow import WorkflowAPI from .analysis import (AnalysisCore, ExposureAnalyzer, FluorescenceEnhancer, FourierRingCorrelator, ProbePropagator, STXMAnalyzer, XMCDAnalyzer) @@ -56,23 +56,19 @@ def configureLogger() -> None: logger.info(f'HDF5 {h5py.version.hdf5_version}') -@dataclass(frozen=True) -class ModelArgs: - settingsFile: Path | None - patternsFile: Path | None - replacementPathPrefix: str | None - - class ModelCore: - def __init__(self, modelArgs: ModelArgs, *, isDeveloperModeEnabled: bool = False) -> None: + def __init__(self, + settingsFile: Path | None = None, + *, + isDeveloperModeEnabled: bool = False) -> None: configureLogger() - self._modelArgs = modelArgs self.rng = numpy.random.default_rng() self._pluginRegistry = PluginRegistry.loadPlugins() self.memoryPresenter = MemoryPresenter() - self.settingsRegistry = SettingsRegistry(modelArgs.replacementPathPrefix) + self.settingsRegistry = SettingsRegistry() + self._patternsCore = PatternsCore(self.settingsRegistry, self._pluginRegistry.diffractionFileReaders, self._pluginRegistry.diffractionFileWriters) @@ -111,23 +107,19 @@ def __init__(self, modelArgs: ModelArgs, *, isDeveloperModeEnabled: bool = False self._pluginRegistry.fluorescenceFileReaders, self._pluginRegistry.fluorescenceFileWriters) self._workflowCore = WorkflowCore(self.settingsRegistry, self._patternsCore.patternsAPI, - self._productCore.productAPI) - self._automationCore = AutomationCore( - self.settingsRegistry, self._patternsCore.patternsAPI, self._productCore.productAPI, - self._productCore.scanAPI, self._productCore.probeAPI, self._productCore.objectAPI, - self._workflowCore, self._pluginRegistry.fileBasedWorkflows) + self._productCore.productAPI, self._productCore.scanAPI, + self._productCore.probeAPI, self._productCore.objectAPI) + self._automationCore = AutomationCore(self.settingsRegistry, + self._workflowCore.workflowAPI, + self._pluginRegistry.fileBasedWorkflows) - def __enter__(self) -> ModelCore: - if self._modelArgs.settingsFile: - self.settingsRegistry.openSettings(self._modelArgs.settingsFile) - - if self._modelArgs.patternsFile: - self._patternsCore.patternsAPI.openPreprocessedPatterns(self._modelArgs.patternsFile) + if settingsFile: + self.settingsRegistry.openSettings(settingsFile) + def __enter__(self) -> ModelCore: self._patternsCore.start() self._workflowCore.start() self._automationCore.start() - return self @overload @@ -223,42 +215,34 @@ def refreshActiveDataset(self) -> None: def refreshAutomationDatasets(self) -> None: self._automationCore.repository.notifyObserversIfRepositoryChanged() - def stageReconstructionInputs(self, stagingDir: Path) -> int: - self.settingsRegistry.saveSettings(stagingDir / 'settings.ini') - self._patternsCore.patternsAPI.savePreprocessedPatterns(stagingDir / 'patterns.npz') - self._productCore.productAPI.saveProduct(0, stagingDir / 'product-in.npz', 'NPZ') - return 0 - - def batchModeReconstruct(self, inputFilePath: Path, outputFilePath: Path) -> int: - inputProductIndex = self._productCore.productAPI.openProduct(inputFilePath, 'NPZ') + def batchModeExecute(self, action: str, inputFilePath: Path, outputFilePath: Path) -> int: + # TODO add enum for actions; implement using workflow API + inputProductIndex = self._productCore.productAPI.openProduct(inputFilePath, fileType='NPZ') if inputProductIndex < 0: logger.error(f'Failed to open product \"{inputFilePath}\"') return -1 - outputProductName = self._productCore.productAPI.getItemName(inputProductIndex) - outputProductIndex = self._reconstructorCore.presenter.reconstruct( - inputProductIndex, outputProductName) - - if outputProductIndex < 0: - logger.error(f'Failed to reconstruct product index=\"{inputProductIndex}\"') + if action.lower() == 'reconstruct': + outputProductName = self._productCore.productAPI.getItemName(inputProductIndex) + outputProductIndex = self._reconstructorCore.presenter.reconstruct( + inputProductIndex, outputProductName) + + if outputProductIndex < 0: + logger.error(f'Failed to reconstruct product index=\"{inputProductIndex}\"') + return -1 + + self._productCore.productAPI.saveProduct(outputProductIndex, + outputFilePath, + fileType='NPZ') + elif action.lower() == 'train': + self._reconstructorCore.presenter.ingestTrainingData(inputProductIndex) + _ = self._reconstructorCore.presenter.train() + self._reconstructorCore.presenter.saveModel(outputFilePath) + else: + logger.error(f'Unknown batch mode action \"{action}\"!') return -1 - self._productCore.productAPI.saveProduct(outputProductIndex, outputFilePath, 'NPZ') - - return 0 - - def batchModeTrain(self, inputFilePath: Path, outputFilePath: Path) -> int: - inputProductIndex = self._productCore.productAPI.openProduct(inputFilePath, 'NPZ') - - if inputProductIndex < 0: - logger.error(f'Failed to open product \"{inputFilePath}\"') - return -1 - - self._reconstructorCore.presenter.ingestTrainingData(inputProductIndex) - _ = self._reconstructorCore.presenter.train() - self._reconstructorCore.presenter.saveModel(outputFilePath) - return 0 @property @@ -333,6 +317,10 @@ def workflowExecutionPresenter(self) -> WorkflowExecutionPresenter: def workflowParametersPresenter(self) -> WorkflowParametersPresenter: return self._workflowCore.parametersPresenter + @property + def workflowAPI(self) -> WorkflowAPI: + return self._workflowCore.workflowAPI + @property def automationPresenter(self) -> AutomationPresenter: return self._automationCore.presenter diff --git a/ptychodus/model/patterns/active.py b/ptychodus/model/patterns/active.py index 5979fc88..0983c52f 100644 --- a/ptychodus/model/patterns/active.py +++ b/ptychodus/model/patterns/active.py @@ -170,7 +170,7 @@ def setAssembledData(self, arrayData: DiffractionPatternArrayType, # TODO use arrayIndexes self._arrayList = [ SimpleDiffractionPatternArray( - label='Restart', + label='Processed', index=0, data=arrayData[...], state=DiffractionPatternState.LOADED, diff --git a/ptychodus/model/patterns/api.py b/ptychodus/model/patterns/api.py index 2ecd056f..a0739c42 100644 --- a/ptychodus/model/patterns/api.py +++ b/ptychodus/model/patterns/api.py @@ -5,7 +5,8 @@ import numpy -from ptychodus.api.patterns import (DiffractionFileReader, DiffractionFileWriter, +from ptychodus.api.geometry import ImageExtent +from ptychodus.api.patterns import (CropCenter, DiffractionFileReader, DiffractionFileWriter, DiffractionMetadata, DiffractionPatternArray, SimpleDiffractionDataset) from ptychodus.api.plugins import PluginChooser @@ -13,16 +14,19 @@ from .active import ActiveDiffractionDataset from .builder import ActiveDiffractionDatasetBuilder +from .settings import PatternSettings logger = logging.getLogger(__name__) class PatternsAPI: - def __init__(self, builder: ActiveDiffractionDatasetBuilder, dataset: ActiveDiffractionDataset, + def __init__(self, settings: PatternSettings, builder: ActiveDiffractionDatasetBuilder, + dataset: ActiveDiffractionDataset, fileReaderChooser: PluginChooser[DiffractionFileReader], fileWriterChooser: PluginChooser[DiffractionFileWriter]) -> None: super().__init__() + self._settings = settings self._builder = builder self._dataset = dataset self._fileReaderChooser = fileReaderChooser @@ -52,8 +56,23 @@ def getOpenFileFilterList(self) -> Sequence[str]: def getOpenFileFilter(self) -> str: return self._fileReaderChooser.currentPlugin.displayName - def openPatterns(self, filePath: Path, fileType: str, *, assemble: bool = True) -> str | None: - self._fileReaderChooser.setCurrentPluginByName(fileType) + def openPatterns(self, + filePath: Path, + *, + fileType: str | None = None, + cropCenter: CropCenter | None = None, + cropExtent: ImageExtent | None = None, + assemble: bool = True) -> str | None: + if cropCenter is not None: + self._settings.cropCenterXInPixels.value = cropCenter.positionXInPixels + self._settings.cropCenterYInPixels.value = cropCenter.positionYInPixels + + if cropExtent is not None: + self._settings.cropWidthInPixels.value = cropExtent.widthInPixels + self._settings.cropHeightInPixels.value = cropExtent.heightInPixels + + fileType_ = self._settings.fileType.value if fileType is None else fileType + self._fileReaderChooser.setCurrentPluginByName(fileType_) if filePath.is_file(): fileReader = self._fileReaderChooser.currentPlugin.strategy @@ -89,9 +108,9 @@ def savePatterns(self, filePath: Path, fileType: str) -> None: writer = self._fileWriterChooser.currentPlugin.strategy writer.write(filePath, self._dataset) - def openPreprocessedPatterns(self, filePath: Path) -> None: + def importProcessedPatterns(self, filePath: Path) -> None: if filePath.is_file(): - logger.debug(f'Reading preprocessed patterns from \"{filePath}\"') + logger.debug(f'Reading processed patterns from \"{filePath}\"') try: contents = numpy.load(filePath) @@ -105,9 +124,10 @@ def openPreprocessedPatterns(self, filePath: Path) -> None: else: logger.warning(f'Refusing to read invalid file path {filePath}') - def savePreprocessedPatterns(self, filePath: Path) -> None: + def exportProcessedPatterns(self, filePath: Path) -> None: contents: dict[str, Any] = { 'indexes': numpy.array(self._dataset.getAssembledIndexes()), 'patterns': numpy.array(self._dataset.getAssembledData()), } + logger.debug(f'Writing processed patterns to \"{filePath}\"') numpy.savez(filePath, **contents) diff --git a/ptychodus/model/patterns/core.py b/ptychodus/model/patterns/core.py index 35ee3c40..fa58db48 100644 --- a/ptychodus/model/patterns/core.py +++ b/ptychodus/model/patterns/core.py @@ -156,8 +156,8 @@ def __init__(self, settingsRegistry: SettingsRegistry, self.dataset = ActiveDiffractionDataset(self.patternSettings, self.patternSizer) self._builder = ActiveDiffractionDatasetBuilder(self.patternSettings, self.dataset) - self.patternsAPI = PatternsAPI(self._builder, self.dataset, fileReaderChooser, - fileWriterChooser) + self.patternsAPI = PatternsAPI(self.patternSettings, self._builder, self.dataset, + fileReaderChooser, fileWriterChooser) self.metadataPresenter = DiffractionMetadataPresenter(self.dataset, self.detector, self.patternSettings, diff --git a/ptychodus/model/product/api.py b/ptychodus/model/product/api.py index 927dd344..e904a99b 100644 --- a/ptychodus/model/product/api.py +++ b/ptychodus/model/product/api.py @@ -6,12 +6,16 @@ from ptychodus.api.plugins import PluginChooser from ptychodus.api.product import ProductFileReader, ProductFileWriter +from ..patterns import ProductSettings from .object.builderFactory import ObjectBuilderFactory +from .object.settings import ObjectSettings from .objectRepository import ObjectRepository from .probe.builderFactory import ProbeBuilderFactory +from .probe.settings import ProbeSettings from .probeRepository import ProbeRepository from .productRepository import ProductRepository from .scan.builderFactory import ScanBuilderFactory +from .scan.settings import ScanSettings from .scanRepository import ScanRepository logger = logging.getLogger(__name__) @@ -19,7 +23,9 @@ class ScanAPI: - def __init__(self, repository: ScanRepository, builderFactory: ScanBuilderFactory) -> None: + def __init__(self, settings: ScanSettings, repository: ScanRepository, + builderFactory: ScanBuilderFactory) -> None: + self._settings = settings self._repository = repository self._builderFactory = builderFactory @@ -59,8 +65,9 @@ def getOpenFileFilterList(self) -> Sequence[str]: def getOpenFileFilter(self) -> str: return self._builderFactory.getOpenFileFilter() - def openScan(self, index: int, filePath: Path, fileFilter: str) -> None: - builder = self._builderFactory.createScanFromFile(filePath, fileFilter) + def openScan(self, index: int, filePath: Path, *, fileType: str | None = None) -> None: + builder = self._builderFactory.createScanFromFile( + filePath, self._settings.fileType.value if fileType is None else fileType) try: item = self._repository[index] @@ -92,18 +99,20 @@ def getSaveFileFilterList(self) -> Sequence[str]: def getSaveFileFilter(self) -> str: return self._builderFactory.getSaveFileFilter() - def saveScan(self, index: int, filePath: Path, fileFilter: str) -> None: + def saveScan(self, index: int, filePath: Path, fileType: str) -> None: try: item = self._repository[index] except IndexError: logger.warning(f'Failed to save scan {index}!') else: - self._builderFactory.saveScan(filePath, fileFilter, item.getScan()) + self._builderFactory.saveScan(filePath, fileType, item.getScan()) class ProbeAPI: - def __init__(self, repository: ProbeRepository, builderFactory: ProbeBuilderFactory) -> None: + def __init__(self, settings: ProbeSettings, repository: ProbeRepository, + builderFactory: ProbeBuilderFactory) -> None: + self._settings = settings self._repository = repository self._builderFactory = builderFactory @@ -143,8 +152,9 @@ def getOpenFileFilterList(self) -> Sequence[str]: def getOpenFileFilter(self) -> str: return self._builderFactory.getOpenFileFilter() - def openProbe(self, index: int, filePath: Path, fileFilter: str) -> None: - builder = self._builderFactory.createProbeFromFile(filePath, fileFilter) + def openProbe(self, index: int, filePath: Path, *, fileType: str | None = None) -> None: + builder = self._builderFactory.createProbeFromFile( + filePath, self._settings.fileType.value if fileType is None else fileType) try: item = self._repository[index] @@ -176,18 +186,20 @@ def getSaveFileFilterList(self) -> Sequence[str]: def getSaveFileFilter(self) -> str: return self._builderFactory.getSaveFileFilter() - def saveProbe(self, index: int, filePath: Path, fileFilter: str) -> None: + def saveProbe(self, index: int, filePath: Path, fileType: str) -> None: try: item = self._repository[index] except IndexError: logger.warning(f'Failed to save probe {index}!') else: - self._builderFactory.saveProbe(filePath, fileFilter, item.getProbe()) + self._builderFactory.saveProbe(filePath, fileType, item.getProbe()) class ObjectAPI: - def __init__(self, repository: ObjectRepository, builderFactory: ObjectBuilderFactory) -> None: + def __init__(self, settings: ObjectSettings, repository: ObjectRepository, + builderFactory: ObjectBuilderFactory) -> None: + self._settings = settings self._repository = repository self._builderFactory = builderFactory @@ -227,8 +239,9 @@ def getOpenFileFilterList(self) -> Sequence[str]: def getOpenFileFilter(self) -> str: return self._builderFactory.getOpenFileFilter() - def openObject(self, index: int, filePath: Path, fileFilter: str) -> None: - builder = self._builderFactory.createObjectFromFile(filePath, fileFilter) + def openObject(self, index: int, filePath: Path, *, fileType: str | None = None) -> None: + builder = self._builderFactory.createObjectFromFile( + filePath, self._settings.fileType.value if fileType is None else fileType) try: item = self._repository[index] @@ -260,26 +273,42 @@ def getSaveFileFilterList(self) -> Sequence[str]: def getSaveFileFilter(self) -> str: return self._builderFactory.getSaveFileFilter() - def saveObject(self, index: int, filePath: Path, fileFilter: str) -> None: + def saveObject(self, index: int, filePath: Path, fileType: str) -> None: try: item = self._repository[index] except IndexError: logger.warning(f'Failed to save object {index}!') else: - self._builderFactory.saveObject(filePath, fileFilter, item.getObject()) + self._builderFactory.saveObject(filePath, fileType, item.getObject()) class ProductAPI: - def __init__(self, repository: ProductRepository, + def __init__(self, settings: ProductSettings, repository: ProductRepository, fileReaderChooser: PluginChooser[ProductFileReader], fileWriterChooser: PluginChooser[ProductFileWriter]) -> None: + self._settings = settings self._repository = repository self._fileReaderChooser = fileReaderChooser self._fileWriterChooser = fileWriterChooser - def insertNewProduct(self, name: str = 'Unnamed', *, likeIndex: int = -1) -> int: - return self._repository.insertNewProduct(name, likeIndex=likeIndex) + def insertNewProduct(self, + name: str = 'Unnamed', + *, + comments: str = '', + detectorDistanceInMeters: float | None = None, + probeEnergyInElectronVolts: float | None = None, + probePhotonsPerSecond: float | None = None, + exposureTimeInSeconds: float | None = None, + likeIndex: int = -1) -> int: + return self._repository.insertNewProduct( + name, + comments=comments, + detectorDistanceInMeters=detectorDistanceInMeters, + probeEnergyInElectronVolts=probeEnergyInElectronVolts, + probePhotonsPerSecond=probePhotonsPerSecond, + exposureTimeInSeconds=exposureTimeInSeconds, + likeIndex=likeIndex) def getItemName(self, productIndex: int) -> str: item = self._repository[productIndex] @@ -291,9 +320,10 @@ def getOpenFileFilterList(self) -> Sequence[str]: def getOpenFileFilter(self) -> str: return self._fileReaderChooser.currentPlugin.displayName - def openProduct(self, filePath: Path, fileFilter: str) -> int: + def openProduct(self, filePath: Path, *, fileType: str | None = None) -> int: if filePath.is_file(): - self._fileReaderChooser.setCurrentPluginByName(fileFilter) + self._fileReaderChooser.setCurrentPluginByName( + self._settings.fileType.value if fileType is None else fileType) fileType = self._fileReaderChooser.currentPlugin.simpleName logger.debug(f'Reading \"{filePath}\" as \"{fileType}\"') fileReader = self._fileReaderChooser.currentPlugin.strategy @@ -315,14 +345,15 @@ def getSaveFileFilterList(self) -> Sequence[str]: def getSaveFileFilter(self) -> str: return self._fileWriterChooser.currentPlugin.displayName - def saveProduct(self, index: int, filePath: Path, fileFilter: str) -> None: + def saveProduct(self, index: int, filePath: Path, *, fileType: str | None = None) -> None: try: item = self._repository[index] except IndexError: logger.warning(f'Failed to save product {index}!') return - self._fileWriterChooser.setCurrentPluginByName(fileFilter) + self._fileWriterChooser.setCurrentPluginByName(self._settings.fileType.value if fileType is + None else fileType) fileType = self._fileWriterChooser.currentPlugin.simpleName logger.debug(f'Writing \"{filePath}\" as \"{fileType}\"') writer = self._fileWriterChooser.currentPlugin.strategy diff --git a/ptychodus/model/product/core.py b/ptychodus/model/product/core.py index 49b27f23..306b9b2a 100644 --- a/ptychodus/model/product/core.py +++ b/ptychodus/model/product/core.py @@ -66,14 +66,16 @@ def __init__( self._scanRepositoryItemFactory, self._probeRepositoryItemFactory, self._objectRepositoryItemFactory) - self.productAPI = ProductAPI(self.productRepository, productFileReaderChooser, + self.productAPI = ProductAPI(settings, self.productRepository, productFileReaderChooser, productFileWriterChooser) self.scanRepository = ScanRepository(self.productRepository) - self.scanAPI = ScanAPI(self.scanRepository, self._scanBuilderFactory) + self.scanAPI = ScanAPI(self._scanSettings, self.scanRepository, self._scanBuilderFactory) self.probeRepository = ProbeRepository(self.productRepository) - self.probeAPI = ProbeAPI(self.probeRepository, self._probeBuilderFactory) + self.probeAPI = ProbeAPI(self._probeSettings, self.probeRepository, + self._probeBuilderFactory) self.objectRepository = ObjectRepository(self.productRepository) - self.objectAPI = ObjectAPI(self.objectRepository, self._objectBuilderFactory) + self.objectAPI = ObjectAPI(self._objectSettings, self.objectRepository, + self._objectBuilderFactory) # TODO vvv refactor vvv productFileReaderChooser.setCurrentPluginByName(settings.fileType.value) diff --git a/ptychodus/model/product/metadataFactory.py b/ptychodus/model/product/metadataFactory.py index 945f509a..d7b1af03 100644 --- a/ptychodus/model/product/metadataFactory.py +++ b/ptychodus/model/product/metadataFactory.py @@ -23,14 +23,30 @@ def __init__(self, repository: Sequence[ProductRepositoryItem], def create(self, metadata: ProductMetadata) -> MetadataRepositoryItem: return MetadataRepositoryItem(self, metadata) - def createDefault(self, name: str, comments: str = '') -> MetadataRepositoryItem: + def createDefault(self, + name: str, + *, + comments: str = '', + detectorDistanceInMeters: float | None = None, + probeEnergyInElectronVolts: float | None = None, + probePhotonsPerSecond: float | None = None, + exposureTimeInSeconds: float | None = None) -> MetadataRepositoryItem: + detectorDistanceInMeters_ = float(self._settings.detectorDistanceInMeters.value) \ + if detectorDistanceInMeters is None else detectorDistanceInMeters + probeEnergyInElectronVolts_ = float(self._settings.probeEnergyInElectronVolts.value) \ + if probeEnergyInElectronVolts is None else probeEnergyInElectronVolts + probePhotonsPerSecond_ = float(self._settings.probePhotonsPerSecond.value) \ + if probePhotonsPerSecond is None else probePhotonsPerSecond + exposureTimeInSeconds_ = float(self._settings.exposureTimeInSeconds.value) \ + if exposureTimeInSeconds is None else exposureTimeInSeconds + metadata = ProductMetadata( name=name, comments=comments, - detectorDistanceInMeters=float(self._settings.detectorDistanceInMeters.value), - probeEnergyInElectronVolts=float(self._settings.probeEnergyInElectronVolts.value), - probePhotonsPerSecond=float(self._settings.probePhotonsPerSecond.value), - exposureTimeInSeconds=float(self._settings.exposureTimeInSeconds.value), + detectorDistanceInMeters=detectorDistanceInMeters_, + probeEnergyInElectronVolts=probeEnergyInElectronVolts_, + probePhotonsPerSecond=probePhotonsPerSecond_, + exposureTimeInSeconds=exposureTimeInSeconds_, ) return self.create(metadata) diff --git a/ptychodus/model/product/productRepository.py b/ptychodus/model/product/productRepository.py index 45009203..8ae7b2c5 100644 --- a/ptychodus/model/product/productRepository.py +++ b/ptychodus/model/product/productRepository.py @@ -62,8 +62,22 @@ def _insertProduct(self, item: ProductRepositoryItem) -> int: return index - def insertNewProduct(self, name: str, *, likeIndex: int) -> int: - metadataItem = self._metadataRepositoryItemFactory.createDefault(name) + def insertNewProduct(self, + name: str, + *, + comments: str = '', + detectorDistanceInMeters: float | None = None, + probeEnergyInElectronVolts: float | None = None, + probePhotonsPerSecond: float | None = None, + exposureTimeInSeconds: float | None = None, + likeIndex: int) -> int: + metadataItem = self._metadataRepositoryItemFactory.createDefault( + name, + comments=comments, + detectorDistanceInMeters=detectorDistanceInMeters, + probeEnergyInElectronVolts=probeEnergyInElectronVolts, + probePhotonsPerSecond=probePhotonsPerSecond, + exposureTimeInSeconds=exposureTimeInSeconds) scanItem = self._scanRepositoryItemFactory.create() geometry = ProductGeometry(self._patternSizer, metadataItem, scanItem) probeItem = self._probeRepositoryItemFactory.create(geometry) diff --git a/ptychodus/model/workflow/api.py b/ptychodus/model/workflow/api.py new file mode 100644 index 00000000..12b28830 --- /dev/null +++ b/ptychodus/model/workflow/api.py @@ -0,0 +1,120 @@ +from collections.abc import Mapping +from pathlib import Path +from typing import Any +import logging + +from ptychodus.api.geometry import ImageExtent +from ptychodus.api.patterns import CropCenter +from ptychodus.api.settings import PathPrefixChange, SettingsRegistry +from ptychodus.api.workflow import WorkflowAPI, WorkflowProductAPI + +from ..patterns import PatternsAPI +from ..product import ObjectAPI, ProbeAPI, ProductAPI, ScanAPI +from .executor import WorkflowExecutor + +logger = logging.getLogger(__name__) + + +class ConcreteWorkflowProductAPI(WorkflowProductAPI): + + def __init__(self, productAPI: ProductAPI, scanAPI: ScanAPI, probeAPI: ProbeAPI, + objectAPI: ObjectAPI, executor: WorkflowExecutor, productIndex: int) -> None: + self._productAPI = productAPI + self._scanAPI = scanAPI + self._probeAPI = probeAPI + self._objectAPI = objectAPI + self._executor = executor + self._productIndex = productIndex + + def openScan(self, filePath: Path, *, fileType: str | None = None) -> None: + self._scanAPI.openScan(self._productIndex, filePath, fileType=fileType) + + def buildScan(self, builderName: str, builderParameters: Mapping[str, Any] = {}) -> None: + self._scanAPI.buildScan(self._productIndex, builderName, builderParameters) + + def openProbe(self, filePath: Path, *, fileType: str | None = None) -> None: + self._probeAPI.openProbe(self._productIndex, filePath, fileType=fileType) + + def buildProbe(self, builderName: str, builderParameters: Mapping[str, Any] = {}) -> None: + self._probeAPI.buildProbe(self._productIndex, builderName, builderParameters) + + def openObject(self, filePath: Path, *, fileType: str | None = None) -> None: + self._objectAPI.openObject(self._productIndex, filePath, fileType=fileType) + + def buildObject(self, builderName: str, builderParameters: Mapping[str, Any] = {}) -> None: + self._objectAPI.buildObject(self._productIndex, builderName, builderParameters) + + def reconstruct(self) -> None: + logger.debug(f'Execute Workflow: index={self._productIndex}') + self._executor.runFlow(self._productIndex) + + def saveProduct(self, filePath: Path, *, fileType: str | None = None) -> None: + self._productAPI.saveProduct(self._productIndex, filePath, fileType=fileType) + + +class ConcreteWorkflowAPI(WorkflowAPI): + + def __init__(self, settingsRegistry: SettingsRegistry, patternsAPI: PatternsAPI, + productAPI: ProductAPI, scanAPI: ScanAPI, probeAPI: ProbeAPI, + objectAPI: ObjectAPI, executor: WorkflowExecutor) -> None: + self._settingsRegistry = settingsRegistry + self._patternsAPI = patternsAPI + self._productAPI = productAPI + self._scanAPI = scanAPI + self._probeAPI = probeAPI + self._objectAPI = objectAPI + self._executor = executor + + def _createProductAPI(self, productIndex: int) -> WorkflowProductAPI: + if productIndex < 0: + raise ValueError(f'Bad product index ({productIndex=})!') + + return ConcreteWorkflowProductAPI(self._productAPI, self._scanAPI, self._probeAPI, + self._objectAPI, self._executor, productIndex) + + def openPatterns( + self, + filePath: Path, + *, + fileType: str | None = None, + cropCenter: CropCenter | None = None, + cropExtent: ImageExtent | None = None, + ) -> None: + self._patternsAPI.openPatterns(filePath, + fileType=fileType, + cropCenter=cropCenter, + cropExtent=cropExtent) + + def importProcessedPatterns(self, filePath: Path) -> None: + self._patternsAPI.importProcessedPatterns(filePath) + + def exportProcessedPatterns(self, filePath: Path) -> None: + self._patternsAPI.exportProcessedPatterns(filePath) + + def openProduct(self, filePath: Path, *, fileType: str | None = None) -> WorkflowProductAPI: + productIndex = self._productAPI.openProduct(filePath, fileType=fileType) + return self._createProductAPI(productIndex) + + def createProduct( + self, + name: str, + *, + comments: str = '', + detectorDistanceInMeters: float | None = None, + probeEnergyInElectronVolts: float | None = None, + probePhotonsPerSecond: float | None = None, + exposureTimeInSeconds: float | None = None, + ) -> WorkflowProductAPI: + productIndex = self._productAPI.insertNewProduct( + name, + comments=comments, + detectorDistanceInMeters=detectorDistanceInMeters, + probeEnergyInElectronVolts=probeEnergyInElectronVolts, + probePhotonsPerSecond=probePhotonsPerSecond, + exposureTimeInSeconds=exposureTimeInSeconds) + return self._createProductAPI(productIndex) + + def saveSettings(self, + filePath: Path, + changePathPrefix: PathPrefixChange | None = None) -> None: + self._settingsRegistry.saveSettings(filePath, changePathPrefix) diff --git a/ptychodus/model/workflow/core.py b/ptychodus/model/workflow/core.py index 238f3a84..6353b1a6 100644 --- a/ptychodus/model/workflow/core.py +++ b/ptychodus/model/workflow/core.py @@ -12,7 +12,8 @@ from ptychodus.api.settings import SettingsRegistry from ..patterns import PatternsAPI -from ..product import ProductAPI +from ..product import ObjectAPI, ProbeAPI, ProductAPI, ScanAPI +from .api import ConcreteWorkflowAPI from .authorizer import WorkflowAuthorizer from .executor import WorkflowExecutor from .locator import DataLocator, OutputDataLocator, SimpleDataLocator @@ -186,8 +187,9 @@ def runFlow(self, inputProductIndex: int) -> None: class WorkflowCore: def __init__(self, settingsRegistry: SettingsRegistry, patternsAPI: PatternsAPI, - productAPI: ProductAPI) -> None: - self._settings = WorkflowSettings.createInstance(settingsRegistry) + productAPI: ProductAPI, scanAPI: ScanAPI, probeAPI: ProbeAPI, + objectAPI: ObjectAPI) -> None: + self._settings = WorkflowSettings(settingsRegistry) self._inputDataLocator = SimpleDataLocator.createInstance(self._settings.group, 'Input') self._computeDataLocator = SimpleDataLocator.createInstance(self._settings.group, 'Compute') @@ -198,6 +200,8 @@ def __init__(self, settingsRegistry: SettingsRegistry, patternsAPI: PatternsAPI, self._executor = WorkflowExecutor(self._settings, self._inputDataLocator, self._computeDataLocator, self._outputDataLocator, settingsRegistry, patternsAPI, productAPI) + self.workflowAPI = ConcreteWorkflowAPI(settingsRegistry, patternsAPI, productAPI, scanAPI, + probeAPI, objectAPI, self._executor) self._thread: threading.Thread | None = None try: @@ -216,10 +220,6 @@ def __init__(self, settingsRegistry: SettingsRegistry, patternsAPI: PatternsAPI, self.statusPresenter = WorkflowStatusPresenter(self._settings, self._statusRepository) self.executionPresenter = WorkflowExecutionPresenter(self._executor) - def executeWorkflow(self, inputProductIndex: int) -> None: - logger.debug(f'Execute Workflow: index={inputProductIndex}') - self._executor.runFlow(inputProductIndex) - @property def areWorkflowsSupported(self) -> bool: return (self._thread is not None) diff --git a/ptychodus/model/workflow/executor.py b/ptychodus/model/workflow/executor.py index 7aab81c3..0dfc81c5 100644 --- a/ptychodus/model/workflow/executor.py +++ b/ptychodus/model/workflow/executor.py @@ -57,7 +57,6 @@ def runFlow(self, inputProductIndex: int) -> None: patternsFile = 'patterns.npz' inputFile = 'product-in.npz' outputFile = 'product-out.npz' - productFileFilter = 'NPZ' try: inputDataPosixPath.mkdir(mode=0o755, parents=True, exist_ok=True) @@ -65,10 +64,12 @@ def runFlow(self, inputProductIndex: int) -> None: logger.warning('Input data POSIX path must be a directory!') return + # TODO use workflow API self._settingsRegistry.saveSettings(inputDataPosixPath / settingsFile) - self._patternsAPI.savePreprocessedPatterns(inputDataPosixPath / patternsFile) - self._productAPI.saveProduct(inputProductIndex, inputDataPosixPath / inputFile, - productFileFilter) + self._patternsAPI.exportProcessedPatterns(inputDataPosixPath / patternsFile) + self._productAPI.saveProduct(inputProductIndex, + inputDataPosixPath / inputFile, + fileType='NPZ') flowInput = { 'input_data_transfer_source_endpoint_id': diff --git a/ptychodus/model/workflow/globus.py b/ptychodus/model/workflow/globus.py index 5fc3f8b0..107d5644 100644 --- a/ptychodus/model/workflow/globus.py +++ b/ptychodus/model/workflow/globus.py @@ -31,28 +31,18 @@ def ptychodus_reconstruct(**data: str) -> None: - import sys - from pathlib import Path - from ptychodus.model import ModelArgs, ModelCore + from ptychodus.model import ModelCore action = data['ptychodus_action'] inputFile = Path(data['ptychodus_input_file']) outputFile = Path(data['ptychodus_output_file']) + settingsFile = Path(data['ptychodus_settings_file']) + patternsFile = Path(data['ptychodus_patterns_file']) - modelArgs = ModelArgs( - settingsFile=Path(data['ptychodus_settings_file']), - patternsFile=Path(data['ptychodus_patterns_file']), - replacementPathPrefix=data.get('ptychodus_path_prefix'), - ) - - with ModelCore(modelArgs) as model: - if action.lower() == 'reconstruct': - model.batchModeReconstruct(inputFile, outputFile) - elif action.lower() == 'train': - model.batchModeTrain(inputFile, outputFile) - else: - print(f'Unknown batch mode action \"{action}\"!', file=sys.stderr) + with ModelCore(settingsFile) as model: + model.workflowAPI.importProcessedPatterns(patternsFile) + model.batchModeExecute(action, inputFile, outputFile) @gladier.generate_flow_definition diff --git a/ptychodus/model/workflow/settings.py b/ptychodus/model/workflow/settings.py index 6950e2be..cb1053cf 100644 --- a/ptychodus/model/workflow/settings.py +++ b/ptychodus/model/workflow/settings.py @@ -2,24 +2,19 @@ from uuid import UUID from ptychodus.api.observer import Observable, Observer -from ptychodus.api.settings import SettingsRegistry, SettingsGroup +from ptychodus.api.settings import SettingsRegistry class WorkflowSettings(Observable, Observer): - def __init__(self, group: SettingsGroup) -> None: + def __init__(self, registry: SettingsRegistry) -> None: super().__init__() - self.group = group - self.computeEndpointID = group.createUUIDEntry('ComputeEndpointID', UUID(int=0)) - self.statusRefreshIntervalInSeconds = group.createIntegerEntry( + self.group = registry.createGroup('Workflow') + self.group.addObserver(self) + self.computeEndpointID = self.group.createUUIDEntry('ComputeEndpointID', UUID(int=0)) + self.statusRefreshIntervalInSeconds = self.group.createIntegerEntry( 'StatusRefreshIntervalInSeconds', 10) - @classmethod - def createInstance(cls, settingsRegistry: SettingsRegistry) -> WorkflowSettings: - settings = cls(settingsRegistry.createGroup('Workflow')) - settings.group.addObserver(settings) - return settings - def update(self, observable: Observable) -> None: if observable is self.group: self.notifyObservers() diff --git a/ptychodus/plugins/automation.py b/ptychodus/plugins/workflow.py similarity index 96% rename from ptychodus/plugins/automation.py rename to ptychodus/plugins/workflow.py index ac015780..2c141644 100644 --- a/ptychodus/plugins/automation.py +++ b/ptychodus/plugins/workflow.py @@ -1,8 +1,8 @@ from pathlib import Path import re -from ptychodus.api.automation import FileBasedWorkflow, WorkflowAPI from ptychodus.api.plugins import PluginRegistry +from ptychodus.api.workflow import FileBasedWorkflow, WorkflowAPI class APS2IDFileBasedWorkflow(FileBasedWorkflow): diff --git a/ptychodus/ptychodusAdImageProcessor.py b/ptychodus/ptychodusAdImageProcessor.py index 15f16a3e..e3c34f2c 100644 --- a/ptychodus/ptychodusAdImageProcessor.py +++ b/ptychodus/ptychodusAdImageProcessor.py @@ -12,14 +12,14 @@ import pvaccess import pvapy +from ptychodus.model import ModelCore import ptychodus -import ptychodus.model class ReconstructionThread(threading.Thread): - def __init__(self, ptychodus: ptychodus.model.ModelCore, inputProductPath: Path, - outputProductPath: Path, reconstructPV: str) -> None: + def __init__(self, ptychodus: ModelCore, inputProductPath: Path, outputProductPath: Path, + reconstructPV: str) -> None: super().__init__() self._ptychodus = ptychodus self._inputProductPath = inputProductPath @@ -37,8 +37,8 @@ def run(self) -> None: logging.debug('ReconstructionThread: Begin assembling scan positions') self._ptychodus.finalizeStreamingWorkflow() logging.debug('ReconstructionThread: End assembling scan positions') - self._ptychodus.batchModeReconstruct(self._inputProductPath, - self._outputProductPath) + self._ptychodus.batchModeExecute('reconstruct', self._inputProductPath, + self._outputProductPath) self._reconstructEvent.clear() # reconstruction done; indicate that results are ready self._channel.put(0) @@ -65,13 +65,8 @@ def __init__(self, configDict: dict[str, Any] = {}) -> None: self.logger.debug(f'{ptychodus.__name__.title()} ({ptychodus.__version__})') - modelArgs = ptychodus.model.ModelArgs( - settingsFile=configDict.get('settingsFile'), - patternsFile=None, - replacementPathPrefix=configDict.get('replacementPathPrefix'), - ) - - self._ptychodus = ptychodus.model.ModelCore(modelArgs) + settingsFile = configDict.get('settingsFile') + self._ptychodus = ModelCore(settingsFile) self._reconstructionThread = ReconstructionThread( self._ptychodus, Path(configDict.get('inputProductPath', 'input.npz')), diff --git a/ptychodus/ptychodus_bdp.py b/ptychodus/ptychodus_bdp.py new file mode 100755 index 00000000..73054bea --- /dev/null +++ b/ptychodus/ptychodus_bdp.py @@ -0,0 +1,227 @@ +#!/usr/bin/env python + +from pathlib import Path +import argparse +import logging +import sys + +from ptychodus.api.geometry import ImageExtent +from ptychodus.api.patterns import CropCenter +from ptychodus.api.settings import PathPrefixChange +from ptychodus.model import ModelCore +import ptychodus + +logger = logging.getLogger(__name__) + + +def versionString() -> str: + return f'{ptychodus.__name__.title()} ({ptychodus.__version__})' + + +class DirectoryType: + + def __init__(self, *, must_exist: bool) -> None: + self._must_exist = must_exist + + def __call__(self, string: str) -> Path: + path = Path(string) + + if self._must_exist and not path.is_dir(): + raise argparse.ArgumentTypeError(f'\"{string}\" is not a directory!') + + return path + + +def main() -> int: + changePathPrefix: PathPrefixChange | None = None + cropCenter: CropCenter | None = None + cropExtent: ImageExtent | None = None + + prog = Path(__file__).stem.lower() + parser = argparse.ArgumentParser( + prog=prog, + description=f'{prog} prepares experiment data for use in beamline data pipelines', + ) + parser.add_argument( + '-c', + '--comment', + help='data product comment', + ) + parser.add_argument( + '-d', + '--dev', + action='store_true', + help=argparse.SUPPRESS, + ) + parser.add_argument( + '-n', + '--name', + help='data product name', + required=True, + ) + parser.add_argument( + '-o', + '--output_directory', + metavar='OUTPUT_DIR', + type=DirectoryType(must_exist=False), + required=True, + ) + parser.add_argument( + '-s', + '--settings', + metavar='SETTINGS_FILE', + help='use default settings from file', + type=argparse.FileType('r'), + required=True, + ) + parser.add_argument( + '--patterns_file_path', + metavar='PATTERNS_FILE', + help='diffraction patterns file path', + type=argparse.FileType('r'), + required=True, + ) + parser.add_argument( + '--crop_center_x_px', + metavar='CENTER_X', + help='crop center x in pixels', + type=int, + ) + parser.add_argument( + '--crop_center_y_px', + metavar='CENTER_Y', + help='crop center y in pixels', + type=int, + ) + parser.add_argument( + '--crop_width_px', + metavar='WIDTH', + help='crop width in pixels', + type=int, + ) + parser.add_argument( + '--crop_height_px', + metavar='HEIGHT', + help='crop height in pixels', + type=int, + ) + parser.add_argument( + '--scan_file_path', + metavar='SCAN_FILE', + help='scan file path', + type=argparse.FileType('r'), + required=True, + ) + parser.add_argument( + '--defocus_distance_m', + metavar='DISTANCE', + help='defocus distance in meters', + type=float, + ) + parser.add_argument( + '--probe_energy_eV', + metavar='ENERGY', + help='probe energy in electron volts', + type=float, + ) + parser.add_argument( + '--probe_photon_flux_Hz', + metavar='FLUX', + help='probe number of photons per second', + type=float, + ) + parser.add_argument( + '--exposure_time_s', + metavar='TIME', + help='exposure time in seconds', + type=float, + ) + parser.add_argument( + '--detector_distance_m', + metavar='DISTANCE', + help='detector distance in meters', + type=float, + ) + parser.add_argument( + '--number_of_gpus', + metavar='INTEGER', + help='number of GPUs to use in reconstruction', + type=int, + ) + parser.add_argument( + '--local_path_prefix', + metavar='PATH_PREFIX', + help='local posix path prefix', + type=DirectoryType(must_exist=True), + ) + parser.add_argument( + '--remote_path_prefix', + metavar='PATH_PREFIX', + help='remote posix path prefix', + type=DirectoryType(must_exist=False), + ) + parser.add_argument( + '-v', + '--version', + action='version', + version=versionString(), + ) + + args = parser.parse_args() + + if args.local_path_prefix is not None and args.remote_path_prefix is not None: + changePathPrefix = PathPrefixChange( + findPathPrefix=args.local_path_prefix, + replacementPathPrefix=args.remote_path_prefix, + ) + elif bool(args.local_path_prefix) ^ bool(args.remote_path_prefix): + parser.error('--local_path_prefix and --remote_path_prefix' + 'must be given together.') + + if args.crop_center_x_px is not None and args.crop_center_y_px is not None: + cropCenter = CropCenter( + positionXInPixels=args.crop_center_x_px, + positionYInPixels=args.crop_center_y_px, + ) + elif bool(args.crop_center_x_px) ^ bool(args.crop_center_y_px): + parser.error('--crop_center_x_px and --crop_center_y_px must be given together.') + + if args.crop_width_px is not None and args.crop_height_px is not None: + cropExtent = ImageExtent( + widthInPixels=args.crop_width_px, + heightInPixels=args.crop_height_px, + ) + elif bool(args.crop_width_px) ^ bool(args.crop_height_px): + parser.error('--crop_width_px and --crop_height_px must be given together.') + + if args.defocus_distance_m is not None: + logger.warning('Defocus distance is not implemented yet!') # TODO + + if args.number_of_gpus is not None: + logger.warning('Number of GPUs is not implemented yet!') # TODO + + with ModelCore(Path(args.settings.name), isDeveloperModeEnabled=args.dev) as model: + model.workflowAPI.openPatterns(Path(args.patterns_file_path.name), + cropCenter=cropCenter, + cropExtent=cropExtent) + + workflowProductAPI = model.workflowAPI.createProduct( + name=args.name, + comments=args.comment, + detectorDistanceInMeters=args.detector_distance_m, + probeEnergyInElectronVolts=args.probe_energy_eV, + probePhotonsPerSecond=args.probe_photon_flux_Hz, + exposureTimeInSeconds=args.exposure_time_s, + ) + workflowProductAPI.openScan(Path(args.scan_file_path.name)) + + stagingDir = args.output_directory + stagingDir.mkdir(parents=True, exist_ok=True) + model.workflowAPI.saveSettings(stagingDir / 'settings.ini', changePathPrefix) + model.workflowAPI.exportProcessedPatterns(stagingDir / 'patterns.npz') + workflowProductAPI.saveProduct(stagingDir / 'product-in.npz', fileType='NPZ') + + return 0 + + +sys.exit(main()) diff --git a/ptychodus/view/core.py b/ptychodus/view/core.py index 95aacdef..71c1076b 100644 --- a/ptychodus/view/core.py +++ b/ptychodus/view/core.py @@ -14,7 +14,7 @@ from .reconstructor import ReconstructorParametersView, ReconstructorPlotView from .repository import RepositoryTableView, RepositoryTreeView from .scan import ScanPlotView -from .settings import SettingsParametersView +from .settings import SettingsView from .workflow import WorkflowParametersView logger = logging.getLogger(__name__) @@ -35,8 +35,8 @@ def __init__(self, parent: QWidget | None) -> None: self.settingsAction = self.navigationToolBar.addAction(QIcon(':/icons/settings'), 'Settings') - self.settingsParametersView = SettingsParametersView.createInstance() - self.settingsEntryView = QTableView() + self.settingsView = SettingsView.createInstance() + self.settingsTableView = QTableView() self.patternsAction = self.navigationToolBar.addAction(QIcon(':/icons/patterns'), 'Patterns') @@ -98,7 +98,7 @@ def createInstance(cls, view.settingsAction.setChecked(True) # maintain same order as navigationToolBar buttons - view.parametersWidget.addWidget(view.settingsParametersView) + view.parametersWidget.addWidget(view.settingsView) view.parametersWidget.addWidget(view.patternsView) view.parametersWidget.addWidget(view.productView) view.parametersWidget.addWidget(view.scanView) @@ -111,7 +111,7 @@ def createInstance(cls, view.splitter.addWidget(view.parametersWidget) # maintain same order as navigationToolBar buttons - view.contentsWidget.addWidget(view.settingsEntryView) + view.contentsWidget.addWidget(view.settingsTableView) view.contentsWidget.addWidget(view.patternsImageView) view.contentsWidget.addWidget(view.productDiagramView) view.contentsWidget.addWidget(view.scanPlotView) diff --git a/ptychodus/view/settings.py b/ptychodus/view/settings.py index eec015dc..b997b054 100644 --- a/ptychodus/view/settings.py +++ b/ptychodus/view/settings.py @@ -1,27 +1,6 @@ from __future__ import annotations -from PyQt5.QtWidgets import (QFormLayout, QGroupBox, QHBoxLayout, QLineEdit, QListView, - QPushButton, QVBoxLayout, QWidget) - - -class SettingsView(QGroupBox): - - def __init__(self, parent: QWidget | None) -> None: - super().__init__('Parameters', parent) - self.replacementPathPrefixLineEdit = QLineEdit() - - @classmethod - def createInstance(cls, parent: QWidget | None = None) -> SettingsView: - view = cls(parent) - - view.replacementPathPrefixLineEdit.setToolTip( - 'Path prefix replacement text used when opening or saving settings files.') - - layout = QFormLayout() - layout.addRow('Replacement Path Prefix:', view.replacementPathPrefixLineEdit) - view.setLayout(layout) - - return view +from PyQt5.QtWidgets import QHBoxLayout, QListView, QPushButton, QVBoxLayout, QWidget class SettingsButtonBox(QWidget): @@ -44,15 +23,15 @@ def createInstance(cls, parent: QWidget | None = None) -> SettingsButtonBox: return view -class SettingsGroupView(QGroupBox): +class SettingsView(QWidget): def __init__(self, parent: QWidget | None) -> None: - super().__init__('Groups', parent) + super().__init__(parent) self.listView = QListView() self.buttonBox = SettingsButtonBox.createInstance() @classmethod - def createInstance(cls, parent: QWidget | None = None) -> SettingsGroupView: + def createInstance(cls, parent: QWidget | None = None) -> SettingsView: view = cls(parent) layout = QVBoxLayout() @@ -61,22 +40,3 @@ def createInstance(cls, parent: QWidget | None = None) -> SettingsGroupView: view.setLayout(layout) return view - - -class SettingsParametersView(QWidget): - - def __init__(self, parent: QWidget | None) -> None: - super().__init__(parent) - self.settingsView = SettingsView.createInstance() - self.groupView = SettingsGroupView.createInstance() - - @classmethod - def createInstance(cls, parent: QWidget | None = None) -> SettingsParametersView: - view = cls(parent) - - layout = QVBoxLayout() - layout.addWidget(view.settingsView) - layout.addWidget(view.groupView) - view.setLayout(layout) - - return view