tomwer 1.2.9__py3-none-any.whl → 1.3.0a0__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 (253) hide show
  1. orangecontrib/tomwer/tutorials/icat_publication.ows +58 -0
  2. orangecontrib/tomwer/widgets/__init__.py +1 -0
  3. orangecontrib/tomwer/widgets/control/DataDiscoveryOW.py +2 -2
  4. orangecontrib/tomwer/widgets/control/DataListOW.py +9 -7
  5. orangecontrib/tomwer/widgets/control/DataSelectorOW.py +21 -10
  6. orangecontrib/tomwer/widgets/control/EDF2NXTomomillOW.py +11 -5
  7. orangecontrib/tomwer/widgets/control/EmailOW.py +4 -4
  8. orangecontrib/tomwer/widgets/control/NXTomomillOW.py +31 -18
  9. orangecontrib/tomwer/widgets/control/NXtomoConcatenate.py +14 -7
  10. orangecontrib/tomwer/widgets/control/NotifierOW.py +1 -0
  11. orangecontrib/tomwer/widgets/control/VolumeSelector.py +7 -4
  12. orangecontrib/tomwer/widgets/control/VolumeSymLinkOW.py +182 -182
  13. orangecontrib/tomwer/widgets/debugtools/DatasetGeneratorOW.py +4 -4
  14. orangecontrib/tomwer/widgets/edit/DarkFlatPatchOW.py +4 -4
  15. orangecontrib/tomwer/widgets/edit/ImageKeyEditorOW.py +3 -3
  16. orangecontrib/tomwer/widgets/edit/ImageKeyUpgraderOW.py +2 -0
  17. orangecontrib/tomwer/widgets/edit/NXtomoEditorOW.py +3 -3
  18. orangecontrib/tomwer/widgets/edit/test/test_nxtomo_editor.py +3 -3
  19. orangecontrib/tomwer/widgets/icat/PublishProcessedDataOW.py +115 -0
  20. orangecontrib/tomwer/widgets/icat/RawDataScreenshotCreatorOW.py +98 -0
  21. orangecontrib/tomwer/widgets/icat/SaveToGalleryAndPublishOW.py +129 -0
  22. orangecontrib/tomwer/widgets/icat/__init__.py +13 -0
  23. orangecontrib/tomwer/widgets/icat/icons/add_gallery.png +0 -0
  24. orangecontrib/tomwer/widgets/icat/icons/add_gallery.svg +82 -0
  25. orangecontrib/tomwer/widgets/icat/icons/publish_processed_data.png +0 -0
  26. orangecontrib/tomwer/widgets/icat/icons/publish_processed_data.svg +95 -0
  27. orangecontrib/tomwer/widgets/icat/icons/raw_screenshots.png +0 -0
  28. orangecontrib/tomwer/widgets/icat/icons/raw_screenshots.svg +143 -0
  29. orangecontrib/tomwer/widgets/icons/tomwer_data_portal.png +0 -0
  30. orangecontrib/tomwer/widgets/icons/tomwer_data_portal.svg +76 -0
  31. orangecontrib/tomwer/widgets/reconstruction/AxisOW.py +9 -8
  32. orangecontrib/tomwer/widgets/reconstruction/CastNabuVolumeOW.py +3 -3
  33. orangecontrib/tomwer/widgets/reconstruction/NabuHelicalPrepareWeightsDoubleOW.py +179 -169
  34. orangecontrib/tomwer/widgets/reconstruction/NabuOW.py +23 -0
  35. orangecontrib/tomwer/widgets/reconstruction/NabuVolumeOW.py +39 -5
  36. orangecontrib/tomwer/widgets/reconstruction/SAAxisOW.py +7 -13
  37. orangecontrib/tomwer/widgets/reconstruction/SADeltaBetaOW.py +7 -17
  38. orangecontrib/tomwer/widgets/reconstruction/SinoNormOW.py +3 -4
  39. orangecontrib/tomwer/widgets/visualization/LivesliceOW.py +1 -1
  40. orangecontrib/tomwer/widgets/visualization/NXtomoMetadataViewerOW.py +3 -3
  41. orangecontrib/tomwer/widgets/visualization/VolumeViewerOW.py +3 -29
  42. tomwer/__main__.py +11 -58
  43. tomwer/app/canvas.py +8 -0
  44. tomwer/app/canvas_launcher/config.py +13 -11
  45. tomwer/app/darkref.py +1 -1
  46. tomwer/app/darkrefpatch.py +1 -1
  47. tomwer/app/imagekeyeditor.py +5 -5
  48. tomwer/app/imagekeyupgrader.py +5 -5
  49. tomwer/app/intensitynormalization.py +2 -2
  50. tomwer/app/radiostack.py +2 -2
  51. tomwer/app/zstitching.py +74 -3
  52. tomwer/core/cluster/cluster.py +26 -0
  53. tomwer/core/log/logger.py +7 -5
  54. tomwer/core/process/conditions/filters.py +1 -1
  55. tomwer/core/process/control/datalistener/datalistener.py +3 -3
  56. tomwer/core/process/control/nxtomoconcatenate.py +13 -13
  57. tomwer/core/process/control/nxtomomill.py +83 -25
  58. tomwer/core/process/control/scantransfer.py +11 -10
  59. tomwer/core/process/control/scanvalidator.py +3 -2
  60. tomwer/core/process/control/test/test_concatenate_nxtomos.py +9 -9
  61. tomwer/core/process/control/test/test_email.py +4 -4
  62. tomwer/core/process/control/test/test_h52nx_process.py +59 -7
  63. tomwer/core/process/control/test/test_volume_link.py +64 -64
  64. tomwer/core/process/control/timer.py +1 -1
  65. tomwer/core/process/control/volumesymlink.py +200 -200
  66. tomwer/core/process/edit/darkflatpatch.py +6 -6
  67. tomwer/core/process/edit/imagekeyeditor.py +17 -18
  68. tomwer/core/process/icat/__init__.py +0 -0
  69. tomwer/core/process/icat/createscreenshots.py +100 -0
  70. tomwer/core/process/icat/gallery.py +377 -0
  71. tomwer/core/process/icat/icatbase.py +36 -0
  72. tomwer/core/process/icat/publish.py +228 -0
  73. tomwer/core/process/icat/screenshots.py +26 -0
  74. tomwer/core/process/output.py +52 -0
  75. tomwer/core/process/reconstruction/axis/axis.py +17 -10
  76. tomwer/core/process/reconstruction/axis/mode.py +4 -0
  77. tomwer/core/process/reconstruction/axis/params.py +9 -4
  78. tomwer/core/process/reconstruction/darkref/darkrefs.py +8 -6
  79. tomwer/core/process/reconstruction/darkref/darkrefscopy.py +1 -1
  80. tomwer/core/process/reconstruction/darkref/params.py +1 -1
  81. tomwer/core/process/reconstruction/lamino/tofu.py +4 -4
  82. tomwer/core/process/reconstruction/nabu/castvolume.py +1 -1
  83. tomwer/core/process/reconstruction/nabu/helical.py +9 -5
  84. tomwer/core/process/reconstruction/nabu/nabucommon.py +32 -62
  85. tomwer/core/process/reconstruction/nabu/nabuscores.py +387 -61
  86. tomwer/core/process/reconstruction/nabu/nabuslices.py +33 -21
  87. tomwer/core/process/reconstruction/nabu/nabuvolume.py +37 -14
  88. tomwer/core/process/reconstruction/nabu/settings.py +2 -2
  89. tomwer/core/process/reconstruction/nabu/utils.py +129 -24
  90. tomwer/core/process/reconstruction/output.py +108 -0
  91. tomwer/core/process/reconstruction/saaxis/saaxis.py +233 -263
  92. tomwer/core/process/reconstruction/sadeltabeta/sadeltabeta.py +140 -86
  93. tomwer/core/process/reconstruction/scores/params.py +4 -1
  94. tomwer/core/process/reconstruction/scores/scores.py +13 -0
  95. tomwer/core/process/reconstruction/test/test_axis_params.py +2 -2
  96. tomwer/core/process/reconstruction/test/test_darkref.py +3 -3
  97. tomwer/core/process/reconstruction/test/test_darkref_copy.py +3 -3
  98. tomwer/core/process/reconstruction/test/test_saaxis.py +3 -4
  99. tomwer/core/process/reconstruction/test/test_sadeltabeta.py +2 -2
  100. tomwer/core/process/stitching/nabustitcher.py +2 -2
  101. tomwer/core/process/test/test_axis.py +6 -6
  102. tomwer/core/process/test/test_dark_and_flat.py +10 -7
  103. tomwer/core/process/test/test_data_transfer.py +7 -6
  104. tomwer/core/process/test/test_nabu.py +4 -4
  105. tomwer/core/process/test/test_normalization.py +2 -2
  106. tomwer/core/scan/edfscan.py +4 -1
  107. tomwer/core/scan/hdf5scan.py +19 -500
  108. tomwer/core/scan/nxtomoscan.py +532 -0
  109. tomwer/core/scan/scanbase.py +42 -20
  110. tomwer/core/scan/scanfactory.py +13 -13
  111. tomwer/core/scan/test/test_future_scan.py +2 -2
  112. tomwer/core/scan/test/test_h5.py +12 -10
  113. tomwer/core/scan/test/test_process_registration.py +2 -2
  114. tomwer/core/scan/test/test_scan.py +4 -3
  115. tomwer/core/settings.py +20 -0
  116. tomwer/core/test/test_scanutils.py +8 -7
  117. tomwer/core/test/test_utils.py +33 -26
  118. tomwer/core/utils/__init__.py +0 -466
  119. tomwer/core/utils/deprecation.py +1 -1
  120. tomwer/core/utils/dictutils.py +14 -0
  121. tomwer/core/utils/lbsram.py +35 -0
  122. tomwer/core/utils/nxtomoutils.py +1 -1
  123. tomwer/core/utils/scanutils.py +6 -6
  124. tomwer/core/utils/spec.py +263 -0
  125. tomwer/core/volume/volumefactory.py +2 -2
  126. tomwer/gui/cluster/slurm.py +260 -60
  127. tomwer/gui/cluster/test/test_cluster.py +13 -0
  128. tomwer/gui/cluster/test/test_supervisor.py +2 -2
  129. tomwer/gui/configuration/__init__.py +0 -0
  130. tomwer/gui/{reconstruction/nabu → configuration}/action.py +1 -32
  131. tomwer/gui/configuration/level.py +22 -0
  132. tomwer/gui/control/actions.py +54 -0
  133. tomwer/gui/control/datalist.py +78 -16
  134. tomwer/gui/control/datalistener.py +4 -16
  135. tomwer/gui/control/{email.py → emailnotifier.py} +9 -18
  136. tomwer/gui/control/history.py +2 -2
  137. tomwer/gui/control/observations.py +2 -2
  138. tomwer/gui/control/reducedarkflatselector.py +1 -1
  139. tomwer/gui/control/selectorwidgetbase.py +36 -9
  140. tomwer/gui/control/serie/seriecreator.py +5 -22
  141. tomwer/gui/control/test/test_email.py +1 -1
  142. tomwer/gui/control/test/test_scanvalidator.py +6 -5
  143. tomwer/gui/control/test/test_single_tomo_obj.py +2 -2
  144. tomwer/gui/control/tomoobjdisplaymode.py +8 -0
  145. tomwer/gui/debugtools/datasetgenerator.py +3 -3
  146. tomwer/gui/edit/dkrfpatch.py +16 -22
  147. tomwer/gui/edit/imagekeyeditor.py +8 -11
  148. tomwer/gui/edit/nxtomoeditor.py +111 -44
  149. tomwer/gui/edit/nxtomowarmer.py +4 -4
  150. tomwer/gui/edit/test/test_dkrf_patch.py +7 -7
  151. tomwer/gui/edit/test/test_image_key_editor.py +3 -3
  152. tomwer/gui/edit/test/test_nx_editor.py +40 -16
  153. tomwer/gui/icat/__init__.py +0 -0
  154. tomwer/gui/icat/createscreenshots.py +80 -0
  155. tomwer/gui/icat/gallery.py +214 -0
  156. tomwer/gui/icat/publish.py +187 -0
  157. tomwer/gui/reconstruction/axis/axis.py +171 -57
  158. tomwer/gui/reconstruction/axis/radioaxis.py +80 -95
  159. tomwer/gui/reconstruction/darkref/darkrefcopywidget.py +3 -2
  160. tomwer/gui/reconstruction/lamino/tofu/projections.py +1 -1
  161. tomwer/gui/reconstruction/lamino/tofu/tofuoutput.py +3 -6
  162. tomwer/gui/reconstruction/nabu/castvolume.py +1 -1
  163. tomwer/gui/reconstruction/nabu/check.py +9 -9
  164. tomwer/gui/reconstruction/nabu/helical.py +29 -12
  165. tomwer/gui/reconstruction/nabu/nabuconfig/base.py +2 -4
  166. tomwer/gui/reconstruction/nabu/nabuconfig/output.py +110 -33
  167. tomwer/gui/reconstruction/nabu/nabuconfig/phase.py +9 -12
  168. tomwer/gui/reconstruction/nabu/nabuconfig/preprocessing.py +219 -29
  169. tomwer/gui/reconstruction/nabu/nabuconfig/reconstruction.py +3 -6
  170. tomwer/gui/reconstruction/nabu/nabuflow.py +12 -20
  171. tomwer/gui/reconstruction/nabu/slices.py +6 -7
  172. tomwer/gui/reconstruction/nabu/volume.py +22 -10
  173. tomwer/gui/reconstruction/normalization/intensity.py +15 -23
  174. tomwer/gui/reconstruction/saaxis/corrangeselector.py +7 -23
  175. tomwer/gui/reconstruction/saaxis/dimensionwidget.py +1 -1
  176. tomwer/gui/reconstruction/saaxis/saaxis.py +7 -9
  177. tomwer/gui/reconstruction/sadeltabeta/saadeltabeta.py +2 -1
  178. tomwer/gui/reconstruction/scores/control.py +2 -9
  179. tomwer/gui/reconstruction/scores/scoreplot.py +11 -5
  180. tomwer/gui/reconstruction/test/test_axis.py +23 -12
  181. tomwer/gui/reconstruction/test/test_lamino.py +8 -3
  182. tomwer/gui/reconstruction/test/test_nabu.py +28 -9
  183. tomwer/gui/reconstruction/test/test_saaxis.py +3 -3
  184. tomwer/gui/reconstruction/test/test_sadeltabeta.py +2 -2
  185. tomwer/gui/settings.py +5 -28
  186. tomwer/gui/stackplot.py +2 -5
  187. tomwer/gui/stitching/action.py +49 -0
  188. tomwer/gui/stitching/config/axisparams.py +7 -24
  189. tomwer/gui/stitching/config/output.py +10 -8
  190. tomwer/gui/stitching/config/positionoveraxis.py +22 -23
  191. tomwer/gui/stitching/normalization.py +117 -0
  192. tomwer/gui/stitching/stitchandbackground.py +4 -6
  193. tomwer/gui/stitching/stitching.py +265 -43
  194. tomwer/gui/stitching/stitching_preview.py +62 -5
  195. tomwer/gui/stitching/stitching_raw.py +2 -5
  196. tomwer/gui/stitching/z_stitching/fineestimation.py +0 -60
  197. tomwer/gui/utils/buttons.py +112 -29
  198. tomwer/gui/utils/inputwidget.py +33 -25
  199. tomwer/gui/utils/scandescription.py +4 -0
  200. tomwer/gui/utils/step.py +144 -0
  201. tomwer/gui/utils/unitsystem.py +2 -5
  202. tomwer/gui/utils/vignettes.py +176 -15
  203. tomwer/gui/visualization/dataviewer.py +1 -4
  204. tomwer/gui/visualization/diffviewer/diffviewer.py +7 -16
  205. tomwer/gui/visualization/diffviewer/shiftwidget.py +2 -5
  206. tomwer/gui/visualization/scanoverview.py +1 -1
  207. tomwer/gui/visualization/sinogramviewer.py +1 -10
  208. tomwer/gui/visualization/test/test_diffviewer.py +3 -3
  209. tomwer/gui/visualization/test/test_nx_tomo_metadata_viewer.py +4 -4
  210. tomwer/gui/visualization/test/test_sinogramviewer.py +2 -2
  211. tomwer/gui/visualization/test/test_stacks.py +3 -3
  212. tomwer/gui/visualization/test/test_volumeviewer.py +2 -2
  213. tomwer/io/utils/raw_and_processed_data.py +84 -0
  214. tomwer/io/utils/tomoobj.py +4 -6
  215. tomwer/resources/gui/icons/ruler.png +0 -0
  216. tomwer/resources/gui/icons/ruler.svg +273 -0
  217. tomwer/resources/gui/icons/short_description.png +0 -0
  218. tomwer/resources/gui/icons/short_description.svg +58 -0
  219. tomwer/resources/gui/icons/url.png +0 -0
  220. tomwer/resources/gui/icons/url.svg +58 -0
  221. tomwer/synctools/stacks/edit/darkflatpatch.py +2 -2
  222. tomwer/synctools/stacks/edit/imagekeyeditor.py +2 -2
  223. tomwer/synctools/stacks/reconstruction/axis.py +4 -4
  224. tomwer/synctools/stacks/reconstruction/castvolume.py +2 -2
  225. tomwer/synctools/stacks/reconstruction/dkrefcopy.py +4 -10
  226. tomwer/synctools/stacks/reconstruction/nabu.py +2 -2
  227. tomwer/synctools/stacks/reconstruction/normalization.py +2 -2
  228. tomwer/synctools/stacks/reconstruction/saaxis.py +2 -2
  229. tomwer/synctools/stacks/reconstruction/sadeltabeta.py +2 -2
  230. tomwer/synctools/test/test_darkRefs.py +7 -58
  231. tomwer/synctools/test/test_foldertransfer.py +6 -6
  232. tomwer/synctools/utils/scanstages.py +6 -6
  233. tomwer/tests/conftest.py +34 -0
  234. tomwer/tests/datasets.py +13 -0
  235. tomwer/tests/test_scripts.py +92 -39
  236. tomwer/tests/utils.py +5 -0
  237. tomwer/version.py +3 -3
  238. {tomwer-1.2.9.dist-info → tomwer-1.3.0a0.dist-info}/METADATA +39 -39
  239. {tomwer-1.2.9.dist-info → tomwer-1.3.0a0.dist-info}/RECORD +248 -209
  240. tomwer/resources/gui/icons/esrf_1.svg +0 -307
  241. tomwer/resources/gui/icons/triangle.svg +0 -80
  242. tomwer/synctools/test/test_scanstages.py +0 -162
  243. tomwer/tests/utils/__init__.py +0 -247
  244. tomwer/tests/utils/utilstest.py +0 -220
  245. /tomwer/app/{saaxis.py → multicor.py} +0 -0
  246. /tomwer/app/{sadeltabeta.py → multipag.py} +0 -0
  247. /tomwer/core/process/control/{email.py → emailnotifier.py} +0 -0
  248. /tomwer-1.2.9-py3.11-nspkg.pth → /tomwer-1.3.0a0-py3.11-nspkg.pth +0 -0
  249. {tomwer-1.2.9.dist-info → tomwer-1.3.0a0.dist-info}/LICENSE +0 -0
  250. {tomwer-1.2.9.dist-info → tomwer-1.3.0a0.dist-info}/WHEEL +0 -0
  251. {tomwer-1.2.9.dist-info → tomwer-1.3.0a0.dist-info}/entry_points.txt +0 -0
  252. {tomwer-1.2.9.dist-info → tomwer-1.3.0a0.dist-info}/namespace_packages.txt +0 -0
  253. {tomwer-1.2.9.dist-info → tomwer-1.3.0a0.dist-info}/top_level.txt +0 -0
@@ -28,13 +28,23 @@ __license__ = "MIT"
28
28
  __date__ = "11/10/2021"
29
29
 
30
30
 
31
+ import logging
31
32
  from typing import Optional
33
+ from functools import lru_cache as cache
32
34
 
33
35
  from silx.gui import qt
34
- from sluurp.utils import get_partitions
36
+ from sluurp.utils import get_partitions, get_partition_gpus
35
37
 
36
38
  from tomwer.core.settings import SlurmSettings, SlurmSettingsMode
37
39
  from tomwer.gui.utils.qt_utils import block_signals
40
+ from tomwer.gui.configuration.action import (
41
+ BasicConfigurationAction,
42
+ ExpertConfigurationAction,
43
+ )
44
+ from tomwer.gui.configuration.level import ConfigurationLevel
45
+ from nxtomomill.io.utils import convert_str_to_tuple
46
+
47
+ _logger = logging.getLogger(__name__)
38
48
 
39
49
 
40
50
  class SlurmSettingsDialog(qt.QDialog):
@@ -58,10 +68,7 @@ class SlurmSettingsDialog(qt.QDialog):
58
68
  self._buttons.button(qt.QDialogButtonBox.Close).clicked.connect(self.close)
59
69
 
60
70
  # connect signal /slot
61
- self._mainWidget.sigConfigChanged.connect(self._configChanged)
62
-
63
- def _configChanged(self, *args, **kwargs):
64
- self.sigConfigChanged.emit()
71
+ self._mainWidget.sigConfigChanged.connect(self.sigConfigChanged)
65
72
 
66
73
  def isSlurmActive(self):
67
74
  return self._mainWidget.isSlurmActive()
@@ -84,6 +91,29 @@ class SlurmSettingsWindow(qt.QMainWindow):
84
91
 
85
92
  def __init__(self, parent: Optional[qt.QWidget] = None) -> None:
86
93
  super().__init__(parent)
94
+
95
+ # define toolbar
96
+ toolbar = qt.QToolBar(self)
97
+ self.addToolBar(qt.Qt.TopToolBarArea, toolbar)
98
+
99
+ self.__configurationModesAction = qt.QAction(self)
100
+ self.__configurationModesAction.setCheckable(False)
101
+ menu = qt.QMenu(self)
102
+ self.__configurationModesAction.setMenu(menu)
103
+ toolbar.addAction(self.__configurationModesAction)
104
+
105
+ self.__configurationModesGroup = qt.QActionGroup(self)
106
+ self.__configurationModesGroup.setExclusive(True)
107
+ self.__configurationModesGroup.triggered.connect(self._userModeChanged)
108
+
109
+ self._basicConfigAction = BasicConfigurationAction(toolbar)
110
+ menu.addAction(self._basicConfigAction)
111
+ self.__configurationModesGroup.addAction(self._basicConfigAction)
112
+ self._expertConfiguration = ExpertConfigurationAction(toolbar)
113
+ menu.addAction(self._expertConfiguration)
114
+ self.__configurationModesGroup.addAction(self._expertConfiguration)
115
+
116
+ # define maini widget
87
117
  self._mainWidget = qt.QWidget(self)
88
118
  self._mainWidget.setLayout(qt.QFormLayout())
89
119
 
@@ -92,13 +122,15 @@ class SlurmSettingsWindow(qt.QMainWindow):
92
122
  self._modeCombox.addItems(SlurmSettingsMode.values())
93
123
  self._modeCombox.setCurrentText(SlurmSettingsMode.GENERIC.value)
94
124
 
95
- self._settingsWidget = SlurmSettingsWidget(self)
125
+ self._settingsWidget = SlurmSettingsWidget(self, jobLimitation=None)
96
126
  self._mainWidget.layout().addRow(self._settingsWidget)
97
127
 
98
128
  self.setCentralWidget(self._mainWidget)
99
129
 
100
130
  # set up
101
131
  self._reloadPredefinedSettings()
132
+ self._basicConfigAction.setChecked(True)
133
+ self._userModeChanged(self._basicConfigAction)
102
134
 
103
135
  # connect signal / slot
104
136
  self._modeCombox.currentIndexChanged.connect(self._reloadPredefinedSettings)
@@ -106,6 +138,17 @@ class SlurmSettingsWindow(qt.QMainWindow):
106
138
  # when the settings widget is edited them we automatically move to 'manual' settings. To notify visually the user
107
139
  self._settingsWidget.sigConfigChanged.connect(self._switchToManual)
108
140
 
141
+ def _userModeChanged(self, action):
142
+ self.__configurationModesAction.setIcon(action.icon())
143
+ self.__configurationModesAction.setToolTip(action.tooltip())
144
+ if action is self._basicConfigAction:
145
+ level = ConfigurationLevel.OPTIONAL
146
+ elif action is self._expertConfiguration:
147
+ level = ConfigurationLevel.ADVANCED
148
+ else:
149
+ raise NotImplementedError
150
+ self._settingsWidget.setConfigurationLevel(level)
151
+
109
152
  def _reloadPredefinedSettings(self, *args, **kkwargs):
110
153
  """
111
154
  reload settings from some predefined configuration
@@ -185,7 +228,8 @@ class SlurmSettingsWidget(qt.QWidget):
185
228
  # n workers
186
229
  self._nWorkers = qt.QSpinBox(self)
187
230
  self._nWorkers.setRange(1, 100)
188
- self.layout().addRow("number of task", self._nWorkers)
231
+ self._nWorkersLabel = qt.QLabel("number of task", self)
232
+ self.layout().addRow(self._nWorkersLabel, self._nWorkers)
189
233
 
190
234
  # ncores active
191
235
  self._nCores = qt.QSpinBox(self)
@@ -218,17 +262,34 @@ class SlurmSettingsWidget(qt.QWidget):
218
262
  self._wallTimeLabel = qt.QLabel("wall time", self)
219
263
  self.layout().addRow(self._wallTimeLabel, self._wallTimeQLE)
220
264
 
221
- # python exe
265
+ # python exe / modules
266
+ self._preProcessingGroup = qt.QGroupBox("pre-processing", self)
267
+ self._preProcessingGroup.setLayout(qt.QFormLayout())
268
+ self._preProcessingButtonGroup = qt.QButtonGroup(self)
269
+ self._preProcessingButtonGroup.setExclusive(True)
270
+ self.layout().addRow(self._preProcessingGroup)
271
+
272
+ # python venv
222
273
  self._pythonVenv = qt.QLineEdit("", self)
223
- self.layout().addRow(
224
- "source script before processing (python venv)", self._pythonVenv
225
- )
274
+ self._sourceScriptCB = qt.QRadioButton("source script (python venv)", self)
275
+ self._preProcessingButtonGroup.addButton(self._sourceScriptCB)
276
+ self._preProcessingGroup.layout().addRow(self._sourceScriptCB, self._pythonVenv)
226
277
  self._pythonVenv.setToolTip(
227
278
  """
228
279
  Optional path to a bash script to source before executing the script.
229
280
  """
230
281
  )
231
282
 
283
+ self._modulesQLE = qt.QLineEdit("tomotools,", self)
284
+ self._modulesCB = qt.QRadioButton("module to load", self)
285
+ self._preProcessingButtonGroup.addButton(self._modulesCB)
286
+ self._preProcessingGroup.layout().addRow(self._modulesCB, self._modulesQLE)
287
+ self._preProcessingGroup.setToolTip(
288
+ """
289
+ Optional list of modules to load before executing the script.
290
+ """
291
+ )
292
+
232
293
  # job name
233
294
  self._jobName = qt.QLineEdit("", self)
234
295
  self._jobName.setToolTip(
@@ -257,6 +318,39 @@ class SlurmSettingsWidget(qt.QWidget):
257
318
  self._dashboardPortLabel = qt.QLabel("dashboard port", self)
258
319
  self.layout().addRow(self._dashboardPortLabel, self._dashboardPort)
259
320
 
321
+ # sbatch advance parameters
322
+ self._sbatchAdvancedParameters = qt.QGroupBox("sbatch advanced settings", self)
323
+ self._sbatchAdvancedParameters.setLayout(qt.QFormLayout())
324
+ self.layout().addRow(self._sbatchAdvancedParameters)
325
+
326
+ ## export parameter
327
+ self._exportValueCM = qt.QComboBox(self)
328
+ self._exportValueCM.addItems(("NONE", "ALL"))
329
+ self._exportValueCM.setItemData(
330
+ self._exportValueCM.findText("NONE"),
331
+ """
332
+ Only SLURM_* variables from the user environment will be defined. User must use absolute path to the binary to be executed that will define the environment. User can not specify explicit environment variables with "NONE". However, Slurm will then implicitly attempt to load the user's environment on the node where the script is being executed, as if --get-user-env was specified. \nThis option is particularly important for jobs that are submitted on one cluster and execute on a different cluster (e.g. with different paths). To avoid steps inheriting environment export settings (e.g. "NONE") from sbatch command, the environment variable SLURM_EXPORT_ENV should be set to "ALL" in the job script.
333
+ """,
334
+ )
335
+ self._exportValueCM.setItemData(
336
+ self._exportValueCM.findText("ALL"),
337
+ """
338
+ All of the user's environment will be loaded (either from the caller's environment or from a clean environment if --get-user-env is specified). \nCan fail when submitting cross-platform jobs and user has some module loaded
339
+ """,
340
+ )
341
+ self._exportValueCM.setCurrentText("NONE")
342
+ self._sbatchAdvancedParameters.layout().addRow("--export", self._exportValueCM)
343
+
344
+ ## gpu card
345
+ self._gpuCardCB = qt.QComboBox(self)
346
+ self._gpuCardCB.setToolTip(
347
+ "Specify a GPU card to be used. Using the -C command from sbatch"
348
+ )
349
+ self._gpuCardCB.setEditable(
350
+ True
351
+ ) # let the user the ability to provide a GPU that is not found for now (expecting he knows what he is doing)
352
+ self._sbatchAdvancedParameters.layout().addRow("-C (gpu card)", self._gpuCardCB)
353
+
260
354
  # simplify gui
261
355
  self._wallTimeLabel.hide()
262
356
  self._wallTimeQLE.hide()
@@ -266,6 +360,9 @@ class SlurmSettingsWidget(qt.QWidget):
266
360
  self._job_nameQLabel.hide()
267
361
  self._portLabel.hide() # for now we don't use the port. This can be done automatically
268
362
  self._port.hide() # for now we don't use the port. This can be done automatically
363
+ # for now nworker == ntask is not used
364
+ self._nWorkers.setVisible(False)
365
+ self._nWorkersLabel.setVisible(False)
269
366
 
270
367
  # set up the gui
271
368
  self._nCores.setValue(SlurmSettings.N_CORES_PER_TASK)
@@ -276,6 +373,10 @@ class SlurmSettingsWidget(qt.QWidget):
276
373
  self._jobName.setText(SlurmSettings.PROJECT_NAME)
277
374
  self._wallTimeQLE.setText(SlurmSettings.DEFAULT_WALLTIME)
278
375
  self._pythonVenv.setText(SlurmSettings.PYTHON_VENV)
376
+ self._sourceScriptCB.setChecked(True)
377
+ self._preProcessingModeChanged()
378
+ self._partitionChanged()
379
+ self._nGpuChanged()
279
380
 
280
381
  # connect signal / slot
281
382
  self._nCores.valueChanged.connect(self._configurationChanged)
@@ -286,8 +387,20 @@ class SlurmSettingsWidget(qt.QWidget):
286
387
  self._jobName.editingFinished.connect(self._configurationChanged)
287
388
  self._wallTimeQLE.editingFinished.connect(self._configurationChanged)
288
389
  self._pythonVenv.editingFinished.connect(self._configurationChanged)
390
+ self._modulesQLE.editingFinished.connect(self._configurationChanged)
391
+ self._preProcessingButtonGroup.buttonClicked.connect(self._configurationChanged)
289
392
  self._port.sigRangeChanged.connect(self._configurationChanged)
290
393
  self._dashboardPort.valueChanged.connect(self._configurationChanged)
394
+ self._preProcessingButtonGroup.buttonClicked.connect(
395
+ self._preProcessingModeChanged
396
+ )
397
+ self._queue.currentTextChanged.connect(self._partitionChanged)
398
+ self._nGpu.valueChanged.connect(self._nGpuChanged)
399
+ self._gpuCardCB.currentTextChanged.connect(self._configurationChanged)
400
+
401
+ def _nGpuChanged(self, *args, **kwargs):
402
+ nGpu = self.getNGPU()
403
+ self._gpuCardCB.setEnabled(nGpu > 0)
291
404
 
292
405
  def _configurationChanged(self, *args, **kwargs):
293
406
  self.sigConfigChanged.emit()
@@ -341,60 +454,118 @@ class SlurmSettingsWidget(qt.QWidget):
341
454
  self._wallTimeQLE.setText(walltime)
342
455
 
343
456
  def getPythonExe(self):
344
- return self._pythonVenv.text()
457
+ if self._sourceScriptCB.isChecked():
458
+ return self._pythonVenv.text()
459
+ else:
460
+ return None
345
461
 
346
462
  def setPythonExe(self, python_venv):
347
463
  self._pythonVenv.setText(python_venv)
464
+ if python_venv != "":
465
+ self._sourceScriptCB.setChecked(True)
466
+
467
+ def getModulesToLoad(self) -> tuple:
468
+ if self._modulesCB.isChecked():
469
+ return convert_str_to_tuple(self._modulesQLE.text())
470
+ else:
471
+ return tuple()
472
+
473
+ def setModulesToLoad(self, modules: tuple):
474
+ if not isinstance(modules, (tuple, list)):
475
+ raise TypeError(
476
+ f"modules is expected to be a tuple or a list. Get {type(modules)} instead"
477
+ )
478
+ self._modulesQLE.setText(str(modules))
479
+ if len(modules) > 0:
480
+ self._modulesCB.setChecked(True)
481
+
482
+ def getGpuCard(self) -> Optional[str]:
483
+ card = self._gpuCardCB.currentText()
484
+ if card == "any" or self._nGpu == 0:
485
+ return None
486
+ else:
487
+ return card
488
+
489
+ def getSBatchExtraParams(self):
490
+ return {
491
+ "export": self._exportValueCM.currentText(),
492
+ "gpu_card": self.getGpuCard(),
493
+ }
494
+
495
+ def setSBatchExtraParams(self, params: dict):
496
+ export_ = params.get("export", None)
497
+ if export_ is not None:
498
+ index = self._exportValueCM.findText(export_)
499
+ if index >= 0:
500
+ self._exportValueCM.setCurrentIndex(index)
501
+ gpu_card = params.get("gpu_card", None)
502
+ if gpu_card is not None:
503
+ index = self._gpuCardCB.findText("gpu_card")
504
+ if index >= 0:
505
+ self._gpuCardCB.setCurrentIndex(index)
506
+ else:
507
+ # policy when setting the extra params: if doesn't exists / found then won't set it.
508
+ # because they can be part of .ows, this parameter is hidden by default.
509
+ # so safer to use 'any' in the case it is unknown (debatable).
510
+ _logger.warning(f"unable to find gpu {gpu_card}. Won't set it")
348
511
 
349
512
  def setConfiguration(self, config: dict) -> None:
350
- old = self.blockSignals(True)
351
- active_slurm = config.get("active_slurm", None)
352
- if active_slurm is not None:
353
- self._slurmCB.setChecked(active_slurm)
354
-
355
- n_cores = config.get("cpu-per-task", None)
356
- if n_cores is not None:
357
- self.setNCores(n_cores)
358
-
359
- n_workers = config.get("n_tasks", None)
360
- if n_workers is not None:
361
- self.setNWorkers(n_workers)
362
-
363
- memory = config.get("memory", None)
364
- if memory is not None:
365
- if isinstance(memory, str):
366
- memory = memory.replace(" ", "").lower().rstrip("gb")
367
- self.setMemory(int(memory))
368
-
369
- queue_ = config.get("partition", None)
370
- if queue_ is not None:
371
- queue_ = queue_.rstrip("'").rstrip('"')
372
- queue_ = queue_.lstrip("'").lstrip('"')
373
- self.setQueue(queue_)
374
-
375
- n_gpu = config.get("n_gpus", None)
376
- if n_gpu is not None:
377
- self.setNGPU(int(n_gpu))
378
-
379
- project_name = config.get("job_name", None)
380
- if project_name is not None:
381
- self.setProjectName(project_name)
382
-
383
- wall_time = config.get("walltime", None)
384
- if wall_time is not None:
385
- self.setWallTime(wall_time)
386
-
387
- python_venv = config.get("python_venv", None)
388
- if python_venv is not None:
389
- python_venv = python_venv.rstrip("'").rstrip('"')
390
- python_venv = python_venv.lstrip("'").lstrip('"')
391
- self.setPythonExe(python_venv)
392
-
393
- n_jobs = config.get("n_jobs", None)
394
- if n_jobs is not None:
395
- self.setNJobs(n_jobs)
396
-
397
- self.blockSignals(old)
513
+ with block_signals(self):
514
+ active_slurm = config.get("active_slurm", None)
515
+ if active_slurm is not None:
516
+ self._slurmCB.setChecked(active_slurm)
517
+
518
+ n_cores = config.get("cpu-per-task", None)
519
+ if n_cores is not None:
520
+ self.setNCores(n_cores)
521
+
522
+ n_workers = config.get("n_tasks", None)
523
+ if n_workers is not None:
524
+ self.setNWorkers(n_workers)
525
+
526
+ memory = config.get("memory", None)
527
+ if memory is not None:
528
+ if isinstance(memory, str):
529
+ memory = memory.replace(" ", "").lower().rstrip("gb")
530
+ self.setMemory(int(memory))
531
+
532
+ queue_ = config.get("partition", None)
533
+ if queue_ is not None:
534
+ queue_ = queue_.rstrip("'").rstrip('"')
535
+ queue_ = queue_.lstrip("'").lstrip('"')
536
+ self.setQueue(queue_)
537
+
538
+ n_gpu = config.get("n_gpus", None)
539
+ if n_gpu is not None:
540
+ self.setNGPU(int(n_gpu))
541
+
542
+ project_name = config.get("job_name", None)
543
+ if project_name is not None:
544
+ self.setProjectName(project_name)
545
+
546
+ wall_time = config.get("walltime", None)
547
+ if wall_time is not None:
548
+ self.setWallTime(wall_time)
549
+
550
+ python_venv = config.get("python_venv", None)
551
+ if python_venv is not None:
552
+ python_venv = python_venv.rstrip("'").rstrip('"')
553
+ python_venv = python_venv.lstrip("'").lstrip('"')
554
+ self.setPythonExe(python_venv)
555
+
556
+ modules = config.get("modules", None)
557
+ if modules is not None:
558
+ modules = convert_str_to_tuple(modules)
559
+ self.setModulesToLoad(modules)
560
+
561
+ sbatch_extra_params = config.get("sbatch_extra_params", {})
562
+ self.setSBatchExtraParams(sbatch_extra_params)
563
+
564
+ n_jobs = config.get("n_jobs", None)
565
+ if n_jobs is not None:
566
+ self.setNJobs(n_jobs)
567
+ self._preProcessingModeChanged() # make sure modules and python venv is enabled according to the activate mode
568
+
398
569
  self.sigConfigChanged.emit()
399
570
 
400
571
  def getConfiguration(self) -> dict:
@@ -408,6 +579,8 @@ class SlurmSettingsWidget(qt.QWidget):
408
579
  "job_name": self.getProjectName(),
409
580
  "walltime": self.getWallTime(),
410
581
  "python_venv": self.getPythonExe(),
582
+ "modules": self.getModulesToLoad(),
583
+ "sbatch_extra_params": self.getSBatchExtraParams(),
411
584
  }
412
585
 
413
586
  def getSlurmClusterConfiguration(self):
@@ -415,6 +588,33 @@ class SlurmSettingsWidget(qt.QWidget):
415
588
 
416
589
  return SlurmClusterConfiguration().from_dict(self.getConfiguration())
417
590
 
591
+ def _preProcessingModeChanged(self):
592
+ self._modulesQLE.setEnabled(self._modulesCB.isChecked())
593
+ self._pythonVenv.setEnabled(self._sourceScriptCB.isChecked())
594
+
595
+ def setConfigurationLevel(self, level: ConfigurationLevel):
596
+ self._sbatchAdvancedParameters.setVisible(level >= ConfigurationLevel.ADVANCED)
597
+
598
+ def _partitionChanged(self, *args, **kwargs):
599
+ partition = self.getQueue()
600
+ gpus = self._getGpus(partition=partition)
601
+ self._gpuCardCB.clear()
602
+ self._gpuCardCB.addItems(gpus)
603
+ self._gpuCardCB.setCurrentText("any")
604
+
605
+ @cache(maxsize=None)
606
+ def _getGpus(self, partition) -> tuple:
607
+ try:
608
+ gpus = get_partition_gpus(partition)
609
+ except Exception as e:
610
+ _logger.error(f"Failed to detect GPU on {partition}. Error is {e}")
611
+ gpus = ("any",)
612
+ else:
613
+ gpus = list(gpus) + [
614
+ "any",
615
+ ]
616
+ return gpus
617
+
418
618
 
419
619
  class _PortRangeSelection(qt.QWidget):
420
620
  sigRangeChanged = qt.Signal()
@@ -61,9 +61,14 @@ class TestSlurmWidget(TestCaseQt):
61
61
  "partition": SlurmSettings.PARTITION,
62
62
  "n_gpus": SlurmSettings.N_GPUS_PER_WORKER,
63
63
  "job_name": "tomwer_{scan}_-_{process}_-_{info}",
64
+ "modules": tuple(),
64
65
  "n_jobs": 1,
65
66
  "python_venv": "/scisoft/tomotools/activate stable",
66
67
  "walltime": "01:00:00",
68
+ "sbatch_extra_params": {
69
+ "export": "NONE",
70
+ "gpu_card": None,
71
+ },
67
72
  }
68
73
  assert dict_res == expected_dict, f"{dict_res} vs {expected_dict}"
69
74
 
@@ -75,6 +80,10 @@ class TestSlurmWidget(TestCaseQt):
75
80
  "memory": 156,
76
81
  "partition": "test-queue",
77
82
  "n_gpus": 5,
83
+ "modules": "mymodule, mysecond/10.3",
84
+ "sbatch_extra_params": {
85
+ "export": "ALL",
86
+ },
78
87
  }
79
88
  )
80
89
 
@@ -83,6 +92,10 @@ class TestSlurmWidget(TestCaseQt):
83
92
  assert self.slurmWidget.getMemory() == 156
84
93
  assert self.slurmWidget.getQueue() == "test-queue"
85
94
  assert self.slurmWidget.getNGPU() == 5
95
+ assert self.slurmWidget.getSBatchExtraParams() == {
96
+ "export": "ALL",
97
+ "gpu_card": None,
98
+ }
86
99
 
87
100
 
88
101
  def test_SlurmSettingsWindow(qtapp): # noqa F811
@@ -36,7 +36,7 @@ from silx.gui import qt
36
36
  from silx.gui.utils.testutils import TestCaseQt
37
37
 
38
38
  from tomwer.core.futureobject import FutureTomwerObject
39
- from tomwer.core.utils.scanutils import MockHDF5
39
+ from tomwer.core.utils.scanutils import MockNXtomo
40
40
  from tomwer.gui.cluster.supervisor import FutureTomwerScanObserverWidget
41
41
 
42
42
 
@@ -52,7 +52,7 @@ class TestSupervisor(TestCaseQt):
52
52
  self._future_tomo_objs = []
53
53
  for i in range(5):
54
54
  # create scan
55
- scan = MockHDF5(
55
+ scan = MockNXtomo(
56
56
  scan_path=os.path.join(self.tempdir, f"scan_test{i}"),
57
57
  n_proj=10,
58
58
  n_ini_proj=10,
File without changes
@@ -1,35 +1,4 @@
1
- # coding: utf-8
2
- # /*##########################################################################
3
- #
4
- # Copyright (c) 2016-2017 European Synchrotron Radiation Facility
5
- #
6
- # Permission is hereby granted, free of charge, to any person obtaining a copy
7
- # of this software and associated documentation files (the "Software"), to deal
8
- # in the Software without restriction, including without limitation the rights
9
- # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
- # copies of the Software, and to permit persons to whom the Software is
11
- # furnished to do so, subject to the following conditions:
12
- #
13
- # The above copyright notice and this permission notice shall be included in
14
- # all copies or substantial portions of the Software.
15
- #
16
- # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
- # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
- # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
- # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
- # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
- # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22
- # THE SOFTWARE.
23
- #
24
- # ###########################################################################*/
25
-
26
- __authors__ = ["H. Payno"]
27
- __license__ = "MIT"
28
- __date__ = "04/08/2020"
29
-
30
-
31
1
  from silx.gui import qt
32
-
33
2
  from tomwer.gui import icons as tomwer_icons
34
3
 
35
4
 
@@ -114,7 +83,7 @@ class FilterAction(qt.QAction):
114
83
  qt.QAction.__init__(self, icon, "filter configuration", parent)
115
84
  self.setToolTip(
116
85
  "If activated will only display the configuration"
117
- "for the active nabu phase"
86
+ "for the active nabu step"
118
87
  )
119
88
  self.setCheckable(True)
120
89
  self.setShortcut(qt.QKeySequence(qt.Qt.Key_F))
@@ -0,0 +1,22 @@
1
+ from silx.utils.enum import Enum as _Enum
2
+
3
+
4
+ class ConfigurationLevel(_Enum):
5
+ REQUIRED = "required"
6
+ OPTIONAL = "optional"
7
+ ADVANCED = "advanced"
8
+
9
+ def _get_num_value(self) -> int:
10
+ if self is self.REQUIRED:
11
+ return 0
12
+ elif self is self.OPTIONAL:
13
+ return 1
14
+ elif self is self.ADVANCED:
15
+ return 2
16
+
17
+ def __le__(self, other):
18
+ if not isinstance(other, ConfigurationLevel):
19
+ raise TypeError(
20
+ f"other is expected to be an instance of ConfigurationLevel. {type(other)} provided"
21
+ )
22
+ return self._get_num_value() <= other._get_num_value()
@@ -29,8 +29,10 @@ __date__ = "23/03/2021"
29
29
 
30
30
 
31
31
  from silx.gui import qt
32
+ from functools import partial
32
33
 
33
34
  from tomwer.gui import icons as tomwer_icons
35
+ from tomwer.gui.control.tomoobjdisplaymode import DisplayMode
34
36
 
35
37
 
36
38
  class NXTomomillParamsAction(qt.QAction):
@@ -68,3 +70,55 @@ class CFGFileActiveLabel(qt.QLabel):
68
70
 
69
71
  def setInactive(self):
70
72
  self.setActive(active=False)
73
+
74
+
75
+ class TomoObjDisplayModeToolButton(qt.QToolButton):
76
+ """
77
+ Button to change the way tomo object are displayed.
78
+ Either using the full url or only a 'short' description.
79
+ """
80
+
81
+ sigDisplayModeChanged = qt.Signal(str)
82
+
83
+ _SHORT_DESC_TOOLTIP = "Use a short description of the tomo object. Two different scans can have the same short desciption"
84
+ _URL_TOOLTIP = (
85
+ "Use the full url to display the tomo object. Url is guaranted to be unique."
86
+ )
87
+
88
+ def __init__(self, parent=None) -> None:
89
+ super().__init__(parent)
90
+
91
+ self._shortDescIcon = tomwer_icons.getQIcon("short_description")
92
+ shortDescAction = qt.QAction(self._shortDescIcon, "short description", self)
93
+ shortDescAction.setToolTip(self._SHORT_DESC_TOOLTIP)
94
+
95
+ self._urlIcon = tomwer_icons.getQIcon("url")
96
+ urlDescAction = qt.QAction(self._urlIcon, "url", self)
97
+ urlDescAction.setToolTip(self._URL_TOOLTIP)
98
+
99
+ menu = qt.QMenu(self)
100
+ menu.addAction(shortDescAction)
101
+ menu.addAction(urlDescAction)
102
+ self.setMenu(menu)
103
+ self.setPopupMode(qt.QToolButton.InstantPopup)
104
+
105
+ # set up
106
+ self.setDisplayMode(DisplayMode.SHORT)
107
+
108
+ # connect signal / slot
109
+ shortDescAction.triggered.connect(
110
+ partial(self.setDisplayMode, DisplayMode.SHORT)
111
+ )
112
+ urlDescAction.triggered.connect(partial(self.setDisplayMode, DisplayMode.URL))
113
+
114
+ def setDisplayMode(self, mode: DisplayMode):
115
+ mode = DisplayMode.from_value(mode)
116
+ if mode is DisplayMode.SHORT:
117
+ self.setIcon(self._shortDescIcon)
118
+ self.setToolTip(self._SHORT_DESC_TOOLTIP)
119
+ elif mode is DisplayMode.URL:
120
+ self.setIcon(self._urlIcon)
121
+ self.setToolTip(self._URL_TOOLTIP)
122
+ else:
123
+ raise ValueError(f"display mode {mode} not handled")
124
+ self.sigDisplayModeChanged.emit(mode.value)