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
@@ -35,17 +35,15 @@ __date__ = "10/02/2021"
35
35
  import copy
36
36
  import logging
37
37
  import os
38
- from typing import Iterable, Optional
38
+ from typing import Optional
39
39
 
40
40
  import h5py
41
41
  import numpy
42
- from nabu.pipeline.config import get_default_nabu_config
43
- from nabu.pipeline.fullfield.nabu_config import (
44
- nabu_config as nabu_fullfield_default_config,
45
- )
46
42
  from processview.core.manager import DatasetState, ProcessManager
47
43
  from processview.core.superviseprocess import SuperviseProcess
48
44
  from silx.io.url import DataUrl
45
+ from silx.utils.deprecation import deprecated_warning
46
+
49
47
  from tomoscan.io import HDF5File
50
48
 
51
49
  import tomwer.version
@@ -76,9 +74,17 @@ from tomwer.core.utils.scanutils import data_identifier_to_scan
76
74
  from tomwer.core.utils.slurm import is_slurm_available
77
75
  from tomwer.core.volume.volumefactory import VolumeFactory
78
76
  from tomwer.io.utils.utils import get_slice_data
77
+ from tomwer.io.utils import format_stderr_stdout
78
+ from tomwer.core.process.reconstruction.nabu.nabucommon import (
79
+ ResultsLocalRun,
80
+ ResultSlurmRun,
81
+ ResultsWithStd,
82
+ )
83
+ from tomwer.core.futureobject import FutureTomwerObject
84
+ from tomwer.core.process.reconstruction.saaxis.params import ReconstructionMode
79
85
 
80
86
  from ..nabu import utils
81
- from .params import ReconstructionMode, SAAxisParams
87
+ from .params import SAAxisParams
82
88
 
83
89
  _logger = logging.getLogger(__name__)
84
90
 
@@ -87,7 +93,9 @@ DEFAULT_RECONS_FOLDER = "saaxis_results"
87
93
 
88
94
 
89
95
  def one_slice_several_cor(
90
- scan, configuration: dict, process_id: Optional[int] = None
96
+ scan,
97
+ configuration: dict,
98
+ process_id: Optional[int] = None,
91
99
  ) -> tuple:
92
100
  """
93
101
  Run a slice reconstruction using nabu per Center Of Rotation (cor) provided
@@ -104,322 +112,40 @@ def one_slice_several_cor(
104
112
  (url, score) as value
105
113
  :rtype: tuple
106
114
  """
107
- if isinstance(configuration, dict):
108
- configuration = SAAxisParams.from_dict(configuration)
109
- elif not isinstance(configuration, SAAxisParams):
110
- raise TypeError(
111
- "configuration should be a dictionary or an instance of SAAxisParams"
112
- )
113
-
114
- configuration.check_configuration()
115
- mode = ReconstructionMode.from_value(configuration.mode)
116
- slice_index = configuration.slice_indexes
117
- cors = configuration.cors
118
- nabu_config = configuration.nabu_params
119
- output_dir = configuration.output_dir
120
- dry_run = configuration.dry_run
121
- nabu_output_config = nabu_config.get("output", {})
122
- file_format = nabu_output_config.get("file_format", "hdf5")
123
- cluster_config = configuration.cluster_config
124
- _logger.info(f"launch reconstruction of slice {slice_index} and cors {cors}")
125
- if mode is ReconstructionMode.VERTICAL:
126
- if isinstance(slice_index, str):
127
- if not slice_index == "middle":
128
- raise ValueError(f"slice index {slice_index} not recognized")
129
- elif not len(slice_index) == 1:
130
- raise ValueError(f"{mode.value} mode only manage one slice")
131
- else:
132
- slice_index = list(slice_index.values())[0]
133
- advancement = Progress(f"saaxis - slice {slice_index} of {scan}")
134
-
135
- _, cor_reconstructions, outs, errs, future_tomo_objs = run_slice_reconstruction(
136
- scan=scan,
137
- slice_index=slice_index,
138
- cor_positions=cors,
139
- config=nabu_config,
140
- output_dir=output_dir,
141
- dry_run=dry_run,
142
- file_format=file_format,
143
- advancement=advancement,
144
- cluster_config=cluster_config,
145
- process_id=process_id,
146
- )
147
- else:
148
- raise ValueError(f"{mode} is not handled for now")
149
-
150
- # treat future
151
- if output_dir is None:
152
- output_dir = os.path.join(scan.path, DEFAULT_RECONS_FOLDER)
153
-
154
- db = None
155
- pag = False
156
- ctf = False
157
- if "phase" in nabu_config:
158
- phase_method = nabu_config["phase"].get("method", "").lower()
159
- if phase_method in ("pag", "paganin"):
160
- pag = True
161
- elif phase_method in ("ctf",):
162
- ctf = True
163
- if "delta_beta" in nabu_config["phase"]:
164
- db = round(float(nabu_config["phase"]["delta_beta"]))
165
-
166
- for cor, future_tomo_obj in future_tomo_objs.items():
167
- future_tomo_obj.results()
168
- # for saaxis we need to retrieve reconstruction url
169
- if future_tomo_obj.cancelled() or future_tomo_obj.exceptions():
170
- continue
171
- else:
172
- _file_name = SingleSliceRunner.get_file_basename_reconstruction(
173
- scan=scan,
174
- slice_index=slice_index,
175
- pag=pag,
176
- db=db,
177
- ctf=ctf,
178
- )
179
- file_prefix = f"cor_{_file_name}_{cor}"
180
-
181
- recons_vol_id = utils.get_recons_volume_identifier(
182
- scan=scan,
183
- file_format=file_format,
184
- file_prefix=file_prefix,
185
- location=output_dir,
186
- slice_index=None,
187
- start_z=None,
188
- end_z=None,
189
- expects_single_slice=True,
190
- )
191
- assert len(recons_vol_id) == 1, "only one volume reconstructed expected"
192
- cor_reconstructions[cor] = recons_vol_id
193
-
194
- class PostProcessing:
195
- def run(self, slice_index):
196
- datasets = self.load_datasets()
197
-
198
- mask_disk_radius = get_disk_mask_radius(datasets)
199
- scores = {}
200
- rois = {}
201
- for cor, (url, data) in datasets.items():
202
- if data is None:
203
- score = None
204
- else:
205
- assert data.ndim == 2
206
- data_roi = apply_roi(data=data, radius=mask_disk_radius, url=url)
207
- rois[cor] = data_roi
208
-
209
- # move data_roi to [0-1] range
210
- # preprocessing: get percentile 0 and 99 from image and
211
- # "clean" highest and lowest pixels from it
212
- min_p, max_p = numpy.percentile(data_roi, (1, 99))
213
- data_roi_int = data_roi[...]
214
- data_roi_int[data_roi_int < min_p] = min_p
215
- data_roi_int[data_roi_int > max_p] = max_p
216
- data_roi_int = (data_roi_int - min_p) / (max_p - min_p)
217
-
218
- if isinstance(scan, EDFTomoScan):
219
- _logger.info("tomo consistency is not handled for EDF scan")
220
- tomo_consistency_score = None
221
- else:
222
- try:
223
- projections_with_angle = scan.projections_with_angle()
224
- angles_ = [
225
- frame_angle
226
- for frame_angle, frame in projections_with_angle.items()
227
- ]
228
- angles = []
229
- for angle in angles_:
230
- if not isinstance(angle, str):
231
- angles.append(angle)
232
- if slice_index == "middle":
233
- if scan.dim_2 is not None:
234
- slice_index = scan.dim_2 // 2
235
- else:
236
- _logger.warning(
237
- "scan.dim_2 returns None, unable to deduce middle "
238
- "pick 1024"
239
- )
240
- slice_index = 1024
241
- tomo_consistency_score = compute_score(
242
- data=data,
243
- method=ScoreMethod.TOMO_CONSISTENCY,
244
- angles=angles,
245
- original_sinogram=scan.get_sinogram(slice_index),
246
- detector_width=scan.dim_1,
247
- original_axis_position=cor + scan.dim_1 / 2.0,
248
- )
249
- except Exception as e:
250
- _logger.error(e)
251
- tomo_consistency_score = None
252
- score = ComputedScore(
253
- tv=compute_score(data=data_roi_int, method=ScoreMethod.TV),
254
- std=compute_score(data=data_roi_int, method=ScoreMethod.STD),
255
- tomo_consistency=tomo_consistency_score,
256
- )
257
- scores[cor] = (url, score)
258
- return scores, rois
259
-
260
- def load_datasets(self):
261
- datasets_ = {}
262
- for cor, volume_identifiers in cor_reconstructions.items():
263
- if len(volume_identifiers) == 0:
264
- # in the case failed to load the url
265
- continue
266
- elif len(volume_identifiers) > 1:
267
- raise ValueError("only one slice reconstructed expected per cor")
268
- volume = VolumeFactory.create_tomo_object_from_identifier(
269
- volume_identifiers[0]
270
- )
271
- urls = tuple(volume.browse_data_urls())
272
- if len(urls) != 1:
273
- raise ValueError(
274
- f"volume is expected to have at most one url (single slice volume). get {len(urls)}"
275
- )
276
- url = urls[0]
277
- if not isinstance(url, (DataUrl, str)):
278
- raise TypeError(
279
- f"url is expected to be a str or DataUrl not {type(url)}"
280
- )
281
-
282
- try:
283
- data = get_slice_data(url=url)
284
- except Exception as e:
285
- _logger.error(
286
- f"Fail to compute a score for {url.path()}. Reason is {e}"
287
- )
288
- datasets_[cor] = (url, None)
289
- else:
290
- if data.ndim == 3:
291
- if data.shape[0] == 1:
292
- data = data.reshape(data.shape[1], data.shape[2])
293
- elif data.shape[2] == 1:
294
- data = data.reshape(data.shape[0], data.shape[1])
295
- else:
296
- raise ValueError(
297
- f"Data is expected to be 2D. Not {data.ndim}D"
298
- )
299
- elif data.ndim == 2:
300
- pass
301
- else:
302
- raise ValueError("Data is expected to be 2D. Not {data.ndim}D")
303
-
304
- datasets_[cor] = (url, data)
305
- return datasets_
306
-
307
- post_processing = PostProcessing()
308
- scores, rois = post_processing.run(slice_index=slice_index)
309
- return scores, outs, errs, rois
310
-
311
-
312
- def run_slice_reconstruction(
313
- scan: TomwerScanBase,
314
- cor_positions: Iterable,
315
- slice_index: int,
316
- config: dict,
317
- output_dir=None,
318
- dry_run: bool = False,
319
- file_format: str = "hdf5",
320
- advancement=None,
321
- cluster_config=None,
322
- process_id: Optional[int] = None,
323
- ) -> tuple:
324
- """
325
- call nabu for a reconstruction on scan with the given configuration
326
-
327
- :param TomwerScanBase scan: scan to reconstruct
328
- :param tuple: cor_positions cor position to used for reconstruction
329
- :param dict config: configuration to run the reconstruction
330
- :param Union[None,str]: output dir folder. If None then this will be store
331
- under the acquisition folder/saaxis_results
332
- :param bool dry_run: do we want to run dry
333
- :param bool local: do we want to run a local reconstruction
334
- :param advancement: optional Progress class to display advancement
335
-
336
- :return: success: bool, cor_results: dict, outs: list, errs: list, future_tomo_obj
337
- recons_urls is a dict with cor value as key (float) and reconstructed slice url
338
- as value
339
- :rtype: dict
340
- """
341
- nabu_configurations = interpret_tomwer_configuration(config, scan=None)
342
- if len(nabu_configurations) == 0:
343
- raise RuntimeWarning(
344
- "Unable to get a valid nabu configuration for " "reconstruction."
345
- )
346
- elif len(nabu_configurations) > 1:
347
- _logger.warning(
348
- "Several configuration found for nabu (you probably "
349
- "ask for several delta/beta value or several slices). "
350
- "Picking the first one."
351
- )
352
-
353
- # work on file name...
354
- if output_dir is None:
355
- output_dir = os.path.join(scan.path, DEFAULT_RECONS_FOLDER)
356
- if scan.process_file is not None:
357
- steps_file_basename, _ = os.path.splitext(scan.process_file)
358
- steps_file_basename = "_".join(
359
- ("steps_file_basename", "nabu", "sinogram", "save", "step")
360
- )
361
- steps_file_basename = steps_file_basename + ".hdf5"
362
- steps_file = os.path.join(output_dir, steps_file_basename)
363
- else:
364
- steps_file = ""
365
-
366
- base_config = nabu_configurations[0][0]
367
- if cluster_config == {}:
368
- cluster_config = None
369
- is_cluster_job = cluster_config is not None
370
- if is_cluster_job and not is_slurm_available():
371
- raise ValueError(
372
- "job on cluster requested but no access to slurm cluster found"
373
- )
374
- configs = {}
375
-
376
- for i_cor, cor in enumerate(cor_positions):
377
- nabu_configuration = copy.deepcopy(base_config)
378
- nabu_configuration["pipeline"] = {
379
- "save_steps": "sinogram" if i_cor == 0 else "",
380
- "resume_from_step": "sinogram",
381
- "steps_file": steps_file,
382
- }
383
- # convert cor from tomwer ref to nabu ref
384
- if scan.dim_1 is not None:
385
- cor_nabu_ref = cor + scan.dim_1 / 2.0
386
- else:
387
- _logger.warning("enable to get image half width. Set it to 1024")
388
- cor_nabu_ref = cor + 1024
389
- # handle reconstruction section
390
- if "reconstruction" not in nabu_configuration:
391
- nabu_configuration["reconstruction"] = {}
392
- nabu_configuration["reconstruction"]["rotation_axis_position"] = str(
393
- cor_nabu_ref
394
- )
395
- # handle output section
396
- if "output" not in nabu_configuration:
397
- nabu_configuration["output"] = {}
398
- nabu_configuration["output"]["location"] = output_dir
399
- nabu_configuration["output"]["file_format"] = file_format
400
- # handle resources section
401
- nabu_configuration["resources"] = utils.get_nabu_resources_desc(
402
- scan=scan, workers=1, method="local"
403
- )
404
- configs[cor] = nabu_configuration
405
- return run_nabu_one_slice_several_config(
406
- nabu_configs=configs,
407
- scan=scan,
408
- slice_index=slice_index,
409
- dry_run=dry_run,
410
- file_format=file_format,
411
- advancement=advancement,
412
- cluster_config=cluster_config.to_dict() if cluster_config is not None else None,
115
+ task = SAAxisTask(
413
116
  process_id=process_id,
117
+ inputs={
118
+ "data": scan,
119
+ "sa_axis_params": configuration,
120
+ "serialize_output_data": False,
121
+ },
122
+ )
123
+ task.run()
124
+ return (
125
+ task.outputs.scores,
126
+ task.outputs.std_out,
127
+ task.outputs.std_err,
128
+ task.outputs.rois,
414
129
  )
415
130
 
416
131
 
417
- class SAAxisProcess(
418
- Task, SuperviseProcess, input_names=("data",), output_names=("data",)
132
+ class SAAxisTask(
133
+ Task,
134
+ SuperviseProcess,
135
+ input_names=("data", "sa_axis_params"),
136
+ output_names=("data", "best_cor"),
137
+ optional_input_names=(
138
+ "dry_run",
139
+ "dump_roi",
140
+ "dump_process",
141
+ "serialize_output_data",
142
+ ),
419
143
  ):
420
144
  """
421
145
  Main process to launch several reconstruction of a single slice with
422
146
  several Center Of Rotation (cor) values
147
+
148
+ As the saaxis is integrating the score calculation we will never get a future_tomo_scan as output
423
149
  """
424
150
 
425
151
  def __init__(
@@ -434,12 +160,12 @@ class SAAxisProcess(
434
160
  )
435
161
  SuperviseProcess.__init__(self, process_id=process_id)
436
162
  self._dry_run = inputs.get("dry_run", False)
437
- self._dump_process = inputs.get("dump_info", True)
163
+ self._dump_process = inputs.get("dump_process", True)
438
164
  self._dump_roi = inputs.get("dump_roi", False)
439
165
  self._std_outs = tuple()
440
166
  self._std_errs = tuple()
441
- if "sa_axis_params" in inputs:
442
- self.set_configuration(inputs["sa_axis_params"])
167
+ self._current_processing = None
168
+ self._cancelled = False
443
169
 
444
170
  @property
445
171
  def std_outs(self):
@@ -464,16 +190,6 @@ class SAAxisProcess(
464
190
  def dump_roi(self, dump):
465
191
  self._dump_roi = dump
466
192
 
467
- def set_configuration(self, configuration: dict) -> None:
468
- if isinstance(configuration, SAAxisParams):
469
- self._settings = configuration.to_dict()
470
- elif isinstance(configuration, dict):
471
- self._settings = configuration
472
- else:
473
- raise TypeError(
474
- "configuration should be an instance of dict or " "SAAxisParams"
475
- )
476
-
477
193
  @staticmethod
478
194
  def autofocus(scan) -> Optional[float]:
479
195
  scores = scan.saaxis_params.scores
@@ -491,11 +207,234 @@ class SAAxisProcess(
491
207
  best_cor, best_score = cor, score
492
208
  scan.saaxis_params.autofocus = best_cor
493
209
  if scan.axis_params is None:
210
+ # create parameter if needed because will set it once he find the best cor
494
211
  scan.axis_params = AxisRP()
495
212
  scan.axis_params.frame_width = scan.dim_1
496
213
  scan.axis_params.set_relative_value(best_cor)
497
214
  return best_cor
498
215
 
216
+ def _config_preprocessing(
217
+ self, scan, config, cor_positions, file_format, output_dir, cluster_config
218
+ ):
219
+ """convert general configuration to nabu - single reconstruction - configuration"""
220
+ nabu_configurations = interpret_tomwer_configuration(config, scan=None)
221
+ if len(nabu_configurations) == 0:
222
+ raise RuntimeWarning(
223
+ "Unable to get a valid nabu configuration for " "reconstruction."
224
+ )
225
+ elif len(nabu_configurations) > 1:
226
+ _logger.warning(
227
+ "Several configuration found for nabu (you probably "
228
+ "ask for several delta/beta value or several slices). "
229
+ "Picking the first one."
230
+ )
231
+
232
+ # work on file name...
233
+ if output_dir is None:
234
+ output_dir = os.path.join(scan.path, DEFAULT_RECONS_FOLDER)
235
+ if scan.process_file is not None:
236
+ steps_file_basename, _ = os.path.splitext(scan.process_file)
237
+ steps_file_basename = "_".join(
238
+ ("steps_file_basename", "nabu", "sinogram", "save", "step")
239
+ )
240
+ steps_file_basename = steps_file_basename + ".hdf5"
241
+ steps_file = os.path.join(output_dir, steps_file_basename)
242
+ else:
243
+ steps_file = ""
244
+
245
+ base_config = nabu_configurations[0][0]
246
+ if cluster_config == {}:
247
+ cluster_config = None
248
+ is_cluster_job = cluster_config is not None
249
+ if is_cluster_job and not is_slurm_available():
250
+ raise ValueError(
251
+ "job on cluster requested but no access to slurm cluster found"
252
+ )
253
+ configs = {}
254
+
255
+ for i_cor, cor in enumerate(cor_positions):
256
+ nabu_configuration = copy.deepcopy(base_config)
257
+ nabu_configuration["pipeline"] = {
258
+ "save_steps": "sinogram" if i_cor == 0 else "",
259
+ "resume_from_step": "sinogram",
260
+ "steps_file": steps_file,
261
+ }
262
+ # convert cor from tomwer ref to nabu ref
263
+ if scan.dim_1 is not None:
264
+ cor_nabu_ref = cor + scan.dim_1 / 2.0
265
+ else:
266
+ _logger.warning("enable to get image half width. Set it to 1024")
267
+ cor_nabu_ref = cor + 1024
268
+ # handle reconstruction section
269
+ if "reconstruction" not in nabu_configuration:
270
+ nabu_configuration["reconstruction"] = {}
271
+ nabu_configuration["reconstruction"]["rotation_axis_position"] = str(
272
+ cor_nabu_ref
273
+ )
274
+ # handle output section
275
+ if "output" not in nabu_configuration:
276
+ nabu_configuration["output"] = {}
277
+ nabu_configuration["output"]["location"] = output_dir
278
+ nabu_configuration["output"]["file_format"] = file_format
279
+ # handle resources section
280
+ nabu_configuration["resources"] = utils.get_nabu_resources_desc(
281
+ scan=scan, workers=1, method="local"
282
+ )
283
+ configs[cor] = nabu_configuration
284
+ return configs
285
+
286
+ def _run_slice_recons_per_cor(
287
+ self,
288
+ scan,
289
+ configs,
290
+ slice_index,
291
+ file_format,
292
+ advancement,
293
+ cluster_config,
294
+ dry_run=False,
295
+ ):
296
+ runners = run_nabu_one_slice_several_config(
297
+ nabu_configs=configs,
298
+ scan=scan,
299
+ slice_index=slice_index,
300
+ dry_run=dry_run,
301
+ file_format=file_format,
302
+ advancement=advancement,
303
+ cluster_config=cluster_config.to_dict()
304
+ if cluster_config is not None
305
+ else None,
306
+ process_id=self.process_id,
307
+ instanciate_classes_only=True,
308
+ output_file_prefix_pattern="cor_{file_name}_{value}", # as the cor is evolving, create different files to make sure the name will be unique
309
+ )
310
+
311
+ future_tomo_objs = {}
312
+ success = True
313
+ recons_urls = {}
314
+ std_outs = []
315
+ std_errs = []
316
+
317
+ for runner in runners:
318
+ if self._cancelled:
319
+ break
320
+ self._current_processing = runner
321
+ try:
322
+ results = runner.run()
323
+ except TimeoutError as e:
324
+ _logger.error(e)
325
+ else:
326
+ assert isinstance(
327
+ results, dict
328
+ ), "results should be a dictionary with cor as key and urls as value"
329
+
330
+ for cor, res in results.items():
331
+ success = success and res.success
332
+ if isinstance(res, ResultsWithStd):
333
+ std_outs.append(res.std_out)
334
+ std_errs.append(res.std_err)
335
+ if isinstance(res, ResultsLocalRun):
336
+ recons_urls[cor] = res.results_urls
337
+ if isinstance(res, ResultSlurmRun):
338
+ future_tomo_obj = FutureTomwerObject(
339
+ tomo_obj=scan,
340
+ process_requester_id=self.process_id,
341
+ futures=res.future_slurm_jobs,
342
+ )
343
+ future_tomo_objs[cor] = future_tomo_obj
344
+ return success, recons_urls, future_tomo_objs, std_outs, std_errs
345
+
346
+ def _resolve_futures(
347
+ self,
348
+ scan,
349
+ nabu_config,
350
+ slice_index,
351
+ file_format,
352
+ cor_reconstructions,
353
+ future_tomo_objs: dict,
354
+ output_dir,
355
+ ):
356
+ """
357
+ in case the task is launching jobs over slurm wait for them to be finished before resuming 'standard processing'
358
+ """
359
+ if output_dir is None:
360
+ output_dir = os.path.join(scan.path, DEFAULT_RECONS_FOLDER)
361
+
362
+ db = None
363
+ pag = False
364
+ ctf = False
365
+ if "phase" in nabu_config:
366
+ phase_method = nabu_config["phase"].get("method", "").lower()
367
+ if phase_method in ("pag", "paganin"):
368
+ pag = True
369
+ elif phase_method in ("ctf",):
370
+ ctf = True
371
+ if "delta_beta" in nabu_config["phase"]:
372
+ db = round(float(nabu_config["phase"]["delta_beta"]))
373
+
374
+ for cor, future_tomo_obj in future_tomo_objs.items():
375
+ if self._cancelled:
376
+ break
377
+ future_tomo_obj.results()
378
+ # for saaxis we need to retrieve reconstruction url
379
+ if future_tomo_obj.cancelled() or future_tomo_obj.exceptions():
380
+ continue
381
+ else:
382
+ _file_name = SingleSliceRunner.get_file_basename_reconstruction(
383
+ scan=scan,
384
+ slice_index=slice_index,
385
+ pag=pag,
386
+ db=db,
387
+ ctf=ctf,
388
+ )
389
+ file_prefix = f"cor_{_file_name}_{cor}"
390
+
391
+ recons_vol_id = utils.get_recons_volume_identifier(
392
+ scan=scan,
393
+ file_format=file_format,
394
+ file_prefix=file_prefix,
395
+ location=output_dir,
396
+ slice_index=None,
397
+ start_z=None,
398
+ end_z=None,
399
+ expects_single_slice=True,
400
+ )
401
+ assert len(recons_vol_id) == 1, "only one volume reconstructed expected"
402
+ cor_reconstructions[cor] = recons_vol_id
403
+
404
+ def _post_processing(self, scan, slice_index, cor_reconstructions):
405
+ """
406
+ compute score along the different slices
407
+ """
408
+ post_processing = _PostProcessing(
409
+ slice_index=slice_index, scan=scan, cor_reconstructions=cor_reconstructions
410
+ )
411
+ post_processing._cancelled = self._cancelled
412
+ self._current_processing = post_processing
413
+ return post_processing.run()
414
+
415
+ def _compute_mess_details(self, mess=""):
416
+ """
417
+ util to join a message and nabu std err and std out
418
+ """
419
+ nabu_logs = []
420
+ for std_err, std_out in zip(self._std_errs, self.std_outs):
421
+ nabu_logs.append(format_stderr_stdout(stdout=std_out, stderr=std_err))
422
+ self._nabu_log = nabu_logs
423
+ nabu_logs.insert(0, mess)
424
+ return "\n".join(nabu_logs)
425
+
426
+ @staticmethod
427
+ def _preprocess_slice_index(slice_index, mode: ReconstructionMode):
428
+ if isinstance(slice_index, str):
429
+ if not slice_index == "middle":
430
+ raise ValueError(f"slice index {slice_index} not recognized")
431
+ else:
432
+ return slice_index
433
+ elif not len(slice_index) == 1:
434
+ raise ValueError(f"{mode.value} mode only manage one slice")
435
+ else:
436
+ return list(slice_index.values())[0]
437
+
499
438
  def run(self):
500
439
  scan = data_identifier_to_scan(self.inputs.data)
501
440
  if scan is None:
@@ -509,7 +448,7 @@ class SAAxisProcess(
509
448
  raise ValueError(f"input type of {scan}: {type(scan)} is not managed")
510
449
  # TODO: look and update if there is some nabu reconstruction
511
450
  # or axis information to be used back
512
- configuration = self.get_configuration()
451
+ configuration = self.inputs.sa_axis_params
513
452
  params = SAAxisParams.from_dict(configuration)
514
453
  # insure output dir is created
515
454
  if params.output_dir in (None, ""):
@@ -536,64 +475,160 @@ class SAAxisProcess(
536
475
  if scan.dim_1 is not None:
537
476
  params.image_width = scan.dim_1
538
477
  scan.saaxis_params = params
539
- cors_res, self._std_outs, self._std_errs, rois = one_slice_several_cor(
478
+
479
+ mode = ReconstructionMode.from_value(params.mode)
480
+ if mode is not ReconstructionMode.VERTICAL:
481
+ raise ValueError(f"{mode} is not handled for now")
482
+
483
+ output_dir = params.output_dir
484
+ if output_dir is None:
485
+ output_dir = os.path.join(scan.path, DEFAULT_RECONS_FOLDER)
486
+ nabu_output_config = configuration.get("output", {})
487
+ file_format = nabu_output_config.get("file_format", "hdf5")
488
+ slice_index = self._preprocess_slice_index(
489
+ params.slice_indexes,
490
+ mode=mode,
491
+ )
492
+ cluster_config = params.cluster_config
493
+ dry_run = self._dry_run
494
+
495
+ # step one: complete nabu configuration(s)
496
+ configs = self._config_preprocessing(
540
497
  scan=scan,
541
- configuration=self.get_configuration(),
542
- process_id=self.process_id,
498
+ config=configuration,
499
+ cor_positions=params.cors,
500
+ file_format=file_format,
501
+ output_dir=output_dir,
502
+ cluster_config=cluster_config,
503
+ )
504
+ # step 2: run reconstructions
505
+ advancement = Progress(
506
+ f"sa-axis - slice {slice_index} of {scan.get_identifier().short_description()}"
507
+ )
508
+ cors_res = {}
509
+ rois = {}
510
+
511
+ try:
512
+ (
513
+ _,
514
+ cor_reconstructions,
515
+ future_tomo_objs,
516
+ self._std_outs,
517
+ self._std_errs,
518
+ ) = self._run_slice_recons_per_cor(
519
+ scan=scan,
520
+ configs=configs,
521
+ slice_index=slice_index,
522
+ file_format=file_format,
523
+ advancement=advancement,
524
+ cluster_config=cluster_config,
525
+ dry_run=dry_run,
526
+ )
527
+ except Exception as e:
528
+ _logger.error(e)
529
+ mess = f"sa-axis -nabu- computation for {str(scan)} failed."
530
+ state = DatasetState.FAILED
531
+ else:
532
+ # step 3: wait for future if any
533
+ self._resolve_futures(
534
+ scan=scan,
535
+ nabu_config=configuration,
536
+ slice_index=slice_index,
537
+ file_format=file_format,
538
+ cor_reconstructions=cor_reconstructions,
539
+ future_tomo_objs=future_tomo_objs,
540
+ output_dir=output_dir,
541
+ )
542
+
543
+ # step 4: run post processing (compute score for each slice)
544
+ try:
545
+ cors_res, rois = self._post_processing(
546
+ scan=scan,
547
+ slice_index=slice_index,
548
+ cor_reconstructions=cor_reconstructions,
549
+ )
550
+ except Exception as e:
551
+ _logger.error(e)
552
+ mess = f"sa-axis -post-processing- computation for {str(scan)} failed."
553
+ state = DatasetState.FAILED
554
+ cors_res = {}
555
+ else:
556
+ state = DatasetState.WAIT_USER_VALIDATION
557
+ mess = "sa-axis computation succeeded"
558
+
559
+ if self._cancelled:
560
+ state = DatasetState.CANCELLED
561
+ mess = "scan cancelled by the user"
562
+
563
+ ProcessManager().notify_dataset_state(
564
+ dataset=scan,
565
+ process=self,
566
+ state=state,
567
+ details=self._compute_mess_details(mess),
543
568
  )
569
+
544
570
  scan.saaxis_params.scores = cors_res
545
571
  best_relative_cor = self.autofocus(scan=scan)
546
572
 
547
- # store nabu settings to be used later like in the volume reconstruction
548
- config = self.get_configuration()["nabu_params"]
549
- # beam shape is not directly used by nabu (uses ctf_geometry directly)
550
- config.get("phase", {}).pop("beam_shape", None)
551
-
552
- # update nabu recons_params used
553
- sc_config = get_default_nabu_config(nabu_fullfield_default_config)
554
- sc_config.update(config)
555
- scan.nabu_recons_params = sc_config
556
573
  if best_relative_cor is not None:
557
- scan.axis_params.relative_cor_values = best_relative_cor
574
+ scan.axis_params.set_relative_value(best_relative_cor)
558
575
 
559
576
  self._process_end(scan=scan, cors_res=cors_res, score_rois=rois)
560
- self.outputs.data = scan
577
+
578
+ if self.get_input_value("serialize_output_data", True):
579
+ self.outputs.data = scan.to_dict()
580
+ else:
581
+ self.outputs.data = scan
582
+ self.outputs.best_cor = best_relative_cor
561
583
 
562
584
  def _process_end(self, scan, cors_res, score_rois):
563
585
  assert isinstance(scan, TomwerScanBase)
564
- try:
565
- extra = {
566
- logconfig.DOC_TITLE: self._scheme_title,
567
- logconfig.SCAN_ID: str(scan),
568
- }
569
- slice_index = self.get_configuration().get("slice_index", None)
570
-
571
- if cors_res is None:
572
- info = f"fail to compute cor scores of slice {slice_index} for scan {scan}."
573
- _logger.processFailed(info, extra=extra)
574
- ProcessManager().notify_dataset_state(
575
- dataset=scan, process=self, state=DatasetState.FAILED, details=info
576
- )
586
+ state = ProcessManager().get_dataset_state(
587
+ dataset_id=scan.get_identifier(), process=self
588
+ )
589
+ if state not in (
590
+ DatasetState.CANCELLED,
591
+ DatasetState.FAILED,
592
+ DatasetState.SKIPPED,
593
+ ):
594
+ try:
595
+ extra = {
596
+ logconfig.DOC_TITLE: self._scheme_title,
597
+ logconfig.SCAN_ID: str(scan),
598
+ }
599
+ slice_index = self.inputs.sa_axis_params.get("slice_index", None)
600
+
601
+ if cors_res is None:
602
+ info = f"fail to compute cor scores of slice {slice_index} for scan {scan}."
603
+ _logger.processFailed(info, extra=extra)
604
+ ProcessManager().notify_dataset_state(
605
+ dataset=scan,
606
+ process=self,
607
+ state=DatasetState.FAILED,
608
+ details=info,
609
+ )
610
+ else:
611
+ info = (
612
+ f"cor scores of slice {slice_index} for scan {scan} computed."
613
+ )
614
+ _logger.processSucceed(info, extra=extra)
615
+ ProcessManager().notify_dataset_state(
616
+ dataset=scan,
617
+ process=self,
618
+ state=DatasetState.WAIT_USER_VALIDATION,
619
+ details=info,
620
+ )
621
+ except Exception as e:
622
+ _logger.error(e)
577
623
  else:
578
- info = f"cor scores of slice {slice_index} for scan {scan} computed."
579
- _logger.processSucceed(info, extra=extra)
580
- ProcessManager().notify_dataset_state(
581
- dataset=scan,
582
- process=self,
583
- state=DatasetState.WAIT_USER_VALIDATION,
584
- details=info,
585
- )
586
- except Exception as e:
587
- _logger.error(e)
588
- else:
589
- if self._dump_process:
590
- process_idx = SAAxisProcess.process_to_tomwer_processes(
591
- scan=scan,
592
- )
593
- if self.dump_roi and process_idx is not None:
594
- self.dump_rois(
595
- scan, score_rois=score_rois, process_index=process_idx
624
+ if self._dump_process:
625
+ process_idx = SAAxisTask.process_to_tomwer_processes(
626
+ scan=scan,
596
627
  )
628
+ if self.dump_roi and process_idx is not None:
629
+ self.dump_rois(
630
+ scan, score_rois=score_rois, process_index=process_idx
631
+ )
597
632
 
598
633
  @staticmethod
599
634
  def dump_rois(scan, score_rois, process_index):
@@ -653,9 +688,9 @@ class SAAxisProcess(
653
688
  configuration=scan.saaxis_params.to_dict(),
654
689
  process_index=process_index,
655
690
  overwrite=True,
656
- process=SAAxisProcess,
691
+ process=SAAxisTask,
657
692
  )
658
- SAAxisProcess._extends_results(
693
+ SAAxisTask._extends_results(
659
694
  scan=scan, entry=entry, process_index=process_index
660
695
  )
661
696
  except Exception as e:
@@ -701,3 +736,156 @@ class SAAxisProcess(
701
736
  results_cor["reconstructed_slice"] = h5py.ExternalLink(
702
737
  link_path, url.data_path()
703
738
  )
739
+
740
+ def cancel(self):
741
+ """
742
+ stop current processing
743
+ """
744
+ if self._current_processing is not None:
745
+ self._cancelled = True
746
+ self._current_processing.cancel()
747
+
748
+
749
+ class _PostProcessing:
750
+ """class used to run SA-axis post-processing on reconstructed slices"""
751
+
752
+ def __init__(self, cor_reconstructions, slice_index, scan) -> None:
753
+ self._cor_reconstructions = cor_reconstructions
754
+ self._slice_index = slice_index
755
+ self._scan = scan
756
+ self._cancelled = False
757
+
758
+ def run(self):
759
+ datasets = self.load_datasets()
760
+
761
+ mask_disk_radius = get_disk_mask_radius(datasets)
762
+ scores = {}
763
+ rois = {}
764
+ for cor, (url, data) in datasets.items():
765
+ if self._cancelled:
766
+ break
767
+
768
+ if data is None:
769
+ score = None
770
+ else:
771
+ assert data.ndim == 2
772
+ data_roi = apply_roi(data=data, radius=mask_disk_radius, url=url)
773
+ rois[cor] = data_roi
774
+
775
+ # move data_roi to [0-1] range
776
+ # preprocessing: get percentile 0 and 99 from image and
777
+ # "clean" highest and lowest pixels from it
778
+ min_p, max_p = numpy.percentile(data_roi, (1, 99))
779
+ data_roi_int = data_roi[...]
780
+ data_roi_int[data_roi_int < min_p] = min_p
781
+ data_roi_int[data_roi_int > max_p] = max_p
782
+ data_roi_int = (data_roi_int - min_p) / (max_p - min_p)
783
+
784
+ if isinstance(self._scan, EDFTomoScan):
785
+ _logger.info("tomo consistency is not handled for EDF scan")
786
+ tomo_consistency_score = None
787
+ else:
788
+ try:
789
+ projections_with_angle = self._scan.projections_with_angle()
790
+ angles_ = [
791
+ frame_angle
792
+ for frame_angle, frame in projections_with_angle.items()
793
+ ]
794
+ angles = []
795
+ for angle in angles_:
796
+ if not isinstance(angle, str):
797
+ angles.append(angle)
798
+ if self._slice_index == "middle":
799
+ if self._scan.dim_2 is not None:
800
+ self._slice_index = self._scan.dim_2 // 2
801
+ else:
802
+ _logger.warning(
803
+ "scan.dim_2 returns None, unable to deduce middle "
804
+ "pick 1024"
805
+ )
806
+ self._slice_index = 1024
807
+ tomo_consistency_score = compute_score(
808
+ data=data,
809
+ method=ScoreMethod.TOMO_CONSISTENCY,
810
+ angles=angles,
811
+ original_sinogram=self._scan.get_sinogram(
812
+ self._slice_index
813
+ ),
814
+ detector_width=self._scan.dim_1,
815
+ original_axis_position=cor + self._scan.dim_1 / 2.0,
816
+ )
817
+ except Exception as e:
818
+ _logger.error(e)
819
+ tomo_consistency_score = None
820
+ score = ComputedScore(
821
+ tv=compute_score(data=data_roi_int, method=ScoreMethod.TV),
822
+ std=compute_score(data=data_roi_int, method=ScoreMethod.STD),
823
+ tomo_consistency=tomo_consistency_score,
824
+ )
825
+ scores[cor] = (url, score)
826
+ return scores, rois
827
+
828
+ def load_datasets(self):
829
+ datasets_ = {}
830
+ for cor, volume_identifiers in self._cor_reconstructions.items():
831
+ if self._cancelled:
832
+ break
833
+
834
+ if len(volume_identifiers) == 0:
835
+ # in the case failed to load the url
836
+ continue
837
+ elif len(volume_identifiers) > 1:
838
+ raise ValueError("only one slice reconstructed expected per cor")
839
+ volume = VolumeFactory.create_tomo_object_from_identifier(
840
+ volume_identifiers[0]
841
+ )
842
+ urls = tuple(volume.browse_data_urls())
843
+ if len(urls) != 1:
844
+ raise ValueError(
845
+ f"volume is expected to have at most one url (single slice volume). get {len(urls)} - most likely nabu reconstruction failed. Do you have GPU ? Are the requested COR values valid ? - Especially for Half-acquisition"
846
+ )
847
+ url = urls[0]
848
+ if not isinstance(url, (DataUrl, str)):
849
+ raise TypeError(
850
+ f"url is expected to be a str or DataUrl not {type(url)}"
851
+ )
852
+
853
+ try:
854
+ data = get_slice_data(url=url)
855
+ except Exception as e:
856
+ _logger.error(
857
+ f"Fail to compute a score for {url.path()}. Reason is {e}"
858
+ )
859
+ datasets_[cor] = (url, None)
860
+ else:
861
+ if data.ndim == 3:
862
+ if data.shape[0] == 1:
863
+ data = data.reshape(data.shape[1], data.shape[2])
864
+ elif data.shape[2] == 1:
865
+ data = data.reshape(data.shape[0], data.shape[1])
866
+ else:
867
+ raise ValueError(f"Data is expected to be 2D. Not {data.ndim}D")
868
+ elif data.ndim == 2:
869
+ pass
870
+ else:
871
+ raise ValueError("Data is expected to be 2D. Not {data.ndim}D")
872
+
873
+ datasets_[cor] = (url, data)
874
+ return datasets_
875
+
876
+ def cancel(self):
877
+ self._cancelled = True
878
+
879
+
880
+ class SAAxisProcess(SAAxisTask):
881
+ def __init__(
882
+ self, process_id=None, inputs=None, varinfo=None, node_attrs=None, execinfo=None
883
+ ):
884
+ deprecated_warning(
885
+ name="tomwer.core.process.reconstruction.saaxis.SAAxisProcess",
886
+ type_="class",
887
+ reason="improve readibility",
888
+ since_version="1.2",
889
+ replacement="SAAxisTask",
890
+ )
891
+ super().__init__(process_id, inputs, varinfo, node_attrs, execinfo)