nxtomomill 2.2.0.dev0__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.
- nxtomomill/__main__.py +1 -1
- nxtomomill/app/h52nx.py +53 -2
- nxtomomill/converter/hdf5/acquisition/baseacquisition.py +130 -89
- nxtomomill/converter/hdf5/acquisition/standardacquisition.py +30 -11
- nxtomomill/converter/hdf5/acquisition/tests/test_acquisition.py +102 -0
- nxtomomill/converter/hdf5/acquisition/utils/_context.py +8 -0
- nxtomomill/converter/hdf5/acquisition/utils/_keys.py +19 -0
- nxtomomill/converter/hdf5/hdf5converter.py +7 -0
- nxtomomill/converter/hdf5/tests/test_err_handling.py +48 -0
- nxtomomill/converter/hdf5/tests/test_h52nx_utils.py +23 -5
- nxtomomill/converter/hdf5/tests/test_hdf5converter.py +297 -0
- nxtomomill/io/config/confighandler.py +2 -1
- nxtomomill/io/config/tests/test_confighandler.py +1 -0
- nxtomomill/models/dx2nx.py +9 -0
- nxtomomill/models/edf2nx/general_section.py +6 -3
- nxtomomill/models/edf2nx/unit_section.py +3 -0
- nxtomomill/models/h52nx/_context.py +2 -0
- nxtomomill/models/h52nx/general_section.py +27 -1
- nxtomomill/models/h52nx/h52nx.py +1 -0
- nxtomomill/models/h52nx/keys_section.py +3 -0
- nxtomomill/models/tests/test_edf2nx_model.py +12 -0
- nxtomomill/models/tests/test_h52nx_model.py +15 -2
- nxtomomill/utils/flat_reducer.py +1 -1
- {nxtomomill-2.2.0.dev0.dist-info → nxtomomill-2.2.0.dev2.dist-info}/METADATA +2 -2
- {nxtomomill-2.2.0.dev0.dist-info → nxtomomill-2.2.0.dev2.dist-info}/RECORD +29 -29
- {nxtomomill-2.2.0.dev0.dist-info → nxtomomill-2.2.0.dev2.dist-info}/WHEEL +1 -1
- nxtomomill/converter/edf/_Blosc2Jp2kWrapper.py +0 -63
- {nxtomomill-2.2.0.dev0.dist-info → nxtomomill-2.2.0.dev2.dist-info}/entry_points.txt +0 -0
- {nxtomomill-2.2.0.dev0.dist-info → nxtomomill-2.2.0.dev2.dist-info}/licenses/LICENSE +0 -0
- {nxtomomill-2.2.0.dev0.dist-info → nxtomomill-2.2.0.dev2.dist-info}/top_level.txt +0 -0
nxtomomill/__main__.py
CHANGED
nxtomomill/app/h52nx.py
CHANGED
|
@@ -10,12 +10,14 @@ 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
|
|
|
17
19
|
from nxtomomill import utils
|
|
18
|
-
from nxtomomill.converter import
|
|
20
|
+
from nxtomomill.converter.hdf5.hdf5converter import _H5ToNxConverter
|
|
19
21
|
from nxtomomill.io.config.confighandler import (
|
|
20
22
|
SETTABLE_PARAMETERS_UNITS,
|
|
21
23
|
TomoHDF5ConfigHandler,
|
|
@@ -317,12 +319,49 @@ def main(argv):
|
|
|
317
319
|
"Sometime relative link cannot be solved (GPFS vs NFS). "
|
|
318
320
|
"In this case you might be forced to use absolute path.",
|
|
319
321
|
)
|
|
322
|
+
parser.add_argument(
|
|
323
|
+
"--no-resolve-vds",
|
|
324
|
+
"--no-resolve-vds-links",
|
|
325
|
+
"--no_resolve_vds",
|
|
326
|
+
dest="resolve_vds",
|
|
327
|
+
action="store_false",
|
|
328
|
+
default=None,
|
|
329
|
+
help="Do not resolve external links and virtual datasets (VDS) down to "
|
|
330
|
+
"their final source when building the output NXtomo. Can speed up the "
|
|
331
|
+
"conversion for datasets with many nested links, at the cost of the "
|
|
332
|
+
"output file containing VDS pointing to other VDS instead of direct "
|
|
333
|
+
"links to the raw data. Defaults to the 'NXTOMOMILL_RESOLVE_VDS' "
|
|
334
|
+
"environment variable if set, else resolving is enabled.",
|
|
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
|
+
)
|
|
320
352
|
options = parser.parse_args(argv[1:])
|
|
321
353
|
if options.request_input:
|
|
322
354
|
callback_det_sel = _ask_for_selecting_detector
|
|
323
355
|
else:
|
|
324
356
|
callback_det_sel = None
|
|
325
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
|
+
|
|
326
365
|
try:
|
|
327
366
|
configuration_handler = TomoHDF5ConfigHandler(options)
|
|
328
367
|
except Exception:
|
|
@@ -335,9 +374,21 @@ def main(argv):
|
|
|
335
374
|
configuration_handler.configuration.log_level
|
|
336
375
|
)
|
|
337
376
|
with tqdm(desc="h52nx", delay=1, bar_format="{l_bar}{bar}") as progress:
|
|
338
|
-
|
|
377
|
+
converter = _H5ToNxConverter(
|
|
339
378
|
configuration=configuration_handler.configuration,
|
|
340
379
|
progress=progress,
|
|
341
380
|
input_callback=None,
|
|
342
381
|
detector_sel_callback=callback_det_sel,
|
|
343
382
|
)
|
|
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()
|
|
393
|
+
if converter.has_failed_acquisitions:
|
|
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,8 +32,9 @@ 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 .
|
|
34
|
-
from .utils.
|
|
35
|
+
from nxtomomill.models.h52nx._context import _Key
|
|
36
|
+
from .utils._keys import keys_to_str_tuple, split_keys_by_priority
|
|
37
|
+
from .utils._context import read_key, MissingValue, InvalidValue
|
|
35
38
|
|
|
36
39
|
_ureg = pint.get_application_registry()
|
|
37
40
|
_logger = logging.getLogger(__name__)
|
|
@@ -300,7 +303,14 @@ class BaseAcquisition:
|
|
|
300
303
|
file_path=output_file,
|
|
301
304
|
data_path=data_path,
|
|
302
305
|
use_abs_path_for_vds=self.configuration.use_abs_path_for_vds,
|
|
306
|
+
overwrite=self.configuration.overwrite,
|
|
303
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__
|
|
304
314
|
# check rotation angle
|
|
305
315
|
if nx_tomo.sample.rotation_angle is not None:
|
|
306
316
|
unique_angles = numpy.unique(
|
|
@@ -408,6 +418,12 @@ class BaseAcquisition:
|
|
|
408
418
|
except MissingValue:
|
|
409
419
|
# skip values containing '*DIS*'
|
|
410
420
|
continue
|
|
421
|
+
except InvalidValue:
|
|
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
|
|
411
427
|
except KeyError as e:
|
|
412
428
|
_logger.info(
|
|
413
429
|
f"Cannot solve key {key}. Most likely one dataset is not there. Error is {e}"
|
|
@@ -613,6 +629,55 @@ class BaseAcquisition:
|
|
|
613
629
|
|
|
614
630
|
return None
|
|
615
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
|
+
|
|
616
681
|
def _get_rotation_angle(self, root_node: h5py.Group, n_frame: int) -> pint.Quantity:
|
|
617
682
|
"""
|
|
618
683
|
return the list of rotation angle for each frame.
|
|
@@ -623,23 +688,19 @@ class BaseAcquisition:
|
|
|
623
688
|
if not isinstance(root_node, h5py.Group):
|
|
624
689
|
raise TypeError("root_node is expected to be a h5py.Group")
|
|
625
690
|
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
except (ValueError, KeyError):
|
|
640
|
-
pass
|
|
641
|
-
else:
|
|
642
|
-
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
|
|
643
704
|
|
|
644
705
|
mess = f"Unable to find rotation angle for {root_node.name}"
|
|
645
706
|
if self.raise_error_if_issue:
|
|
@@ -651,19 +712,15 @@ class BaseAcquisition:
|
|
|
651
712
|
|
|
652
713
|
def _get_sample_x(self, root_node, n_frame) -> pint.Quantity:
|
|
653
714
|
"""return the list of translation for each frame"""
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
except (ValueError, KeyError):
|
|
664
|
-
pass
|
|
665
|
-
else:
|
|
666
|
-
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
|
|
667
724
|
|
|
668
725
|
mess = f"Unable to find sample x for {root_node.name}"
|
|
669
726
|
if self.raise_error_if_issue:
|
|
@@ -675,19 +732,15 @@ class BaseAcquisition:
|
|
|
675
732
|
|
|
676
733
|
def _get_sample_y(self, root_node, n_frame) -> pint.Quantity:
|
|
677
734
|
"""return the list of translation for each frame"""
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
except (ValueError, KeyError):
|
|
688
|
-
pass
|
|
689
|
-
else:
|
|
690
|
-
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
|
|
691
744
|
|
|
692
745
|
mess = f"Unable to find sample y for {root_node.name}"
|
|
693
746
|
if self.raise_error_if_issue:
|
|
@@ -697,21 +750,17 @@ class BaseAcquisition:
|
|
|
697
750
|
_logger.warning(mess)
|
|
698
751
|
return ([0] * n_frame) * _ureg.meter
|
|
699
752
|
|
|
700
|
-
def _get_translation_x(self, root_node, n_frame) -> pint.
|
|
753
|
+
def _get_translation_x(self, root_node, n_frame) -> pint.Quantity | None:
|
|
701
754
|
"""return the list of translation for each frame"""
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
except (ValueError, KeyError):
|
|
712
|
-
pass
|
|
713
|
-
else:
|
|
714
|
-
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
|
|
715
764
|
|
|
716
765
|
mess = f"Unable to find translation x for {root_node.name}"
|
|
717
766
|
if self.raise_error_if_issue:
|
|
@@ -721,21 +770,17 @@ class BaseAcquisition:
|
|
|
721
770
|
_logger.warning(mess)
|
|
722
771
|
return ([0] * n_frame) * _ureg.meter
|
|
723
772
|
|
|
724
|
-
def _get_translation_y(self, root_node, n_frame) -> pint.
|
|
773
|
+
def _get_translation_y(self, root_node, n_frame) -> pint.Quantity | None:
|
|
725
774
|
"""return the list of translation for each frame"""
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
except (ValueError, KeyError):
|
|
736
|
-
pass
|
|
737
|
-
else:
|
|
738
|
-
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
|
|
739
784
|
|
|
740
785
|
mess = f"Unable to find translation y for {root_node.name}"
|
|
741
786
|
if self.raise_error_if_issue:
|
|
@@ -748,20 +793,16 @@ class BaseAcquisition:
|
|
|
748
793
|
@staticmethod
|
|
749
794
|
def get_translation_z_frm(
|
|
750
795
|
root_node, n_frame: int, configuration: H52nxModel
|
|
751
|
-
) -> pint.Quantity:
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
except (ValueError, KeyError):
|
|
762
|
-
pass
|
|
763
|
-
else:
|
|
764
|
-
return z_tr * unit
|
|
796
|
+
) -> pint.Quantity | None:
|
|
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
|
|
765
806
|
|
|
766
807
|
mess = f"Unable to find translation z on node {root_node.name}"
|
|
767
808
|
if configuration.raises_error:
|
|
@@ -771,7 +812,7 @@ class BaseAcquisition:
|
|
|
771
812
|
_logger.warning(mess)
|
|
772
813
|
return ([0] * n_frame) * _ureg.meter
|
|
773
814
|
|
|
774
|
-
def _get_translation_z(self, root_node, n_frame) -> pint.Quantity:
|
|
815
|
+
def _get_translation_z(self, root_node, n_frame) -> pint.Quantity | None:
|
|
775
816
|
"""return the list of translation z for each frame"""
|
|
776
817
|
return self.get_translation_z_frm(
|
|
777
818
|
root_node=root_node,
|
|
@@ -804,7 +845,7 @@ class BaseAcquisition:
|
|
|
804
845
|
else:
|
|
805
846
|
mess += "default value will be set. (0)"
|
|
806
847
|
_logger.warning(mess)
|
|
807
|
-
return
|
|
848
|
+
return numpy.zeros(n_frame) * _ureg.second
|
|
808
849
|
|
|
809
850
|
def get_axis_scale_types(self):
|
|
810
851
|
"""
|
|
@@ -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,20 +350,35 @@ 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
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
)
|
|
356
|
-
except OSError as e:
|
|
357
|
-
# This can happen if one fine cannot be found. In test for example where we are only using the master file
|
|
358
|
-
# and low level bliss / tango .h5 files are not existing
|
|
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
359
|
_logger.warning(
|
|
360
|
-
"
|
|
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."
|
|
361
363
|
)
|
|
362
364
|
solved_camera_dataset_urls = (camera_dataset_url,)
|
|
365
|
+
elif self.configuration.resolve_vds:
|
|
366
|
+
try:
|
|
367
|
+
solved_camera_dataset_urls = resolve_link(
|
|
368
|
+
dataset_url=camera_dataset_url,
|
|
369
|
+
dataset=data_dataset,
|
|
370
|
+
dataset_link=data_dataset_link,
|
|
371
|
+
open_mode_for_reading=self.configuration.open_mode_for_reading,
|
|
372
|
+
)
|
|
373
|
+
except OSError as e:
|
|
374
|
+
# This can happen if one fine cannot be found. In test for example where we are only using the master file
|
|
375
|
+
# and low level bliss / tango .h5 files are not existing
|
|
376
|
+
_logger.warning(
|
|
377
|
+
"Fail to solve camera dataset links and VDS. Error is %s", e
|
|
378
|
+
)
|
|
379
|
+
solved_camera_dataset_urls = (camera_dataset_url,)
|
|
380
|
+
else:
|
|
381
|
+
solved_camera_dataset_urls = (camera_dataset_url,)
|
|
363
382
|
|
|
364
383
|
n_total_frame_solved = 0
|
|
365
384
|
for solved_camera_dataset_url in solved_camera_dataset_urls:
|
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
import os
|
|
4
4
|
import tempfile
|
|
5
5
|
|
|
6
|
+
import logging
|
|
7
|
+
|
|
8
|
+
import h5py
|
|
9
|
+
import numpy
|
|
6
10
|
import pytest
|
|
7
11
|
from silx.io.url import DataUrl
|
|
8
12
|
|
|
@@ -12,6 +16,7 @@ from nxtomomill.converter.hdf5.acquisition.baseacquisition import (
|
|
|
12
16
|
get_dataset_name_from_motor,
|
|
13
17
|
)
|
|
14
18
|
from nxtomomill.io.config import TomoHDF5Config
|
|
19
|
+
from nxtomomill.models.h52nx._context import _Key
|
|
15
20
|
from nxtomomill.tests.datasets import GitlabDataset
|
|
16
21
|
from nxtomomill import converter
|
|
17
22
|
from nxtomo.application.nxtomo import NXtomo
|
|
@@ -52,6 +57,103 @@ def test_get_dataset_name_from_motor():
|
|
|
52
57
|
get_dataset_name_from_motor(set_3, "rotation")
|
|
53
58
|
|
|
54
59
|
|
|
60
|
+
@pytest.mark.parametrize("raises_error", (True, False))
|
|
61
|
+
def test_translation_with_err_value(tmp_path, raises_error):
|
|
62
|
+
"""
|
|
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).
|
|
66
|
+
"""
|
|
67
|
+
file_path = os.path.join(tmp_path, "test.h5")
|
|
68
|
+
with HDF5File(file_path, mode="w") as h5f:
|
|
69
|
+
h5f["/entry/instrument/positioners/tx"] = "ERR"
|
|
70
|
+
h5f["/entry/instrument/positioners/ty"] = "ERR"
|
|
71
|
+
h5f["/entry/instrument/positioners/tz"] = "ERR"
|
|
72
|
+
h5f["/entry/instrument/positioners/sz"] = 2.0
|
|
73
|
+
h5f["/entry/instrument/positioners/sz"].attrs["units"] = "mm"
|
|
74
|
+
|
|
75
|
+
config = TomoHDF5Config()
|
|
76
|
+
config.raises_error = raises_error
|
|
77
|
+
config.translation_x_keys = ("tx",)
|
|
78
|
+
config.translation_y_keys = ("ty",)
|
|
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
|
+
)
|
|
137
|
+
|
|
138
|
+
url = DataUrl(file_path=file_path, data_path="/entry", scheme="silx")
|
|
139
|
+
std_acq = BaseAcquisition(
|
|
140
|
+
root_url=url,
|
|
141
|
+
configuration=config,
|
|
142
|
+
detector_sel_callback=None,
|
|
143
|
+
start_index=0,
|
|
144
|
+
)
|
|
145
|
+
with h5py.File(file_path, mode="r") as h5f:
|
|
146
|
+
root_node = h5f["/entry"]
|
|
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
|
|
155
|
+
|
|
156
|
+
|
|
55
157
|
def test_frame_flips(tmp_path, monkeypatch):
|
|
56
158
|
# make sure the result is obtained by using the "instrument/{active_config_tomo_detector}/data_axes" path.
|
|
57
159
|
monkeypatch.setattr(
|
|
@@ -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)
|
|
@@ -163,6 +164,10 @@ class MissingValue(ValueError):
|
|
|
163
164
|
"""Raised if a dataset contains 'DIS' - not saved"""
|
|
164
165
|
|
|
165
166
|
|
|
167
|
+
class InvalidValue(ValueError):
|
|
168
|
+
"""Raised if a dataset contains 'ERR' - value could not be read from the hardware"""
|
|
169
|
+
|
|
170
|
+
|
|
166
171
|
def read_key(
|
|
167
172
|
key: str,
|
|
168
173
|
node: h5py.Group,
|
|
@@ -210,6 +215,9 @@ def read_key(
|
|
|
210
215
|
# skip values containing '*DIS*'
|
|
211
216
|
if isinstance(values, str) and values == "*DIS*":
|
|
212
217
|
raise MissingValue(f"dataset {dataset_name} has not been recorded")
|
|
218
|
+
# dataset reports an 'ERR' value if the hardware failed to record it
|
|
219
|
+
if isinstance(values, str) and values == "ERR":
|
|
220
|
+
raise InvalidValue(f"dataset {dataset_name} reports an 'ERR' value")
|
|
213
221
|
|
|
214
222
|
first_dataset = dataset_names[0]
|
|
215
223
|
result_unit = dataset_units[first_dataset] or expected_unit
|
|
@@ -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
|
|
@@ -146,6 +146,7 @@ class _H5ToNxConverter(BaseConverter):
|
|
|
146
146
|
self._detector_sel_callback = detector_sel_callback
|
|
147
147
|
self._acquisitions = []
|
|
148
148
|
self._entries_created = []
|
|
149
|
+
self._conversion_errors: list[tuple[str, Exception]] = []
|
|
149
150
|
self._z_series_v2_v3: list[list[ZSeriesBaseAcquisition]] = []
|
|
150
151
|
# bliss z-series for version 2 and 3. Can be used for post-processing
|
|
151
152
|
self.preprocess()
|
|
@@ -176,6 +177,11 @@ class _H5ToNxConverter(BaseConverter):
|
|
|
176
177
|
def acquisitions(self):
|
|
177
178
|
return self._acquisitions
|
|
178
179
|
|
|
180
|
+
@property
|
|
181
|
+
def has_failed_acquisitions(self) -> bool:
|
|
182
|
+
"""True if at least one acquisition failed to be converted (and the error was not raised, see 'raises_error')"""
|
|
183
|
+
return len(self._conversion_errors) > 0
|
|
184
|
+
|
|
179
185
|
def preprocess(self):
|
|
180
186
|
self._preprocess_urls()
|
|
181
187
|
self._check_conversion_is_possible()
|
|
@@ -311,6 +317,7 @@ class _H5ToNxConverter(BaseConverter):
|
|
|
311
317
|
exc_info=e,
|
|
312
318
|
)
|
|
313
319
|
acquisition_to_nxtomo[acquisition] = None
|
|
320
|
+
self._conversion_errors.append((root_location, e))
|
|
314
321
|
else:
|
|
315
322
|
res.extend(new_entries)
|
|
316
323
|
acquisition_to_nxtomo[acquisition] = new_entries
|