nabu 2024.1.9__py3-none-any.whl → 2024.2.0__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 (151) hide show
  1. nabu/__init__.py +1 -1
  2. nabu/app/bootstrap.py +2 -3
  3. nabu/app/cast_volume.py +4 -2
  4. nabu/app/cli_configs.py +5 -0
  5. nabu/app/composite_cor.py +1 -1
  6. nabu/app/create_distortion_map_from_poly.py +5 -6
  7. nabu/app/diag_to_pix.py +7 -19
  8. nabu/app/diag_to_rot.py +14 -29
  9. nabu/app/double_flatfield.py +32 -44
  10. nabu/app/parse_reconstruction_log.py +3 -0
  11. nabu/app/reconstruct.py +53 -15
  12. nabu/app/reconstruct_helical.py +2 -2
  13. nabu/app/stitching.py +27 -13
  14. nabu/app/tests/test_reduce_dark_flat.py +4 -1
  15. nabu/cuda/kernel.py +11 -2
  16. nabu/cuda/processing.py +2 -2
  17. nabu/cuda/src/cone.cu +77 -0
  18. nabu/cuda/src/hierarchical_backproj.cu +271 -0
  19. nabu/cuda/utils.py +0 -6
  20. nabu/estimation/alignment.py +5 -19
  21. nabu/estimation/cor.py +173 -599
  22. nabu/estimation/cor_sino.py +356 -26
  23. nabu/estimation/focus.py +63 -11
  24. nabu/estimation/tests/test_cor.py +124 -58
  25. nabu/estimation/tests/test_focus.py +6 -6
  26. nabu/estimation/tilt.py +2 -1
  27. nabu/estimation/utils.py +5 -33
  28. nabu/io/__init__.py +1 -1
  29. nabu/io/cast_volume.py +1 -1
  30. nabu/io/reader.py +416 -21
  31. nabu/io/tests/test_readers.py +422 -0
  32. nabu/io/tests/test_writers.py +1 -102
  33. nabu/io/writer.py +4 -433
  34. nabu/opencl/kernel.py +14 -3
  35. nabu/opencl/processing.py +8 -0
  36. nabu/pipeline/config_validators.py +5 -2
  37. nabu/pipeline/datadump.py +12 -5
  38. nabu/pipeline/estimators.py +162 -188
  39. nabu/pipeline/fullfield/chunked.py +168 -92
  40. nabu/pipeline/fullfield/chunked_cuda.py +7 -3
  41. nabu/pipeline/fullfield/computations.py +2 -7
  42. nabu/pipeline/fullfield/dataset_validator.py +0 -4
  43. nabu/pipeline/fullfield/nabu_config.py +37 -13
  44. nabu/pipeline/fullfield/processconfig.py +22 -13
  45. nabu/pipeline/fullfield/reconstruction.py +13 -9
  46. nabu/pipeline/helical/helical_chunked_regridded.py +1 -1
  47. nabu/pipeline/helical/helical_chunked_regridded_cuda.py +1 -0
  48. nabu/pipeline/helical/helical_reconstruction.py +1 -1
  49. nabu/pipeline/params.py +21 -1
  50. nabu/pipeline/processconfig.py +1 -12
  51. nabu/pipeline/reader.py +146 -0
  52. nabu/pipeline/tests/test_estimators.py +44 -72
  53. nabu/pipeline/utils.py +4 -2
  54. nabu/pipeline/writer.py +10 -2
  55. nabu/preproc/ccd_cuda.py +1 -1
  56. nabu/preproc/ctf.py +14 -7
  57. nabu/preproc/ctf_cuda.py +2 -3
  58. nabu/preproc/double_flatfield.py +5 -12
  59. nabu/preproc/double_flatfield_cuda.py +2 -2
  60. nabu/preproc/flatfield.py +5 -1
  61. nabu/preproc/flatfield_cuda.py +5 -1
  62. nabu/preproc/phase.py +24 -73
  63. nabu/preproc/phase_cuda.py +5 -8
  64. nabu/preproc/tests/test_ctf.py +11 -7
  65. nabu/preproc/tests/test_flatfield.py +67 -122
  66. nabu/preproc/tests/test_paganin.py +54 -30
  67. nabu/processing/azim.py +206 -0
  68. nabu/processing/convolution_cuda.py +1 -1
  69. nabu/processing/fft_cuda.py +15 -17
  70. nabu/processing/histogram.py +2 -0
  71. nabu/processing/histogram_cuda.py +2 -1
  72. nabu/processing/kernel_base.py +3 -0
  73. nabu/processing/muladd_cuda.py +1 -0
  74. nabu/processing/padding_opencl.py +1 -1
  75. nabu/processing/roll_opencl.py +1 -0
  76. nabu/processing/rotation_cuda.py +2 -2
  77. nabu/processing/tests/test_fft.py +17 -10
  78. nabu/processing/unsharp_cuda.py +1 -1
  79. nabu/reconstruction/cone.py +104 -40
  80. nabu/reconstruction/fbp.py +3 -0
  81. nabu/reconstruction/fbp_base.py +7 -2
  82. nabu/reconstruction/filtering.py +20 -7
  83. nabu/reconstruction/filtering_cuda.py +7 -1
  84. nabu/reconstruction/hbp.py +424 -0
  85. nabu/reconstruction/mlem.py +99 -0
  86. nabu/reconstruction/reconstructor.py +2 -0
  87. nabu/reconstruction/rings_cuda.py +19 -19
  88. nabu/reconstruction/sinogram_cuda.py +1 -0
  89. nabu/reconstruction/sinogram_opencl.py +3 -1
  90. nabu/reconstruction/tests/test_cone.py +10 -5
  91. nabu/reconstruction/tests/test_deringer.py +7 -6
  92. nabu/reconstruction/tests/test_fbp.py +124 -10
  93. nabu/reconstruction/tests/test_filtering.py +13 -11
  94. nabu/reconstruction/tests/test_halftomo.py +30 -4
  95. nabu/reconstruction/tests/test_mlem.py +91 -0
  96. nabu/reconstruction/tests/test_reconstructor.py +8 -3
  97. nabu/resources/dataset_analyzer.py +142 -92
  98. nabu/resources/gpu.py +1 -0
  99. nabu/resources/nxflatfield.py +134 -125
  100. nabu/resources/templates/id16a_fluo.conf +42 -0
  101. nabu/resources/tests/test_extract.py +10 -0
  102. nabu/resources/tests/test_nxflatfield.py +2 -2
  103. nabu/stitching/alignment.py +80 -24
  104. nabu/stitching/config.py +105 -68
  105. nabu/stitching/definitions.py +1 -0
  106. nabu/stitching/frame_composition.py +68 -60
  107. nabu/stitching/overlap.py +91 -51
  108. nabu/stitching/single_axis_stitching.py +32 -0
  109. nabu/stitching/slurm_utils.py +6 -6
  110. nabu/stitching/stitcher/__init__.py +0 -0
  111. nabu/stitching/stitcher/base.py +124 -0
  112. nabu/stitching/stitcher/dumper/__init__.py +3 -0
  113. nabu/stitching/stitcher/dumper/base.py +94 -0
  114. nabu/stitching/stitcher/dumper/postprocessing.py +356 -0
  115. nabu/stitching/stitcher/dumper/preprocessing.py +60 -0
  116. nabu/stitching/stitcher/post_processing.py +555 -0
  117. nabu/stitching/stitcher/pre_processing.py +1068 -0
  118. nabu/stitching/stitcher/single_axis.py +484 -0
  119. nabu/stitching/stitcher/stitcher.py +0 -0
  120. nabu/stitching/stitcher/y_stitcher.py +13 -0
  121. nabu/stitching/stitcher/z_stitcher.py +45 -0
  122. nabu/stitching/stitcher_2D.py +278 -0
  123. nabu/stitching/tests/test_config.py +12 -37
  124. nabu/stitching/tests/test_frame_composition.py +33 -59
  125. nabu/stitching/tests/test_overlap.py +149 -7
  126. nabu/stitching/tests/test_utils.py +1 -1
  127. nabu/stitching/tests/test_y_preprocessing_stitching.py +132 -0
  128. nabu/stitching/tests/{test_z_stitching.py → test_z_postprocessing_stitching.py} +167 -561
  129. nabu/stitching/tests/test_z_preprocessing_stitching.py +431 -0
  130. nabu/stitching/utils/__init__.py +1 -0
  131. nabu/stitching/utils/post_processing.py +281 -0
  132. nabu/stitching/utils/tests/test_post-processing.py +21 -0
  133. nabu/stitching/{utils.py → utils/utils.py} +79 -52
  134. nabu/stitching/y_stitching.py +27 -0
  135. nabu/stitching/z_stitching.py +32 -2263
  136. nabu/testutils.py +1 -152
  137. nabu/thirdparty/tomocupy_remove_stripe.py +43 -9
  138. nabu/utils.py +158 -61
  139. {nabu-2024.1.9.dist-info → nabu-2024.2.0.dist-info}/METADATA +10 -3
  140. {nabu-2024.1.9.dist-info → nabu-2024.2.0.dist-info}/RECORD +144 -121
  141. nabu/io/tiffwriter_zmm.py +0 -99
  142. nabu/pipeline/fallback_utils.py +0 -149
  143. nabu/pipeline/helical/tests/test_accumulator.py +0 -158
  144. nabu/pipeline/helical/tests/test_pipeline_elements_full.py +0 -355
  145. nabu/pipeline/helical/tests/test_strategy.py +0 -61
  146. nabu/pipeline/helical/utils.py +0 -51
  147. nabu/pipeline/tests/test_chunk_reader.py +0 -74
  148. {nabu-2024.1.9.dist-info → nabu-2024.2.0.dist-info}/LICENSE +0 -0
  149. {nabu-2024.1.9.dist-info → nabu-2024.2.0.dist-info}/WHEEL +0 -0
  150. {nabu-2024.1.9.dist-info → nabu-2024.2.0.dist-info}/entry_points.txt +0 -0
  151. {nabu-2024.1.9.dist-info → nabu-2024.2.0.dist-info}/top_level.txt +0 -0
@@ -1,149 +0,0 @@
1
- """ This module is meant to contain classes which are in the process of being superseed by new classes depending on recent packages
2
- with fast development cycles in order to be able to fall-back in two cases :
3
- -- the new packages, or one of their parts, break from one version to another.
4
- -- For parts of Nabu which need some extra time to adapt.
5
- """
6
- from ..resources.logger import LoggerOrPrint
7
- from ..utils import check_supported
8
- from ..io.writer import Writers, LegacyNXProcessWriter
9
- from ..resources.utils import is_hdf5_extension
10
- from os import path, mkdir
11
- from .params import files_formats
12
-
13
-
14
- class WriterConfigurator:
15
- """No dependency on tomoscan for this class. The new class would be WriterManager which depend on tomoscan."""
16
-
17
- _overwrite_warned = False
18
-
19
- def __init__(
20
- self,
21
- output_dir,
22
- file_prefix,
23
- file_format="hdf5",
24
- overwrite=False,
25
- start_index=None,
26
- logger=None,
27
- nx_info=None,
28
- write_histogram=False,
29
- histogram_entry="entry",
30
- writer_options=None,
31
- extra_options=None,
32
- ):
33
- """
34
- Create a Writer from a set of parameters.
35
-
36
- Parameters
37
- ----------
38
- output_dir: str
39
- Directory where the file(s) will be written.
40
- file_prefix: str
41
- File prefix (without leading path)
42
- start_index: int, optional
43
- Index to start the files numbering (filename_0123.ext).
44
- Default is 0.
45
- Ignored for HDF5 extension.
46
- logger: nabu.resources.logger.Logger, optional
47
- Logger object
48
- nx_info: dict, optional
49
- Dictionary containing the nexus information.
50
- write_histogram: bool, optional
51
- Whether to also write a histogram of data. If set to True, it will configure
52
- an additional "writer".
53
- histogram_entry: str, optional
54
- Name of the HDF5 entry for the output histogram file, if write_histogram is True.
55
- Ignored if the output format is already HDF5 : in this case, nx_info["entry"] is taken.
56
- writer_options: dict, optional
57
- Other advanced options to pass to Writer class.
58
- """
59
- self.logger = LoggerOrPrint(logger)
60
- self.start_index = start_index
61
- self.write_histogram = write_histogram
62
- self.overwrite = overwrite
63
- writer_options = writer_options or {}
64
- self.extra_options = extra_options or {}
65
-
66
- check_supported(file_format, list(Writers.keys()), "output file format")
67
-
68
- self._set_output_dir(output_dir)
69
- self._set_file_name(file_prefix, file_format)
70
-
71
- # Init Writer
72
- writer_cls = Writers[file_format]
73
- writer_args = [self.fname]
74
- writer_kwargs = self._get_initial_writer_kwarg()
75
- self._writer_exec_args = []
76
- self._writer_exec_kwargs = {}
77
- self._is_hdf5_output = is_hdf5_extension(file_format)
78
-
79
- if self._is_hdf5_output:
80
- writer_kwargs["entry"] = nx_info["entry"]
81
- writer_kwargs["filemode"] = "a"
82
- writer_kwargs["overwrite"] = overwrite
83
- self._writer_exec_args.append(nx_info["process_name"])
84
- self._writer_exec_kwargs["processing_index"] = nx_info["processing_index"]
85
- self._writer_exec_kwargs["config"] = nx_info["config"]
86
- else:
87
- writer_kwargs["start_index"] = self.start_index
88
- if writer_options.get("tiff_single_file", False) and "tif" in file_format:
89
- do_append = writer_options.get("single_tiff_initialized", False)
90
- writer_kwargs.update({"multiframe": True, "append": do_append})
91
-
92
- if files_formats.get(file_format, None) == "jp2":
93
- cratios = self.extra_options.get("jpeg2000_compression_ratio", None)
94
- if cratios is not None:
95
- cratios = [cratios]
96
- writer_kwargs["cratios"] = cratios
97
- writer_kwargs["float_clip_values"] = self.extra_options.get("float_clip_values", None)
98
- self.writer = writer_cls(*writer_args, **writer_kwargs)
99
-
100
- if self.write_histogram and not (self._is_hdf5_output):
101
- self._init_separate_histogram_writer(histogram_entry)
102
-
103
- def _get_initial_writer_kwarg(self):
104
- return {}
105
-
106
- def _set_output_dir(self, output_dir):
107
- self.output_dir = output_dir
108
- if path.exists(self.output_dir):
109
- if not (path.isdir(self.output_dir)):
110
- raise ValueError(
111
- "Unable to create directory %s: already exists and is not a directory" % self.output_dir
112
- )
113
- else:
114
- self.logger.debug("Creating directory %s" % self.output_dir)
115
- mkdir(self.output_dir)
116
-
117
- def _set_file_name(self, file_prefix, file_format):
118
- self.file_prefix = file_prefix
119
- self.file_format = file_format
120
- self.fname = path.join(self.output_dir, file_prefix + "." + file_format)
121
- if path.exists(self.fname):
122
- err = "File already exists: %s" % self.fname
123
- if self.overwrite:
124
- if not (WriterConfigurator._overwrite_warned):
125
- self.logger.warning(err + ". It will be overwritten as requested in configuration")
126
- WriterConfigurator._overwrite_warned = True
127
- else:
128
- self.logger.fatal(err)
129
- raise ValueError(err)
130
-
131
- def _init_separate_histogram_writer(self, hist_entry):
132
- hist_fname = path.join(self.output_dir, "histogram_%06d.hdf5" % self.start_index)
133
- self.histogram_writer = LegacyNXProcessWriter(
134
- hist_fname,
135
- entry=hist_entry,
136
- filemode="w",
137
- overwrite=True,
138
- )
139
-
140
- def get_histogram_writer(self):
141
- if not (self.write_histogram):
142
- return None
143
- if self._is_hdf5_output:
144
- return self.writer
145
- else:
146
- return self.histogram_writer
147
-
148
- def write_data(self, data):
149
- self.writer.write(data, *self._writer_exec_args, **self._writer_exec_kwargs)
@@ -1,158 +0,0 @@
1
- from nabu.pipeline.helical import gridded_accumulator, span_strategy
2
- from nabu.testutils import get_data, __do_long_tests__
3
-
4
- import pytest
5
- import numpy as np
6
- import os
7
- import h5py
8
-
9
-
10
- @pytest.fixture(scope="class")
11
- def bootstrap(request):
12
- cls = request.cls
13
-
14
- # This is a helical dataset derived
15
- # from "crayon" dataset, using 5 slices and covering 2.5 x 360 angular span
16
- # in halftomo, with vertical translations.
17
-
18
- helical_dataset = get_data("small_sparse_helical_dataset.npz")
19
-
20
- helical_dataset = dict([item for item in helical_dataset.items()])
21
- # the radios, in the dataset file, are stored by swapping angular and x dimension
22
- # so that the fast running dimension runs over the projections.
23
- # Due to the sparsity of the dataset, where only an handful of slices
24
- # has signal, this gives a much better compression even when the axis is translating
25
- # vertically
26
- helical_dataset["radios"] = np.array(np.swapaxes(helical_dataset["radios_transposed"], 0, 2))
27
- del helical_dataset["radios_transposed"]
28
-
29
- # adding members: radios, dark, flats, z_pix_per_proj, x_pix_per_proj, projection_angles_deg, pixel_size_mm, phase_margin_pix,
30
- # weigth_field=weigth_field, double_flat
31
- dataset_keys = [
32
- "dark",
33
- "flats",
34
- "z_pix_per_proj",
35
- "x_pix_per_proj",
36
- "projection_angles_deg",
37
- "pixel_size_mm",
38
- "phase_margin_pix",
39
- "weights_field",
40
- "double_flat",
41
- "rotation_axis_position",
42
- "detector_shape_vh",
43
- "result_inset",
44
- "radios",
45
- ]
46
- for key in dataset_keys:
47
- setattr(cls, key, helical_dataset[key])
48
-
49
- cls.rtol_regridded = 1.0e-6
50
-
51
-
52
- @pytest.mark.skipif(not (__do_long_tests__), reason="need environment variable NABU_LONG_TESTS=1")
53
- @pytest.mark.usefixtures("bootstrap")
54
- class TestGriddedAccumulator:
55
- """
56
- Test the GriddedAccumulator.
57
- Rebuilds the sinogram for some selected slices of the crayon dataset
58
- """
59
-
60
- def test_regridding(self):
61
- span_info = span_strategy.SpanStrategy(
62
- z_pix_per_proj=self.z_pix_per_proj,
63
- x_pix_per_proj=self.x_pix_per_proj,
64
- detector_shape_vh=self.detector_shape_vh,
65
- phase_margin_pix=self.phase_margin_pix,
66
- projection_angles_deg=self.projection_angles_deg,
67
- require_redundancy=True,
68
- pixel_size_mm=self.pixel_size_mm,
69
- logger=None,
70
- )
71
-
72
- # I would like to reconstruct from feaseable height 15 to feaseable height 18
73
- # relatively to the first doable slice in the vertical translation direction
74
- # I get the heights in the detector frame of the first and of the last
75
-
76
- reconstruction_space = gridded_accumulator.get_reconstruction_space(
77
- span_info=span_info, min_scanwise_z=15, end_scanwise_z=18, phase_margin_pix=self.phase_margin_pix
78
- )
79
-
80
- chunk_info = span_info.get_chunk_info((reconstruction_space.my_z_min, reconstruction_space.my_z_end))
81
-
82
- sub_region = (
83
- reconstruction_space.my_z_min - self.phase_margin_pix,
84
- reconstruction_space.my_z_end + self.phase_margin_pix,
85
- )
86
-
87
- ## useful projections
88
- proj_num_start, proj_num_end = chunk_info.angle_index_span
89
-
90
- # the first of the chunk angular range
91
- my_first_pnum = proj_num_start
92
-
93
- self.accumulator = gridded_accumulator.GriddedAccumulator(
94
- gridded_radios=reconstruction_space.gridded_radios,
95
- gridded_weights=reconstruction_space.gridded_cumulated_weights,
96
- diagnostic_radios=reconstruction_space.diagnostic_radios,
97
- diagnostic_weights=reconstruction_space.diagnostic_weights,
98
- diagnostic_angles=reconstruction_space.diagnostic_proj_angle,
99
- diagnostic_searched_angles_rad_clipped=reconstruction_space.diagnostic_searched_angles_rad_clipped,
100
- diagnostic_zpix_transl=reconstruction_space.diagnostic_zpix_transl,
101
- dark=self.dark,
102
- flat_indexes=[0, 7501],
103
- flats=self.flats,
104
- weights=self.weights_field,
105
- double_flat=self.double_flat,
106
- diag_zpro_run=0,
107
- )
108
-
109
- # splitting in sub ranges of 100 projections
110
- n_granularity = 100
111
- pnum_start_list = list(np.arange(proj_num_start, proj_num_end, n_granularity))
112
- pnum_end_list = pnum_start_list[1:] + [proj_num_end]
113
-
114
- for pnum_start, pnum_end in zip(pnum_start_list, pnum_end_list):
115
- start_in_chunk = pnum_start - my_first_pnum
116
- end_in_chunk = pnum_end - my_first_pnum
117
-
118
- self._read_data_and_apply_flats(
119
- slice(pnum_start, pnum_end), slice(start_in_chunk, end_in_chunk), chunk_info, sub_region, span_info
120
- )
121
-
122
- res = reconstruction_space.gridded_radios / reconstruction_space.gridded_cumulated_weights
123
-
124
- # check only a sub part to avoid further increasing of the file on edna site
125
- errmax = np.max(np.abs(res[:200, 1, -500:] - self.result_inset) / np.max(res))
126
- assert errmax < self.rtol_regridded, "Max error is too high"
127
-
128
- # uncomment this to see
129
- # h5py.File("processed_sinogram.h5","w")["sinogram"] = res
130
-
131
- def _read_data_and_apply_flats(self, sub_total_prange_slice, subchunk_slice, chunk_info, sub_region, span_info):
132
- my_integer_shifts_v = chunk_info.integer_shift_v[subchunk_slice]
133
- fract_complement_shifts_v = chunk_info.fract_complement_to_integer_shift_v[subchunk_slice]
134
- x_shifts_list = chunk_info.x_pix_per_proj[subchunk_slice]
135
- subr_start_z, subr_end_z = sub_region
136
-
137
- subr_start_z_list = subr_start_z - my_integer_shifts_v
138
- subr_end_z_list = subr_end_z - my_integer_shifts_v + 1
139
-
140
- dtasrc_start_z = max(0, subr_start_z_list.min())
141
- dtasrc_end_z = min(span_info.detector_shape_vh[0], subr_end_z_list.max())
142
-
143
- data_raw = self.radios[sub_total_prange_slice, slice(dtasrc_start_z, dtasrc_end_z), :]
144
-
145
- subsampling_file_slice = sub_total_prange_slice
146
-
147
- # my_subsampled_indexes = self.chunk_reader._sorted_files_indices[subsampling_file_slice]
148
- my_subsampled_indexes = (np.arange(10000))[subsampling_file_slice]
149
-
150
- self.accumulator.extract_preprocess_with_flats(
151
- subchunk_slice,
152
- my_subsampled_indexes,
153
- chunk_info,
154
- np.array((subr_start_z, subr_end_z), "i"),
155
- np.array((dtasrc_start_z, dtasrc_end_z), "i"),
156
- data_raw,
157
- sub_total_prange_slice,
158
- )
@@ -1,355 +0,0 @@
1
- from nabu.pipeline.helical import gridded_accumulator, span_strategy
2
- from nabu.testutils import get_data, __do_long_tests__
3
- import os
4
- import numpy as np
5
- import pytest
6
- from nabu.preproc.ccd import Log, CCDFilter
7
- from nabu.preproc.phase import PaganinPhaseRetrieval
8
- from nabu.cuda.utils import get_cuda_context, __has_pycuda__
9
- from nabu.pipeline.helical import gridded_accumulator, span_strategy
10
- from nabu.pipeline.helical.weight_balancer import WeightBalancer
11
- from nabu.pipeline.helical.helical_utils import find_mirror_indexes
12
-
13
- from nabu.cuda.utils import get_cuda_context, __has_pycuda__, __pycuda_error_msg__, replace_array_memory
14
-
15
- if __has_pycuda__:
16
- import pycuda.gpuarray as garray
17
- from nabu.pipeline.helical.fbp import BackprojectorHelical as FBPClass
18
-
19
-
20
- @pytest.fixture(scope="class")
21
- def bootstrap(request):
22
- cls = request.cls
23
-
24
- # This is a helical dataset derived
25
- # from "crayon" dataset, using 5 slices and covering 2.5 x 360 angular span
26
- # in halftomo, with vertical translations.
27
-
28
- # >>> d=load("small_sparse_helical_dataset.npz")
29
- # dd=dict( d.items() )
30
- # >>> dd["median_clip_threshold"] = 0.04
31
- # >>> savez("retouched_test.npz",**dd)
32
-
33
- helical_dataset = get_data("small_sparse_helical_dataset.npz")
34
-
35
- helical_dataset = dict(list(helical_dataset.items()))
36
- # the radios, in the dataset file, are stored by swapping angular and x dimension
37
- # so that the fast running dimension runs over the projections.
38
- # Due to the sparsity of the dataset, where only an handful of slices
39
- # has signal, this gives a much better compression even when the axis is translating
40
- # vertically
41
- helical_dataset["radios"] = np.array(np.swapaxes(helical_dataset["radios_transposed"], 0, 2))
42
- del helical_dataset["radios_transposed"]
43
-
44
- # adding members: radios, dark, flats, z_pix_per_proj, x_pix_per_proj, projection_angles_deg,
45
- # pixel_size_mm, phase_margin_pix,
46
- # weigth_field=weigth_field, double_flat
47
- dataset_keys = [
48
- "dark",
49
- "flats",
50
- "z_pix_per_proj",
51
- "x_pix_per_proj",
52
- "projection_angles_deg",
53
- "pixel_size_mm",
54
- "phase_margin_pix",
55
- "weights_field",
56
- "double_flat",
57
- "rotation_axis_position",
58
- "detector_shape_vh",
59
- "result_inset",
60
- "radios",
61
- # further parameters, added on top of the test data which was originally made for gridded_accumulator
62
- "median_clip_threshold",
63
- "distance_m",
64
- "energy_kev",
65
- "delta_beta",
66
- "pixel_size_m",
67
- "padding_type",
68
- "phase_margin_for_pag",
69
- "rec_reference",
70
- "ref_tol",
71
- "ref_start",
72
- "ref_end",
73
- ]
74
- # the test dataset is the original one from the accumulator test
75
- # plus some patched metadeta information for phase retrieval.
76
- # The original dataset had phase_margin_pix and had 3 usefule slices.
77
- # Here, to test phase retrieval, we redefine the phase margin to the maximum
78
- # that we can do with such a small dataset
79
- helical_dataset["phase_margin_pix"] = helical_dataset["phase_margin_for_pag"]
80
-
81
- for key in dataset_keys:
82
- setattr(cls, key, helical_dataset[key])
83
-
84
- cls.padding_type = str(cls.padding_type)
85
- cls.rotation_axis_position = float(cls.rotation_axis_position)
86
- cls.rtol_regridded = 1.0e-6
87
-
88
- cls.projection_angles_rad = np.rad2deg(cls.projection_angles_deg)
89
-
90
-
91
- @pytest.mark.skipif(not (__do_long_tests__), reason="need environment variable NABU_LONG_TESTS=1")
92
- @pytest.mark.skipif(not (__has_pycuda__), reason="Needs pycuda for this test")
93
- @pytest.mark.usefixtures("bootstrap")
94
- class TestGriddedAccumulator:
95
- """
96
- Test the GriddedAccumulator.
97
- Rebuilds the sinogram for some selected slices of the crayon dataset
98
- """
99
-
100
- def test_regridding(self):
101
- span_info = span_strategy.SpanStrategy(
102
- z_pix_per_proj=self.z_pix_per_proj,
103
- x_pix_per_proj=self.x_pix_per_proj,
104
- detector_shape_vh=self.detector_shape_vh,
105
- phase_margin_pix=self.phase_margin_pix,
106
- projection_angles_deg=self.projection_angles_deg,
107
- require_redundancy=True,
108
- pixel_size_mm=self.pixel_size_mm,
109
- logger=None,
110
- )
111
-
112
- # I would like to reconstruct from feaseable height 15 to feaseable height 18
113
- # relatively to the first doable slice in the vertical translation direction
114
- # I get the heights in the detector frame of the first and of the last
115
-
116
- self.reconstruction_space = gridded_accumulator.get_reconstruction_space(
117
- span_info=span_info, min_scanwise_z=15, end_scanwise_z=18, phase_margin_pix=self.phase_margin_pix
118
- )
119
-
120
- chunk_info = span_info.get_chunk_info((self.reconstruction_space.my_z_min, self.reconstruction_space.my_z_end))
121
-
122
- sub_region = (
123
- self.reconstruction_space.my_z_min - self.phase_margin_pix,
124
- self.reconstruction_space.my_z_end + self.phase_margin_pix,
125
- )
126
-
127
- # useful projections
128
- proj_num_start, proj_num_end = chunk_info.angle_index_span
129
-
130
- # the first of the chunk angular range
131
- my_first_pnum = proj_num_start
132
-
133
- self.accumulator = gridded_accumulator.GriddedAccumulator(
134
- gridded_radios=self.reconstruction_space.gridded_radios,
135
- gridded_weights=self.reconstruction_space.gridded_cumulated_weights,
136
- diagnostic_radios=self.reconstruction_space.diagnostic_radios,
137
- diagnostic_weights=self.reconstruction_space.diagnostic_weights,
138
- diagnostic_angles=self.reconstruction_space.diagnostic_proj_angle,
139
- dark=self.dark,
140
- flat_indexes=[0, 7501],
141
- flats=self.flats,
142
- weights=self.weights_field,
143
- double_flat=self.double_flat,
144
- )
145
-
146
- # splitting in sub ranges of 100 projections
147
- n_granularity = 100
148
- pnum_start_list = list(np.arange(proj_num_start, proj_num_end, n_granularity))
149
- pnum_end_list = pnum_start_list[1:] + [proj_num_end]
150
-
151
- for pnum_start, pnum_end in zip(pnum_start_list, pnum_end_list):
152
- start_in_chunk = pnum_start - my_first_pnum
153
- end_in_chunk = pnum_end - my_first_pnum
154
-
155
- self._read_data_and_apply_flats(
156
- slice(pnum_start, pnum_end), slice(start_in_chunk, end_in_chunk), chunk_info, sub_region, span_info
157
- )
158
-
159
- res_flatfielded = self.reconstruction_space.gridded_radios / self.reconstruction_space.gridded_cumulated_weights
160
-
161
- # but in real pipeline the radio_shape is obtained from the pipeline get_shape utility method
162
- self._init_ccd_corrections(res_flatfielded.shape[1:])
163
-
164
- # but in the actual pipeline the argument is not given, and the processed stack is the one internally
165
- # kept by the pipeline object ( self.gridded_radios in the pipeline )
166
- self._ccd_corrections(res_flatfielded)
167
-
168
- self._init_phase(res_flatfielded.shape[1:])
169
- processed_radios = self._retrieve_phase(res_flatfielded)
170
-
171
- self._init_mlog(processed_radios.shape)
172
- self._take_log(processed_radios)
173
-
174
- top_margin = -self.phase_margin_pix if self.phase_margin_pix else None
175
- processed_weights = self.reconstruction_space.gridded_cumulated_weights[
176
- :, self.phase_margin_pix : top_margin, :
177
- ]
178
-
179
- self._init_weight_balancer()
180
- self._balance_weights(processed_weights)
181
-
182
- self._init_reconstructor(processed_radios.shape)
183
-
184
- i_slice = 0
185
-
186
- self.d_radios_slim.set(processed_radios[:, i_slice, :])
187
- self._filter()
188
-
189
- self._apply_weights(i_slice, processed_weights)
190
-
191
- res = self._reconstruct()
192
-
193
- test_slicing = slice(self.ref_start, self.ref_end)
194
- tested_inset = res[test_slicing, test_slicing]
195
- assert np.max(np.abs(tested_inset - self.rec_reference)) < self.ref_tol
196
-
197
- # uncomment the four following lines to get the slice image
198
- # import fabio
199
- # edf = fabio.edfimage.edfimage()
200
- # edf.data = res
201
- # edf.write("reconstructed_slice.edf")
202
-
203
- # put the test here
204
-
205
- def _reconstruct(self):
206
- axis_corrections = np.zeros_like(self.reconstruction_space.gridded_angles_rad)
207
- self.reconstruction.set_custom_angles_and_axis_corrections(
208
- self.reconstruction_space.gridded_angles_rad, axis_corrections
209
- )
210
-
211
- self.reconstruction.backprojection(self.d_radios_slim, output=self.d_rec_res)
212
-
213
- self.d_rec_res.get(self.rec_res)
214
-
215
- return self.rec_res
216
-
217
- def _apply_weights(self, i_slice, weights):
218
- """d_radios_slim is on gpu"""
219
- n_provided_angles = self.d_radios_slim.shape[0]
220
-
221
- for first_angle_index in range(0, n_provided_angles, self.num_weight_radios_per_app):
222
- end_angle_index = min(n_provided_angles, first_angle_index + self.num_weight_radios_per_app)
223
- self._d_radios_weights[: end_angle_index - first_angle_index].set(
224
- weights[first_angle_index:end_angle_index, i_slice]
225
- )
226
- self.d_radios_slim[first_angle_index:end_angle_index] *= self._d_radios_weights[
227
- : end_angle_index - first_angle_index
228
- ]
229
-
230
- def _filter(self):
231
- self.mirror_angle_relative_indexes = find_mirror_indexes(self.reconstruction_space.gridded_angles_deg)
232
-
233
- self.reconstruction.sino_filter.filter_sino(
234
- self.d_radios_slim,
235
- mirror_indexes=self.mirror_angle_relative_indexes,
236
- rot_center=self.rotation_axis_position,
237
- output=self.d_radios_slim,
238
- )
239
-
240
- def _init_reconstructor(self, processed_radios_shape):
241
- one_slice_data_shape = processed_radios_shape[:1] + processed_radios_shape[2:]
242
-
243
- self.d_radios_slim = garray.zeros(one_slice_data_shape, np.float32)
244
-
245
- # let's make room for loading chunck of weights without necessarily doubling the memory footprint.
246
- # The weights will be used to multiplied the d_radios_slim.
247
- # We will proceed by bunches
248
- self.num_weight_radios_per_app = 200
249
- self._d_radios_weights = garray.zeros((self.num_weight_radios_per_app,) + one_slice_data_shape[1:], np.float32)
250
-
251
- pixel_size_cm = self.pixel_size_m * 100
252
-
253
- radio_size_h = processed_radios_shape[-1]
254
-
255
- assert (
256
- 2 * self.rotation_axis_position > radio_size_h
257
- ), """The code of this test is adapted for HA on the right. This seems to be a case
258
- of HA on the left because self.rotation_axis_position={self.rotation_axis_position} and radio_size_h = {radio_size_h}
259
- """
260
-
261
- rec_dim = int(round(2 * self.rotation_axis_position))
262
-
263
- self.reconstruction = FBPClass(
264
- one_slice_data_shape,
265
- angles=np.zeros(processed_radios_shape[0], "f"),
266
- rot_center=self.rotation_axis_position,
267
- filter_name=None,
268
- slice_roi=(0, rec_dim, 0, rec_dim),
269
- extra_options={
270
- "scale_factor": 1.0 / pixel_size_cm,
271
- "axis_correction": np.zeros(processed_radios_shape[0], "f"),
272
- "padding_mode": "edge",
273
- },
274
- )
275
- self.reconstruction.fbp = self.reconstruction.backproj
276
-
277
- self.d_rec_res = garray.zeros((rec_dim, rec_dim), np.float32)
278
- self.rec_res = np.zeros((rec_dim, rec_dim), np.float32)
279
-
280
- def _init_weight_balancer(self):
281
- self.weight_balancer = WeightBalancer(self.rotation_axis_position, self.reconstruction_space.gridded_angles_rad)
282
-
283
- def _balance_weights(self, weights):
284
- self.weight_balancer.balance_weights(weights)
285
-
286
- def _retrieve_phase(self, radios):
287
- processed_radios = np.zeros(
288
- (radios.shape[0],) + (radios.shape[1] - 2 * self.phase_margin_pix,) + (radios.shape[2],), radios.dtype
289
- )
290
- for i in range(radios.shape[0]):
291
- processed_radios[i] = self.phase_retrieval.apply_filter(radios[i])
292
- return processed_radios
293
-
294
- def _read_data_and_apply_flats(self, sub_total_prange_slice, subchunk_slice, chunk_info, sub_region, span_info):
295
- my_integer_shifts_v = chunk_info.integer_shift_v[subchunk_slice]
296
-
297
- subr_start_z, subr_end_z = sub_region
298
-
299
- subr_start_z_list = subr_start_z - my_integer_shifts_v
300
- subr_end_z_list = subr_end_z - my_integer_shifts_v + 1
301
-
302
- dtasrc_start_z = max(0, subr_start_z_list.min())
303
- dtasrc_end_z = min(span_info.detector_shape_vh[0], subr_end_z_list.max())
304
-
305
- data_raw = self.radios[sub_total_prange_slice, slice(dtasrc_start_z, dtasrc_end_z), :]
306
-
307
- subsampling_file_slice = sub_total_prange_slice
308
-
309
- # my_subsampled_indexes = self.chunk_reader._sorted_files_indices[subsampling_file_slice]
310
- my_subsampled_indexes = (np.arange(10000))[subsampling_file_slice]
311
-
312
- self.accumulator.extract_preprocess_with_flats(
313
- subchunk_slice,
314
- my_subsampled_indexes,
315
- chunk_info,
316
- np.array((subr_start_z, subr_end_z), "i"),
317
- np.array((dtasrc_start_z, dtasrc_end_z), "i"),
318
- data_raw,
319
- )
320
-
321
- def _init_ccd_corrections(self, radio_shape):
322
- # but in real pipeline the radio_shape is obtained from the pipeline get_shape utility method
323
- self.ccd_correction = CCDFilter(radio_shape, median_clip_thresh=self.median_clip_threshold)
324
-
325
- def _ccd_corrections(self, radios):
326
- _tmp_radio = np.empty_like(radios[0])
327
- for i in range(radios.shape[0]):
328
- self.ccd_correction.median_clip_correction(radios[i], output=_tmp_radio)
329
- radios[i][:] = _tmp_radio[:]
330
-
331
- def _take_log(self, radios):
332
- self.mlog.take_logarithm(radios)
333
-
334
- def _init_mlog(self, radios_shape):
335
- log_shape = radios_shape
336
- clip_min = 1.0e-6
337
- clip_max = 1.1
338
- self.mlog = Log(log_shape, clip_min=clip_min, clip_max=clip_max)
339
-
340
- def _init_phase(self, raw_shape):
341
- self.phase_retrieval = PaganinPhaseRetrieval(
342
- raw_shape,
343
- distance=self.distance_m,
344
- energy=self.energy_kev,
345
- delta_beta=self.delta_beta,
346
- pixel_size=self.pixel_size_m,
347
- padding=self.padding_type,
348
- margin=((self.phase_margin_pix,) * 2, (0, 0)),
349
- use_R2C=True,
350
- fftw_num_threads=True, # TODO tune in advanced params of nabu config file
351
- )
352
- if self.phase_retrieval.use_fftw:
353
- self.logger.debug(
354
- "PaganinPhaseRetrieval using FFTW with %d threads" % self.phase_retrieval.fftw.num_threads
355
- )