nxtomomill 2.2.0.dev1__py3-none-any.whl → 2.2.0.dev2__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 (25) hide show
  1. nxtomomill/app/h52nx.py +35 -1
  2. nxtomomill/converter/hdf5/acquisition/baseacquisition.py +122 -102
  3. nxtomomill/converter/hdf5/acquisition/standardacquisition.py +17 -1
  4. nxtomomill/converter/hdf5/acquisition/tests/test_acquisition.py +75 -6
  5. nxtomomill/converter/hdf5/acquisition/utils/_context.py +1 -0
  6. nxtomomill/converter/hdf5/acquisition/utils/_keys.py +19 -0
  7. nxtomomill/converter/hdf5/tests/test_err_handling.py +48 -0
  8. nxtomomill/converter/hdf5/tests/test_h52nx_utils.py +9 -5
  9. nxtomomill/converter/hdf5/tests/test_hdf5converter.py +132 -0
  10. nxtomomill/io/config/confighandler.py +1 -1
  11. nxtomomill/models/dx2nx.py +9 -0
  12. nxtomomill/models/edf2nx/general_section.py +6 -3
  13. nxtomomill/models/edf2nx/unit_section.py +3 -0
  14. nxtomomill/models/h52nx/_context.py +2 -0
  15. nxtomomill/models/h52nx/general_section.py +5 -1
  16. nxtomomill/models/h52nx/keys_section.py +3 -0
  17. nxtomomill/models/tests/test_edf2nx_model.py +12 -0
  18. nxtomomill/models/tests/test_h52nx_model.py +15 -2
  19. nxtomomill/utils/flat_reducer.py +1 -1
  20. {nxtomomill-2.2.0.dev1.dist-info → nxtomomill-2.2.0.dev2.dist-info}/METADATA +1 -1
  21. {nxtomomill-2.2.0.dev1.dist-info → nxtomomill-2.2.0.dev2.dist-info}/RECORD +25 -24
  22. {nxtomomill-2.2.0.dev1.dist-info → nxtomomill-2.2.0.dev2.dist-info}/WHEEL +1 -1
  23. {nxtomomill-2.2.0.dev1.dist-info → nxtomomill-2.2.0.dev2.dist-info}/entry_points.txt +0 -0
  24. {nxtomomill-2.2.0.dev1.dist-info → nxtomomill-2.2.0.dev2.dist-info}/licenses/LICENSE +0 -0
  25. {nxtomomill-2.2.0.dev1.dist-info → nxtomomill-2.2.0.dev2.dist-info}/top_level.txt +0 -0
nxtomomill/app/h52nx.py CHANGED
@@ -10,7 +10,9 @@ For a complete tutorial you can have a look at: :ref:`Tomoh52nx`
10
10
  """
11
11
 
12
12
  import argparse
13
+ import cProfile
13
14
  import logging
15
+ import os
14
16
 
15
17
  from tqdm import tqdm
16
18
 
@@ -331,12 +333,35 @@ def main(argv):
331
333
  "links to the raw data. Defaults to the 'NXTOMOMILL_RESOLVE_VDS' "
332
334
  "environment variable if set, else resolving is enabled.",
333
335
  )
336
+ parser.add_argument(
337
+ "--profiling",
338
+ "--profile",
339
+ help="Profile the conversion with 'cProfile' and dump the statistics "
340
+ "to a file (see '--profiling-output-file').",
341
+ action="store_true",
342
+ default=False,
343
+ )
344
+ parser.add_argument(
345
+ "--profiling-output-file",
346
+ "--profiling_output_file",
347
+ default="profiling.out",
348
+ help="Output file to dump the 'cProfile' statistics to when "
349
+ "'--profiling' is provided. If this file already exists the "
350
+ "conversion won't be launched (default: 'profiling.out').",
351
+ )
334
352
  options = parser.parse_args(argv[1:])
335
353
  if options.request_input:
336
354
  callback_det_sel = _ask_for_selecting_detector
337
355
  else:
338
356
  callback_det_sel = None
339
357
 
358
+ if options.profiling and os.path.exists(options.profiling_output_file):
359
+ _logger.error(
360
+ f"Profiling output file already exists: '{options.profiling_output_file}'. "
361
+ "Remove it or provide another one with '--profiling-output-file'."
362
+ )
363
+ return 1
364
+
340
365
  try:
341
366
  configuration_handler = TomoHDF5ConfigHandler(options)
342
367
  except Exception:
@@ -355,6 +380,15 @@ def main(argv):
355
380
  input_callback=None,
356
381
  detector_sel_callback=callback_det_sel,
357
382
  )
358
- converter.convert()
383
+ if options.profiling:
384
+ profiler = cProfile.Profile()
385
+ profiler.enable()
386
+ try:
387
+ converter.convert()
388
+ finally:
389
+ profiler.disable()
390
+ profiler.dump_stats(options.profiling_output_file)
391
+ else:
392
+ converter.convert()
359
393
  if converter.has_failed_acquisitions:
360
394
  return 1
@@ -12,7 +12,9 @@ import copy
12
12
 
13
13
  import h5py
14
14
 
15
+ import nxtomomill
15
16
  from nxtomo.application.nxtomo import NXtomo
17
+ from nxtomo.io import HDF5File
16
18
  from nxtomomill.utils.h5pyutils import from_data_url_to_virtual_source
17
19
  from nxtomomill.utils.hdf5 import EntryReader
18
20
  from nxtomomill.utils.utils import embed_url # noqa F401
@@ -30,7 +32,8 @@ from silx.io.url import DataUrl
30
32
  from silx.io.utils import h5py_read_dataset
31
33
 
32
34
  from nxtomomill.models.h52nx.h52nx import H52nxModel
33
- from .utils._keys import keys_to_str_tuple
35
+ from nxtomomill.models.h52nx._context import _Key
36
+ from .utils._keys import keys_to_str_tuple, split_keys_by_priority
34
37
  from .utils._context import read_key, MissingValue, InvalidValue
35
38
 
36
39
  _ureg = pint.get_application_registry()
@@ -302,6 +305,12 @@ class BaseAcquisition:
302
305
  use_abs_path_for_vds=self.configuration.use_abs_path_for_vds,
303
306
  overwrite=self.configuration.overwrite,
304
307
  )
308
+ # record the nxtomomill version used to create this NXtomo as attributes
309
+ # on the NXtomo entry group itself
310
+ with HDF5File(output_file, mode="a") as h5f:
311
+ entry_grp = h5f[data_path]
312
+ entry_grp.attrs["creator"] = "nxtomomill"
313
+ entry_grp.attrs["creator_version"] = nxtomomill.__version__
305
314
  # check rotation angle
306
315
  if nx_tomo.sample.rotation_angle is not None:
307
316
  unique_angles = numpy.unique(
@@ -410,10 +419,11 @@ class BaseAcquisition:
410
419
  # skip values containing '*DIS*'
411
420
  continue
412
421
  except InvalidValue:
413
- # dataset reports an 'ERR' value: let it propagate, the
414
- # caller decides how to handle it (e.g. translations
415
- # fall back to None instead of trying other keys)
416
- raise
422
+ # dataset reports an 'ERR' value: skip it and try the next key
423
+ _logger.warning(
424
+ f"{info_retrieve} reports an 'ERR' value for {node.name} - {key}. This key will be skipped."
425
+ )
426
+ continue
417
427
  except KeyError as e:
418
428
  _logger.info(
419
429
  f"Cannot solve key {key}. Most likely one dataset is not there. Error is {e}"
@@ -619,6 +629,55 @@ class BaseAcquisition:
619
629
 
620
630
  return None
621
631
 
632
+ @staticmethod
633
+ def _get_values_from_nodes(
634
+ nodes: tuple[h5py.Group | None, ...],
635
+ n_frame: int | None,
636
+ keys: tuple[_Key, ...],
637
+ info_retrieve: str,
638
+ expected_unit: pint.Unit,
639
+ ) -> pint.Quantity | None:
640
+ """
641
+ Look for the values of 'keys' in 'nodes'.
642
+
643
+ Keys resolved from the bliss-tomo 'tomoconfig' are looked at first (on all nodes, arrays
644
+ first then scalars). The other keys (default keys) are only used as a fallback if none of
645
+ the 'tomoconfig' keys could be found.
646
+
647
+ :return: values as a pint.Quantity or None if no key could be found.
648
+ """
649
+ tomo_config_keys, fallback_keys = split_keys_by_priority(keys)
650
+
651
+ def look_for_keys(keys_to_check: tuple[str, ...]) -> pint.Quantity | None:
652
+ for node in nodes:
653
+ if node is None:
654
+ continue
655
+ try:
656
+ values, unit = BaseAcquisition._get_node_values_for_frame_array(
657
+ node=node,
658
+ n_frame=n_frame,
659
+ keys=keys_to_check,
660
+ info_retrieve=info_retrieve,
661
+ expected_unit=expected_unit,
662
+ )
663
+ except (ValueError, KeyError):
664
+ pass
665
+ else:
666
+ return values * unit
667
+ return None
668
+
669
+ if len(tomo_config_keys) > 0:
670
+ result = look_for_keys(tomo_config_keys)
671
+ if result is not None:
672
+ return result
673
+ _logger.warning(
674
+ f"Unable to find {info_retrieve} from the bliss-tomo 'tomoconfig' datasets {tomo_config_keys}. "
675
+ f"Will look at the fallback datasets {fallback_keys}."
676
+ )
677
+ if len(fallback_keys) > 0:
678
+ return look_for_keys(fallback_keys)
679
+ return None
680
+
622
681
  def _get_rotation_angle(self, root_node: h5py.Group, n_frame: int) -> pint.Quantity:
623
682
  """
624
683
  return the list of rotation angle for each frame.
@@ -629,23 +688,19 @@ class BaseAcquisition:
629
688
  if not isinstance(root_node, h5py.Group):
630
689
  raise TypeError("root_node is expected to be a h5py.Group")
631
690
 
632
- for grp in (
633
- self._get_positioners_node(root_node),
634
- root_node,
635
- self._get_measurement_node(root_node),
636
- ):
637
- try:
638
- angles, unit = self._get_node_values_for_frame_array(
639
- node=grp,
640
- n_frame=n_frame,
641
- keys=keys_to_str_tuple(self.configuration.rotation_angle_keys),
642
- info_retrieve="rotation angle",
643
- expected_unit=_ureg.degree,
644
- )
645
- except (ValueError, KeyError):
646
- pass
647
- else:
648
- return angles * unit
691
+ angles = self._get_values_from_nodes(
692
+ nodes=(
693
+ self._get_positioners_node(root_node),
694
+ root_node,
695
+ self._get_measurement_node(root_node),
696
+ ),
697
+ n_frame=n_frame,
698
+ keys=self.configuration.rotation_angle_keys,
699
+ info_retrieve="rotation angle",
700
+ expected_unit=_ureg.degree,
701
+ )
702
+ if angles is not None:
703
+ return angles
649
704
 
650
705
  mess = f"Unable to find rotation angle for {root_node.name}"
651
706
  if self.raise_error_if_issue:
@@ -657,19 +712,15 @@ class BaseAcquisition:
657
712
 
658
713
  def _get_sample_x(self, root_node, n_frame) -> pint.Quantity:
659
714
  """return the list of translation for each frame"""
660
- for grp in self._get_positioners_node(root_node), root_node:
661
- try:
662
- x_tr, unit = self._get_node_values_for_frame_array(
663
- node=grp,
664
- n_frame=n_frame,
665
- keys=keys_to_str_tuple(self.configuration.sample_x_keys),
666
- info_retrieve="sample x translation",
667
- expected_unit=_ureg.mm,
668
- )
669
- except (ValueError, KeyError):
670
- pass
671
- else:
672
- return x_tr * unit
715
+ translation = self._get_values_from_nodes(
716
+ nodes=(self._get_positioners_node(root_node), root_node),
717
+ n_frame=n_frame,
718
+ keys=self.configuration.sample_x_keys,
719
+ info_retrieve="sample x translation",
720
+ expected_unit=_ureg.mm,
721
+ )
722
+ if translation is not None:
723
+ return translation
673
724
 
674
725
  mess = f"Unable to find sample x for {root_node.name}"
675
726
  if self.raise_error_if_issue:
@@ -681,19 +732,15 @@ class BaseAcquisition:
681
732
 
682
733
  def _get_sample_y(self, root_node, n_frame) -> pint.Quantity:
683
734
  """return the list of translation for each frame"""
684
- for grp in self._get_positioners_node(root_node), root_node:
685
- try:
686
- y_tr, unit = self._get_node_values_for_frame_array(
687
- node=grp,
688
- n_frame=n_frame,
689
- keys=keys_to_str_tuple(self.configuration.sample_y_keys),
690
- info_retrieve="sample y translation",
691
- expected_unit=_ureg.mm,
692
- )
693
- except (ValueError, KeyError):
694
- pass
695
- else:
696
- return y_tr * unit
735
+ translation = self._get_values_from_nodes(
736
+ nodes=(self._get_positioners_node(root_node), root_node),
737
+ n_frame=n_frame,
738
+ keys=self.configuration.sample_y_keys,
739
+ info_retrieve="sample y translation",
740
+ expected_unit=_ureg.mm,
741
+ )
742
+ if translation is not None:
743
+ return translation
697
744
 
698
745
  mess = f"Unable to find sample y for {root_node.name}"
699
746
  if self.raise_error_if_issue:
@@ -705,24 +752,15 @@ class BaseAcquisition:
705
752
 
706
753
  def _get_translation_x(self, root_node, n_frame) -> pint.Quantity | None:
707
754
  """return the list of translation for each frame"""
708
- for grp in self._get_positioners_node(root_node), root_node:
709
- try:
710
- x_tr, unit = self._get_node_values_for_frame_array(
711
- node=grp,
712
- n_frame=n_frame,
713
- keys=keys_to_str_tuple(self.configuration.translation_x_keys),
714
- info_retrieve="x base translation",
715
- expected_unit=_ureg.mm,
716
- )
717
- except InvalidValue:
718
- _logger.error(
719
- f"translation x reports an 'ERR' value for {root_node.name}. Translation x will be set to None."
720
- )
721
- return None
722
- except (ValueError, KeyError):
723
- pass
724
- else:
725
- return x_tr * unit
755
+ translation = self._get_values_from_nodes(
756
+ nodes=(self._get_positioners_node(root_node), root_node),
757
+ n_frame=n_frame,
758
+ keys=self.configuration.translation_x_keys,
759
+ info_retrieve="x base translation",
760
+ expected_unit=_ureg.mm,
761
+ )
762
+ if translation is not None:
763
+ return translation
726
764
 
727
765
  mess = f"Unable to find translation x for {root_node.name}"
728
766
  if self.raise_error_if_issue:
@@ -734,24 +772,15 @@ class BaseAcquisition:
734
772
 
735
773
  def _get_translation_y(self, root_node, n_frame) -> pint.Quantity | None:
736
774
  """return the list of translation for each frame"""
737
- for grp in self._get_positioners_node(root_node), root_node:
738
- try:
739
- y_tr, unit = self._get_node_values_for_frame_array(
740
- node=grp,
741
- n_frame=n_frame,
742
- keys=keys_to_str_tuple(self.configuration.translation_y_keys),
743
- info_retrieve="y base translation",
744
- expected_unit=_ureg.mm,
745
- )
746
- except InvalidValue:
747
- _logger.error(
748
- f"translation y reports an 'ERR' value for {root_node.name}. Translation y will be set to None."
749
- )
750
- return None
751
- except (ValueError, KeyError):
752
- pass
753
- else:
754
- return y_tr * unit
775
+ translation = self._get_values_from_nodes(
776
+ nodes=(self._get_positioners_node(root_node), root_node),
777
+ n_frame=n_frame,
778
+ keys=self.configuration.translation_y_keys,
779
+ info_retrieve="y base translation",
780
+ expected_unit=_ureg.mm,
781
+ )
782
+ if translation is not None:
783
+ return translation
755
784
 
756
785
  mess = f"Unable to find translation y for {root_node.name}"
757
786
  if self.raise_error_if_issue:
@@ -765,24 +794,15 @@ class BaseAcquisition:
765
794
  def get_translation_z_frm(
766
795
  root_node, n_frame: int, configuration: H52nxModel
767
796
  ) -> pint.Quantity | None:
768
- for grp in BaseAcquisition._get_positioners_node(root_node), root_node:
769
- try:
770
- z_tr, unit = BaseAcquisition._get_node_values_for_frame_array(
771
- node=grp,
772
- n_frame=n_frame,
773
- keys=keys_to_str_tuple(configuration.translation_z_keys),
774
- info_retrieve="z translation",
775
- expected_unit=_ureg.mm,
776
- )
777
- except InvalidValue:
778
- _logger.error(
779
- f"translation z reports an 'ERR' value for {root_node.name}. Translation z will be set to None."
780
- )
781
- return None
782
- except (ValueError, KeyError):
783
- pass
784
- else:
785
- return z_tr * unit
797
+ translation = BaseAcquisition._get_values_from_nodes(
798
+ nodes=(BaseAcquisition._get_positioners_node(root_node), root_node),
799
+ n_frame=n_frame,
800
+ keys=configuration.translation_z_keys,
801
+ info_retrieve="z translation",
802
+ expected_unit=_ureg.mm,
803
+ )
804
+ if translation is not None:
805
+ return translation
786
806
 
787
807
  mess = f"Unable to find translation z on node {root_node.name}"
788
808
  if configuration.raises_error:
@@ -65,6 +65,10 @@ from .utils._keys import keys_to_str_tuple
65
65
  _ureg = pint.get_application_registry()
66
66
  _logger = logging.getLogger(__name__)
67
67
 
68
+ _MAX_VDS_SOURCES_TO_RESOLVE = 100
69
+ """If a camera dataset virtual source relies on that many (or more) distinct files, skip
70
+ resolving external links / virtual dataset sources for it."""
71
+
68
72
 
69
73
  __all__ = ["StandardAcquisition"]
70
74
 
@@ -346,7 +350,19 @@ class StandardAcquisition(BaseAcquisition):
346
350
  image_key_control = frame_type.to_image_key_control()
347
351
  self._image_key_control.extend([image_key_control.value] * n_frame)
348
352
 
349
- if self.configuration.resolve_vds:
353
+ n_vds_files = (
354
+ len({vs_info.file_name for vs_info in data_dataset.virtual_sources()})
355
+ if data_dataset.is_virtual
356
+ else 0
357
+ )
358
+ if n_vds_files >= _MAX_VDS_SOURCES_TO_RESOLVE:
359
+ _logger.warning(
360
+ f"{camera_dataset_url.path()} relies on {n_vds_files} files "
361
+ f"(>= {_MAX_VDS_SOURCES_TO_RESOLVE}). Skip external link / VDS "
362
+ "resolution to avoid opening too many files."
363
+ )
364
+ solved_camera_dataset_urls = (camera_dataset_url,)
365
+ elif self.configuration.resolve_vds:
350
366
  try:
351
367
  solved_camera_dataset_urls = resolve_link(
352
368
  dataset_url=camera_dataset_url,
@@ -3,7 +3,10 @@
3
3
  import os
4
4
  import tempfile
5
5
 
6
+ import logging
7
+
6
8
  import h5py
9
+ import numpy
7
10
  import pytest
8
11
  from silx.io.url import DataUrl
9
12
 
@@ -13,6 +16,7 @@ from nxtomomill.converter.hdf5.acquisition.baseacquisition import (
13
16
  get_dataset_name_from_motor,
14
17
  )
15
18
  from nxtomomill.io.config import TomoHDF5Config
19
+ from nxtomomill.models.h52nx._context import _Key
16
20
  from nxtomomill.tests.datasets import GitlabDataset
17
21
  from nxtomomill import converter
18
22
  from nxtomo.application.nxtomo import NXtomo
@@ -56,20 +60,80 @@ def test_get_dataset_name_from_motor():
56
60
  @pytest.mark.parametrize("raises_error", (True, False))
57
61
  def test_translation_with_err_value(tmp_path, raises_error):
58
62
  """
59
- Test that translation x, y and z are set to None (instead of failing)
60
- when the corresponding dataset reports an 'ERR' value.
63
+ Test that translation datasets reporting an 'ERR' value are skipped:
64
+ the next valid key is used and, if none is valid, it behaves as if the
65
+ translation was not found (raises or default to 0).
61
66
  """
62
67
  file_path = os.path.join(tmp_path, "test.h5")
63
68
  with HDF5File(file_path, mode="w") as h5f:
64
69
  h5f["/entry/instrument/positioners/tx"] = "ERR"
65
70
  h5f["/entry/instrument/positioners/ty"] = "ERR"
66
71
  h5f["/entry/instrument/positioners/tz"] = "ERR"
72
+ h5f["/entry/instrument/positioners/sz"] = 2.0
73
+ h5f["/entry/instrument/positioners/sz"].attrs["units"] = "mm"
67
74
 
68
75
  config = TomoHDF5Config()
69
76
  config.raises_error = raises_error
70
77
  config.translation_x_keys = ("tx",)
71
78
  config.translation_y_keys = ("ty",)
72
- config.translation_z_keys = ("tz",)
79
+ config.translation_z_keys = ("tz", "sz")
80
+
81
+ url = DataUrl(file_path=file_path, data_path="/entry", scheme="silx")
82
+ std_acq = BaseAcquisition(
83
+ root_url=url,
84
+ configuration=config,
85
+ detector_sel_callback=None,
86
+ start_index=0,
87
+ )
88
+ with h5py.File(file_path, mode="r") as h5f:
89
+ root_node = h5f["/entry"]
90
+ # 'tz' reports 'ERR': fall back to 'sz'
91
+ z_tr = std_acq._get_translation_z(root_node=root_node, n_frame=1)
92
+ assert z_tr.to("mm").magnitude.tolist() == [2.0]
93
+
94
+ # no valid key: same behavior as a missing translation
95
+ if raises_error:
96
+ with pytest.raises(ValueError):
97
+ std_acq._get_translation_x(root_node=root_node, n_frame=1)
98
+ with pytest.raises(ValueError):
99
+ std_acq._get_translation_y(root_node=root_node, n_frame=1)
100
+ else:
101
+ x_tr = std_acq._get_translation_x(root_node=root_node, n_frame=1)
102
+ assert x_tr.magnitude.tolist() == [0]
103
+ y_tr = std_acq._get_translation_y(root_node=root_node, n_frame=1)
104
+ assert y_tr.magnitude.tolist() == [0]
105
+
106
+
107
+ def test_tomo_config_keys_priority(tmp_path, caplog):
108
+ """
109
+ Test that keys resolved from the bliss-tomo 'tomoconfig' are looked at first (array then scalar)
110
+ and that the default keys are only used as fallback.
111
+ """
112
+ n_frame = 3
113
+ file_path = os.path.join(tmp_path, "test.h5")
114
+ with HDF5File(file_path, mode="w") as h5f:
115
+ # tomoconfig motor is static (scalar) while a default key records an array
116
+ h5f["/entry/instrument/positioners/real_sx"] = 1.0
117
+ h5f["/entry/instrument/positioners/samx"] = numpy.array([5.0, 6.0, 7.0])
118
+ # tomoconfig motor only exists in root_node while a default key exists in positioners
119
+ h5f["/entry/real_sy"] = numpy.array([1.0, 2.0, 3.0])
120
+ h5f["/entry/instrument/positioners/samy"] = numpy.array([5.0, 6.0, 7.0])
121
+ # tomoconfig motor doesn't exist: fallback on the default key
122
+ h5f["/entry/instrument/positioners/sz"] = numpy.array([5.0, 6.0, 7.0])
123
+
124
+ config = TomoHDF5Config()
125
+ config.sample_x_keys = (
126
+ _Key(dataset="real_sx", contexts=set(), resolved_from_tomo_config=True),
127
+ _Key(dataset="samx", contexts=set()),
128
+ )
129
+ config.sample_y_keys = (
130
+ _Key(dataset="real_sy", contexts=set(), resolved_from_tomo_config=True),
131
+ _Key(dataset="samy", contexts=set()),
132
+ )
133
+ config.translation_z_keys = (
134
+ _Key(dataset="real_tz", contexts=set(), resolved_from_tomo_config=True),
135
+ _Key(dataset="sz", contexts=set()),
136
+ )
73
137
 
74
138
  url = DataUrl(file_path=file_path, data_path="/entry", scheme="silx")
75
139
  std_acq = BaseAcquisition(
@@ -80,9 +144,14 @@ def test_translation_with_err_value(tmp_path, raises_error):
80
144
  )
81
145
  with h5py.File(file_path, mode="r") as h5f:
82
146
  root_node = h5f["/entry"]
83
- assert std_acq._get_translation_x(root_node=root_node, n_frame=1) is None
84
- assert std_acq._get_translation_y(root_node=root_node, n_frame=1) is None
85
- assert std_acq._get_translation_z(root_node=root_node, n_frame=1) is None
147
+ sample_x = std_acq._get_sample_x(root_node=root_node, n_frame=n_frame)
148
+ assert sample_x.magnitude.tolist() == [1.0] * n_frame
149
+ sample_y = std_acq._get_sample_y(root_node=root_node, n_frame=n_frame)
150
+ assert sample_y.magnitude.tolist() == [1.0, 2.0, 3.0]
151
+ with caplog.at_level(logging.WARNING):
152
+ z_tr = std_acq._get_translation_z(root_node=root_node, n_frame=n_frame)
153
+ assert z_tr.magnitude.tolist() == [5.0, 6.0, 7.0]
154
+ assert "Will look at the fallback datasets" in caplog.text
86
155
 
87
156
 
88
157
  def test_frame_flips(tmp_path, monkeypatch):
@@ -94,6 +94,7 @@ def update_config_keys_from_tomo_config_context(
94
94
  _Key(
95
95
  dataset=real_dataset_name,
96
96
  contexts=key.contexts - {Proxy.TOMO_CONFIG},
97
+ resolved_from_tomo_config=True,
97
98
  )
98
99
  )
99
100
  return tuple(candidates)
@@ -8,3 +8,22 @@ def keys_to_str_tuple(keys: tuple[_Key]) -> tuple[str]:
8
8
  if not isinstance(key, _Key):
9
9
  raise TypeError(f"key should be an instance of {_Key}. Got {type(key)}")
10
10
  return tuple([key.dataset for key in keys])
11
+
12
+
13
+ def split_keys_by_priority(
14
+ keys: tuple[_Key, ...],
15
+ ) -> tuple[tuple[str, ...], tuple[str, ...]]:
16
+ """
17
+ Split keys between the ones resolved from the bliss-tomo 'tomoconfig' (to be looked at first)
18
+ and the fallback ones (default keys).
19
+ """
20
+ for key in keys:
21
+ if not isinstance(key, _Key):
22
+ raise TypeError(f"key should be an instance of {_Key}. Got {type(key)}")
23
+ tomo_config_keys = tuple(
24
+ key.dataset for key in keys if key.resolved_from_tomo_config
25
+ )
26
+ fallback_keys = tuple(
27
+ key.dataset for key in keys if not key.resolved_from_tomo_config
28
+ )
29
+ return tomo_config_keys, fallback_keys
@@ -0,0 +1,48 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+
5
+ import h5py
6
+ import numpy
7
+ import pytest
8
+
9
+ from nxtomo import NXtomo
10
+ from nxtomomill.models.h52nx import H52nxModel
11
+ from nxtomomill.converter.hdf5.hdf5converter import from_h5_to_nx
12
+ from nxtomomill.tests.datasets import GitlabDataset
13
+
14
+
15
+ @pytest.fixture()
16
+ def dataset_with_err():
17
+ dataset_path = (
18
+ "h5_datasets/handling_ERR/15.12um_A327_lung_overview__TA2_REF_B_000_0001.h5"
19
+ )
20
+ source_file = GitlabDataset.get_dataset(dataset_path)
21
+ if source_file is None:
22
+ raise FileNotFoundError(f"Unable to download {dataset_path}")
23
+ return source_file
24
+
25
+
26
+ def test_err_translation_fallback(tmp_path, dataset_with_err):
27
+ """
28
+ Make sure that if a translation candidate reports an 'ERR' value
29
+ ('mrsz') the next valid candidate ('sz') is used.
30
+ """
31
+ with h5py.File(dataset_with_err, mode="r") as h5f:
32
+ assert h5f["4.1/instrument/positioners/mrsz"][()] == b"ERR"
33
+ expected_z = h5f["4.1/instrument/positioners/sz"][()]
34
+
35
+ output_file = os.path.join(tmp_path, "output.nx")
36
+ model = H52nxModel(
37
+ input_file=dataset_with_err,
38
+ output_file=output_file,
39
+ single_file=True,
40
+ )
41
+ from_h5_to_nx(configuration=model)
42
+ assert os.path.exists(output_file)
43
+
44
+ nxtomo = NXtomo().load(file_path=output_file, data_path="entry0000")
45
+ # ESRF z axis is stored as the NXtomo (McStas) y axis
46
+ y_translation = nxtomo.sample.y_translation
47
+ assert y_translation is not None
48
+ numpy.testing.assert_allclose(y_translation.to("mm").magnitude, expected_z)
@@ -97,15 +97,19 @@ def test_update_config_keys_from_tomo_config_context():
97
97
  # the real (per-frame) motor name is tried first, the bliss alias (static / scalar value) is
98
98
  # kept as a fallback in case the real motor name doesn't exist in a given (sub-)scan.
99
99
  assert model.sample_x_keys == (
100
- _Key(dataset="real_sx", contexts=set()),
101
- _Key(dataset="alias_sx", contexts=set()),
100
+ _Key(dataset="real_sx", contexts=set(), resolved_from_tomo_config=True),
101
+ _Key(dataset="alias_sx", contexts=set(), resolved_from_tomo_config=True),
102
102
  )
103
103
  assert model.sample_y_keys == (
104
- _Key(dataset="real_sy+real_ty", contexts=set()),
105
- _Key(dataset="alias_sy+alias_ty", contexts=set()),
104
+ _Key(dataset="real_sy+real_ty", contexts=set(), resolved_from_tomo_config=True),
105
+ _Key(
106
+ dataset="alias_sy+alias_ty", contexts=set(), resolved_from_tomo_config=True
107
+ ),
106
108
  )
107
109
  # when tomo_config only registered a single name there is nothing to fall back to: no duplicate
108
- assert model.valid_camera_names == (_Key(dataset="single_su", contexts=set()),)
110
+ assert model.valid_camera_names == (
111
+ _Key(dataset="single_su", contexts=set(), resolved_from_tomo_config=True),
112
+ )
109
113
 
110
114
 
111
115
  def test_update_config_keys_from_beamline_context():
@@ -27,6 +27,7 @@ from nxtomomill.converter.hdf5.utils import (
27
27
  get_default_output_file,
28
28
  )
29
29
  from nxtomomill.models.h52nx import H52nxModel
30
+ import nxtomomill.converter.hdf5.acquisition.standardacquisition as standardacquisition_module
30
31
 
31
32
  from tomoscan.scans.nxtomoscan import NXtomoScan
32
33
 
@@ -1415,6 +1416,49 @@ def test_id16a_use_case(tmp_path):
1415
1416
  transformation.transformation_values.magnitude == numpy.array([0.0])
1416
1417
 
1417
1418
 
1419
+ def test_creator_and_creator_version_are_saved(tmp_path):
1420
+ """
1421
+ Test that each NXtomo entry created by h52nx records the nxtomomill
1422
+ version used to create it, as 'creator' / 'creator_version' attributes
1423
+ on the NXtomo entry group itself.
1424
+ """
1425
+ import nxtomomill
1426
+
1427
+ folder = tmp_path / "output_test_creator_and_creator_version_are_saved"
1428
+ folder.mkdir()
1429
+ bliss_mock = MockBlissAcquisition(
1430
+ n_sample=1,
1431
+ n_sequence=1,
1432
+ n_scan_per_sequence=2,
1433
+ n_darks=1,
1434
+ n_flats=1,
1435
+ with_nx_detector_attr=True,
1436
+ output_dir=folder,
1437
+ detector_name="pcolinux",
1438
+ create_tomo_config=False,
1439
+ )
1440
+ sample = bliss_mock.samples[0]
1441
+
1442
+ config = TomoHDF5Config()
1443
+ config.input_file = sample.sample_file
1444
+ config.output_file = str(tmp_path / "output.nx")
1445
+ config.single_file = True
1446
+ config.sample_x_keys = ("sx",)
1447
+ config.sample_y_keys = ("sy",)
1448
+ config.translation_x_keys = ("tx",)
1449
+
1450
+ converter.from_h5_to_nx(configuration=config)
1451
+ assert os.path.exists(config.output_file)
1452
+
1453
+ with HDF5File(config.output_file, mode="r") as h5f:
1454
+ entries = [name for name in h5f.keys() if "creator" in h5f[name].attrs]
1455
+ assert len(entries) > 0
1456
+ for entry in entries:
1457
+ entry_grp = h5f[entry]
1458
+ assert entry_grp.attrs["creator"] == "nxtomomill"
1459
+ assert entry_grp.attrs["creator_version"] == nxtomomill.__version__
1460
+
1461
+
1418
1462
  def test_resolve_vds_option(tmp_path, monkeypatch):
1419
1463
  """
1420
1464
  Test that the 'resolve_vds' configuration option correctly enables /
@@ -1472,6 +1516,94 @@ def test_resolve_vds_option(tmp_path, monkeypatch):
1472
1516
  assert os.path.exists(config.output_file)
1473
1517
 
1474
1518
 
1519
+ def test_skip_vds_resolution_when_too_many_files(tmp_path, monkeypatch):
1520
+ """
1521
+ Test that VDS / external link resolution is skipped entirely - even with
1522
+ 'resolve_vds' enabled - when the camera dataset virtual source spans at
1523
+ least '_MAX_VDS_SOURCES_TO_RESOLVE' distinct files.
1524
+ """
1525
+
1526
+ n_chunks = 3
1527
+ monkeypatch.setattr(
1528
+ standardacquisition_module, "_MAX_VDS_SOURCES_TO_RESOLVE", n_chunks
1529
+ )
1530
+
1531
+ folder = tmp_path / "output_test_skip_vds_resolution_when_too_many_files"
1532
+ folder.mkdir()
1533
+ bliss_mock = MockBlissAcquisition(
1534
+ n_sample=1,
1535
+ n_sequence=1,
1536
+ n_scan_per_sequence=2,
1537
+ n_darks=1,
1538
+ n_flats=1,
1539
+ with_nx_detector_attr=True,
1540
+ output_dir=folder,
1541
+ detector_name="pcolinux",
1542
+ create_tomo_config=False,
1543
+ )
1544
+ sample = bliss_mock.samples[0]
1545
+ sample_dir = os.path.dirname(sample.sample_file)
1546
+
1547
+ # turn every (plain) detector dataset into a virtual dataset spanning
1548
+ # 'n_chunks' distinct files, like a real bliss acquisition chunked
1549
+ # across many small files.
1550
+ with h5py.File(sample.sample_file, "a") as h5f:
1551
+ target_entries = [
1552
+ entry_name
1553
+ for entry_name, entry_grp in h5f.items()
1554
+ if (det_grp := entry_grp.get("instrument/pcolinux")) is not None
1555
+ and "data" in det_grp
1556
+ and det_grp["data"].shape[0] >= n_chunks
1557
+ ]
1558
+ assert len(target_entries) > 0, "no suitable entry found"
1559
+
1560
+ for entry_name in target_entries:
1561
+ det_grp = h5f[entry_name]["instrument/pcolinux"]
1562
+ original_data = det_grp["data"][()]
1563
+ del det_grp["data"]
1564
+ n_frames = original_data.shape[0]
1565
+ boundaries = numpy.linspace(0, n_frames, n_chunks + 1, dtype=int)
1566
+ layout = h5py.VirtualLayout(
1567
+ shape=original_data.shape, dtype=original_data.dtype
1568
+ )
1569
+ for i in range(n_chunks):
1570
+ start, stop = boundaries[i], boundaries[i + 1]
1571
+ chunk_file = os.path.join(sample_dir, f"{entry_name}_det_chunk_{i}.h5")
1572
+ with h5py.File(chunk_file, "w") as chunk_h5:
1573
+ chunk_h5.create_dataset("data", data=original_data[start:stop])
1574
+ with h5py.File(chunk_file, "r") as chunk_h5:
1575
+ layout[start:stop] = h5py.VirtualSource(
1576
+ path_or_dataset=chunk_h5["data"]
1577
+ )
1578
+ det_grp.create_virtual_dataset("data", layout)
1579
+
1580
+ call_counter = {"n": 0}
1581
+ original_resolve_link = standardacquisition_module.resolve_link
1582
+
1583
+ def _counting_resolve_link(*args, **kwargs):
1584
+ call_counter["n"] += 1
1585
+ return original_resolve_link(*args, **kwargs)
1586
+
1587
+ monkeypatch.setattr(
1588
+ standardacquisition_module, "resolve_link", _counting_resolve_link
1589
+ )
1590
+
1591
+ config = TomoHDF5Config()
1592
+ config.input_file = sample.sample_file
1593
+ config.output_file = str(tmp_path / "output.nx")
1594
+ config.single_file = True
1595
+ # explicitly enabled: the file-count threshold must override it
1596
+ config.resolve_vds = True
1597
+ config.sample_x_keys = ("sx",)
1598
+ config.sample_y_keys = ("sy",)
1599
+ config.translation_x_keys = ("tx",)
1600
+
1601
+ converter.from_h5_to_nx(configuration=config)
1602
+
1603
+ assert call_counter["n"] == 0
1604
+ assert os.path.exists(config.output_file)
1605
+
1606
+
1475
1607
  def test_single_file_and_overwrite_from_cli(tmp_path):
1476
1608
  """
1477
1609
  Test that the `--single-file` and `--overwrite` options provided from
@@ -8,11 +8,11 @@ from __future__ import annotations
8
8
 
9
9
  import logging
10
10
  import os
11
- from nxtomomill.utils._os import abspath
12
11
 
13
12
  from nxtomomill.converter.hdf5.utils import get_default_output_file
14
13
 
15
14
  from nxtomomill.models.h52nx.h52nx import H52nxModel
15
+ from nxtomomill.utils._os import abspath
16
16
 
17
17
  _logger = logging.getLogger(__name__)
18
18
 
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from nxtomo.nxobject.nxdetector import FieldOfView
6
+ from nxtomomill.utils._os import abspath
6
7
  from .base.general_section import GeneralSection
7
8
  from pydantic import (
8
9
  Field,
@@ -57,6 +58,14 @@ class DX2nxModel(GeneralSection):
57
58
  )
58
59
  energy: float | None = Field(default=None, description="Energy in keV")
59
60
 
61
+ @field_validator(
62
+ "input_file",
63
+ mode="before",
64
+ )
65
+ @classmethod
66
+ def cast_input_file_to_abspath(cls, value: str) -> str:
67
+ return abspath(value)
68
+
60
69
  @field_validator(
61
70
  "field_of_view",
62
71
  )
@@ -83,9 +83,12 @@ class GeneralSection(_GeneralSectionBase):
83
83
  mode="plain",
84
84
  )
85
85
  @classmethod
86
- def cast_to_tuple(cls, value: tuple[str, ...] | str | None) -> tuple[str]:
87
- if isinstance(value, tuple):
88
- return value
86
+ def cast_to_tuple(
87
+ cls, value: tuple[str, ...] | list[str] | str | None
88
+ ) -> tuple[str]:
89
+ if isinstance(value, (tuple, list)):
90
+ # a list is obtained if the configuration went through a json serialization for example
91
+ return tuple(value)
89
92
  value = filter_str_def(value)
90
93
  return convert_str_to_tuple(value) or ()
91
94
 
@@ -63,6 +63,9 @@ class UnitSection(BaseModel):
63
63
  )
64
64
  @classmethod
65
65
  def cast_to_pint_unit(cls, value: str | FieldOfView) -> pint.Unit | None:
66
+ if value is None:
67
+ # pint >= 0.26.2 casts None to 'dimensionless'
68
+ raise TypeError("unit cannot be None")
66
69
  if value in ("kiloelectron_volt", "kev"):
67
70
  return _ureg.keV
68
71
  if value == "milliampere":
@@ -53,6 +53,8 @@ _CONTEXT_CLASSES = {Proxy, Beamline}
53
53
  class _Key(BaseModel):
54
54
  dataset: str
55
55
  contexts: set[_CONTEXT]
56
+ resolved_from_tomo_config: bool = False
57
+ """True if the dataset name has been resolved from the bliss-tomo 'tomoconfig'. Those keys have priority over the other (fallback) keys."""
56
58
 
57
59
  @field_validator(
58
60
  "dataset",
@@ -12,6 +12,7 @@ from nxtomomill.models.utils import (
12
12
  convert_str_to_bool,
13
13
  convert_str_to_tuple,
14
14
  )
15
+ from nxtomomill.utils._os import abspath
15
16
  from ..base.general_section import GeneralSection as _GeneralSectionBase
16
17
 
17
18
 
@@ -159,7 +160,10 @@ class GeneralSection(_GeneralSectionBase):
159
160
  )
160
161
  @classmethod
161
162
  def cast_general_section_to_str(cls, value: str | None) -> str | None:
162
- return filter_str_def(value)
163
+ value = filter_str_def(value)
164
+ if not value:
165
+ return value
166
+ return abspath(value)
163
167
 
164
168
  @field_validator(
165
169
  "no_input",
@@ -17,6 +17,9 @@ def _cast_keys_section_to_tuple(value):
17
17
  if isinstance(value, str):
18
18
  # convert "sa, id16a:sx" to ("sa", "id16a:sx")
19
19
  value: tuple = convert_str_to_tuple(value)
20
+ elif isinstance(value, list):
21
+ # a list is obtained if the configuration went through a json serialization for example
22
+ value = tuple(value)
20
23
 
21
24
  def invalid_type():
22
25
  raise TypeError(
@@ -289,3 +289,15 @@ def test_TomoEDFConfig_setters():
289
289
 
290
290
  detector_section_dict = config_dict["DETECTOR_SECTION"]
291
291
  assert detector_section_dict["field_of_view"] == FieldOfView.HALF.value
292
+
293
+
294
+ def test_edf2nx_model_json_round_trip():
295
+ """
296
+ Make sure the configuration can be loaded back once dumped to json (tuples are converted to lists).
297
+ """
298
+ import json
299
+
300
+ config = EDF2nxModel()
301
+ as_json = json.loads(json.dumps(config.to_nested_model().model_dump(), default=str))
302
+ assert isinstance(as_json["general_section"]["patterns_to_ignores"], list)
303
+ assert EDF2nxModel.from_dict(as_json) == config
@@ -26,6 +26,7 @@ from nxtomomill.models.h52nx.extra_params_section import (
26
26
  ExtraParamsSection,
27
27
  )
28
28
  from nxtomomill.converter.hdf5.acquisition.utils._keys import _Key
29
+ from nxtomomill.utils._os import abspath
29
30
 
30
31
  from nxtomomill.tests.resources import h52nx_config_files as _h52nx_config_files
31
32
  from configparser import ConfigParser
@@ -67,7 +68,7 @@ def test_moving_to_pydantic_modified_config_file():
67
68
 
68
69
  config = H52nxModel.from_cfg_file(ref_file)
69
70
  # general section
70
- assert config.input_file == "input_file.h5"
71
+ assert config.input_file == abspath("input_file.h5")
71
72
  assert config.output_file == "output_file.nx"
72
73
  assert config.overwrite is True
73
74
  assert config.file_extension is FileExtension.H5
@@ -226,7 +227,7 @@ def test_generate_default_hdf5_config():
226
227
 
227
228
  # check input and output file
228
229
  general_information = output["GENERAL_SECTION"]
229
- assert general_information["input_file"] == "toto.h5"
230
+ assert general_information["input_file"] == abspath("toto.h5")
230
231
  assert general_information["output_file"] == "toto.nx"
231
232
 
232
233
 
@@ -333,3 +334,15 @@ def test_hdf5config_to_and_from_cfg_file(tmp_path):
333
334
  assert os.path.exists(file_path)
334
335
  loaded_config = H52nxModel.from_cfg_file(file_path=file_path)
335
336
  assert isinstance(loaded_config, H52nxModel)
337
+
338
+
339
+ def test_h52nx_model_json_round_trip():
340
+ """
341
+ Make sure the configuration can be loaded back once dumped to json (tuples are converted to lists).
342
+ """
343
+ import json
344
+
345
+ config = H52nxModel()
346
+ as_json = json.loads(json.dumps(config.to_nested_model().model_dump(), default=str))
347
+ assert isinstance(as_json["keys_section"]["valid_camera_names"], list)
348
+ assert H52nxModel.from_dict(as_json) == config
@@ -135,9 +135,9 @@ class __RefsDarks:
135
135
  ):
136
136
  self.dict_or_file_name = dict_or_file_name
137
137
  self.entry_name = entry_name
138
+ self._open_mode_for_reading = open_mode_for_reading
138
139
  self.flat_image, self.flat_current = self._take_image_and_meta("flat")
139
140
  self.dark_image, self.dark_current = self._take_image_and_meta("dark")
140
- self._open_mode_for_reading = open_mode_for_reading
141
141
 
142
142
  def _take_image_and_meta(self, what) -> tuple:
143
143
  """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nxtomomill
3
- Version: 2.2.0.dev1
3
+ Version: 2.2.0.dev2
4
4
  Summary: applications and library to convert raw format to NXtomo format
5
5
  Author-email: Henri Payno <henri.payno@esrf.fr>, Pierre Paleo <pierre.paleo@esrf.fr>, Pierre-Olivier Autran <pierre-olivier.autran@esrf.fr>, Jérôme Lesaint <jerome.lesaint@esrf.fr>, Alessandro Mirone <mirone@esrf.fr>
6
6
  License:
@@ -13,7 +13,7 @@ nxtomomill/app/edfconfig.py,sha256=TJDQfkeIBX1Rk13HBl1otdKB8uJwqAyKuFvhmtti3Kg,1
13
13
  nxtomomill/app/fluo2nx.py,sha256=mI8m18rQ9qOuY6CPIKoWJRaIuu4jgI2Z65Fi1P03FeI,4524
14
14
  nxtomomill/app/fluoconfig.py,sha256=M2b6pd8HmtcLx2wC0NcQbjXB2l550JuQap16JWhVQss,948
15
15
  nxtomomill/app/fscan2nx.py,sha256=uejEHirz6TDAvh-blkrORsMlx8ZW1MRGoxj8dtLwhlw,2363
16
- nxtomomill/app/h52nx.py,sha256=3Kv8W_dtC9-Ns5vTWok7Pd563BOhirCsfZn1KGAzdDI,11938
16
+ nxtomomill/app/h52nx.py,sha256=qHLrT9SgkBtYtRogUdKWw8mvdpy1aI24bA9ZBujyWNM,13193
17
17
  nxtomomill/app/h5config.py,sha256=OpRLjwGlDbnHmh8W_fCU-JXAjts2GOcrfHu5I4oLCMk,1527
18
18
  nxtomomill/app/nxcopy.py,sha256=KknTJ0r61erBzQ83i7OJkFgTT1bH8FHo0llfbj0TzPk,2477
19
19
  nxtomomill/app/patch_nx.py,sha256=oXMevqyYe1ZS3ylYz6w8jYfu8_YfdNJLkeAMv7OaT9E,9839
@@ -58,14 +58,14 @@ nxtomomill/converter/hdf5/utils.py,sha256=uS8HymNSwMEWYvhD1yUxkco7xi41LAeG1pNXFs
58
58
  nxtomomill/converter/hdf5/acquisition/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
59
59
  nxtomomill/converter/hdf5/acquisition/_bliss_activeconfig.py,sha256=mgC-apxxu6PuVmLnve3W619LMkKnu9weWPTVbgvQrRQ,943
60
60
  nxtomomill/converter/hdf5/acquisition/backandforth.py,sha256=PbhoELpvfHRYvl25GFnhyGjf6L42UV8uxYcrXHZ4Wxk,677
61
- nxtomomill/converter/hdf5/acquisition/baseacquisition.py,sha256=en_kOFU7Oa6aJxr71Jce7y9gnNzz4tRqFfs9nZAsvGM,34330
61
+ nxtomomill/converter/hdf5/acquisition/baseacquisition.py,sha256=mclCgRPxUbi5bme3SdDu8bnlNcI05ZKSZRBYLqvF3H0,35041
62
62
  nxtomomill/converter/hdf5/acquisition/blisstomoconfig.py,sha256=9-LxXeyNPDUPLybwJ48o7XHx8ggqvy-K-FnhJ0qie-c,6953
63
63
  nxtomomill/converter/hdf5/acquisition/multitomo.py,sha256=N5fHVwKSqqHPDEycRhJ0mtXLijvxCHwY_IiDtQ5SFq4,19280
64
64
  nxtomomill/converter/hdf5/acquisition/pcotomoacquisition.py,sha256=1CgX1rFzekIckUlT8u2eb-xw5wCIF3TOoesmIg1y1oc,619
65
- nxtomomill/converter/hdf5/acquisition/standardacquisition.py,sha256=EklnKyvKn_WjDn9WulQ4kM8EDpiDDxwvdAbXwBfHzTM,62515
65
+ nxtomomill/converter/hdf5/acquisition/standardacquisition.py,sha256=pcDoq8HlC-HGwUeRQC3cwJRO7FdQ9gIjnt6nNMaiLcs,63260
66
66
  nxtomomill/converter/hdf5/acquisition/zseriesacquisition.py,sha256=15VZvPMqp6pluGXo2rRh5IVfbAegGMy2ByHo5doCeDM,10634
67
67
  nxtomomill/converter/hdf5/acquisition/tests/bliss_tomo_datasets.py,sha256=pcusczTBMttXRJGeEag4Uel6y8riWd981g-OVxW2M_o,874
68
- nxtomomill/converter/hdf5/acquisition/tests/test_acquisition.py,sha256=dWdbwjvg60J4d-U6pq3_e_GV_dTEt-6hekeDe2r7O58,4357
68
+ nxtomomill/converter/hdf5/acquisition/tests/test_acquisition.py,sha256=_g0PHHcReRT2gD3IrMoLhiBP8G25IZm3uvW6qkYpBZI,7480
69
69
  nxtomomill/converter/hdf5/acquisition/tests/test_acquisition_utils.py,sha256=UTAq1J_c_cYNtfZdhBTqwCzu4h1gIdGwDWlyJNXQg3M,9271
70
70
  nxtomomill/converter/hdf5/acquisition/tests/test_back_and_forth.py,sha256=l2NbPcM21kdN0gGEC5xUHVsYE_tsS5AeZfa0idV3x8w,4573
71
71
  nxtomomill/converter/hdf5/acquisition/tests/test_bliss_tomo_config.py,sha256=sukeUc6P2Plg30hUeq26v15WcXN4zv2KfIX5LTF7RVo,3256
@@ -74,8 +74,8 @@ nxtomomill/converter/hdf5/acquisition/tests/test_legacy.py,sha256=EVj1s8NBVXQoAg
74
74
  nxtomomill/converter/hdf5/acquisition/tests/test_multitomo_zseries.py,sha256=wPUhG3VihCND_Uoq-yHec4qwpNQyzyb3qvzHPOOhfzQ,3654
75
75
  nxtomomill/converter/hdf5/acquisition/tests/test_multitomoacquisition.py,sha256=PCfpLhXvm8x3WU1d_pc57Ny4QpavNEPGy47BYaQ3oXc,7651
76
76
  nxtomomill/converter/hdf5/acquisition/utils/__init__.py,sha256=tc3O9HTgBxoG2hr2qIy41d9ARQBN1rPMb8Zo44eamHs,443
77
- nxtomomill/converter/hdf5/acquisition/utils/_context.py,sha256=eWk6PGdKl6WAJtRRCVydXCy-6Rd9dqQuDWHEYMtiIE8,9241
78
- nxtomomill/converter/hdf5/acquisition/utils/_keys.py,sha256=jQIjrL_WD_0o8KzqKBrba5o4hiBGf8H8MyVckVlyWJY,343
77
+ nxtomomill/converter/hdf5/acquisition/utils/_context.py,sha256=Wfd5nM1RKA-5kgYdW_mMO7f611PPQtHObgzmOnHpETc,9301
78
+ nxtomomill/converter/hdf5/acquisition/utils/_keys.py,sha256=vGbVJcW4qPKsJOtc91AYvSFaRNJR3wwLMjLQcJuhSS8,1001
79
79
  nxtomomill/converter/hdf5/acquisition/utils/_scan_type_finder.py,sha256=n3Vf3aCMwi6bD7LJlyojqk2JZTYgxDgaP5x9AB0ijv8,3695
80
80
  nxtomomill/converter/hdf5/acquisition/utils/bliss_scan_type.py,sha256=s72IPhbpU-lv79VshxDPzJMjRyaTIQPUFYbDIzol0R0,1378
81
81
  nxtomomill/converter/hdf5/acquisition/utils/detector.py,sha256=WHaHw7_082NL8p0J5ReI1yJ5faGmh_7hpHqbrD9bCR0,1926
@@ -84,13 +84,14 @@ nxtomomill/converter/hdf5/acquisition/utils/series.py,sha256=nIUcBSEIIsGAEHK11-D
84
84
  nxtomomill/converter/hdf5/acquisition/utils/timestamps.py,sha256=cvTi4Ol5DwA1u6st3sY8DNxq95jnivMrFF2VGxf7agc,461
85
85
  nxtomomill/converter/hdf5/post_processing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
86
86
  nxtomomill/converter/hdf5/post_processing/dark_flat_copy.py,sha256=nBbXT9r0mxlGI1la3pqT-g7pmpmvMSVCj0uBoZMenb8,9751
87
+ nxtomomill/converter/hdf5/tests/test_err_handling.py,sha256=rSVSp5lL581t9M5CSGgMNOUJHpNucTJ_SgKUttkSjXY,1558
87
88
  nxtomomill/converter/hdf5/tests/test_frame_flip.py,sha256=0S0FmMM9RVj1UQNwCsXQyI_bwQ8hx6ORdl4-EsWUjf8,2330
88
- nxtomomill/converter/hdf5/tests/test_h52nx_utils.py,sha256=iPC52Ly5I8yH7hV2d-z39VDfC-hoIDINtTQC1X2aRIo,11806
89
- nxtomomill/converter/hdf5/tests/test_hdf5converter.py,sha256=izTJWIpxSG-F6Eh7c5msmsSlZVTo34ggJh6_akyigmk,59154
89
+ nxtomomill/converter/hdf5/tests/test_h52nx_utils.py,sha256=QGAmTeaf9feqJvlWPXBbw_wjOChX0hWr8Kl5I2OfyoU,12002
90
+ nxtomomill/converter/hdf5/tests/test_hdf5converter.py,sha256=0iSp-pDRElp3168Jm27lNG5mMrZ8q0Y9jq0cIS85PMU,64022
90
91
  nxtomomill/io/__init__.py,sha256=gxUEBT0A0o8kJJ19N4pIIYVTA-7TuvuJq2BEepqbrNQ,91
91
92
  nxtomomill/io/config/__init__.py,sha256=q1RV3iBYSqpV-r8H794YBXl_1DiA0KNZVWEmWpF04x4,348
92
93
  nxtomomill/io/config/configbase.py,sha256=pLPGV91Vwxuqidh7UnkWscXauPzsrNbAI9T7DJQHMEo,554
93
- nxtomomill/io/config/confighandler.py,sha256=6kSG01z0lqPJVGrUSHJj2MsBFe2N9X7UqRN_p871ufU,6475
94
+ nxtomomill/io/config/confighandler.py,sha256=55bvQJNtlM1DIeDpfCjMrrofNxDP-h5zqQf0f0RC5V8,6475
94
95
  nxtomomill/io/config/dxconfig.py,sha256=33gYIWsr0R7qL8uY7IDoE6kUNuWHtWp3_tfaY6TSGJk,590
95
96
  nxtomomill/io/config/edfconfig.py,sha256=J04CZoDYkV3XZ-ZowNdyx5Lf8W1-QbAXKPnSPksqSLA,911
96
97
  nxtomomill/io/config/fluoconfig.py,sha256=R-Cy1pYROzqS9CDj3T9kO7m17oCgyazIPz2d8-BRZwY,933
@@ -100,7 +101,7 @@ nxtomomill/io/config/tests/test_confighandler.py,sha256=-O7tbbKI3n7lSbXfDxa-mwt1
100
101
  nxtomomill/io/tests/test_frame_group.py,sha256=P0weUFFwlNLhIDk4gpJsCrFSZ_mzF54SNNqjMVr_lhw,5719
101
102
  nxtomomill/io/tests/utils.py,sha256=z_-AVtX26c6gsmpDgol1RqXmbdzdaj2jMXY3wfx2UyI,1950
102
103
  nxtomomill/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
103
- nxtomomill/models/dx2nx.py,sha256=XoQ48YuSmVfuYeH8b0lgcV-rie4p-RCQO53juEXE5Pk,3136
104
+ nxtomomill/models/dx2nx.py,sha256=QAP32N2BsAqDLHgmhRDgeoL_OP3Oi5t2HT26zVUHpck,3358
104
105
  nxtomomill/models/utils.py,sha256=VB34e3AXfcFka7OL-eeP6byKBiSP_b4dNiqBuclpgyU,3096
105
106
  nxtomomill/models/base/ConfigBase.py,sha256=6QZBVAdKIoarm0oMvmYFLL5AmldA-F76QP1p-Pp64j4,971
106
107
  nxtomomill/models/base/FrmFlatToNestedBase.py,sha256=HvvMSmZrCfH4HqTCJgr4Bv3_K8IQSRRoriT6tdHIWFk,1053
@@ -116,28 +117,28 @@ nxtomomill/models/edf2nx/__init__.py,sha256=btz-4hb-LOIodmLSSihq1FhbF0W7vGTcwt6j
116
117
  nxtomomill/models/edf2nx/dark_and_flat_section.py,sha256=AvgAxdMQ_EWlnR7h5nUeEYr1LyI7KjE0YaZsUf3YOL8,1711
117
118
  nxtomomill/models/edf2nx/detector_section.py,sha256=54K2Ezb1ktjs46kkfktcfRP9ptJaN1_VRyBMCWyD7bk,1372
118
119
  nxtomomill/models/edf2nx/edf2nx.py,sha256=RCTgD4Y_zbmiAjED8I1PqXymcYwZbd7EP416RNRMo7w,6696
119
- nxtomomill/models/edf2nx/general_section.py,sha256=wY2UIt9iswsFgNFTQHRDTvUkZ2cdH0779-eB1QaqWxM,5073
120
+ nxtomomill/models/edf2nx/general_section.py,sha256=wCPxjCxeuy517QL2600BLqRm95sfW0swlKYrRDCFA0Y,5214
120
121
  nxtomomill/models/edf2nx/keys_section.py,sha256=H04pODXv0Wtun3Qbd3JFB7xXq58PIL_5wtUVPVkc4XY,2056
121
122
  nxtomomill/models/edf2nx/sample_section.py,sha256=wEPBuKIDwXTn84-oFahUOKZ-Em3Kpb6wmZJl7VaPNlk,1640
122
- nxtomomill/models/edf2nx/unit_section.py,sha256=nvxmHpDq0ClRmlRUQyfvy6PTwOlmCnpzrfR55q4CGPM,3903
123
+ nxtomomill/models/edf2nx/unit_section.py,sha256=220wL2YT6XSw5M9kmp7IIMkU93_CawdxoA4gkSm03Gs,4039
123
124
  nxtomomill/models/fluo2nx/__init__.py,sha256=E0KmuFqL10lxFF2K7IBnsv8VpQYmNh-FQFESwNcN_4o,120
124
125
  nxtomomill/models/fluo2nx/fluo2nx.py,sha256=gWVNsytqyd6hIj6TffBRTObdj2Elvqf4ECVFhsHjr2o,3613
125
126
  nxtomomill/models/fluo2nx/general_section.py,sha256=JmfAmnWZJWsCbzR-cNOLi1gIUrKTb_hqWNDjnQdVcAk,2621
126
127
  nxtomomill/models/h52nx/FrameGroup.py,sha256=mjw-pQfNm_WVR10JkktVfK0-NK05Hf3cct3mKtCCyOI,9363
127
128
  nxtomomill/models/h52nx/__init__.py,sha256=F2B5npgvXjYWw3npIJsA_Jt0IyTeM8rsl_HBQYu8zJI,112
128
129
  nxtomomill/models/h52nx/_acquisitionstep.py,sha256=YFA0kmcKUUKv6Qi3GMdm26RlT00BJ_afBy-v97VAnOU,2206
129
- nxtomomill/models/h52nx/_context.py,sha256=Z_ofzqAFMyrjVQOXTnQw9PrzrU8iLNaV4OfSAG9YZKU,3549
130
+ nxtomomill/models/h52nx/_context.py,sha256=OVVugpf1eRAt5hJDic2hWTDyeOTHr_iqJaCR46ITF08,3737
130
131
  nxtomomill/models/h52nx/entries_and_title_section.py,sha256=qEs2VGRf6Nciq3FupJUZCKpNA2ZGBeK694O_kDOWo1Y,4703
131
132
  nxtomomill/models/h52nx/extra_params_section.py,sha256=ZnxzwJVOjUqOd6ngmmrIb2xZVABfomBvKSELiT00Nmc,1547
132
133
  nxtomomill/models/h52nx/frame_type_section.py,sha256=U-akT0f8lOvzjOQ6ZuUSwy3-pYAYkiVXeY7bBtZ0o44,3716
133
- nxtomomill/models/h52nx/general_section.py,sha256=d9EujPhiMzSGIplo41XQthV7gzmk5iX57cXrn02x4JU,6891
134
+ nxtomomill/models/h52nx/general_section.py,sha256=z69qLCEIFKa7xFdt1zcVmed_jZSRqLFzB7-T6CpCnPc,7010
134
135
  nxtomomill/models/h52nx/h52nx.py,sha256=4EwwMBQZBPQQ-um6rCgk5uhv-S01zjDzDVo1us2WT88,8603
135
- nxtomomill/models/h52nx/keys_section.py,sha256=Hpu0pUrUyI3fuwZ1y5j-qdxpzDNQXQZCwdcWG6nNtL4,5923
136
+ nxtomomill/models/h52nx/keys_section.py,sha256=FFGb39DWlLaN-XOcAEOOgIBc64RdETjugtH8X68ACcs,6082
136
137
  nxtomomill/models/h52nx/multitomo_section.py,sha256=aOM3oxopLjlBFzs0UXInxbkZ_NOw0QXS0BGI_2uhzLk,1819
137
138
  nxtomomill/models/tests/test_context.py,sha256=w0A02DXL5hEhw-imMleSeue7ejqotuu_qr0aUJrEhxg,1711
138
- nxtomomill/models/tests/test_edf2nx_model.py,sha256=SdYov4dX8fN4D8DfFN8EQjuU3MQjEyKMl6Udve5lg3g,11385
139
+ nxtomomill/models/tests/test_edf2nx_model.py,sha256=Tf8GaRkvVettIY_FO_br8KJ30LvTBWTkxV-6vfaQw_c,11812
139
140
  nxtomomill/models/tests/test_fluo2nx_model.py,sha256=v-_B0-2ZIt4wapKWjtTjNUEDBCbU2x-WtVDQGvvYms0,5247
140
- nxtomomill/models/tests/test_h52nx_model.py,sha256=5XgaM9HbWORPu0mpf9WRtEqdZTYinrfujdEVN8GwJn8,12736
141
+ nxtomomill/models/tests/test_h52nx_model.py,sha256=K1o0q5QQpCbNwxTmi5G988nXnB9gY502SZS5MRYOugY,13215
141
142
  nxtomomill/models/tests/test_io_utils.py,sha256=ZvHx5XpVAx3C-3phFWwy0RFamtf98idt-bWTcpLljkU,1576
142
143
  nxtomomill/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
143
144
  nxtomomill/tests/datasets.py,sha256=hjEAtM2F6GNIJoE8VaX5cnAikWM5Wt12J-nRyTi14kY,344
@@ -151,7 +152,7 @@ nxtomomill/tests/utils/bliss.py,sha256=fslMDgGqssY_3KjlmkpaVhfqMpjqukI0QdOrH3-bE
151
152
  nxtomomill/tests/utils/dxfile.py,sha256=LSoBQtBBs8dOFvoJw0t0QpWsWVF_FrPeAKTTfAPyD6k,1892
152
153
  nxtomomill/utils/__init__.py,sha256=HiwjKOWdwZ9wV0F16HQcE1qUWCuk_rL_N2bsSIBZVwo,568
153
154
  nxtomomill/utils/_os.py,sha256=Mzz2_d4Inz7fmxZWl0wI03GQUb0oVo1soiX52Dkq49Y,368
154
- nxtomomill/utils/flat_reducer.py,sha256=J05n7rVzuwm3gybaaROF4HI0-EDaBREXM2_Wu6Jud68,12416
155
+ nxtomomill/utils/flat_reducer.py,sha256=Ul-Gi3CYR0JU6tbWaFaZntJ7T-oRtjzYusQbwg0Zrps,12416
155
156
  nxtomomill/utils/h5pyutils.py,sha256=tLSnUfFp-A3RaL3YXu4b_deiToQKFi8xn45Cp0r_1Dw,8400
156
157
  nxtomomill/utils/hdf5.py,sha256=KuJdr3LnvA8eX17ECdwlPgVARZAf_8Z5xIJZx3Otplw,4983
157
158
  nxtomomill/utils/io.py,sha256=f3pdEqH2Fca5vl7xPry1cPp5yP4Tnas8DGbfPqz-94Q,3158
@@ -162,9 +163,9 @@ nxtomomill/utils/tests/test_flat_reducer.py,sha256=kMatWooQ5vRcsf03rkMDEEdgkmjv9
162
163
  nxtomomill/utils/tests/test_h5pyutils.py,sha256=L9h0-m68rvLCWIFzK71K3Hg_9YH-_PynEsPghSkz5qY,8890
163
164
  nxtomomill/utils/tests/test_nexus_utils.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
164
165
  nxtomomill/utils/tests/test_utils.py,sha256=D4I_r2Xux4pAc8E4lhi4BHA0-0kyiOqlU9JNnJGS3Es,53064
165
- nxtomomill-2.2.0.dev1.dist-info/licenses/LICENSE,sha256=3-51vily0513UmUtHb-gWYFOhge5Ldp8HTjIOPtIdBo,1257
166
- nxtomomill-2.2.0.dev1.dist-info/METADATA,sha256=XBd-CMot2l3Ou-AMxd-wJ4_hSghDRU3BdSppMXSZUxM,5318
167
- nxtomomill-2.2.0.dev1.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
168
- nxtomomill-2.2.0.dev1.dist-info/entry_points.txt,sha256=kZUAunNCjlk8gda1r07SPNRO9TR5rP0wVPa8-jVx5ts,56
169
- nxtomomill-2.2.0.dev1.dist-info/top_level.txt,sha256=FxntqOpZgeTW3KHuyJ7gjFwOcTSdNNIGex2gPf07-Vg,15
170
- nxtomomill-2.2.0.dev1.dist-info/RECORD,,
166
+ nxtomomill-2.2.0.dev2.dist-info/licenses/LICENSE,sha256=3-51vily0513UmUtHb-gWYFOhge5Ldp8HTjIOPtIdBo,1257
167
+ nxtomomill-2.2.0.dev2.dist-info/METADATA,sha256=1J8Des6wS0p8z_lMZB2-UXA9RBsW1C6S2LISFKo1IVY,5318
168
+ nxtomomill-2.2.0.dev2.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
169
+ nxtomomill-2.2.0.dev2.dist-info/entry_points.txt,sha256=kZUAunNCjlk8gda1r07SPNRO9TR5rP0wVPa8-jVx5ts,56
170
+ nxtomomill-2.2.0.dev2.dist-info/top_level.txt,sha256=FxntqOpZgeTW3KHuyJ7gjFwOcTSdNNIGex2gPf07-Vg,15
171
+ nxtomomill-2.2.0.dev2.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (84.0.0)
2
+ Generator: setuptools (84.2.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5