Skip to content

Commit

Permalink
S31workflow (#96)
Browse files Browse the repository at this point in the history
- prototype VSPI algorithm for ptycho-enhanced XRF
- update two-step ptycho-enhanced XRF algorithms to use coordinate conversions from object API 
- extract reconstructor API
- implement S31 batch reconstruction workflow
- add ptychoshelves product file reader
  • Loading branch information
stevehenke authored Sep 5, 2024
1 parent 8f65b0f commit d3ce8c2
Show file tree
Hide file tree
Showing 34 changed files with 920 additions and 467 deletions.
2 changes: 1 addition & 1 deletion ptychodus/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ def main() -> int:
view = ViewCore.createInstance(parsedArgs.dev)

from ptychodus.controller import ControllerCore
controller = ControllerCore.createInstance(model, view)
controller = ControllerCore(model, view)
controller.showMainWindow(versionString())

return app.exec()
Expand Down
47 changes: 29 additions & 18 deletions ptychodus/api/object.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,19 @@
import numpy
import numpy.typing

from .geometry import ImageExtent, PixelGeometry, Point2D
from .geometry import ImageExtent, PixelGeometry
from .scan import ScanPoint

ObjectArrayType: TypeAlias = numpy.typing.NDArray[numpy.complexfloating[Any, Any]]


@dataclass(frozen=True)
class ObjectPoint:
index: int
positionXInPixels: float
positionYInPixels: float


@dataclass(frozen=True)
class ObjectGeometry:
widthInPixels: int
Expand All @@ -39,29 +46,33 @@ def minimumXInMeters(self) -> float:
def minimumYInMeters(self) -> float:
return self.centerYInMeters - self.heightInMeters / 2.

@property
def _radiusX(self) -> float:
return self.widthInPixels / 2

@property
def _radiusY(self) -> float:
return self.heightInPixels / 2

def getPixelGeometry(self) -> PixelGeometry:
return PixelGeometry(
widthInMeters=self.pixelWidthInMeters,
heightInMeters=self.pixelHeightInMeters,
)

def mapObjectPointToScanPoint(self, objectPoint: Point2D) -> Point2D:
x = self.centerXInMeters + self.pixelWidthInMeters * (objectPoint.x - self._radiusX)
y = self.centerYInMeters + self.pixelHeightInMeters * (objectPoint.y - self._radiusY)
return Point2D(x, y)
def mapObjectPointToScanPoint(self, point: ObjectPoint) -> ScanPoint:
rx_px = self.widthInPixels / 2
ry_px = self.heightInPixels / 2
dx_m = self.pixelWidthInMeters
dy_m = self.pixelHeightInMeters

x_m = self.centerXInMeters + dx_m * (point.positionXInPixels - rx_px)
y_m = self.centerYInMeters + dy_m * (point.positionYInPixels - ry_px)

return ScanPoint(point.index, x_m, y_m)

def mapScanPointToObjectPoint(self, point: ScanPoint) -> ObjectPoint:
rx_px = self.widthInPixels / 2
ry_px = self.heightInPixels / 2
dx_m = self.pixelWidthInMeters
dy_m = self.pixelHeightInMeters

x_px = (point.positionXInMeters - self.centerXInMeters) / dx_m + rx_px
y_px = (point.positionYInMeters - self.centerYInMeters) / dy_m + ry_px

def mapScanPointToObjectPoint(self, scanPoint: Point2D) -> Point2D:
x = (scanPoint.x - self.centerXInMeters) / self.pixelWidthInMeters + self._radiusX
y = (scanPoint.y - self.centerYInMeters) / self.pixelHeightInMeters + self._radiusY
return Point2D(x, y)
return ObjectPoint(point.index, x_px, y_px)

def contains(self, geometry: ObjectGeometry) -> bool:
dx = self.centerXInMeters - geometry.centerXInMeters
Expand Down Expand Up @@ -109,7 +120,7 @@ def __init__(self,
expectedLayers = self.numberOfLayers
actualLayers = len(self._layerDistanceInMeters)

if actualLayers != expectedLayers:
if actualLayers < expectedLayers:
raise ValueError(f'Expected {expectedLayers} layer distances; got {actualLayers}!')

self._pixelWidthInMeters = pixelWidthInMeters
Expand Down
29 changes: 19 additions & 10 deletions ptychodus/api/workflow.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from __future__ import annotations
from abc import ABC, abstractmethod
from collections.abc import Mapping
from pathlib import Path
Expand All @@ -15,35 +16,37 @@ def openScan(self, filePath: Path, *, fileType: str | None = None) -> None:
pass

@abstractmethod
def buildScan(self, builderName: str, builderParameters: Mapping[str, Any] = {}) -> None:
def buildScan(self,
builderName: str | None = None,
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 buildProbeFromSettings(self) -> None:
def buildProbe(self,
builderName: str | None = None,
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:
def buildObject(self,
builderName: str | None = None,
builderParameters: Mapping[str, Any] = {}) -> None:
pass

@abstractmethod
def buildObjectFromSettings(self) -> None:
def reconstructLocal(self, outputProductName: str) -> WorkflowProductAPI:
pass

@abstractmethod
def reconstruct(self) -> None:
def reconstructRemote(self) -> None:
pass

@abstractmethod
Expand Down Expand Up @@ -103,8 +106,14 @@ def saveSettings(self,

class FileBasedWorkflow(ABC):

@property
@abstractmethod
def isWatchRecursive(self) -> bool:
'''indicates whether the data directory must be watched recursively'''
pass

@abstractmethod
def getFilePattern(self) -> str:
def getWatchFilePattern(self) -> str:
'''UNIX-style filename pattern. For rules see fnmatch from Python standard library.'''
pass

Expand Down
11 changes: 8 additions & 3 deletions ptychodus/controller/automation.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,8 @@ def __init__(self, core: AutomationCore, presenter: AutomationPresenter,
self._processingPresenter = processingPresenter
self._listModel = AutomationProcessingListModel(processingPresenter)
self._view = view
self._timer = QTimer()
self._executeWaitingTasksTimer = QTimer()
self._automationTimer = QTimer()

@classmethod
def createInstance(cls, core: AutomationCore, presenter: AutomationPresenter,
Expand All @@ -174,8 +175,12 @@ def createInstance(cls, core: AutomationCore, presenter: AutomationPresenter,
view.clearButton.clicked.connect(presenter.clearDatasetRepository)

controller._syncModelToView()
controller._timer.timeout.connect(core.executeWaitingTasks)
controller._timer.start(60 * 1000) # TODO customize (in milliseconds)

controller._executeWaitingTasksTimer.timeout.connect(core.executeWaitingTasks)
controller._executeWaitingTasksTimer.start(60 * 1000) # TODO customize (in milliseconds)

controller._automationTimer.timeout.connect(core.refreshDatasetRepository)
controller._automationTimer.start(10 * 1000) # TODO customize (in milliseconds)

return controller

Expand Down
23 changes: 5 additions & 18 deletions ptychodus/controller/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,7 @@ class ControllerCore:
def __init__(self, model: ModelCore, view: ViewCore) -> None:
self.view = view

self._memoryController = MemoryController.createInstance(model.memoryPresenter,
view.memoryProgressBar)
self._memoryController = MemoryController(model.memoryPresenter, view.memoryProgressBar)
self._fileDialogFactory = FileDialogFactory()
self._ptychonnViewControllerFactory = PtychoNNViewControllerFactory(
model.ptychonnReconstructorLibrary, self._fileDialogFactory)
Expand Down Expand Up @@ -85,27 +84,15 @@ def __init__(self, model: ModelCore, view: ViewCore) -> None:
self._automationController = AutomationController.createInstance(
model._automationCore, model.automationPresenter, model.automationProcessingPresenter,
view.automationView, self._fileDialogFactory)
self._refreshDataTimer = QTimer()
self._automationTimer = QTimer()
self._processMessagesTimer = QTimer()

@classmethod
def createInstance(cls, model: ModelCore, view: ViewCore) -> ControllerCore:
controller = cls(model, view)
self._refreshDataTimer = QTimer()
self._refreshDataTimer.timeout.connect(model.refreshActiveDataset)
self._refreshDataTimer.start(1000) # TODO make configurable

view.navigationActionGroup.triggered.connect(
lambda action: controller.swapCentralWidgets(action))

lambda action: self.swapCentralWidgets(action))
view.workflowAction.setVisible(model.areWorkflowsSupported)

controller._refreshDataTimer.timeout.connect(model.refreshActiveDataset)
controller._refreshDataTimer.start(1000) # TODO make configurable

controller._automationTimer.timeout.connect(model.refreshAutomationDatasets)
controller._automationTimer.start(1000) # TODO make configurable

return controller

def showMainWindow(self, windowTitle: str) -> None:
self.view.setWindowTitle(windowTitle)
self.view.show()
Expand Down
19 changes: 5 additions & 14 deletions ptychodus/controller/memory.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,14 @@

class MemoryController:

def __init__(self, presenter: MemoryPresenter, progressBar: QProgressBar,
timer: QTimer) -> None:
def __init__(self, presenter: MemoryPresenter, progressBar: QProgressBar) -> None:
self._presenter = presenter
self._progressBar = progressBar
self._timer = timer
self._timer = QTimer()
self._timer.timeout.connect(self._updateProgressBar)

@classmethod
def createInstance(cls, presenter: MemoryPresenter,
progressBar: QProgressBar) -> MemoryController:
timer = QTimer()
controller = cls(presenter, progressBar, timer)
controller._updateProgressBar()

timer.timeout.connect(controller._updateProgressBar)
timer.start(10 * 1000) # TODO customize (in milliseconds)

return controller
self._updateProgressBar()
self._timer.start(10 * 1000) # TODO customize (in milliseconds)

def _updateProgressBar(self) -> None:
stats = self._presenter.getStatistics()
Expand Down
2 changes: 1 addition & 1 deletion ptychodus/controller/probe/exposure.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from ...model.analysis import ExposureAnalyzer, ExposureMap
from ...model.visualization import VisualizationEngine
from ...view.object import ExposureDialog
from ...view.probe import ExposureDialog
from ...view.widgets import ExceptionDialog
from ..data import FileDialogFactory
from ..visualization import VisualizationParametersController, VisualizationWidgetController
Expand Down
11 changes: 10 additions & 1 deletion ptychodus/controller/probe/fluorescence.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from ...model.analysis import FluorescenceEnhancer
from ...model.visualization import VisualizationEngine
from ...view.object import FluorescenceDialog
from ...view.probe import FluorescenceDialog
from ...view.widgets import ExceptionDialog
from ..data import FileDialogFactory
from ..visualization import VisualizationParametersController, VisualizationWidgetController
Expand Down Expand Up @@ -40,6 +40,8 @@ def __init__(self, enhancer: FluorescenceEnhancer, engine: VisualizationEngine,
self._engine = engine
self._fileDialogFactory = fileDialogFactory
self._dialog = FluorescenceDialog()
self._enhancementModel = QStringListModel()
self._enhancementModel.setStringList(self._enhancer.getEnhancementStrategyList())
self._upscalingModel = QStringListModel()
self._upscalingModel.setStringList(self._enhancer.getUpscalingStrategyList())
self._deconvolutionModel = QStringListModel()
Expand All @@ -49,6 +51,11 @@ def __init__(self, enhancer: FluorescenceEnhancer, engine: VisualizationEngine,
self._dialog.fluorescenceParametersView.openButton.clicked.connect(
self._openMeasuredDataset)

self._dialog.fluorescenceParametersView.enhancementStrategyComboBox.setModel(
self._enhancementModel)
self._dialog.fluorescenceParametersView.enhancementStrategyComboBox.textActivated.connect(
enhancer.setEnhancementStrategy)

self._dialog.fluorescenceParametersView.upscalingStrategyComboBox.setModel(
self._upscalingModel)
self._dialog.fluorescenceParametersView.upscalingStrategyComboBox.textActivated.connect(
Expand Down Expand Up @@ -127,6 +134,8 @@ def _saveEnhancedDataset(self) -> None:
ExceptionDialog.showException(title, err)

def _syncModelToView(self) -> None:
self._dialog.fluorescenceParametersView.enhancementStrategyComboBox.setCurrentText(
self._enhancer.getEnhancementStrategy())
self._dialog.fluorescenceParametersView.upscalingStrategyComboBox.setCurrentText(
self._enhancer.getUpscalingStrategy())
self._dialog.fluorescenceParametersView.deconvolutionStrategyComboBox.setCurrentText(
Expand Down
2 changes: 1 addition & 1 deletion ptychodus/controller/probe/stxm.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from ...model.analysis import STXMSimulator
from ...model.visualization import VisualizationEngine
from ...view.object import STXMDialog
from ...view.probe import STXMDialog
from ...view.widgets import ExceptionDialog
from ..data import FileDialogFactory
from ..visualization import VisualizationParametersController, VisualizationWidgetController
Expand Down
3 changes: 2 additions & 1 deletion ptychodus/model/analysis/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ def __init__(self, settingsRegistry: SettingsRegistry,
upscalingStrategyChooser,
deconvolutionStrategyChooser,
fluorescenceFileReaderChooser,
fluorescenceFileWriterChooser)
fluorescenceFileWriterChooser,
settingsRegistry)
self.fluorescenceVisualizationEngine = VisualizationEngine(isComplex=False)
self.xmcdAnalyzer = XMCDAnalyzer(objectRepository)
self.xmcdVisualizationEngine = VisualizationEngine(isComplex=False)
Loading

0 comments on commit d3ce8c2

Please sign in to comment.