diff --git a/README.rst b/README.rst index 781d2bd1..ae05c0e9 100644 --- a/README.rst +++ b/README.rst @@ -78,7 +78,7 @@ Developer Installation .. code-block:: shell - $ conda install -n ptychodus -c conda-forge ptychonn + $ conda install -n ptychodus -c conda-forge ptychonn pytorch-gpu * To launch the `ptychodus` GUI (with the "ptychodus" conda environment activated): diff --git a/ptychodus/api/data.py b/ptychodus/api/data.py index aaa2db3a..c5d44df5 100644 --- a/ptychodus/api/data.py +++ b/ptychodus/api/data.py @@ -86,6 +86,7 @@ class DiffractionMetadata: detectorDistanceInMeters: Optional[Decimal] = None detectorExtentInPixels: Optional[ImageExtent] = None detectorPixelGeometry: Optional[PixelGeometry] = None + detectorBitDepth: Optional[int] = None cropCenterInPixels: Optional[Array2D[int]] = None probeEnergyInElectronVolts: Optional[Decimal] = None filePath: Optional[Path] = None diff --git a/ptychodus/controller/data/file.py b/ptychodus/controller/data/file.py index c655b0e5..c855bf9d 100644 --- a/ptychodus/controller/data/file.py +++ b/ptychodus/controller/data/file.py @@ -3,7 +3,7 @@ import logging import re -from PyQt5.QtCore import Qt, QDir, QModelIndex, QSortFilterProxyModel +from PyQt5.QtCore import Qt, QDir, QFileInfo, QModelIndex, QSortFilterProxyModel from PyQt5.QtWidgets import QAbstractItemView, QFileSystemModel from ...api.observer import Observable, Observer @@ -49,8 +49,12 @@ def createInstance(cls, presenter: DiffractionDatasetInputOutputPresenter, controller._setNameFiltersInFileSystemModel) controller._syncModelToView() - controller._fileSystemModel.rootPathChanged.connect( - view.contentsView.filePathLineEdit.setText) + view.contentsView.directoryComboBox.addItem( + str(fileDialogFactory.getOpenWorkingDirectory())) + view.contentsView.directoryComboBox.addItem(str(Path.home())) + view.contentsView.directoryComboBox.setEditable(True) + view.contentsView.directoryComboBox.textActivated.connect( + controller._handleDirectoryComboBoxActivated) controller._setRootPath(fileDialogFactory.getOpenWorkingDirectory()) view.contentsView.fileSystemTableView.doubleClicked.connect( @@ -68,6 +72,14 @@ def _setRootPath(self, rootPath: Path) -> None: index = self._fileSystemModel.setRootPath(str(rootPath)) proxyIndex = self._fileSystemProxyModel.mapFromSource(index) self._view.contentsView.fileSystemTableView.setRootIndex(proxyIndex) + self._view.contentsView.directoryComboBox.setCurrentText(str(rootPath)) + self._fileDialogFactory.setOpenWorkingDirectory(rootPath) + + def _handleDirectoryComboBoxActivated(self, text: str) -> None: + fileInfo = QFileInfo(text) + + if fileInfo.isDir(): + self._setRootPath(Path(fileInfo.canonicalFilePath())) def _handleFileSystemTableDoubleClicked(self, proxyIndex: QModelIndex) -> None: index = self._fileSystemProxyModel.mapToSource(proxyIndex) diff --git a/ptychodus/controller/data/metadata.py b/ptychodus/controller/data/metadata.py index 10d4a1ef..a731d6a8 100644 --- a/ptychodus/controller/data/metadata.py +++ b/ptychodus/controller/data/metadata.py @@ -31,6 +31,9 @@ def _importMetadata(self) -> None: if self._view.contentsView.detectorPixelSizeCheckBox.isChecked(): self._presenter.syncDetectorPixelSize() + if self._view.contentsView.detectorBitDepthCheckBox.isChecked(): + self._presenter.syncDetectorBitDepth() + if self._view.contentsView.detectorDistanceCheckBox.isChecked(): self._presenter.syncDetectorDistance() @@ -50,6 +53,10 @@ def _syncModelToView(self) -> None: self._view.contentsView.detectorPixelSizeCheckBox.setVisible(canSyncDetectorPixelSize) self._view.contentsView.detectorPixelSizeCheckBox.setChecked(canSyncDetectorPixelSize) + canSyncDetectorBitDepth = self._presenter.canSyncDetectorBitDepth() + self._view.contentsView.detectorBitDepthCheckBox.setVisible(canSyncDetectorBitDepth) + self._view.contentsView.detectorBitDepthCheckBox.setChecked(canSyncDetectorBitDepth) + canSyncDetectorDistance = self._presenter.canSyncDetectorDistance() self._view.contentsView.detectorDistanceCheckBox.setVisible(canSyncDetectorDistance) self._view.contentsView.detectorDistanceCheckBox.setChecked(canSyncDetectorDistance) diff --git a/ptychodus/controller/data/patterns.py b/ptychodus/controller/data/patterns.py index 3d0517b5..58c96d9f 100644 --- a/ptychodus/controller/data/patterns.py +++ b/ptychodus/controller/data/patterns.py @@ -157,8 +157,10 @@ def createInstance(cls, presenter: DiffractionPatternPresenter, controller = cls(presenter, view) presenter.addObserver(controller) - view.thresholdCheckBox.toggled.connect(presenter.setThresholdEnabled) - view.thresholdSpinBox.valueChanged.connect(presenter.setThresholdValue) + view.valueLowerBoundCheckBox.toggled.connect(presenter.setValueLowerBoundEnabled) + view.valueLowerBoundSpinBox.valueChanged.connect(presenter.setValueLowerBound) + view.valueUpperBoundCheckBox.toggled.connect(presenter.setValueUpperBoundEnabled) + view.valueUpperBoundSpinBox.valueChanged.connect(presenter.setValueUpperBound) view.flipXCheckBox.toggled.connect(presenter.setFlipXEnabled) view.flipYCheckBox.toggled.connect(presenter.setFlipYEnabled) @@ -166,13 +168,23 @@ def createInstance(cls, presenter: DiffractionPatternPresenter, return controller def _syncModelToView(self) -> None: - self._view.thresholdCheckBox.setChecked(self._presenter.isThresholdEnabled()) - - self._view.thresholdSpinBox.blockSignals(True) - self._view.thresholdSpinBox.setRange(self._presenter.getThresholdValueLimits().lower, - self._presenter.getThresholdValueLimits().upper) - self._view.thresholdSpinBox.setValue(self._presenter.getThresholdValue()) - self._view.thresholdSpinBox.blockSignals(False) + self._view.valueLowerBoundCheckBox.setChecked(self._presenter.isValueLowerBoundEnabled()) + + self._view.valueLowerBoundSpinBox.blockSignals(True) + self._view.valueLowerBoundSpinBox.setRange( + self._presenter.getValueLowerBoundLimits().lower, + self._presenter.getValueLowerBoundLimits().upper) + self._view.valueLowerBoundSpinBox.setValue(self._presenter.getValueLowerBound()) + self._view.valueLowerBoundSpinBox.blockSignals(False) + + self._view.valueUpperBoundCheckBox.setChecked(self._presenter.isValueUpperBoundEnabled()) + + self._view.valueUpperBoundSpinBox.blockSignals(True) + self._view.valueUpperBoundSpinBox.setRange( + self._presenter.getValueUpperBoundLimits().lower, + self._presenter.getValueUpperBoundLimits().upper) + self._view.valueUpperBoundSpinBox.setValue(self._presenter.getValueUpperBound()) + self._view.valueUpperBoundSpinBox.blockSignals(False) self._view.flipXCheckBox.setChecked(self._presenter.isFlipXEnabled()) self._view.flipYCheckBox.setChecked(self._presenter.isFlipYEnabled()) diff --git a/ptychodus/controller/detector/parameters.py b/ptychodus/controller/detector/parameters.py index 577881cb..81934007 100644 --- a/ptychodus/controller/detector/parameters.py +++ b/ptychodus/controller/detector/parameters.py @@ -26,6 +26,7 @@ def createInstance(cls, presenter: DetectorPresenter, apparatusPresenter: Appara view.numberOfPixelsYSpinBox.valueChanged.connect(presenter.setNumberOfPixelsY) view.pixelSizeXWidget.lengthChanged.connect(presenter.setPixelSizeXInMeters) view.pixelSizeYWidget.lengthChanged.connect(presenter.setPixelSizeYInMeters) + view.bitDepthSpinBox.valueChanged.connect(presenter.setBitDepth) view.detectorDistanceWidget.lengthChanged.connect(presenter.setDetectorDistanceInMeters) controller._syncModelToView() @@ -49,6 +50,13 @@ def _syncModelToView(self) -> None: self._view.pixelSizeXWidget.setLengthInMeters(self._presenter.getPixelSizeXInMeters()) self._view.pixelSizeYWidget.setLengthInMeters(self._presenter.getPixelSizeYInMeters()) + + self._view.bitDepthSpinBox.blockSignals(True) + self._view.bitDepthSpinBox.setRange(self._presenter.getBitDepthLimits().lower, + self._presenter.getBitDepthLimits().upper) + self._view.bitDepthSpinBox.setValue(self._presenter.getBitDepth()) + self._view.bitDepthSpinBox.blockSignals(False) + self._view.detectorDistanceWidget.setLengthInMeters( self._presenter.getDetectorDistanceInMeters()) diff --git a/ptychodus/controller/reconstructor.py b/ptychodus/controller/reconstructor.py index 95b7b6bd..a5684e5e 100644 --- a/ptychodus/controller/reconstructor.py +++ b/ptychodus/controller/reconstructor.py @@ -210,7 +210,10 @@ def _redrawPlot(self) -> None: ax.set_xlabel(axisX.label) ax.set_ylabel(axisY.label) ax.grid(True) - ax.legend(loc='best') + + if len(axisX.series) > 0: + ax.legend(loc='upper right') + self._plotView.figureCanvas.draw() def _syncModelToView(self) -> None: diff --git a/ptychodus/controller/scan/transformController.py b/ptychodus/controller/scan/transformController.py index 159f5434..9aa1da6d 100644 --- a/ptychodus/controller/scan/transformController.py +++ b/ptychodus/controller/scan/transformController.py @@ -28,7 +28,11 @@ def createInstance(cls, item: ScanRepositoryItem, view.transformComboBox.currentTextChanged.connect(item.setTransformByName) view.jitterRadiusWidget.lengthChanged.connect(item.setJitterRadiusInMeters) + + view.centroidXCheckBox.toggled.connect(item.setOverrideCentroidXEnabled) view.centroidXWidget.lengthChanged.connect(item.setCentroidXInMeters) + + view.centroidYCheckBox.toggled.connect(item.setOverrideCentroidYEnabled) view.centroidYWidget.lengthChanged.connect(item.setCentroidYInMeters) controller._syncModelToView() @@ -39,7 +43,13 @@ def _syncModelToView(self) -> None: self._view.indexFilterComboBox.setCurrentText(self._item.getIndexFilterName()) self._view.transformComboBox.setCurrentText(self._item.getTransformName()) self._view.jitterRadiusWidget.setLengthInMeters(self._item.getJitterRadiusInMeters()) + + self._view.centroidXCheckBox.setChecked(self._item.isOverrideCentroidXEnabled) + self._view.centroidXWidget.setEnabled(self._item.isOverrideCentroidXEnabled) self._view.centroidXWidget.setLengthInMeters(self._item.getCentroidXInMeters()) + + self._view.centroidYCheckBox.setChecked(self._item.isOverrideCentroidYEnabled) + self._view.centroidYWidget.setEnabled(self._item.isOverrideCentroidYEnabled) self._view.centroidYWidget.setLengthInMeters(self._item.getCentroidYInMeters()) def update(self, observable: Observable) -> None: diff --git a/ptychodus/model/core.py b/ptychodus/model/core.py index 779eb1e7..27272771 100644 --- a/ptychodus/model/core.py +++ b/ptychodus/model/core.py @@ -48,6 +48,7 @@ def configureLogger() -> None: encoding='utf-8', level=logging.DEBUG) logging.getLogger('matplotlib').setLevel(logging.WARNING) + logging.getLogger('tike').setLevel(logging.WARNING) logger.info(f'Ptychodus {version("ptychodus")}') logger.info(f'NumPy {version("numpy")}') diff --git a/ptychodus/model/data/active.py b/ptychodus/model/data/active.py index 34dab48b..b45636c1 100644 --- a/ptychodus/model/data/active.py +++ b/ptychodus/model/data/active.py @@ -106,9 +106,14 @@ def insertArray(self, array: DiffractionPatternArray) -> None: if array.getState() == DiffractionPatternState.LOADED: data = self._diffractionPatternSizer(array.getData()) - if self._patternSettings.thresholdEnabled.value: - thresholdValue = self._patternSettings.thresholdValue.value - data[data < thresholdValue] = thresholdValue + if self._patternSettings.valueUpperBoundEnabled.value: + valueLowerBound = self._patternSettings.valueLowerBound.value + valueUpperBound = self._patternSettings.valueUpperBound.value + data[data >= valueUpperBound] = valueLowerBound + + if self._patternSettings.valueLowerBoundEnabled.value: + valueLowerBound = self._patternSettings.valueLowerBound.value + data[data < valueLowerBound] = valueLowerBound if self._patternSettings.flipXEnabled.value: data = numpy.flip(data, axis=-1) diff --git a/ptychodus/model/data/patterns.py b/ptychodus/model/data/patterns.py index 6145788c..e99cc68b 100644 --- a/ptychodus/model/data/patterns.py +++ b/ptychodus/model/data/patterns.py @@ -77,20 +77,35 @@ def isFlipYEnabled(self) -> bool: def setFlipYEnabled(self, value: bool) -> None: self._settings.flipYEnabled.value = value - def isThresholdEnabled(self) -> bool: - return self._settings.thresholdEnabled.value + def isValueLowerBoundEnabled(self) -> bool: + return self._settings.valueLowerBoundEnabled.value - def setThresholdEnabled(self, value: bool) -> None: - self._settings.thresholdEnabled.value = value + def setValueLowerBoundEnabled(self, value: bool) -> None: + self._settings.valueLowerBoundEnabled.value = value - def getThresholdValueLimits(self) -> Interval[int]: + def getValueLowerBoundLimits(self) -> Interval[int]: return Interval[int](0, self.MAX_INT) - def getThresholdValue(self) -> int: - return self._settings.thresholdValue.value + def getValueLowerBound(self) -> int: + return self._settings.valueLowerBound.value - def setThresholdValue(self, value: int) -> None: - self._settings.thresholdValue.value = value + def setValueLowerBound(self, value: int) -> None: + self._settings.valueLowerBound.value = value + + def isValueUpperBoundEnabled(self) -> bool: + return self._settings.valueUpperBoundEnabled.value + + def setValueUpperBoundEnabled(self, value: bool) -> None: + self._settings.valueUpperBoundEnabled.value = value + + def getValueUpperBoundLimits(self) -> Interval[int]: + return Interval[int](0, self.MAX_INT) + + def getValueUpperBound(self) -> int: + return self._settings.valueUpperBound.value + + def setValueUpperBound(self, value: int) -> None: + self._settings.valueUpperBound.value = value def update(self, observable: Observable) -> None: if observable is self._sizer: diff --git a/ptychodus/model/data/settings.py b/ptychodus/model/data/settings.py index 24143ac1..8bea35d6 100644 --- a/ptychodus/model/data/settings.py +++ b/ptychodus/model/data/settings.py @@ -40,8 +40,12 @@ def __init__(self, settingsGroup: SettingsGroup) -> None: self.cropExtentYInPixels = settingsGroup.createIntegerEntry('CropExtentYInPixels', 64) self.flipXEnabled = settingsGroup.createBooleanEntry('FlipXEnabled', False) self.flipYEnabled = settingsGroup.createBooleanEntry('FlipYEnabled', False) - self.thresholdEnabled = settingsGroup.createBooleanEntry('ThresholdEnabled', False) - self.thresholdValue = settingsGroup.createIntegerEntry('ThresholdValue', 0) + self.valueLowerBoundEnabled = settingsGroup.createBooleanEntry( + 'ValueLowerBoundEnabled', False) + self.valueLowerBound = settingsGroup.createIntegerEntry('ValueLowerBound', 0) + self.valueUpperBoundEnabled = settingsGroup.createBooleanEntry( + 'ValueUpperBoundEnabled', False) + self.valueUpperBound = settingsGroup.createIntegerEntry('ValueUpperBound', 65535) @classmethod def createInstance(cls, settingsRegistry: SettingsRegistry) -> DiffractionPatternSettings: diff --git a/ptychodus/model/detector.py b/ptychodus/model/detector.py index 3e7f8323..b7e6dfe1 100644 --- a/ptychodus/model/detector.py +++ b/ptychodus/model/detector.py @@ -18,6 +18,7 @@ def __init__(self, settingsGroup: SettingsGroup) -> None: self.pixelSizeXInMeters = settingsGroup.createRealEntry('PixelSizeXInMeters', '75e-6') self.numberOfPixelsY = settingsGroup.createIntegerEntry('NumberOfPixelsY', 1024) self.pixelSizeYInMeters = settingsGroup.createRealEntry('PixelSizeYInMeters', '75e-6') + self.bitDepth = settingsGroup.createIntegerEntry('BitDepth', 8) self.detectorDistanceInMeters = settingsGroup.createRealEntry( 'DetectorDistanceInMeters', '2') @@ -56,6 +57,9 @@ def getPixelGeometry(self) -> PixelGeometry: heightInMeters=max(Decimal(), self._settings.pixelSizeYInMeters.value), ) + def getBitDepth(self) -> int: + return max(1, self._settings.bitDepth.value) + def getDetectorDistanceInMeters(self) -> Decimal: return max(Decimal(), self._settings.detectorDistanceInMeters.value) @@ -108,6 +112,16 @@ def getPixelSizeYInMeters(self) -> Decimal: def setPixelSizeYInMeters(self, value: Decimal) -> None: self._settings.pixelSizeYInMeters.value = value + def getBitDepthLimits(self) -> Interval[int]: + return Interval[int](1, 64) + + def getBitDepth(self) -> int: + limits = self.getBitDepthLimits() + return limits.clamp(self._settings.bitDepth.value) + + def setBitDepth(self, value: int) -> None: + self._settings.bitDepth.value = value + def getDetectorDistanceInMeters(self) -> Decimal: return self._detector.getDetectorDistanceInMeters() diff --git a/ptychodus/model/metadata.py b/ptychodus/model/metadata.py index 8fbf6d7e..f02542e9 100644 --- a/ptychodus/model/metadata.py +++ b/ptychodus/model/metadata.py @@ -54,6 +54,13 @@ def syncDetectorPixelSize(self) -> None: self._detectorSettings.pixelSizeYInMeters.value = \ self._metadata.detectorPixelGeometry.heightInMeters + def canSyncDetectorBitDepth(self) -> bool: + return (self._metadata.detectorBitDepth is not None) + + def syncDetectorBitDepth(self) -> None: + if self._metadata.detectorBitDepth: + self._detectorSettings.bitDepth.value = self._metadata.detectorBitDepth + def canSyncDetectorDistance(self) -> bool: return (self._metadata.detectorDistanceInMeters is not None) diff --git a/ptychodus/model/object/core.py b/ptychodus/model/object/core.py index bbfc93d4..f7e1f107 100644 --- a/ptychodus/model/object/core.py +++ b/ptychodus/model/object/core.py @@ -146,14 +146,8 @@ def getSelectedObject(self) -> str: return self._object.getSelectedName() def getSelectedObjectArray(self) -> Optional[ObjectArrayType]: - array: Optional[ObjectArrayType] = None - - try: - array = self._objectAPI.getSelectedObjectArray() - except ValueError as err: - logger.debug(err) - - return array + selectedObject = self._object.getSelectedItem() + return None if selectedObject is None else selectedObject.getArray() def getSelectableNames(self) -> Sequence[str]: return self._object.getSelectableNames() diff --git a/ptychodus/model/probe/repository.py b/ptychodus/model/probe/repository.py index bad963c2..f0f7e8ab 100644 --- a/ptychodus/model/probe/repository.py +++ b/ptychodus/model/probe/repository.py @@ -104,15 +104,16 @@ def reinitialize(self) -> None: return try: - initialProbe = self._initializer() + array = self._initializer() except Exception: logger.exception('Failed to reinitialize probe!') return - numberOfModes = max(self._numberOfModes, 1) - array = self._modesFactory.build(initialProbe, numberOfModes, - self._orthogonalizeModesEnabled, self._modeDecayType, - self._modeDecayRatio) + if self._numberOfModes > 0: + array = self._modesFactory.build(array, self._numberOfModes, + self._orthogonalizeModesEnabled, self._modeDecayType, + self._modeDecayRatio) + self._setArray(array) def getInitializerSimpleName(self) -> str: @@ -170,7 +171,7 @@ def getNumberOfModesLimits(self) -> Interval[int]: return Interval[int](1, self.MAX_INT) def getNumberOfModes(self) -> int: - return self._array.shape[0] + return self._array.shape[-3] def setNumberOfModes(self, number: int) -> None: if self._numberOfModes != number: @@ -210,7 +211,7 @@ def getMode(self, mode: int) -> ProbeArrayType: return self._array[mode, :, :] def getModesFlattened(self) -> ProbeArrayType: - return self._array.transpose((1, 0, 2)).reshape(self._array.shape[1], -1) + return self._array.transpose((1, 0, 2)).reshape(self._array.shape[-2], -1) def getModeRelativePower(self, mode: int) -> Decimal: if numpy.isnan(self._array).any(): diff --git a/ptychodus/model/scan/repository.py b/ptychodus/model/scan/repository.py index a9ba3c89..4fceb97d 100644 --- a/ptychodus/model/scan/repository.py +++ b/ptychodus/model/scan/repository.py @@ -8,6 +8,7 @@ import numpy +from ...api.geometry import Interval from ...api.observer import Observable, Observer from ...api.scan import Scan, ScanPoint, TabularScan from ..itemRepository import ItemRepository @@ -48,7 +49,6 @@ def __call__(self) -> Scan: class ScanRepositoryItem(Scan, Observable, Observer): - '''container for items that can be stored in a scan repository''' def __init__(self, rng: numpy.random.Generator, @@ -63,9 +63,49 @@ def __init__(self, self._transform.addObserver(self) self._indexFilter = SelectableScanIndexFilter() self._indexFilter.addObserver(self) + self._overrideCentroidXEnabled = False + self._overrideCentroidXInMeters = Decimal() + self._overrideCentroidYEnabled = False + self._overrideCentroidYInMeters = Decimal() self._jitterRadiusInMeters = Decimal() - self._centroidX = Decimal() - self._centroidY = Decimal() + + self._cachedNumberOfPoints = 0 + self._cachedLengthInMeters = 0. + self._cachedCentroidXInMeters = 0. + self._cachedCentroidYInMeters = 0. + self._cachedSizeInBytes = 0 + + self._updateCacheAndNotifyObservers() + + def _updateCacheAndNotifyObservers(self) -> None: + pointList = [point for point in self.values()] + lengthInMeters = 0. + + try: + point = pointList[0] + except IndexError: + self._cachedCentroidXInMeters = 0. + self._cachedCentroidYInMeters = 0. + logger.debug('Scan is empty!') + else: + rangeX = Interval[float](point.x, point.x) + rangeY = Interval[float](point.y, point.y) + + for pointA, pointB in zip(pointList[:-1], pointList[1:]): + rangeX = rangeX.hull(pointB.x) + rangeY = rangeY.hull(pointB.y) + + dx = pointB.x - pointA.x + dy = pointB.y - pointA.y + lengthInMeters += numpy.hypot(dx, dy) + + self._cachedCentroidXInMeters = rangeX.midrange + self._cachedCentroidYInMeters = rangeY.midrange + + self._cachedNumberOfPoints = len(pointList) + self._cachedLengthInMeters = lengthInMeters + self._cachedSizeInBytes = self._getSizeInBytes() + self.notifyObservers() @property def nameHint(self) -> str: @@ -83,17 +123,15 @@ def reinitialize(self) -> None: except Exception: logger.exception('Failed to reinitialize scan!') else: - self.notifyObservers() + self._updateCacheAndNotifyObservers() def getInitializerSimpleName(self) -> str: return 'FromMemory' if self._initializer is None else self._initializer.simpleName def getInitializer(self) -> Optional[ScanInitializer]: - '''returns the initializer''' return self._initializer def setInitializer(self, initializer: ScanInitializer) -> None: - '''sets the initializer''' if self._initializer is not None: self._initializer.removeObserver(self) @@ -105,16 +143,24 @@ def syncFromSettings(self, settings: ScanSettings) -> None: self._indexFilter.selectFilterByName(settings.indexFilter.value) self._transform.selectTransformByName(settings.transform.value) self._jitterRadiusInMeters = settings.jitterRadiusInMeters.value - self._centroidX = settings.centroidXInMeters.value - self._centroidY = settings.centroidYInMeters.value + self._overrideCentroidXEnabled = settings.overrideCentroidXEnabled.value + self._overrideCentroidXInMeters = settings.overrideCentroidXInMeters.value + self._overrideCentroidYEnabled = settings.overrideCentroidYEnabled.value + self._overrideCentroidYInMeters = settings.overrideCentroidYInMeters.value self.notifyObservers() def syncToSettings(self, settings: ScanSettings) -> None: settings.indexFilter.value = self._indexFilter.simpleName settings.transform.value = self._transform.simpleName settings.jitterRadiusInMeters.value = self._jitterRadiusInMeters - settings.centroidXInMeters.value = self._centroidX - settings.centroidYInMeters.value = self._centroidX + settings.overrideCentroidXEnabled.value = self._overrideCentroidXEnabled + settings.overrideCentroidXInMeters.value = self._overrideCentroidXInMeters + settings.overrideCentroidYEnabled.value = self._overrideCentroidYEnabled + settings.overrideCentroidYInMeters.value = self._overrideCentroidYInMeters + + @property + def untransformed(self) -> Scan: + return self._scan def __iter__(self) -> Iterator[int]: it = iter(self._scan) @@ -134,6 +180,14 @@ def __getitem__(self, index: int) -> ScanPoint: raise KeyError point = self._transform(self._scan[index]) + posX = point.x + posY = point.y + + if self.isOverrideCentroidXEnabled: + posX += float(self._overrideCentroidXInMeters) - self._cachedCentroidXInMeters + + if self.isOverrideCentroidYEnabled: + posY += float(self._overrideCentroidYInMeters) - self._cachedCentroidYInMeters if self._jitterRadiusInMeters > Decimal(): rad = self._rng.uniform() @@ -141,33 +195,18 @@ def __getitem__(self, index: int) -> ScanPoint: dirY = self._rng.normal() scalar = float(self._jitterRadiusInMeters) * numpy.sqrt(rad / (dirX**2 + dirY**2)) - point = ScanPoint(point.x + scalar * dirX, point.y + scalar * dirY) + posX += scalar * dirX + posY += scalar * dirY - return ScanPoint( - float(self._centroidX) + point.x, - float(self._centroidY) + point.y, - ) + return ScanPoint(posX, posY) def __len__(self) -> int: - return sum(1 for index in iter(self)) - - @property - def untransformed(self) -> Scan: - return self._scan + return self._cachedNumberOfPoints def getLengthInMeters(self) -> float: - pointList = [point for point in self._scan.values()] - lengthInMeters = 0. + return self._cachedLengthInMeters - for pointA, pointB in zip(pointList[:-1], pointList[1:]): - dx = pointB.x - pointA.x - dy = pointB.y - pointA.y - lengthInMeters += numpy.hypot(dx, dy) - - return lengthInMeters - - def getSizeInBytes(self) -> int: - # TODO verify getSizeInBytes; implement __sizeof__ operator in scan class + def _getSizeInBytes(self) -> int: sizeInBytes = sys.getsizeof(self._scan) for index, point in self._scan.items(): @@ -176,6 +215,9 @@ def getSizeInBytes(self) -> int: return sizeInBytes + def getSizeInBytes(self) -> int: + return self._cachedSizeInBytes + def getIndexFilterNameList(self) -> Sequence[str]: return self._indexFilter.getSelectableFilters() @@ -195,33 +237,47 @@ def setTransformByName(self, name: str) -> None: self._transform.selectTransformByName(name) def getJitterRadiusInMeters(self) -> Decimal: - '''gets the jitter radius''' return self._jitterRadiusInMeters def setJitterRadiusInMeters(self, jitterRadiusInMeters: Decimal) -> None: - '''sets the jitter radius''' if self._jitterRadiusInMeters != jitterRadiusInMeters: self._jitterRadiusInMeters = jitterRadiusInMeters self.notifyObservers() + @property + def isOverrideCentroidXEnabled(self) -> bool: + return self._overrideCentroidXEnabled + + def setOverrideCentroidXEnabled(self, enabled: bool) -> None: + if self._overrideCentroidXEnabled != enabled: + self._overrideCentroidXEnabled = enabled + self.notifyObservers() + def getCentroidXInMeters(self) -> Decimal: - '''gets the x centroid''' - return self._centroidX + return self._overrideCentroidXInMeters if self._overrideCentroidXEnabled \ + else Decimal(repr(self._cachedCentroidXInMeters)) def setCentroidXInMeters(self, value: Decimal) -> None: - '''sets the x centroid''' - if self._centroidX != value: - self._centroidX = value + if self._overrideCentroidXInMeters != value: + self._overrideCentroidXInMeters = value + self.notifyObservers() + + @property + def isOverrideCentroidYEnabled(self) -> bool: + return self._overrideCentroidYEnabled + + def setOverrideCentroidYEnabled(self, enabled: bool) -> None: + if self._overrideCentroidYEnabled != enabled: + self._overrideCentroidYEnabled = enabled self.notifyObservers() def getCentroidYInMeters(self) -> Decimal: - '''gets the y centroid''' - return self._centroidY + return self._overrideCentroidYInMeters if self._overrideCentroidYEnabled \ + else Decimal(repr(self._cachedCentroidYInMeters)) def setCentroidYInMeters(self, value: Decimal) -> None: - '''sets the y centroid''' - if self._centroidY != value: - self._centroidY = value + if self._overrideCentroidYInMeters != value: + self._overrideCentroidYInMeters = value self.notifyObservers() def update(self, observable: Observable) -> None: diff --git a/ptychodus/model/scan/settings.py b/ptychodus/model/scan/settings.py index 41da98c1..8ee0ec60 100644 --- a/ptychodus/model/scan/settings.py +++ b/ptychodus/model/scan/settings.py @@ -19,8 +19,14 @@ def __init__(self, settingsGroup: SettingsGroup) -> None: self.angularShiftInTurns = settingsGroup.createRealEntry('AngularShiftInTurns', '0.25') self.angularStepXInTurns = settingsGroup.createRealEntry('AngularStepXInTurns', '0.03') self.angularStepYInTurns = settingsGroup.createRealEntry('AngularStepYInTurns', '0.04') - self.centroidXInMeters = settingsGroup.createRealEntry('CentroidXInMeters', '0') - self.centroidYInMeters = settingsGroup.createRealEntry('CentroidYInMeters', '0') + self.overrideCentroidXEnabled = settingsGroup.createBooleanEntry( + 'OverrideCentroidXEnabled', False) + self.overrideCentroidXInMeters = settingsGroup.createRealEntry( + 'OverrideCentroidXInMeters', '0') + self.overrideCentroidYEnabled = settingsGroup.createBooleanEntry( + 'OverrideCentroidYEnabled', False) + self.overrideCentroidYInMeters = settingsGroup.createRealEntry( + 'OverrideCentroidYInMeters', '0') self.jitterRadiusInMeters = settingsGroup.createRealEntry('JitterRadiusInMeters', '0') self.numberOfPointsX = settingsGroup.createIntegerEntry('NumberOfPointsX', 10) self.numberOfPointsY = settingsGroup.createIntegerEntry('NumberOfPointsY', 10) diff --git a/ptychodus/model/tike/core.py b/ptychodus/model/tike/core.py index bcf81cad..4bc2a460 100644 --- a/ptychodus/model/tike/core.py +++ b/ptychodus/model/tike/core.py @@ -57,7 +57,10 @@ def setNumBatch(self, value: int) -> None: self._settings.numBatch.value = value def getBatchMethodList(self) -> Sequence[str]: - return ['by_scan_grid', 'by_scan_stripes', 'wobbly_center', 'compact'] + return [ + 'by_scan_grid', 'by_scan_stripes', 'wobbly_center', 'wobbly_center_random_bootstrap', + 'compact' + ] def getBatchMethod(self) -> str: return self._settings.batchMethod.value diff --git a/ptychodus/plugins/aps2idDiffractionFile.py b/ptychodus/plugins/aps2idDiffractionFile.py new file mode 100644 index 00000000..395526cd --- /dev/null +++ b/ptychodus/plugins/aps2idDiffractionFile.py @@ -0,0 +1,77 @@ +from collections.abc import Mapping +from pathlib import Path +import logging +import re + +import h5py + +from ptychodus.api.data import (DiffractionDataset, DiffractionFileReader, DiffractionMetadata, + DiffractionPatternArray, SimpleDiffractionDataset) +from ptychodus.api.image import ImageExtent +from ptychodus.api.plugins import PluginRegistry +from ptychodus.api.tree import SimpleTreeNode +from .h5DiffractionFile import H5DiffractionPatternArray + +logger = logging.getLogger(__name__) + + +class APS2IDDiffractionFileReader(DiffractionFileReader): + + def _getFileSeries(self, filePath: Path) -> tuple[Mapping[int, Path], str]: + filePathDict: dict[int, Path] = dict() + + digits = re.findall(r'\d+', filePath.stem) + longest_digits = max(digits, key=len) + filePattern = filePath.name.replace(longest_digits, f'(\\d{{{len(longest_digits)}}})') + + for fp in filePath.parent.iterdir(): + z = re.match(filePattern, fp.name) + + if z: + index = int(z.group(1)) + filePathDict[index] = fp + + return filePathDict, filePattern + + def read(self, filePath: Path) -> DiffractionDataset: + dataset = SimpleDiffractionDataset.createNullInstance(filePath) + dataPath = '/entry/data/data' + + filePathMapping, filePattern = self._getFileSeries(filePath) + contentsTree = SimpleTreeNode.createRoot(['Name', 'Type', 'Details']) + arrayList: list[DiffractionPatternArray] = list() + + for idx, fp in sorted(filePathMapping.items()): + array = H5DiffractionPatternArray(fp.stem, idx, fp, dataPath) + contentsTree.createChild([array.getLabel(), 'HDF5', str(idx)]) + arrayList.append(array) + + try: + with h5py.File(filePath, 'r') as h5File: + try: + h5data = h5File[dataPath] + except KeyError: + logger.info(f'File {filePath} is not an APS 2-ID data file.') + else: + numberOfPatternsPerArray, detectorHeight, detectorWidth = h5data.shape + metadata = DiffractionMetadata( + numberOfPatternsPerArray=numberOfPatternsPerArray, + numberOfPatternsTotal=numberOfPatternsPerArray * len(arrayList), + patternDataType=h5data.dtype, + detectorExtentInPixels=ImageExtent(detectorWidth, detectorHeight), + filePath=filePath.parent / filePattern, + ) + + dataset = SimpleDiffractionDataset(metadata, contentsTree, arrayList) + except OSError: + logger.debug(f'Unable to read file \"{filePath}\".') + + return dataset + + +def registerPlugins(registry: PluginRegistry) -> None: + registry.diffractionFileReaders.registerPlugin( + APS2IDDiffractionFileReader(), + simpleName='2ID', + displayName='APS 2-ID HDF5 Files (*.h5 *.hdf5)', + ) diff --git a/ptychodus/plugins/matObjectFile.py b/ptychodus/plugins/matObjectFile.py index 8a59912d..fac514e8 100644 --- a/ptychodus/plugins/matObjectFile.py +++ b/ptychodus/plugins/matObjectFile.py @@ -1,6 +1,5 @@ from pathlib import Path -import numpy import scipy.io from ptychodus.api.object import ObjectArrayType, ObjectFileReader, ObjectFileWriter @@ -11,15 +10,13 @@ class MATObjectFileReader(ObjectFileReader): def read(self, filePath: Path) -> ObjectArrayType: matDict = scipy.io.loadmat(filePath) - array = matDict['object'] - return numpy.transpose(array, [x for x in reversed(range(array.ndim))]) + return matDict['object'] class MATObjectFileWriter(ObjectFileWriter): def write(self, filePath: Path, array: ObjectArrayType) -> None: - object_ = numpy.transpose(array, [x for x in reversed(range(array.ndim))]) - matDict = {'object': object_} + matDict = {'object': array} scipy.io.savemat(filePath, matDict) diff --git a/ptychodus/plugins/matProbeFile.py b/ptychodus/plugins/matProbeFile.py index 958284f0..e1645819 100644 --- a/ptychodus/plugins/matProbeFile.py +++ b/ptychodus/plugins/matProbeFile.py @@ -12,14 +12,23 @@ class MATProbeFileReader(ProbeFileReader): def read(self, filePath: Path) -> ProbeArrayType: matDict = scipy.io.loadmat(filePath) probes = matDict['probe'] - return numpy.transpose(probes, [x for x in reversed(range(probes.ndim))]) + + if probes.ndim == 4: + # probes[width, height, num_shared_modes, num_varying_modes] + probes = probes[..., 0] + + if probes.ndim == 3: + # probes[width, height, num_shared_modes] + probes = probes.transpose(2, 0, 1) + + return probes class MATProbeFileWriter(ProbeFileWriter): def write(self, filePath: Path, array: ProbeArrayType) -> None: probes = numpy.transpose(array, [x for x in reversed(range(array.ndim))]) - matDict = {'probe': probes} + matDict = {'probe': probes.transpose(1, 2, 0)} scipy.io.savemat(filePath, matDict) diff --git a/ptychodus/plugins/mdaScanFile.py b/ptychodus/plugins/mdaScanFile.py index fca1d94f..0c32fc37 100644 --- a/ptychodus/plugins/mdaScanFile.py +++ b/ptychodus/plugins/mdaScanFile.py @@ -485,10 +485,15 @@ def registerPlugins(registry: PluginRegistry) -> None: simpleName='MDA', displayName='EPICS MDA Files (*.mda)', ) + registry.scanFileReaders.registerPlugin( + MDAScanFileReader(), + simpleName='2ID', + displayName='APS 2-ID MDA Files (*.mda)', + ) registry.scanFileReaders.registerPlugin( HXNScanFileReader(), simpleName='HXN', - displayName='HXN Scan Files (*.mda)', + displayName='CNM/APS HXN Scan Files (*.mda)', ) diff --git a/ptychodus/plugins/neXus/neXusDiffractionFile.py b/ptychodus/plugins/neXus/neXusDiffractionFile.py index 57d1f205..0b092cc6 100644 --- a/ptychodus/plugins/neXus/neXusDiffractionFile.py +++ b/ptychodus/plugins/neXus/neXusDiffractionFile.py @@ -65,9 +65,9 @@ def __len__(self) -> int: class DetectorSpecificGroup: nimages: int ntrigger: int - photon_energy_eV: float - x_pixels_in_detector: int - y_pixels_in_detector: int + photonEnergyInElectronVolts: Decimal + xPixelsInDetector: int + yPixelsInDetector: int @property def numberOfPatternsTotal(self) -> int: @@ -82,23 +82,23 @@ def read(cls, group: h5py.Group) -> DetectorSpecificGroup: xPixelsInDetector = group['x_pixels_in_detector'] yPixelsInDetector = group['y_pixels_in_detector'] return cls( - nimages[()], - ntrigger[()], - float(photonEnergy[()]), - xPixelsInDetector[()], - yPixelsInDetector[()], + int(nimages[()]), + int(ntrigger[()]), + Decimal(repr(photonEnergy[()])), + int(xPixelsInDetector[()]), + int(yPixelsInDetector[()]), ) @dataclass(frozen=True) class DetectorGroup: detectorSpecific: DetectorSpecificGroup - detector_distance_m: float - beam_center_x_px: int - beam_center_y_px: int - bit_depth_image: int - x_pixel_size_m: float - y_pixel_size_m: float + detectorDistanceInMeters: Decimal + beamCenterXInPixels: int + beamCenterYInPixels: int + bitDepthReadout: int + xPixelSizeInMeters: Decimal + yPixelSizeInMeters: Decimal @classmethod def read(cls, group: h5py.Group) -> DetectorGroup: @@ -109,19 +109,19 @@ def read(cls, group: h5py.Group) -> DetectorGroup: assert h5BeamCenterX.attrs['units'] == b'pixel' h5BeamCenterY = group['beam_center_y'] assert h5BeamCenterY.attrs['units'] == b'pixel' - h5BitDepthImage = group['bit_depth_image'] + h5BitDepthReadout = group['bit_depth_readout'] h5XPixelSize = group['x_pixel_size'] assert h5XPixelSize.attrs['units'] == b'm' h5YPixelSize = group['y_pixel_size'] assert h5YPixelSize.attrs['units'] == b'm' return cls( detectorSpecific, - float(h5DetectorDistance[()]), - h5BeamCenterX[()], - h5BeamCenterY[()], - h5BitDepthImage[()], - float(h5XPixelSize[()]), - float(h5YPixelSize[()]), + Decimal(repr(h5DetectorDistance[()])), + int(h5BeamCenterX[()]), + int(h5BeamCenterY[()]), + int(h5BitDepthReadout[()]), + Decimal(repr(h5XPixelSize[()])), + Decimal(repr(h5YPixelSize[()])), ) @@ -137,7 +137,7 @@ def read(cls, group: h5py.Group) -> InstrumentGroup: @dataclass(frozen=True) class GoniometerGroup: - chi_deg: float + chiDeg: float @classmethod def read(cls, group: h5py.Group) -> GoniometerGroup: @@ -147,13 +147,13 @@ def read(cls, group: h5py.Group) -> GoniometerGroup: assert chiItem.attrs['units'] == b'degree' if chiSpace.get_simple_extent_type() == h5py.h5s.SCALAR: - chi_deg = float(chiItem[()]) + chiDeg = float(chiItem[()]) elif isinstance(chiItem, h5py.Dataset): - chi_deg = float(chiItem[0]) + chiDeg = float(chiItem[0]) else: raise ValueError('Failed to read goniometer angle (chi)!') - return cls(chi_deg) + return cls(chiDeg) @dataclass(frozen=True) @@ -235,28 +235,29 @@ def read(self, filePath: Path) -> DiffractionDataset: else: detector = entry.instrument.detector detectorPixelGeometry = PixelGeometry( - Decimal(repr(detector.x_pixel_size_m)), - Decimal(repr(detector.y_pixel_size_m)), + detector.xPixelSizeInMeters, + detector.yPixelSizeInMeters, ) cropCenterInPixels = Array2D[int]( - int(round(detector.beam_center_x_px)), - int(round(detector.beam_center_y_px)), + detector.beamCenterXInPixels, + detector.beamCenterYInPixels, ) detectorSpecific = detector.detectorSpecific detectorExtentInPixels = ImageExtent( - int(detectorSpecific.x_pixels_in_detector), - int(detectorSpecific.y_pixels_in_detector), + detectorSpecific.xPixelsInDetector, + detectorSpecific.yPixelsInDetector, ) - probeEnergyInElectronVolts = Decimal(repr(detectorSpecific.photon_energy_eV)) + probeEnergyInElectronVolts = detectorSpecific.photonEnergyInElectronVolts metadata = DiffractionMetadata( numberOfPatternsPerArray=h5Dataset.shape[0], numberOfPatternsTotal=detectorSpecific.numberOfPatternsTotal, patternDataType=h5Dataset.dtype, - detectorDistanceInMeters=Decimal(repr(detector.detector_distance_m)), + detectorDistanceInMeters=detector.detectorDistanceInMeters, detectorExtentInPixels=detectorExtentInPixels, detectorPixelGeometry=detectorPixelGeometry, + detectorBitDepth=detector.bitDepthReadout, cropCenterInPixels=cropCenterInPixels, probeEnergyInElectronVolts=probeEnergyInElectronVolts, filePath=filePath, @@ -265,7 +266,7 @@ def read(self, filePath: Path) -> DiffractionDataset: dataset = NeXusDiffractionDataset(metadata, contentsTree, entry) # vvv TODO This is a hack; remove when able! vvv - self.stageRotationInDegrees = entry.sample.goniometer.chi_deg + self.stageRotationInDegrees = entry.sample.goniometer.chiDeg except OSError: logger.debug(f'Unable to read file \"{filePath}\".') diff --git a/ptychodus/plugins/ptychoShelvesScanFile.py b/ptychodus/plugins/ptychoShelvesScanFile.py index 8096b007..e5c0382b 100644 --- a/ptychodus/plugins/ptychoShelvesScanFile.py +++ b/ptychodus/plugins/ptychoShelvesScanFile.py @@ -27,7 +27,7 @@ def read(self, filePath: Path) -> Scan: else: raise ScanPointParseError('Coordinate array shape mismatch!') - for x, y in zip(ppX[:, 0], ppY[:, 0]): + for x, y in zip(ppX, ppY): point = ScanPoint(x, y) pointList.append(point) except OSError: diff --git a/ptychodus/view/data.py b/ptychodus/view/data.py index ccc01af0..fd634799 100644 --- a/ptychodus/view/data.py +++ b/ptychodus/view/data.py @@ -55,7 +55,7 @@ class DatasetFileView(QGroupBox): def __init__(self, parent: Optional[QWidget]) -> None: super().__init__('Diffraction Dataset', parent) - self.filePathLineEdit = QLineEdit() + self.directoryComboBox = QComboBox() self.fileSystemTableView = QTableView() self.fileTypeComboBox = QComboBox() @@ -64,7 +64,7 @@ def createInstance(cls, parent: Optional[QWidget] = None) -> DatasetFileView: view = cls(parent) layout = QVBoxLayout() - layout.addWidget(view.filePathLineEdit) + layout.addWidget(view.directoryComboBox) layout.addWidget(view.fileSystemTableView) layout.addWidget(view.fileTypeComboBox) view.setLayout(layout) @@ -78,6 +78,7 @@ def __init__(self, parent: Optional[QWidget]) -> None: super().__init__('Metadata', parent) self.detectorPixelCountCheckBox = QCheckBox('Detector Pixel Count') self.detectorPixelSizeCheckBox = QCheckBox('Detector Pixel Size') + self.detectorBitDepthCheckBox = QCheckBox('Detector Bit Depth') self.detectorDistanceCheckBox = QCheckBox('Detector Distance') self.patternCropCenterCheckBox = QCheckBox('Pattern Crop Center') self.patternCropExtentCheckBox = QCheckBox('Pattern Crop Extent') @@ -90,6 +91,7 @@ def createInstance(cls, parent: Optional[QWidget] = None) -> MetadataView: layout = QVBoxLayout() layout.addWidget(view.detectorPixelCountCheckBox) layout.addWidget(view.detectorPixelSizeCheckBox) + layout.addWidget(view.detectorBitDepthCheckBox) layout.addWidget(view.detectorDistanceCheckBox) layout.addWidget(view.patternCropCenterCheckBox) layout.addWidget(view.patternCropExtentCheckBox) @@ -150,8 +152,10 @@ class PatternTransformView(QGroupBox): def __init__(self, parent: Optional[QWidget]) -> None: super().__init__('Transform', parent) - self.thresholdCheckBox = QCheckBox('Threshold:') - self.thresholdSpinBox = QSpinBox() + self.valueLowerBoundCheckBox = QCheckBox('Value Lower Bound:') + self.valueLowerBoundSpinBox = QSpinBox() + self.valueUpperBoundCheckBox = QCheckBox('Value Upper Bound:') + self.valueUpperBoundSpinBox = QSpinBox() self.axesLabel = QLabel('Axes:') self.flipXCheckBox = QCheckBox('Flip X') self.flipYCheckBox = QCheckBox('Flip Y') @@ -161,13 +165,15 @@ def createInstance(cls, parent: Optional[QWidget] = None) -> PatternTransformVie view = cls(parent) layout = QGridLayout() - layout.addWidget(view.thresholdCheckBox, 0, 0) - layout.addWidget(view.thresholdSpinBox, 0, 1, 1, 2) - layout.addWidget(view.axesLabel, 1, 0) - layout.addWidget(view.flipXCheckBox, 1, 1, Qt.AlignHCenter) - layout.addWidget(view.flipYCheckBox, 1, 2, Qt.AlignHCenter) - layout.setColumnStretch(1, 1) + layout.addWidget(view.valueLowerBoundCheckBox, 0, 0) + layout.addWidget(view.valueLowerBoundSpinBox, 0, 1, 1, 2) + layout.addWidget(view.valueUpperBoundCheckBox, 1, 0) + layout.addWidget(view.valueUpperBoundSpinBox, 1, 1, 1, 2) + layout.addWidget(view.axesLabel, 2, 0) + layout.addWidget(view.flipXCheckBox, 2, 1, Qt.AlignHCenter) + layout.addWidget(view.flipYCheckBox, 2, 2, Qt.AlignHCenter) layout.setColumnStretch(2, 1) + layout.setColumnStretch(3, 1) view.setLayout(layout) return view diff --git a/ptychodus/view/detector.py b/ptychodus/view/detector.py index 7e5d60ad..0d862dd3 100644 --- a/ptychodus/view/detector.py +++ b/ptychodus/view/detector.py @@ -15,6 +15,7 @@ def __init__(self, parent: Optional[QWidget]) -> None: self.numberOfPixelsYSpinBox = QSpinBox() self.pixelSizeXWidget = LengthWidget.createInstance() self.pixelSizeYWidget = LengthWidget.createInstance() + self.bitDepthSpinBox = QSpinBox() self.detectorDistanceWidget = LengthWidget.createInstance() self.fresnelNumberWidget = DecimalLineEdit.createInstance() @@ -27,6 +28,7 @@ def createInstance(cls, parent: Optional[QWidget] = None) -> DetectorParametersV layout.addRow('Number of Pixels Y:', view.numberOfPixelsYSpinBox) layout.addRow('Pixel Size X:', view.pixelSizeXWidget) layout.addRow('Pixel Size Y:', view.pixelSizeYWidget) + layout.addRow('Bit Depth:', view.bitDepthSpinBox) layout.addRow('Detector-Object Distance:', view.detectorDistanceWidget) layout.addRow('Fresnel Number:', view.fresnelNumberWidget) view.setLayout(layout) diff --git a/ptychodus/view/scan.py b/ptychodus/view/scan.py index 84837141..64a6b219 100644 --- a/ptychodus/view/scan.py +++ b/ptychodus/view/scan.py @@ -1,8 +1,8 @@ from __future__ import annotations from typing import Generic, Optional, TypeVar -from PyQt5.QtWidgets import (QAbstractButton, QComboBox, QDialog, QDialogButtonBox, QFormLayout, - QGroupBox, QLabel, QSpinBox, QVBoxLayout, QWidget) +from PyQt5.QtWidgets import (QAbstractButton, QCheckBox, QComboBox, QDialog, QDialogButtonBox, + QFormLayout, QGroupBox, QLabel, QSpinBox, QVBoxLayout, QWidget) from matplotlib.backends.backend_qt5agg import FigureCanvas from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar @@ -153,8 +153,10 @@ def __init__(self, parent: Optional[QWidget]) -> None: self.indexFilterComboBox = QComboBox() self.transformComboBox = QComboBox() self.jitterRadiusWidget = LengthWidget.createInstance() - self.centroidXWidget = LengthWidget.createInstance() - self.centroidYWidget = LengthWidget.createInstance() + self.centroidXCheckBox = QCheckBox('Centroid X:') + self.centroidXWidget = LengthWidget.createInstance(isSigned=True) + self.centroidYCheckBox = QCheckBox('Centroid Y:') + self.centroidYWidget = LengthWidget.createInstance(isSigned=True) @classmethod def createInstance(cls, parent: Optional[QWidget] = None) -> ScanTransformView: @@ -164,8 +166,8 @@ def createInstance(cls, parent: Optional[QWidget] = None) -> ScanTransformView: layout.addRow('Index Filter:', view.indexFilterComboBox) layout.addRow('(x,y) \u2192', view.transformComboBox) layout.addRow('Jitter Radius:', view.jitterRadiusWidget) - layout.addRow('Centroid X:', view.centroidXWidget) - layout.addRow('Centroid Y:', view.centroidYWidget) + layout.addRow(view.centroidXCheckBox, view.centroidXWidget) + layout.addRow(view.centroidYCheckBox, view.centroidYWidget) view.setLayout(layout) return view diff --git a/ptychodus/view/widgets/angleWidget.py b/ptychodus/view/widgets/angleWidget.py index 62eaf5f0..c488fefa 100644 --- a/ptychodus/view/widgets/angleWidget.py +++ b/ptychodus/view/widgets/angleWidget.py @@ -15,7 +15,7 @@ class AngleWidget(QWidget): def __init__(self, parent: Optional[QWidget]) -> None: super().__init__(parent) self.angleInTurns = Decimal() - self.angleLineEdit = DecimalLineEdit.createNonNegativeInstance() + self.angleLineEdit = DecimalLineEdit.createInstance(isSigned=False) self.unitsComboBox = QComboBox() @classmethod diff --git a/ptychodus/view/widgets/decimalLineEdit.py b/ptychodus/view/widgets/decimalLineEdit.py index 368a168e..8e3ce994 100644 --- a/ptychodus/view/widgets/decimalLineEdit.py +++ b/ptychodus/view/widgets/decimalLineEdit.py @@ -22,7 +22,10 @@ def __init__(self, parent: Optional[QWidget]) -> None: self._maximum: Optional[Decimal] = None @classmethod - def createInstance(cls, parent: Optional[QWidget] = None) -> DecimalLineEdit: + def createInstance(cls, + *, + isSigned: bool = False, + parent: Optional[QWidget] = None) -> DecimalLineEdit: widget = cls(parent) widget._lineEdit.setValidator(widget._validator) @@ -34,12 +37,9 @@ def createInstance(cls, parent: Optional[QWidget] = None) -> DecimalLineEdit: layout.addWidget(widget._lineEdit) widget.setLayout(layout) - return widget + if not isSigned: + widget._validator.setBottom(0.) - @classmethod - def createNonNegativeInstance(cls, parent: Optional[QWidget] = None) -> DecimalLineEdit: - widget = cls.createInstance(parent) - widget._validator.setBottom(0.) return widget def isReadOnly(self) -> bool: diff --git a/ptychodus/view/widgets/energyWidget.py b/ptychodus/view/widgets/energyWidget.py index 557e6228..9a53330d 100644 --- a/ptychodus/view/widgets/energyWidget.py +++ b/ptychodus/view/widgets/energyWidget.py @@ -14,7 +14,7 @@ class EnergyWidget(QWidget): def __init__(self, parent: Optional[QWidget]) -> None: super().__init__(parent) self.energyInElectronVolts = Decimal() - self.magnitudeLineEdit = DecimalLineEdit.createNonNegativeInstance() + self.magnitudeLineEdit = DecimalLineEdit.createInstance(isSigned=False) self.unitsComboBox = QComboBox() @classmethod diff --git a/ptychodus/view/widgets/lengthWidget.py b/ptychodus/view/widgets/lengthWidget.py index fa1bedaf..e04decb5 100644 --- a/ptychodus/view/widgets/lengthWidget.py +++ b/ptychodus/view/widgets/lengthWidget.py @@ -11,18 +11,23 @@ class LengthWidget(QWidget): lengthChanged = pyqtSignal(Decimal) - def __init__(self, parent: Optional[QWidget]) -> None: + def __init__(self, isSigned: bool, parent: Optional[QWidget]) -> None: super().__init__(parent) self.lengthInMeters = Decimal() - self.magnitudeLineEdit = DecimalLineEdit.createNonNegativeInstance() + self.lineEdit = DecimalLineEdit.createInstance(isSigned=isSigned) self.unitsComboBox = QComboBox() @classmethod - def createInstance(cls, parent: Optional[QWidget] = None) -> LengthWidget: - widget = cls(parent) + def createInstance(cls, + *, + isSigned: bool = False, + parent: Optional[QWidget] = None) -> LengthWidget: + widget = cls(isSigned, parent) - widget.magnitudeLineEdit.setMinimum(Decimal()) - widget.magnitudeLineEdit.valueChanged.connect(widget._setLengthInMetersFromWidgets) + if not isSigned: + widget.lineEdit.setMinimum(Decimal()) + + widget.lineEdit.valueChanged.connect(widget._setLengthInMetersFromWidgets) widget.unitsComboBox.addItem('m', 0) widget.unitsComboBox.addItem('mm', -3) @@ -34,17 +39,17 @@ def createInstance(cls, parent: Optional[QWidget] = None) -> LengthWidget: layout = QHBoxLayout() layout.setContentsMargins(0, 0, 0, 0) - layout.addWidget(widget.magnitudeLineEdit) + layout.addWidget(widget.lineEdit) layout.addWidget(widget.unitsComboBox) widget.setLayout(layout) return widget def isReadOnly(self) -> bool: - return self.magnitudeLineEdit.isReadOnly() + return self.lineEdit.isReadOnly() def setReadOnly(self, enable: bool) -> None: - self.magnitudeLineEdit.setReadOnly(enable) + self.lineEdit.setReadOnly(enable) def getLengthInMeters(self) -> Decimal: return self.lengthInMeters @@ -52,9 +57,9 @@ def getLengthInMeters(self) -> Decimal: def setLengthInMeters(self, lengthInMeters: Decimal) -> None: self.lengthInMeters = lengthInMeters - if lengthInMeters > Decimal(): + if not lengthInMeters.is_zero(): exponent = 3 * int( - (lengthInMeters.log10() / 3).to_integral_exact(rounding=ROUND_FLOOR)) + (abs(lengthInMeters).log10() / 3).to_integral_exact(rounding=ROUND_FLOOR)) index = self.unitsComboBox.findData(exponent) if index != -1: @@ -74,4 +79,4 @@ def _setLengthInMetersFromWidgets(self, magnitude: Decimal) -> None: def _updateDisplay(self) -> None: lengthInDisplayUnits = self.lengthInMeters / self._scaleToMeters - self.magnitudeLineEdit.setValue(lengthInDisplayUnits) + self.lineEdit.setValue(lengthInDisplayUnits) diff --git a/setup.cfg b/setup.cfg index c658a754..184e9465 100644 --- a/setup.cfg +++ b/setup.cfg @@ -20,7 +20,7 @@ install_requires = watchdog ==3.* [options.extras_require] -ptychonn = ptychonn ==0.1.2 +ptychonn = ptychonn ==0.1.3 tike = tike ==0.24.* gui = pyqt5 ==5.* globus =