Skip to content

Commit f77b695

Browse files
authored
ptychozoon integration (#129)
- Integrate ptychozoon GPU-accelerated fluorescence enhancement algorithm
1 parent 7b8548d commit f77b695

10 files changed

Lines changed: 527 additions & 0 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ module = [
7575
"ptycho.*",
7676
"ptycho_torch.*",
7777
"ptychonn.*",
78+
"ptychozoon.*",
7879
"pvaccess",
7980
"pvapy.*",
8081
"scipy.*",

src/ptychodus/controller/core.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ def __init__(
129129
model.fluorescence_core.enhancer_chooser,
130130
model.fluorescence_core.two_step_enhancer,
131131
model.fluorescence_core.vspi_enhancer,
132+
model.fluorescence_core.ptychozoon_enhancer,
132133
model.fluorescence_core.task_monitor,
133134
model.fluorescence_core.visualization_engine,
134135
view.probe_view,

src/ptychodus/controller/probe/core.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from ...model.fluorescence import (
1414
FluorescenceAPI,
1515
FluorescenceTaskMonitor,
16+
PtychozoonFluorescenceEnhancer,
1617
TwoStepFluorescenceEnhancer,
1718
VSPIFluorescenceEnhancer,
1819
)
@@ -52,6 +53,7 @@ def __init__(
5253
fluorescence_enhancer_chooser: PluginChooser[FluorescenceEnhancer],
5354
fluorescence_two_step_enhancer: TwoStepFluorescenceEnhancer,
5455
fluorescence_vspi_enhancer: VSPIFluorescenceEnhancer,
56+
fluorescence_ptychozoon_enhancer: PtychozoonFluorescenceEnhancer | None,
5557
fluorescence_task_monitor: FluorescenceTaskMonitor,
5658
fluorescence_visualization_engine: VisualizationEngine,
5759
view: RepositoryTreeView,
@@ -79,6 +81,7 @@ def __init__(
7981
fluorescence_enhancer_chooser,
8082
fluorescence_two_step_enhancer,
8183
fluorescence_vspi_enhancer,
84+
fluorescence_ptychozoon_enhancer,
8285
fluorescence_task_monitor,
8386
fluorescence_visualization_engine,
8487
file_dialog_factory,

src/ptychodus/controller/probe/fluorescence.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,14 @@
1212
from ...model.fluorescence import (
1313
FluorescenceAPI,
1414
FluorescenceTaskMonitor,
15+
PtychozoonFluorescenceEnhancer,
1516
TwoStepFluorescenceEnhancer,
1617
VSPIFluorescenceEnhancer,
1718
)
1819
from ...model.visualization import VisualizationEngine
1920
from ...view.probe import (
2021
FluorescenceDialog,
22+
FluorescencePtychozoonParametersView,
2123
FluorescenceStatusView,
2224
FluorescenceTwoStepParametersView,
2325
FluorescenceVSPIParametersView,
@@ -125,6 +127,69 @@ def _update(self, observable: Observable) -> None:
125127
self._sync_model_to_view()
126128

127129

130+
class FluorescencePtychozoonViewController(Observer):
131+
MAX_INT: Final[int] = 0x7FFFFFFF
132+
133+
def __init__(self, enhancer: PtychozoonFluorescenceEnhancer) -> None:
134+
super().__init__()
135+
self._enhancer = enhancer
136+
self._view = FluorescencePtychozoonParametersView()
137+
138+
self._view.damping_factor_line_edit.value_changed.connect(
139+
self._sync_damping_factor_to_model
140+
)
141+
self._view.gradient_smoothness_line_edit.value_changed.connect(
142+
self._sync_gradient_smoothness_to_model
143+
)
144+
self._view.max_iterations_spin_box.setRange(1, self.MAX_INT)
145+
self._view.max_iterations_spin_box.valueChanged.connect(enhancer.set_max_iterations)
146+
self._view.atol_line_edit.value_changed.connect(self._sync_atol_to_model)
147+
self._view.btol_line_edit.value_changed.connect(self._sync_btol_to_model)
148+
self._view.checkpoint_interval_spin_box.setRange(1, self.MAX_INT)
149+
self._view.checkpoint_interval_spin_box.valueChanged.connect(
150+
enhancer.set_checkpoint_interval
151+
)
152+
self._view.use_gpu_check_box.toggled.connect(enhancer.set_gpu_enabled)
153+
self._view.gpu_device_index_spin_box.setRange(0, self.MAX_INT)
154+
self._view.gpu_device_index_spin_box.valueChanged.connect(enhancer.set_gpu_device_index)
155+
156+
enhancer.add_observer(self)
157+
self._sync_model_to_view()
158+
159+
def get_widget(self) -> QWidget:
160+
return self._view
161+
162+
def _sync_damping_factor_to_model(self, value: Decimal) -> None:
163+
self._enhancer.set_damping_factor(float(value))
164+
165+
def _sync_gradient_smoothness_to_model(self, value: Decimal) -> None:
166+
self._enhancer.set_gradient_smoothness(float(value))
167+
168+
def _sync_atol_to_model(self, value: Decimal) -> None:
169+
self._enhancer.set_atol(float(value))
170+
171+
def _sync_btol_to_model(self, value: Decimal) -> None:
172+
self._enhancer.set_btol(float(value))
173+
174+
def _sync_model_to_view(self) -> None:
175+
self._view.damping_factor_line_edit.set_value(
176+
Decimal(repr(self._enhancer.get_damping_factor()))
177+
)
178+
self._view.gradient_smoothness_line_edit.set_value(
179+
Decimal(repr(self._enhancer.get_gradient_smoothness()))
180+
)
181+
self._view.max_iterations_spin_box.setValue(self._enhancer.get_max_iterations())
182+
self._view.atol_line_edit.set_value(Decimal(repr(self._enhancer.get_atol())))
183+
self._view.btol_line_edit.set_value(Decimal(repr(self._enhancer.get_btol())))
184+
self._view.checkpoint_interval_spin_box.setValue(self._enhancer.get_checkpoint_interval())
185+
self._view.use_gpu_check_box.setChecked(self._enhancer.is_gpu_enabled())
186+
self._view.gpu_device_index_spin_box.setValue(self._enhancer.get_gpu_device_index())
187+
188+
def _update(self, observable: Observable) -> None:
189+
if observable is self._enhancer:
190+
self._sync_model_to_view()
191+
192+
128193
class FluorescenceStatusController(Observer):
129194
def __init__(
130195
self,
@@ -170,6 +235,7 @@ def __init__(
170235
enhancer_chooser: PluginChooser,
171236
two_step_enhancer: TwoStepFluorescenceEnhancer,
172237
vspi_enhancer: VSPIFluorescenceEnhancer,
238+
ptychozoon_enhancer: PtychozoonFluorescenceEnhancer | None,
173239
task_monitor: FluorescenceTaskMonitor,
174240
engine: VisualizationEngine,
175241
file_dialog_factory: FileDialogFactory,
@@ -214,6 +280,23 @@ def __init__(
214280
vspi_view_controller.get_widget()
215281
)
216282

283+
# Registered last, matching the enhancer_chooser order, so the combo-box
284+
# index selects the correct stacked page. Only present when ptychozoon is
285+
# installed (enhancer is None otherwise).
286+
self._ptychozoon_view_controller: FluorescencePtychozoonViewController | None = None
287+
288+
if ptychozoon_enhancer is not None:
289+
self._ptychozoon_view_controller = FluorescencePtychozoonViewController(
290+
ptychozoon_enhancer
291+
)
292+
self._dialog.fluorescence_parameters_view.algorithm_combo_box.addItem(
293+
PtychozoonFluorescenceEnhancer.DISPLAY_NAME,
294+
self._dialog.fluorescence_parameters_view.algorithm_combo_box.count(),
295+
)
296+
self._dialog.fluorescence_parameters_view.stacked_widget.addWidget(
297+
self._ptychozoon_view_controller.get_widget()
298+
)
299+
217300
self._dialog.fluorescence_parameters_view.algorithm_combo_box.textActivated.connect(
218301
enhancer_chooser.set_current_plugin
219302
)

src/ptychodus/model/fluorescence/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
FluorescenceDatasetEmitter,
66
FluorescenceTaskMonitor,
77
)
8+
from .ptychozoon import PtychozoonFluorescenceEnhancer
89
from .two_step import TwoStepFluorescenceEnhancer
910
from .vspi import VSPIFluorescenceEnhancer
1011

@@ -14,6 +15,7 @@
1415
'FluorescenceCore',
1516
'FluorescenceDatasetEmitter',
1617
'FluorescenceTaskMonitor',
18+
'PtychozoonFluorescenceEnhancer',
1719
'TwoStepFluorescenceEnhancer',
1820
'VSPIFluorescenceEnhancer',
1921
]
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
"""Subprocess worker that runs the ptychozoon (GPU VSPI) fluorescence enhancement.
2+
3+
This module is executed in a freshly ``spawn``ed process so that the CuPy GPU
4+
context is created cleanly and fully released when the process exits. It must not
5+
import ``ptychozoon`` (or CuPy) at module load time; the import happens inside the
6+
worker function so that the parent process never pulls GPU libraries into its own
7+
interpreter.
8+
9+
All data crosses the process boundary as plain numpy arrays and scalars (see
10+
:class:`PtychozoonPayload`). Messages are streamed back over a single queue as
11+
tagged tuples:
12+
13+
- ``('result', iteration, [(element_name, counts_per_second_array), ...])`` per checkpoint
14+
- ``('log', levelno, message)`` for each captured log record from the ptychozoon logger
15+
- ``('error', traceback_str)`` on failure
16+
17+
followed by a ``None`` sentinel. Because a single child thread produces every
18+
message, ordering is preserved (a log line arrives before the result it precedes).
19+
"""
20+
21+
from __future__ import annotations
22+
23+
import logging
24+
from dataclasses import dataclass
25+
from multiprocessing.queues import Queue
26+
from typing import Any
27+
28+
import numpy
29+
30+
__all__ = [
31+
'PtychozoonPayload',
32+
'run_vspi_enhancement',
33+
]
34+
35+
36+
class _QueueLogHandler(logging.Handler):
37+
"""Logging handler that forwards formatted records to the parent over the result queue."""
38+
39+
def __init__(self, result_queue: 'Queue[Any]') -> None:
40+
super().__init__()
41+
self._result_queue = result_queue
42+
43+
def emit(self, record: logging.LogRecord) -> None:
44+
try:
45+
self._result_queue.put(('log', record.levelno, self.format(record)))
46+
except Exception:
47+
# Never let logging failures break the enhancement worker.
48+
pass
49+
50+
51+
@dataclass(frozen=True)
52+
class PtychozoonPayload:
53+
"""Picklable inputs for one ptychozoon enhancement run."""
54+
55+
# Ptychography product (plain arrays / scalars)
56+
probe_positions_m: numpy.ndarray # (N, 2) float [y, x] meters
57+
probe: numpy.ndarray # (n_opr, modes, height, width) complex
58+
object_array: numpy.ndarray # (height, width) complex
59+
pixel_size_m: tuple[float, float] # (pixel_height_m, pixel_width_m)
60+
object_center_m: tuple[float, float] # (center_y_m, center_x_m)
61+
opr_mode_weights: numpy.ndarray | None # (n_opr, N) float or None
62+
63+
# Fluorescence element maps
64+
element_maps: list[tuple[str, numpy.ndarray]] # [(name, counts_per_second), ...]
65+
66+
# Solver settings
67+
damping_factor: float
68+
gradient_smoothness: float
69+
max_iterations: int
70+
atol: float
71+
btol: float
72+
checkpoint_interval: int
73+
use_gpu: bool
74+
gpu_device_index: int
75+
76+
# Effective log level of the parent's ptychozoon logger, so the child only
77+
# forwards records that the parent would actually surface.
78+
log_level: int
79+
80+
81+
def run_vspi_enhancement(payload: PtychozoonPayload, result_queue: 'Queue[Any]') -> None:
82+
"""Run ptychozoon VSPI enhancement and stream checkpoint results over a queue.
83+
84+
Intended as the target of a ``spawn``ed ``multiprocessing.Process``. Puts
85+
``('result', iteration, [(name, cps), ...])`` tuples per checkpoint and
86+
``('log', levelno, message)`` tuples for ptychozoon log records, then ``None``
87+
when complete, or ``('error', traceback_str)`` on failure.
88+
"""
89+
# Forward ptychozoon's own log output to the parent so it appears in the
90+
# Enhance Fluorescence status view (this process has no ptychodus handlers).
91+
log_handler = _QueueLogHandler(result_queue)
92+
log_handler.setFormatter(logging.Formatter('%(message)s'))
93+
ptychozoon_logger = logging.getLogger('ptychozoon')
94+
ptychozoon_logger.addHandler(log_handler)
95+
ptychozoon_logger.setLevel(payload.log_level)
96+
97+
try:
98+
from ptychozoon.data_structures import (
99+
ElementMap,
100+
FluorescenceDataset,
101+
PtychographyProduct,
102+
)
103+
from ptychozoon.settings import (
104+
DeconvolutionEnhancementSettings,
105+
InterpolationTypes,
106+
)
107+
from ptychozoon.vspi_enhance import VSPIFluorescenceEnhancingAlgorithm
108+
109+
product = PtychographyProduct(
110+
probe_positions=payload.probe_positions_m,
111+
probe=payload.probe,
112+
object_array=payload.object_array,
113+
pixel_size_m=payload.pixel_size_m,
114+
object_center_m=payload.object_center_m,
115+
opr_mode_weights=payload.opr_mode_weights,
116+
)
117+
dataset = FluorescenceDataset(
118+
element_maps=[
119+
ElementMap(name=name, counts_per_second=cps) for name, cps in payload.element_maps
120+
]
121+
)
122+
123+
settings = DeconvolutionEnhancementSettings()
124+
settings.lsmr.damping_factor = payload.damping_factor
125+
settings.lsmr.gradient_smoothness = payload.gradient_smoothness
126+
settings.lsmr.max_iter = payload.max_iterations
127+
settings.lsmr.atol = payload.atol
128+
settings.lsmr.btol = payload.btol
129+
settings.lsmr.checkpoint_interval = payload.checkpoint_interval
130+
settings.gpu.enabled = payload.use_gpu
131+
settings.gpu.index = payload.gpu_device_index
132+
# Fourier interpolation requires the GPU; fall back to Barycentric on CPU.
133+
settings._interpolation = (
134+
InterpolationTypes.FOURIER if payload.use_gpu else InterpolationTypes.BARYCENTRIC
135+
)
136+
137+
algorithm = VSPIFluorescenceEnhancingAlgorithm()
138+
139+
for enhanced_dataset, iteration in algorithm.enhance(dataset, product, settings=settings):
140+
result_queue.put(
141+
(
142+
'result',
143+
int(iteration),
144+
[
145+
(emap.name, numpy.asarray(emap.counts_per_second))
146+
for emap in enhanced_dataset.element_maps
147+
],
148+
)
149+
)
150+
except Exception:
151+
import traceback
152+
153+
result_queue.put(('error', traceback.format_exc()))
154+
finally:
155+
ptychozoon_logger.removeHandler(log_handler)
156+
result_queue.put(None)

src/ptychodus/model/fluorescence/core.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,13 @@
1515
from ..visualization import VisualizationEngine
1616
from .api import FluorescenceAPI
1717
from .monitor import FluorescenceTaskMonitor
18+
from .ptychozoon import PtychozoonFluorescenceEnhancer
1819
from .settings import FluorescenceSettings
1920
from .two_step import TwoStepFluorescenceEnhancer
2021
from .vspi import VSPIFluorescenceEnhancer
2122

23+
logger = logging.getLogger(__name__)
24+
2225

2326
class FluorescenceCore:
2427
def __init__(
@@ -48,6 +51,26 @@ def __init__(
4851
simple_name=VSPIFluorescenceEnhancer.SIMPLE_NAME,
4952
display_name=VSPIFluorescenceEnhancer.DISPLAY_NAME,
5053
)
54+
55+
# The GPU VSPI enhancer is optional: register it only when ptychozoon is
56+
# installed so it silently disappears otherwise. Register it last so its
57+
# combo-box entry and stacked GUI page stay aligned by index. Importing
58+
# the top-level package is cheap and does not pull in CuPy (that happens
59+
# only inside the spawned subprocess), so probing it here is safe.
60+
self.ptychozoon_enhancer: PtychozoonFluorescenceEnhancer | None = None
61+
62+
try:
63+
import ptychozoon # noqa: F401
64+
except ModuleNotFoundError:
65+
logger.info('ptychozoon not found.')
66+
else:
67+
self.ptychozoon_enhancer = PtychozoonFluorescenceEnhancer(self._settings)
68+
self.enhancer_chooser.register_plugin(
69+
self.ptychozoon_enhancer,
70+
simple_name=PtychozoonFluorescenceEnhancer.SIMPLE_NAME,
71+
display_name=PtychozoonFluorescenceEnhancer.DISPLAY_NAME,
72+
)
73+
5174
self.enhancer_chooser.synchronize_with_parameter(self._settings.algorithm)
5275

5376
file_reader_chooser.synchronize_with_parameter(self._settings.file_type)

0 commit comments

Comments
 (0)