tomwer 1.2.0a1__py3-none-any.whl → 1.2.0a3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (219) hide show
  1. orangecontrib/tomwer/tutorials/append_raw_darks_and_flats_frames_to_NXtomos.ows +44 -0
  2. orangecontrib/tomwer/tutorials/copy_reduced_darks_and_flats_meth1.ows +55 -0
  3. orangecontrib/tomwer/tutorials/copy_reduced_darks_and_flats_meth2.ows +48 -0
  4. orangecontrib/tomwer/tutorials/default_cor_search.ows +40 -0
  5. orangecontrib/tomwer/tutorials/hello_world_python_script.ows +50 -0
  6. orangecontrib/tomwer/tutorials/simple_slice_reconstruction_on_slurm.ows +50 -0
  7. orangecontrib/tomwer/tutorials/simple_volume_to_slurm_reconstruction.ows +8 -8
  8. orangecontrib/tomwer/widgets/__init__.py +1 -1
  9. orangecontrib/tomwer/widgets/cluster/FutureSupervisorOW.py +0 -1
  10. orangecontrib/tomwer/widgets/cluster/SlurmClusterOW.py +8 -6
  11. orangecontrib/tomwer/widgets/control/AdvancementOW.py +0 -1
  12. orangecontrib/tomwer/widgets/control/DataDiscoveryOW.py +1 -6
  13. orangecontrib/tomwer/widgets/control/DataListOW.py +0 -1
  14. orangecontrib/tomwer/widgets/control/DataListenerOW.py +4 -4
  15. orangecontrib/tomwer/widgets/control/DataSelectorOW.py +0 -1
  16. orangecontrib/tomwer/widgets/control/DataTransfertOW.py +7 -7
  17. orangecontrib/tomwer/widgets/control/DataValidatorOW.py +0 -1
  18. orangecontrib/tomwer/widgets/control/DataWatcherOW.py +0 -3
  19. orangecontrib/tomwer/widgets/control/EDF2NXTomomillOW.py +3 -2
  20. orangecontrib/tomwer/widgets/control/EmailOW.py +82 -0
  21. orangecontrib/tomwer/widgets/control/FilterOW.py +3 -3
  22. orangecontrib/tomwer/widgets/control/NXTomomillOW.py +1 -1
  23. orangecontrib/tomwer/widgets/control/NotifierOW.py +0 -1
  24. orangecontrib/tomwer/widgets/control/ReduceDarkFlatSelectorOW.py +93 -0
  25. orangecontrib/tomwer/widgets/control/SingleTomoObjOW.py +29 -5
  26. orangecontrib/tomwer/widgets/control/TimerOW.py +1 -2
  27. orangecontrib/tomwer/widgets/control/TomoObjSerieOW.py +0 -1
  28. orangecontrib/tomwer/widgets/control/VolumeSelector.py +0 -1
  29. orangecontrib/tomwer/widgets/control/VolumeSymLinkOW.py +4 -10
  30. orangecontrib/tomwer/widgets/control/icons/email.png +0 -0
  31. orangecontrib/tomwer/widgets/control/icons/email.svg +58 -0
  32. orangecontrib/tomwer/widgets/control/icons/reduced_darkflat_selector.png +0 -0
  33. orangecontrib/tomwer/widgets/control/icons/reduced_darkflat_selector.svg +199 -0
  34. orangecontrib/tomwer/widgets/debugtools/DatasetGeneratorOW.py +0 -1
  35. orangecontrib/tomwer/widgets/debugtools/ObjectInspectorOW.py +0 -1
  36. orangecontrib/tomwer/widgets/edit/DarkFlatPatchOW.py +1 -2
  37. orangecontrib/tomwer/widgets/edit/ImageKeyEditorOW.py +1 -2
  38. orangecontrib/tomwer/widgets/edit/ImageKeyUpgraderOW.py +0 -1
  39. orangecontrib/tomwer/widgets/edit/NXtomoEditorOW.py +0 -1
  40. orangecontrib/tomwer/widgets/other/PythonScriptOW.py +29 -1
  41. orangecontrib/tomwer/widgets/other/TomoObjsHub.py +28 -0
  42. orangecontrib/tomwer/widgets/other/icons/hub.png +0 -0
  43. orangecontrib/tomwer/widgets/other/icons/hub.svg +113 -0
  44. orangecontrib/tomwer/widgets/reconstruction/AxisOW.py +18 -12
  45. orangecontrib/tomwer/widgets/reconstruction/CastNabuVolumeOW.py +0 -2
  46. orangecontrib/tomwer/widgets/reconstruction/DarkRefAndCopyOW.py +21 -6
  47. orangecontrib/tomwer/widgets/reconstruction/NabuOW.py +29 -7
  48. orangecontrib/tomwer/widgets/reconstruction/NabuVolumeOW.py +18 -5
  49. orangecontrib/tomwer/widgets/reconstruction/SAAxisOW.py +40 -13
  50. orangecontrib/tomwer/widgets/reconstruction/SADeltaBetaOW.py +37 -10
  51. orangecontrib/tomwer/widgets/reconstruction/SinoNormOW.py +2 -3
  52. orangecontrib/tomwer/widgets/reconstruction/TofuOW.py +5 -4
  53. orangecontrib/tomwer/widgets/stitching/StitcherOW.py +0 -1
  54. orangecontrib/tomwer/widgets/stitching/ZStitchingConfigOW.py +0 -1
  55. orangecontrib/tomwer/widgets/visualization/DataViewerOW.py +10 -4
  56. orangecontrib/tomwer/widgets/visualization/DiffViewerOW.py +1 -1
  57. orangecontrib/tomwer/widgets/visualization/LivesliceOW.py +0 -1
  58. orangecontrib/tomwer/widgets/visualization/NXtomoMetadataViewerOW.py +0 -1
  59. orangecontrib/tomwer/widgets/visualization/RadioStackOW.py +7 -5
  60. orangecontrib/tomwer/widgets/visualization/SampleMovedOW.py +1 -1
  61. orangecontrib/tomwer/widgets/visualization/SinogramViewerOW.py +0 -3
  62. orangecontrib/tomwer/widgets/visualization/SliceStackOW.py +7 -5
  63. orangecontrib/tomwer/widgets/visualization/VolumeViewerOW.py +4 -4
  64. tomwer/__main__.py +139 -5
  65. tomwer/app/axis.py +16 -5
  66. tomwer/app/canvas_launcher/config.py +1 -1
  67. tomwer/app/canvas_launcher/mainwindow.py +164 -6
  68. tomwer/app/darkref.py +10 -181
  69. tomwer/app/darkrefpatch.py +10 -131
  70. tomwer/app/diffframe.py +11 -0
  71. tomwer/app/imagekeyeditor.py +12 -19
  72. tomwer/app/intensitynormalization.py +1 -0
  73. tomwer/app/lamino.py +7 -2
  74. tomwer/app/patchrawdarkflat.py +131 -0
  75. tomwer/app/radiostack.py +10 -0
  76. tomwer/app/reducedarkflat.py +205 -0
  77. tomwer/app/saaxis.py +27 -8
  78. tomwer/app/sadeltabeta.py +29 -8
  79. tomwer/app/samplemoved.py +11 -0
  80. tomwer/app/scanviewer.py +12 -0
  81. tomwer/app/sinogramviewer.py +11 -0
  82. tomwer/app/slicestack.py +11 -0
  83. tomwer/app/zstitching.py +12 -0
  84. tomwer/core/futureobject.py +4 -2
  85. tomwer/core/process/conditions/filters.py +26 -4
  86. tomwer/core/process/control/datadiscovery.py +4 -0
  87. tomwer/core/process/control/datawatcher/datawatcher.py +5 -1
  88. tomwer/core/process/control/email.py +148 -0
  89. tomwer/core/process/control/nxtomoconcatenate.py +9 -2
  90. tomwer/core/process/control/nxtomomill.py +58 -16
  91. tomwer/core/process/control/scanselector.py +4 -0
  92. tomwer/core/process/control/scantransfer.py +52 -23
  93. tomwer/core/process/control/test/test_concatenate_nxtomos.py +1 -0
  94. tomwer/core/process/control/test/test_email.py +52 -0
  95. tomwer/core/process/control/test/test_h52nx_process.py +106 -0
  96. tomwer/core/process/control/test/test_volume_link.py +5 -4
  97. tomwer/core/process/control/timer.py +27 -6
  98. tomwer/core/process/control/tomoobjserie.py +4 -0
  99. tomwer/core/process/control/volumeselector.py +4 -0
  100. tomwer/core/process/control/volumesymlink.py +47 -8
  101. tomwer/core/process/edit/darkflatpatch.py +49 -8
  102. tomwer/core/process/edit/imagekeyeditor.py +63 -13
  103. tomwer/core/process/reconstruction/axis/__init__.py +1 -1
  104. tomwer/core/process/reconstruction/axis/axis.py +61 -41
  105. tomwer/core/process/reconstruction/axis/params.py +4 -6
  106. tomwer/core/process/reconstruction/darkref/darkrefs.py +53 -16
  107. tomwer/core/process/reconstruction/darkref/darkrefscopy.py +12 -2
  108. tomwer/core/process/reconstruction/lamino/__init__.py +1 -1
  109. tomwer/core/process/reconstruction/lamino/tofu.py +22 -2
  110. tomwer/core/process/reconstruction/nabu/nabucommon.py +93 -14
  111. tomwer/core/process/reconstruction/nabu/nabuscores.py +70 -33
  112. tomwer/core/process/reconstruction/nabu/nabuslices.py +219 -41
  113. tomwer/core/process/reconstruction/nabu/nabuvolume.py +240 -108
  114. tomwer/core/process/reconstruction/nabu/utils.py +10 -36
  115. tomwer/core/process/reconstruction/normalization/normalization.py +10 -3
  116. tomwer/core/process/reconstruction/saaxis/__init__.py +1 -0
  117. tomwer/core/process/reconstruction/saaxis/saaxis.py +564 -376
  118. tomwer/core/process/reconstruction/sadeltabeta/__init__.py +1 -0
  119. tomwer/core/process/reconstruction/sadeltabeta/sadeltabeta.py +481 -268
  120. tomwer/core/process/reconstruction/scores/params.py +21 -8
  121. tomwer/core/process/reconstruction/test/test_darkref_copy.py +2 -0
  122. tomwer/core/process/reconstruction/test/test_saaxis.py +21 -8
  123. tomwer/core/process/reconstruction/test/test_sadeltabeta.py +8 -5
  124. tomwer/core/process/script/python.py +7 -2
  125. tomwer/core/process/stitching/nabustitcher.py +10 -3
  126. tomwer/core/process/task.py +2 -9
  127. tomwer/core/process/test/test_axis.py +25 -15
  128. tomwer/core/process/test/test_conditions.py +6 -6
  129. tomwer/core/process/test/test_dark_and_flat.py +20 -15
  130. tomwer/core/process/test/test_data_transfer.py +8 -8
  131. tomwer/core/process/test/test_data_watcher.py +1 -1
  132. tomwer/core/process/test/test_lamino.py +6 -6
  133. tomwer/core/process/test/test_nabu.py +13 -8
  134. tomwer/core/process/test/test_normalization.py +1 -0
  135. tomwer/core/process/test/test_timer.py +6 -6
  136. tomwer/core/process/visualization/dataviewer.py +4 -0
  137. tomwer/core/process/visualization/diffviewer.py +4 -0
  138. tomwer/core/process/visualization/imagestackviewer.py +4 -0
  139. tomwer/core/process/visualization/radiostack.py +4 -0
  140. tomwer/core/process/visualization/samplemoved.py +4 -0
  141. tomwer/core/process/visualization/sinogramviewer.py +4 -0
  142. tomwer/core/process/visualization/slicestack.py +4 -0
  143. tomwer/core/process/visualization/volumeviewer.py +4 -0
  144. tomwer/core/scan/hdf5scan.py +4 -4
  145. tomwer/core/scan/scanbase.py +5 -1
  146. tomwer/core/scan/test/test_process_registration.py +9 -9
  147. tomwer/core/settings.py +59 -1
  148. tomwer/core/test/test_lamino.py +2 -1
  149. tomwer/core/utils/__init__.py +16 -0
  150. tomwer/core/utils/locker.py +0 -1
  151. tomwer/core/utils/resource.py +6 -11
  152. tomwer/core/utils/scanutils.py +2 -0
  153. tomwer/gui/cluster/slurm.py +91 -7
  154. tomwer/gui/cluster/supervisor.py +16 -11
  155. tomwer/gui/cluster/test/test_cluster.py +16 -1
  156. tomwer/gui/conditions/filter.py +3 -3
  157. tomwer/gui/control/datalist.py +24 -11
  158. tomwer/gui/control/email.py +183 -0
  159. tomwer/gui/control/reducedarkflatselector.py +545 -0
  160. tomwer/gui/control/singletomoobj.py +23 -1
  161. tomwer/gui/control/test/test_email.py +35 -0
  162. tomwer/gui/control/test/test_reducedarkflat_selector.py +280 -0
  163. tomwer/gui/reconstruction/axis/CompareImages.py +1 -1
  164. tomwer/gui/reconstruction/axis/axis.py +10 -6
  165. tomwer/gui/reconstruction/axis/radioaxis.py +14 -6
  166. tomwer/gui/reconstruction/darkref/darkrefcopywidget.py +2 -0
  167. tomwer/gui/reconstruction/darkref/darkrefwidget.py +4 -4
  168. tomwer/gui/reconstruction/nabu/nabuconfig/phase.py +3 -1
  169. tomwer/gui/reconstruction/nabu/nabuconfig/preprocessing.py +34 -33
  170. tomwer/gui/reconstruction/nabu/nabuconfig/reconstruction.py +1 -1
  171. tomwer/gui/reconstruction/normalization/intensity.py +5 -5
  172. tomwer/gui/reconstruction/saaxis/corrangeselector.py +1 -0
  173. tomwer/gui/reconstruction/saaxis/saaxis.py +6 -6
  174. tomwer/gui/reconstruction/sadeltabeta/saadeltabeta.py +6 -6
  175. tomwer/gui/reconstruction/scores/scoreplot.py +6 -4
  176. tomwer/gui/samplemoved/__init__.py +2 -2
  177. tomwer/gui/stackplot.py +18 -7
  178. tomwer/gui/stacks.py +2 -2
  179. tomwer/gui/stitching/stitchandbackground.py +2 -2
  180. tomwer/gui/stitching/stitching.py +1 -1
  181. tomwer/gui/stitching/stitching_raw.py +1 -1
  182. tomwer/gui/utils/__init__.py +1 -85
  183. tomwer/gui/utils/illustrations.py +1 -1
  184. tomwer/gui/utils/inputwidget.py +41 -36
  185. tomwer/gui/utils/slider.py +2 -2
  186. tomwer/gui/utils/utils.py +93 -0
  187. tomwer/gui/visualization/dataviewer.py +8 -5
  188. tomwer/gui/visualization/diffviewer/diffviewer.py +4 -4
  189. tomwer/gui/visualization/reconstructionparameters.py +26 -6
  190. tomwer/gui/visualization/sinogramviewer.py +7 -1
  191. tomwer/gui/visualization/test/test_reconstruction_parameters.py +2 -4
  192. tomwer/gui/visualization/volumeviewer.py +2 -0
  193. tomwer/resources/__init__.py +55 -43
  194. tomwer/resources/gui/icons/compose.png +0 -0
  195. tomwer/resources/gui/icons/compose.svg +75 -0
  196. tomwer/synctools/datatransfert.py +3 -1
  197. tomwer/synctools/stacks/edit/darkflatpatch.py +39 -34
  198. tomwer/synctools/stacks/edit/imagekeyeditor.py +8 -27
  199. tomwer/synctools/stacks/processingstack.py +45 -9
  200. tomwer/synctools/stacks/reconstruction/axis.py +6 -5
  201. tomwer/synctools/stacks/reconstruction/dkrefcopy.py +1 -0
  202. tomwer/synctools/stacks/reconstruction/lamino.py +3 -3
  203. tomwer/synctools/stacks/reconstruction/nabu.py +49 -140
  204. tomwer/synctools/stacks/reconstruction/normalization.py +1 -0
  205. tomwer/synctools/stacks/reconstruction/saaxis.py +19 -33
  206. tomwer/synctools/stacks/reconstruction/sadeltabeta.py +16 -32
  207. tomwer/synctools/test/test_darkRefs.py +19 -10
  208. tomwer/synctools/test/test_foldertransfer.py +7 -7
  209. tomwer/third_party/nabu/preproc/phase.py +6 -8
  210. tomwer/third_party/nabu/utils.py +2 -3
  211. tomwer/version.py +1 -1
  212. {tomwer-1.2.0a1.dist-info → tomwer-1.2.0a3.dist-info}/METADATA +15 -54
  213. {tomwer-1.2.0a1.dist-info → tomwer-1.2.0a3.dist-info}/RECORD +219 -192
  214. {tomwer-1.2.0a1.dist-info → tomwer-1.2.0a3.dist-info}/WHEEL +1 -1
  215. {tomwer-1.2.0a1.dist-info → tomwer-1.2.0a3.dist-info}/entry_points.txt +3 -3
  216. /tomwer-1.2.0a1-py3.9-nspkg.pth → /tomwer-1.2.0a3-py3.11-nspkg.pth +0 -0
  217. {tomwer-1.2.0a1.dist-info → tomwer-1.2.0a3.dist-info}/LICENSE +0 -0
  218. {tomwer-1.2.0a1.dist-info → tomwer-1.2.0a3.dist-info}/namespace_packages.txt +0 -0
  219. {tomwer-1.2.0a1.dist-info → tomwer-1.2.0a3.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,545 @@
1
+ from typing import Optional
2
+ from copy import copy
3
+ import os
4
+ import numpy
5
+ import logging
6
+
7
+ from silx.gui import qt
8
+ from silx.gui.plot import Plot2D
9
+ from silx.io.dictdump import h5todict
10
+ from silx.io.url import DataUrl
11
+ from silx.gui.dialog.DataFileDialog import DataFileDialog
12
+
13
+ from tomoscan.esrf.scan.utils import cwd_context
14
+ from tomoscan.framereducerbase import REDUCER_TARGET
15
+ from tomoscan.io import HDF5File
16
+
17
+ from tomwer.io.utils import get_default_directory
18
+
19
+ _logger = logging.getLogger(__name__)
20
+
21
+
22
+ class ReduceDarkFlatSelectorTableWidget(qt.QWidget):
23
+ """
24
+ Table widget used to refgister and select a list of reduces frames
25
+ """
26
+
27
+ sigActiveChanged = qt.Signal(object)
28
+ """Signal emit when the active frame changed (the one to be displayed)"""
29
+
30
+ sigUpdated = qt.Signal()
31
+ """Signal emmit when the table is updated"""
32
+
33
+ def __init__(self, parent: Optional[qt.QWidget] = None) -> None:
34
+ super().__init__(parent)
35
+ self._iDict = 0
36
+ self._dicts = {}
37
+ # key are 'dict name', value are dict of the reduced frame (acquisition index as key, 2D numpy array as value).
38
+ self.setLayout(qt.QVBoxLayout())
39
+ self._tree = qt.QTreeWidget(self)
40
+ self._tree.setHeaderLabels(("reduce frames", "acq index", "select"))
41
+ self._tree.header().setStretchLastSection(False)
42
+ self._tree.header().setSectionResizeMode(0, qt.QHeaderView.Stretch)
43
+ self._tree.setColumnWidth(1, 80)
44
+ self._tree.setColumnWidth(2, 50)
45
+ self.layout().addWidget(self._tree)
46
+
47
+ self.setAcceptDrops(True)
48
+
49
+ # connect signal / slot
50
+ self._tree.currentItemChanged.connect(self._updateActiveChanged)
51
+
52
+ def _updateActiveChanged(self, item, column: int):
53
+ frame = item.data(0, qt.Qt.UserRole)
54
+ self.sigActiveChanged.emit(frame)
55
+
56
+ def popDictIndex(self) -> int:
57
+ self._iDict = self._iDict + 1
58
+ return self._iDict - 1
59
+
60
+ def addReduceFrames(self, reduced_frames: dict, selected: tuple = ()):
61
+ """
62
+ Add the reduced frames in the interface.
63
+ :param dict reduced_frames: frame index as key and numpy.ndarray as value
64
+ :param tuple selected: if the frame index is in selected tuple then this item will be automatically selected
65
+ """
66
+ reduced_frames = copy(reduced_frames)
67
+ dict_key = (
68
+ reduced_frames.pop("reduce_frames_name", None)
69
+ or f"reduced frames #{self.popDictIndex()}"
70
+ )
71
+ # if the key already exists: remove it (avoid )
72
+ if dict_key in self._dicts:
73
+ self._removeReduceFramesByLabel(dict_key)
74
+ self._dicts[dict_key] = reduced_frames
75
+
76
+ topLevelItem = qt.QTreeWidgetItem(self._tree)
77
+ topLevelItem.setText(0, dict_key)
78
+
79
+ for frame_index, frame in reduced_frames.items():
80
+ if not isinstance(frame, numpy.ndarray):
81
+ _logger.error(
82
+ f"frame are expected to be numpy.ndarray. Get {type(frame)} instead - will not add it"
83
+ )
84
+ continue
85
+ if frame.ndim != 2:
86
+ _logger.error(f"frame are expected to be 2D. Get {frame.shape} instead")
87
+ continue
88
+
89
+ # frame info
90
+ frameInfo = qt.QTreeWidgetItem(topLevelItem)
91
+ frameInfo.setText(0, f"shape: {frame.shape}, dtype {frame.dtype}")
92
+ frameInfo.setData(0, qt.Qt.UserRole, frame)
93
+ # frame index
94
+ indexItem = qt.QLineEdit(self._tree)
95
+ # index can be provided as absolute indexes (integers)
96
+ validator = qt.QRegExpValidator(
97
+ qt.QRegExp(r"([+-]?([0-9]*[.])?[0-9]+)*[r]"), self
98
+ )
99
+ indexItem.setValidator(validator)
100
+ indexItem.setText(str(frame_index))
101
+ self._tree.setItemWidget(frameInfo, 1, indexItem)
102
+ indexItem.setToolTip(
103
+ "Index of the reduced frames in the acquisition. Value can be provided as an absolute value - integer or as a relative value. In this case we expect users to provide a value between [0 and 1.0[ and postfix by 'r'"
104
+ )
105
+ # item selection
106
+ selectCB = qt.QCheckBox(self._tree)
107
+ selectCB.setChecked(frame_index in selected)
108
+ self._tree.setItemWidget(frameInfo, 2, selectCB)
109
+ self.sigUpdated.emit()
110
+
111
+ def clear(self):
112
+ self._dicts.clear()
113
+ self._tree.clear()
114
+ self.sigUpdated.emit()
115
+
116
+ def getConfiguration(self) -> tuple:
117
+ """
118
+ return configuration as a tuple of 'reduced frames'. Each reduced_frames are provided as:
119
+ {
120
+ "reduce_frames_name": str: name of the reduced frame
121
+ "reduce_frames": tuple of dict for each reduce frame.
122
+ this dict contains:
123
+ * index: index of this frame in the acquisition
124
+ * data: the reduce frame as a 2D array
125
+ * selected: is the frame selected by the user
126
+ }
127
+ """
128
+ config = []
129
+ for i_child in range(self._tree.topLevelItemCount()):
130
+ child = self._tree.topLevelItem(i_child)
131
+ reduce_frames_infos = []
132
+ for j_child in range(child.childCount()):
133
+ sub_child = child.child(j_child)
134
+ reduce_frames_infos.append(
135
+ {
136
+ "data": sub_child.data(0, qt.Qt.UserRole),
137
+ "index": self._get_reduce_frame_index(
138
+ self._tree.itemWidget(sub_child, 1).text()
139
+ ),
140
+ "selected": self._tree.itemWidget(sub_child, 2).isChecked(),
141
+ }
142
+ )
143
+ config.append(
144
+ {
145
+ "reduce_frames_name": child.text(0),
146
+ "reduce_frames": reduce_frames_infos,
147
+ }
148
+ )
149
+ return tuple(config)
150
+
151
+ def setConfiguration(self, config: Optional[tuple]):
152
+ if config is None:
153
+ return
154
+ self.clear()
155
+ for reduce_frames in config:
156
+ frames_dict = {
157
+ "reduce_frames_name": reduce_frames.get("reduce_frames_name", None)
158
+ }
159
+ selected = []
160
+ for reduce_frame in reduce_frames["reduce_frames"]:
161
+ assert isinstance(
162
+ reduce_frame, dict
163
+ ), f"reduce_frames is expected to contain a list of dict. Get {type(reduce_frame)} instead"
164
+ frame_index = reduce_frame["index"]
165
+ assert isinstance(frame_index, int) or (
166
+ isinstance(frame_index, str) and frame_index.endswith("r")
167
+ ), f"frame index should be an int or a str ending by 'r', get {type(frame_index)}"
168
+ frames_dict[frame_index] = reduce_frame["data"]
169
+ if reduce_frame["selected"]:
170
+ selected.append(reduce_frame["index"])
171
+ self.addReduceFrames(frames_dict, selected)
172
+ self.sigUpdated.emit()
173
+
174
+ @staticmethod
175
+ def _get_reduce_frame_index(index_as_str):
176
+ """
177
+ simple util to return expected frame index depending if the index is provided as relative or as absolute.
178
+ """
179
+ if index_as_str.endswith("r"):
180
+ return index_as_str
181
+ else:
182
+ return int(index_as_str)
183
+
184
+ def getSelectedReduceFrames(self) -> dict:
185
+ """
186
+ Return the selected reduced frame as dict
187
+ """
188
+ reduce_frames = {}
189
+ for i_child in range(self._tree.topLevelItemCount()):
190
+ child = self._tree.topLevelItem(i_child)
191
+ for j_child in range(child.childCount()):
192
+ sub_child = child.child(j_child)
193
+ is_selected = self._tree.itemWidget(sub_child, 2).isChecked()
194
+ if not is_selected:
195
+ continue
196
+
197
+ frame_index = self._get_reduce_frame_index(
198
+ self._tree.itemWidget(sub_child, 1).text()
199
+ )
200
+ data = sub_child.data(0, qt.Qt.UserRole)
201
+ assert isinstance(
202
+ data, numpy.ndarray
203
+ ), f"frame are expected to be numpy.ndarray. Get {type(data)} instead"
204
+ assert (
205
+ data.ndim == 2
206
+ ), f"frame are expected to be 2D. Get {data.shape} instead"
207
+ if frame_index in reduce_frames:
208
+ _logger.error(
209
+ f"frame index {frame_index} defined twice. Ignore last one"
210
+ )
211
+ reduce_frames[frame_index] = data
212
+ return reduce_frames
213
+
214
+ def clearSelection(self):
215
+ """
216
+ Uncheck any check Combobox found
217
+ """
218
+ for i_child in range(self._tree.topLevelItemCount()):
219
+ child = self._tree.topLevelItem(i_child)
220
+ for j_child in range(child.childCount()):
221
+ sub_child = child.child(j_child)
222
+ if self._tree.itemWidget(sub_child, 2).isChecked():
223
+ self._tree.itemWidget(sub_child, 2).setChecked(False)
224
+
225
+ def _removeSelected(self):
226
+ # note: simplest way to remove item it to reset the configuration
227
+ configuration = self.getConfiguration()
228
+ configuration = filter_unselected_reduced_frames(configuration=configuration)
229
+ self.setConfiguration(configuration)
230
+ self.sigUpdated.emit()
231
+
232
+ def _removeReduceFramesByLabel(self, label: str):
233
+ """
234
+ remove the label named 'label' if exists.
235
+ The simplest way to remove reduced frames are by reseting the configuration for now
236
+ """
237
+ configuration = list(self.getConfiguration())
238
+ new_configuration = tuple(
239
+ filter(
240
+ lambda my_dict: my_dict.get("reduce_frames_name", None) != label,
241
+ configuration,
242
+ ),
243
+ )
244
+
245
+ if len(configuration) != len(new_configuration):
246
+ self.setConfiguration(new_configuration)
247
+
248
+ def _guessReduceFramesFromFile(self, file_path: str) -> tuple:
249
+ if not os.path.exists(file_path):
250
+ _logger.error(f"file doesn't exists ({file_path})")
251
+
252
+ with HDF5File(file_path, mode="r") as h5f:
253
+ entries = tuple(h5f.keys())
254
+
255
+ res = []
256
+ for entry in entries:
257
+ res.extend(
258
+ self.get_reduce_frames(
259
+ DataUrl(
260
+ file_path=file_path,
261
+ data_path=entry,
262
+ )
263
+ )
264
+ )
265
+ return tuple(res)
266
+
267
+ @staticmethod
268
+ def get_reduce_frames(url: DataUrl) -> tuple:
269
+ """
270
+ try to guess location of darks / flats according to provided url.
271
+ The idea is to be more robust if the user provide a file or an higher level data path
272
+
273
+ :warning: Return a tuple and not a dict. Because in the case the user provide an entry which contains
274
+ a 'darks' and a flats' groups we want to return both reduced_frames.
275
+ And we cannot return them as a dict as they can have the same index - used as dict key
276
+ """
277
+ if not isinstance(url, DataUrl):
278
+ raise TypeError(f"url is expected to be a DataUrl. Get {type(url)} instead")
279
+ if not os.path.exists(url.file_path()):
280
+ _logger.error(f"file doesn't exists ({url.file_path()})")
281
+ return tuple()
282
+
283
+ result = []
284
+ with cwd_context(url.file_path()):
285
+ reduced_info_dict = h5todict(
286
+ h5file=url.file_path(),
287
+ path=url.data_path(),
288
+ )
289
+ for target in REDUCER_TARGET.values():
290
+ if target in reduced_info_dict:
291
+ reduced_frames = reduced_info_dict[target]
292
+ reduced_frames[
293
+ "reduce_frames_name"
294
+ ] = f"{target} from {url.data_path()}@{os.path.basename(url.file_path())}"
295
+ result.append(reduced_frames)
296
+
297
+ if len(reduced_frames) == 0:
298
+ # else we consider the data_path is the valid one
299
+ reduced_frames[
300
+ "reduce_frames_name"
301
+ ] = f"{url.data_path()}@{os.path.basename(url.file_path())}"
302
+ result.append(reduced_frames)
303
+
304
+ return tuple(result)
305
+
306
+ # drag / drop handling
307
+
308
+ def dropEvent(self, event):
309
+ if event.mimeData().hasFormat("text/uri-list"):
310
+ for url in event.mimeData().urls():
311
+ reduced_frames_list = self._guessReduceFramesFromFile(
312
+ file_path=url.path()
313
+ )
314
+ for reduced_frames in reduced_frames_list:
315
+ self.addReduceFrames(reduced_frames)
316
+
317
+ def supportedDropActions(self):
318
+ """Inherited method to redefine supported drop actions."""
319
+ return qt.Qt.CopyAction | qt.Qt.MoveAction
320
+
321
+ def dragEnterEvent(self, event):
322
+ if hasattr(event, "mimeData") and event.mimeData().hasFormat("text/uri-list"):
323
+ event.accept()
324
+ event.setDropAction(qt.Qt.CopyAction)
325
+ else:
326
+ try:
327
+ qt.QListWidget.dragEnterEvent(self, event)
328
+ except TypeError:
329
+ pass
330
+
331
+ def dragMoveEvent(self, event):
332
+ if hasattr(event, "mimeDatamyitems") and event.mimeDatamyitems().hasFormat(
333
+ "text/uri-list"
334
+ ):
335
+ event.setDropAction(qt.Qt.CopyAction)
336
+ event.accept()
337
+ else:
338
+ try:
339
+ qt.QListWidget.dragMoveEvent(self, event)
340
+ except TypeError:
341
+ pass
342
+
343
+
344
+ class ReduceDarkFlatSelectorWidget(qt.QSplitter):
345
+ """
346
+ Widget combining ReduceDarkFlatSelectorTableWidget and plot of the active item
347
+ """
348
+
349
+ sigUpdated = qt.Signal()
350
+ """Signal emmit when the table is updated"""
351
+
352
+ def __init__(self, parent: Optional[qt.QWidget] = None) -> None:
353
+ super().__init__(parent)
354
+ self._first_display = True
355
+ self._plot = Plot2D(self)
356
+ self.addWidget(self._plot)
357
+ self._table = ReduceDarkFlatSelectorTableWidget(parent=self)
358
+ self.addWidget(self._table)
359
+
360
+ # connect signal / slot
361
+ self._table.sigActiveChanged.connect(self._updatePlot)
362
+ self._table.sigUpdated.connect(self._tableUpdated)
363
+
364
+ def _tableUpdated(self, *args, **kwargs):
365
+ self.sigUpdated.emit()
366
+
367
+ def _updatePlot(self, obj: Optional[numpy.ndarray]):
368
+ if obj is None:
369
+ self._plot.clear()
370
+ else:
371
+ self._plot.addImage(
372
+ data=obj,
373
+ replace=True,
374
+ resetzoom=self._first_display,
375
+ )
376
+ self._first_display = False
377
+
378
+ # expose API
379
+ def addReduceFrames(self, *args, **kwargs):
380
+ self._table.addReduceFrames(*args, **kwargs)
381
+
382
+ def getConfiguration(self) -> tuple:
383
+ return self._table.getConfiguration()
384
+
385
+ def setConfiguration(self, *args, **kwargs):
386
+ self._table.setConfiguration(*args, **kwargs)
387
+
388
+ def getSelectedReduceFrames(self) -> dict:
389
+ return self._table.getSelectedReduceFrames()
390
+
391
+ def clearSelection(self) -> None:
392
+ self._table.clearSelection()
393
+
394
+ def _removeSelected(self) -> None:
395
+ self._table._removeSelected()
396
+
397
+ def clear(self) -> None:
398
+ self._table.clear()
399
+
400
+
401
+ class ReduceDarkFlatSelectorDialog(qt.QDialog):
402
+ """
403
+ 'Final' dialog to select reduce frames
404
+ """
405
+
406
+ sigClearSelection = qt.Signal()
407
+ sigSelectActiveAsDarks = qt.Signal(dict)
408
+ sigSelectActiveAsFlats = qt.Signal(dict)
409
+
410
+ sigUpdated = qt.Signal()
411
+ """Signal emmit when the table is updated"""
412
+
413
+ def __init__(self, parent=None) -> None:
414
+ super().__init__(parent)
415
+
416
+ self.setLayout(qt.QVBoxLayout())
417
+ # add list
418
+ self._widget = ReduceDarkFlatSelectorWidget()
419
+ self.layout().addWidget(self._widget)
420
+ # add buttons
421
+ self._buttons = qt.QDialogButtonBox(parent=self)
422
+ # select reduce flats
423
+ self._selectAsFlatButton = qt.QPushButton(
424
+ "Select as reduce flat(s)", parent=self
425
+ )
426
+ self._buttons.addButton(
427
+ self._selectAsFlatButton, qt.QDialogButtonBox.AcceptRole
428
+ )
429
+ # select reduce darks
430
+ self._selectAsDarkButton = qt.QPushButton(
431
+ "Select as reduce dark(s)", parent=self
432
+ )
433
+ self._buttons.addButton(
434
+ self._selectAsDarkButton, qt.QDialogButtonBox.AcceptRole
435
+ )
436
+ # clear
437
+ self._clearSelectionButton = qt.QPushButton("clear selection", parent=self)
438
+ self._buttons.addButton(
439
+ self._clearSelectionButton, qt.QDialogButtonBox.ResetRole
440
+ )
441
+ # remove selected
442
+ self._removeSelectionButton = qt.QPushButton("remove selection", parent=self)
443
+ self._buttons.addButton(
444
+ self._removeSelectionButton, qt.QDialogButtonBox.ActionRole
445
+ )
446
+ # remove selected
447
+ self._addButton = qt.QPushButton("add", parent=self)
448
+ self._buttons.addButton(self._addButton, qt.QDialogButtonBox.ActionRole)
449
+ self.layout().addWidget(self._buttons)
450
+
451
+ # connect signal / slot
452
+ self._selectAsFlatButton.released.connect(self._flatSelected)
453
+ self._selectAsDarkButton.released.connect(self._darkSelected)
454
+ self._clearSelectionButton.released.connect(self._clearSelection)
455
+ self._removeSelectionButton.released.connect(self._removeSelected)
456
+ self._addButton.released.connect(self._addFromFileSelection)
457
+ self._widget.sigUpdated.connect(self._tableUpdated)
458
+
459
+ def _tableUpdated(self, *args, **kwargs):
460
+ self.sigUpdated.emit()
461
+
462
+ def _flatSelected(self):
463
+ self.sigSelectActiveAsFlats.emit(
464
+ self._widget.getSelectedReduceFrames(),
465
+ )
466
+
467
+ def _darkSelected(self):
468
+ self.sigSelectActiveAsDarks.emit(
469
+ self._widget.getSelectedReduceFrames(),
470
+ )
471
+
472
+ def _clearSelection(self):
473
+ self._widget.clearSelection()
474
+
475
+ def _removeSelected(self):
476
+ self._widget._removeSelected()
477
+
478
+ def _addFromFileSelection(self):
479
+ dialog = DataFileDialog()
480
+ dialog.setDirectory(get_default_directory())
481
+ if dialog.exec_():
482
+ url = dialog.selectedUrl()
483
+ reduced_frames_tuple = ReduceDarkFlatSelectorTableWidget.get_reduce_frames(
484
+ DataUrl(path=url)
485
+ )
486
+ for reduced_frames in reduced_frames_tuple:
487
+ try:
488
+ self.addReduceFrames(reduced_frames)
489
+ except Exception as e:
490
+ _logger.error(e)
491
+
492
+ # expose API
493
+ def addReduceFrames(self, *args, **kwargs):
494
+ self._widget.addReduceFrames(*args, **kwargs)
495
+
496
+ def getConfiguration(self) -> tuple:
497
+ self._widget.getConfiguration()
498
+
499
+ def setConfiguration(self, configuration: tuple) -> None:
500
+ self._widget.setConfiguration(configuration)
501
+
502
+
503
+ def filter_selected_reduced_frames(configuration: tuple):
504
+ """
505
+ remove all the frames not 'selected' from the configuration.
506
+ If a reduce frame set becomes empty it will also be removed
507
+
508
+ :param dict configuration: configuration of the reduced frames
509
+ """
510
+ return _filter_reduced_frames(configuration=configuration, filter_selected=True)
511
+
512
+
513
+ def filter_unselected_reduced_frames(configuration: tuple):
514
+ """
515
+ remove all the frames 'selected' from the configuration.
516
+ If a reduce frame set becomes empty it will also be removed
517
+
518
+ :param dict configuration: configuration of the reduced frames
519
+ """
520
+ return _filter_reduced_frames(configuration=configuration, filter_selected=False)
521
+
522
+
523
+ def _filter_reduced_frames(configuration: tuple, filter_selected: bool):
524
+ assert isinstance(
525
+ configuration, (tuple, list, set)
526
+ ), f"configuration is expected to be a tuple. Get {type(configuration)}"
527
+ result = []
528
+ for reduce_group in configuration:
529
+ reduce_frames = reduce_group.get("reduce_frames", tuple())
530
+ reduce_group_name = reduce_group.get("reduce_frames_name", None)
531
+ res_set = tuple(
532
+ filter(
533
+ lambda my_dict: my_dict["selected"] == filter_selected,
534
+ reduce_frames,
535
+ )
536
+ )
537
+ if len(res_set) > 0:
538
+ result_group = {}
539
+ if reduce_group_name is not None:
540
+ result_group["reduce_frames_name"] = reduce_group_name
541
+ result_group["reduce_frames"] = res_set
542
+
543
+ result.append(result_group)
544
+
545
+ return tuple(result)
@@ -107,7 +107,9 @@ class SingleTomoObj(qt.QWidget):
107
107
  path = os.path.abspath(path)
108
108
 
109
109
  try:
110
- return ScanFactory.create_scan_objects(path)
110
+ scans = ScanFactory.create_scan_objects(path)
111
+ if scans is None or len(scans) == 0:
112
+ raise ValueError
111
113
  except: # noqa E722
112
114
  try:
113
115
  volumes = guess_volumes(
@@ -136,11 +138,17 @@ class SingleTomoObj(qt.QWidget):
136
138
 
137
139
  volumes = tuple(filter(is_not_histogram, volumes))
138
140
  return volumes
141
+ else:
142
+ return scans
139
143
 
140
144
 
141
145
  class _TomoObjQLE(QLFileSystem):
142
146
  """QLineEdit that try to get a Tomo object identifier once dropped"""
143
147
 
148
+ def supportedDropActions(self):
149
+ """Inherited method to redefine supported drop actions."""
150
+ return qt.Qt.CopyAction | qt.Qt.MoveAction
151
+
144
152
  def dropEvent(self, a0) -> None:
145
153
  if hasattr(a0, "mimeData") and a0.mimeData().hasFormat("text/uri-list"):
146
154
  for url in a0.mimeData().urls():
@@ -155,3 +163,17 @@ class _TomoObjQLE(QLFileSystem):
155
163
  self.editingFinished.emit()
156
164
  else:
157
165
  return super().dropEvent(a0)
166
+
167
+ def dragEnterEvent(self, event):
168
+ if event.mimeData().hasFormat("text/uri-list"):
169
+ event.accept()
170
+ event.setDropAction(qt.Qt.CopyAction)
171
+ else:
172
+ qt.QListWidget.dragEnterEvent(self, event)
173
+
174
+ def dragMoveEvent(self, event):
175
+ if event.mimeData().hasFormat("text/uri-list"):
176
+ event.setDropAction(qt.Qt.CopyAction)
177
+ event.accept()
178
+ else:
179
+ qt.QListWidget.dragMoveEvent(self, event)
@@ -0,0 +1,35 @@
1
+ from tomwer.tests.conftest import qtapp # noqa F401
2
+ from tomwer.gui.control.email import Emailwidget
3
+
4
+
5
+ def test_Emailwidget(qtapp): # noqa F811
6
+ """test of Emailwidget"""
7
+ widget = Emailwidget()
8
+ widget.show()
9
+
10
+ assert widget.getConfiguration() == {
11
+ "from_addr": "",
12
+ "to_addrs": ("",),
13
+ "subject": "tomwer: {tomo_obj_short_id} received",
14
+ "text": "{tomo_obj_id} received at {timestamp} \n\nprocesses:\n{dataset_processing_states}\n\nfile listening:\n{ls_tomo_obj}\n\nInfo: {footnote}",
15
+ "port": 0,
16
+ "host": "smtps.esrf.fr",
17
+ }
18
+
19
+ new_configuration = {
20
+ "from_addr": "toto.esrf.fr",
21
+ "to_addrs": "toto.esrf.fr;tata.esrf.fr",
22
+ "subject": "this is a scan",
23
+ "text": "",
24
+ "port": 445,
25
+ "host": "smtps.esrf.en",
26
+ }
27
+ widget.setConfiguration(new_configuration)
28
+
29
+ configuration = widget.getConfiguration()
30
+ new_configuration.pop("to_addrs")
31
+ assert configuration.pop("to_addrs") in (
32
+ ("toto.esrf.fr", "tata.esrf.fr"),
33
+ ("tata.esrf.fr", "toto.esrf.fr"),
34
+ )
35
+ assert configuration == new_configuration