ChessAnalysisPipeline 0.0.2__py3-none-any.whl → 0.0.4__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.

Potentially problematic release.


This version of ChessAnalysisPipeline might be problematic. Click here for more details.

Files changed (47) hide show
  1. CHAP/__init__.py +3 -0
  2. CHAP/common/__init__.py +19 -0
  3. CHAP/common/models/__init__.py +2 -0
  4. CHAP/common/models/integration.py +515 -0
  5. CHAP/common/models/map.py +535 -0
  6. CHAP/common/processor.py +644 -0
  7. CHAP/common/reader.py +119 -0
  8. CHAP/common/utils/__init__.py +37 -0
  9. CHAP/common/utils/fit.py +2613 -0
  10. CHAP/common/utils/general.py +1225 -0
  11. CHAP/common/utils/material.py +231 -0
  12. CHAP/common/utils/scanparsers.py +785 -0
  13. CHAP/common/writer.py +96 -0
  14. CHAP/edd/__init__.py +7 -0
  15. CHAP/edd/models.py +215 -0
  16. CHAP/edd/processor.py +321 -0
  17. CHAP/edd/reader.py +5 -0
  18. CHAP/edd/writer.py +5 -0
  19. CHAP/inference/__init__.py +3 -0
  20. CHAP/inference/processor.py +68 -0
  21. CHAP/inference/reader.py +5 -0
  22. CHAP/inference/writer.py +5 -0
  23. CHAP/pipeline.py +1 -1
  24. CHAP/processor.py +11 -818
  25. CHAP/reader.py +18 -113
  26. CHAP/saxswaxs/__init__.py +6 -0
  27. CHAP/saxswaxs/processor.py +5 -0
  28. CHAP/saxswaxs/reader.py +5 -0
  29. CHAP/saxswaxs/writer.py +5 -0
  30. CHAP/sin2psi/__init__.py +7 -0
  31. CHAP/sin2psi/processor.py +5 -0
  32. CHAP/sin2psi/reader.py +5 -0
  33. CHAP/sin2psi/writer.py +5 -0
  34. CHAP/tomo/__init__.py +5 -0
  35. CHAP/tomo/models.py +125 -0
  36. CHAP/tomo/processor.py +2009 -0
  37. CHAP/tomo/reader.py +5 -0
  38. CHAP/tomo/writer.py +5 -0
  39. CHAP/writer.py +17 -167
  40. {ChessAnalysisPipeline-0.0.2.dist-info → ChessAnalysisPipeline-0.0.4.dist-info}/METADATA +1 -1
  41. ChessAnalysisPipeline-0.0.4.dist-info/RECORD +50 -0
  42. CHAP/async.py +0 -56
  43. ChessAnalysisPipeline-0.0.2.dist-info/RECORD +0 -17
  44. {ChessAnalysisPipeline-0.0.2.dist-info → ChessAnalysisPipeline-0.0.4.dist-info}/LICENSE +0 -0
  45. {ChessAnalysisPipeline-0.0.2.dist-info → ChessAnalysisPipeline-0.0.4.dist-info}/WHEEL +0 -0
  46. {ChessAnalysisPipeline-0.0.2.dist-info → ChessAnalysisPipeline-0.0.4.dist-info}/entry_points.txt +0 -0
  47. {ChessAnalysisPipeline-0.0.2.dist-info → ChessAnalysisPipeline-0.0.4.dist-info}/top_level.txt +0 -0
CHAP/__init__.py CHANGED
@@ -0,0 +1,3 @@
1
+ from CHAP.reader import Reader
2
+ from CHAP.processor import Processor
3
+ from CHAP.writer import Writer
@@ -0,0 +1,19 @@
1
+ from CHAP.common.reader import (BinaryFileReader,
2
+ MultipleReader,
3
+ NexusReader,
4
+ URLReader,
5
+ YAMLReader)
6
+ from CHAP.common.processor import (AsyncProcessor,
7
+ IntegrationProcessor,
8
+ IntegrateMapProcessor,
9
+ MapProcessor,
10
+ NexusToNumpyProcessor,
11
+ NexusToXarrayProcessor,
12
+ PrintProcessor,
13
+ StrainAnalysisProcessor,
14
+ URLResponseProcessor,
15
+ XarrayToNexusProcessor,
16
+ XarrayToNumpyProcessor)
17
+ from CHAP.common.writer import (ExtractArchiveWriter,
18
+ NexusWriter,
19
+ YAMLWriter)
@@ -0,0 +1,2 @@
1
+ from CHAP.common.models.map import MapConfig
2
+ from CHAP.common.models.integration import IntegrationConfig
@@ -0,0 +1,515 @@
1
+ import copy
2
+ from functools import cache, lru_cache
3
+ import json
4
+ import logging
5
+ import os
6
+ from time import time
7
+ from typing import Literal, Optional
8
+
9
+ # from multiprocessing.pool import ThreadPool
10
+ # from nexusformat.nexus import (NXdata,
11
+ # NXdetector,
12
+ # NXfield,
13
+ # NXprocess,
14
+ # NXroot)
15
+ import numpy as np
16
+ from pydantic import (BaseModel,
17
+ validator,
18
+ constr,
19
+ conlist,
20
+ conint,
21
+ confloat,
22
+ FilePath)
23
+ #import pyFAI, pyFAI.multi_geometry, pyFAI.units
24
+ from pyFAI import load as pyfai_load
25
+ from pyFAI.multi_geometry import MultiGeometry
26
+ from pyFAI.units import AZIMUTHAL_UNITS, RADIAL_UNITS
27
+ #from pyspec.file.tiff import TiffFile
28
+
29
+ #from .map import MapConfig, SpecScans
30
+
31
+
32
+ class Detector(BaseModel):
33
+ """
34
+ Detector class to represent a single detector used in the experiment.
35
+
36
+ :param prefix: Prefix of the detector in the SPEC file.
37
+ :type prefix: str
38
+ :param poni_file: Path to the poni file.
39
+ :type poni_file: str
40
+ :param mask_file: Optional path to the mask file.
41
+ :type mask_file: str, optional
42
+ """
43
+ prefix: constr(strip_whitespace=True, min_length=1)
44
+ poni_file: FilePath
45
+ mask_file: Optional[FilePath]
46
+ @validator('poni_file', allow_reuse=True)
47
+ def validate_poni_file(cls, poni_file):
48
+ """
49
+ Validate the poni file by checking if it's a valid PONI file.
50
+
51
+ :param poni_file: Path to the poni file.
52
+ :type poni_file: str
53
+ :raises ValueError: If poni_file is not a valid PONI file.
54
+ :returns: Absolute path to the poni file.
55
+ :rtype: str
56
+ """
57
+ poni_file = os.path.abspath(poni_file)
58
+ try:
59
+ ai = azimuthal_integrator(poni_file)
60
+ except:
61
+ raise(ValueError(f'{poni_file} is not a valid PONI file'))
62
+ else:
63
+ return(poni_file)
64
+ @validator('mask_file', allow_reuse=True)
65
+ def validate_mask_file(cls, mask_file, values):
66
+ """
67
+ Validate the mask file. If a mask file is provided, it checks if it's a valid TIFF file.
68
+
69
+ :param mask_file: Path to the mask file.
70
+ :type mask_file: str or None
71
+ :param values: A dictionary of the Detector fields.
72
+ :type values: dict
73
+ :raises ValueError: If mask_file is provided and it's not a valid TIFF file.
74
+ :raises ValueError: If `'poni_file'` is not provided in `values`.
75
+ :returns: Absolute path to the mask file or None.
76
+ :rtype: str or None
77
+ """
78
+ if mask_file is None:
79
+ return(mask_file)
80
+ else:
81
+ mask_file = os.path.abspath(mask_file)
82
+ poni_file = values.get('poni_file')
83
+ if poni_file is None:
84
+ raise(ValueError('Cannot validate mask file without a PONI file.'))
85
+ else:
86
+ try:
87
+ mask_array = get_mask_array(mask_file, poni_file)
88
+ except BaseException as e:
89
+ raise(ValueError(f'Unable to open {mask_file} as a TIFF file'))
90
+ else:
91
+ return(mask_file)
92
+ @property
93
+ def azimuthal_integrator(self):
94
+ return(azimuthal_integrator(self.poni_file))
95
+ @property
96
+ def mask_array(self):
97
+ return(get_mask_array(self.mask_file, self.poni_file))
98
+
99
+ @cache
100
+ def azimuthal_integrator(poni_file:str):
101
+ if not isinstance(poni_file, str):
102
+ poni_file = str(poni_file)
103
+ return(pyfai_load(poni_file))
104
+ @cache
105
+ def get_mask_array(mask_file:str, poni_file:str):
106
+ if mask_file is not None:
107
+ if not isinstance(mask_file, str):
108
+ mask_file = str(mask_file)
109
+
110
+ from pyspec.file.tiff import TiffFile
111
+ with TiffFile(mask_file) as tiff:
112
+ mask_array = tiff.asarray()
113
+ else:
114
+ mask_array = np.zeros(azimuthal_integrator(poni_file).detector.shape)
115
+ return(mask_array)
116
+
117
+ class IntegrationConfig(BaseModel):
118
+ """
119
+ Class representing the configuration for a raw detector data integration.
120
+
121
+ :ivar tool_type: type of integration tool; always set to "integration"
122
+ :type tool_type: str, optional
123
+ :ivar title: title of the integration
124
+ :type title: str
125
+ :ivar integration_type: type of integration, one of "azimuthal", "radial", or "cake"
126
+ :type integration_type: str
127
+ :ivar detectors: list of detectors used in the integration
128
+ :type detectors: List[Detector]
129
+ :ivar radial_units: radial units for the integration, defaults to `'q_A^-1'`
130
+ :type radial_units: str, optional
131
+ :ivar radial_min: minimum radial value for the integration range
132
+ :type radial_min: float, optional
133
+ :ivar radial_max: maximum radial value for the integration range
134
+ :type radial_max: float, optional
135
+ :ivar radial_npt: number of points in the radial range for the integration
136
+ :type radial_npt: int, optional
137
+ :ivar azimuthal_units: azimuthal units for the integration
138
+ :type azimuthal_units: str, optional
139
+ :ivar azimuthal_min: minimum azimuthal value for the integration range
140
+ :type azimuthal_min: float, optional
141
+ :ivar azimuthal_max: maximum azimuthal value for the integration range
142
+ :type azimuthal_max: float, optional
143
+ :ivar azimuthal_npt: number of points in the azimuthal range for the integration
144
+ :type azimuthal_npt: int, optional
145
+ :ivar error_model: error model for the integration, one of "poisson" or "azimuthal"
146
+ :type error_model: str, optional
147
+ """
148
+ tool_type: Literal['integration'] = 'integration'
149
+ title: constr(strip_whitespace=True, min_length=1)
150
+ integration_type: Literal['azimuthal', 'radial', 'cake']
151
+ detectors: conlist(item_type=Detector, min_items=1)
152
+ radial_units: str = 'q_A^-1'
153
+ radial_min: confloat(ge=0)
154
+ radial_max: confloat(gt=0)
155
+ radial_npt: conint(gt=0) = 1800
156
+ azimuthal_units: str = 'chi_deg'
157
+ azimuthal_min: confloat(ge=-180) = -180
158
+ azimuthal_max: confloat(le=360) = 180
159
+ azimuthal_npt: conint(gt=0) = 3600
160
+ error_model: Optional[Literal['poisson', 'azimuthal']]
161
+ sequence_index: Optional[conint(gt=0)]
162
+ @validator('radial_units', allow_reuse=True)
163
+ def validate_radial_units(cls, radial_units):
164
+ """
165
+ Validate the radial units for the integration.
166
+
167
+ :param radial_units: unvalidated radial units for the integration
168
+ :type radial_units: str
169
+ :raises ValueError: if radial units are not one of the recognized radial units
170
+ :return: validated radial units
171
+ :rtype: str
172
+ """
173
+ if radial_units in RADIAL_UNITS.keys():
174
+ return(radial_units)
175
+ else:
176
+ raise(ValueError(f'Invalid radial units: {radial_units}. Must be one of {", ".join(RADIAL_UNITS.keys())}'))
177
+ @validator('azimuthal_units', allow_reuse=True)
178
+ def validate_azimuthal_units(cls, azimuthal_units):
179
+ """
180
+ Validate that `azimuthal_units` is one of the keys in the
181
+ `pyFAI.units.AZIMUTHAL_UNITS` dictionary.
182
+
183
+ :param azimuthal_units: The string representing the unit to be validated.
184
+ :type azimuthal_units: str
185
+ :raises ValueError: If `azimuthal_units` is not one of the keys in `pyFAI.units.AZIMUTHAL_UNITS`
186
+ :return: The original supplied value, if is one of the keys in `pyFAI.units.AZIMUTHAL_UNITS`.
187
+ :rtype: str
188
+ """
189
+ if azimuthal_units in AZIMUTHAL_UNITS.keys():
190
+ return(azimuthal_units)
191
+ else:
192
+ raise(ValueError(f'Invalid azimuthal units: {azimuthal_units}. Must be one of {", ".join(AZIMUTHAL_UNITS.keys())}'))
193
+ def validate_range_max(range_name:str):
194
+ """Validate the maximum value of an integration range.
195
+
196
+ :param range_name: The name of the integration range (e.g. radial, azimuthal).
197
+ :type range_name: str
198
+ :return: The callable that performs the validation.
199
+ :rtype: callable
200
+ """
201
+ def _validate_range_max(cls, range_max, values):
202
+ """Check if the maximum value of the integration range is greater than its minimum value.
203
+
204
+ :param range_max: The maximum value of the integration range.
205
+ :type range_max: float
206
+ :param values: The values of the other fields being validated.
207
+ :type values: dict
208
+ :raises ValueError: If the maximum value of the integration range is not greater than its minimum value.
209
+ :return: The validated maximum range value
210
+ :rtype: float
211
+ """
212
+ range_min = values.get(f'{range_name}_min')
213
+ if range_min < range_max:
214
+ return(range_max)
215
+ else:
216
+ raise(ValueError(f'Maximum value of integration range must be greater than minimum value of integration range ({range_name}_min={range_min}).'))
217
+ return(_validate_range_max)
218
+ _validate_radial_max = validator('radial_max', allow_reuse=True)(validate_range_max('radial'))
219
+ _validate_azimuthal_max = validator('azimuthal_max', allow_reuse=True)(validate_range_max('azimuthal'))
220
+ def validate_for_map_config(self, map_config:BaseModel):
221
+ """
222
+ Validate the existence of the detector data file for all scan points in `map_config`.
223
+
224
+ :param map_config: The `MapConfig` instance to validate against.
225
+ :type map_config: MapConfig
226
+ :raises RuntimeError: If a detector data file could not be found for a scan point occurring in `map_config`.
227
+ :return: None
228
+ :rtype: None
229
+ """
230
+ for detector in self.detectors:
231
+ for scans in map_config.spec_scans:
232
+ for scan_number in scans.scan_numbers:
233
+ scanparser = scans.get_scanparser(scan_number)
234
+ for scan_step_index in range(scanparser.spec_scan_npts):
235
+ # Make sure the detector data file exists for all scan points
236
+ try:
237
+ detector_data_file = scanparser.get_detector_data_file(detector.prefix, scan_step_index)
238
+ except:
239
+ raise(RuntimeError(f'Could not find data file for detector prefix {detector.prefix} on scan number {scan_number} in spec file {scans.spec_file}'))
240
+ def get_azimuthal_adjustments(self):
241
+ """To enable a continuous range of integration in the azimuthal direction
242
+ for radial and cake integration, obtain adjusted values for this
243
+ `IntegrationConfig`'s `azimuthal_min` and `azimuthal_max` values, the
244
+ angle amount by which those values were adjusted, and the proper location
245
+ of the discontinuity in the azimuthal direction.
246
+
247
+ :return: Adjusted chi_min, adjusted chi_max, chi_offset, chi_discontinuity
248
+ :rtype: tuple[float,float,float,float]
249
+ """
250
+ return(get_azimuthal_adjustments(self.azimuthal_min, self.azimuthal_max))
251
+ def get_azimuthal_integrators(self):
252
+ """Get a list of `AzimuthalIntegrator`s that correspond to the detector
253
+ configurations in this instance of `IntegrationConfig`.
254
+
255
+ The returned `AzimuthalIntegrator`s are (if need be) artificially rotated
256
+ in the azimuthal direction to achieve a continuous range of integration
257
+ in the azimuthal direction.
258
+
259
+ :returns: A list of `AzimuthalIntegrator`s appropriate for use by this
260
+ `IntegrationConfig` tool
261
+ :rtype: list[pyFAI.azimuthalIntegrator.AzimuthalIntegrator]
262
+ """
263
+ chi_min, chi_max, chi_offset, chi_disc = self.get_azimuthal_adjustments()
264
+ return(get_azimuthal_integrators(tuple([detector.poni_file for detector in self.detectors]), chi_offset=chi_offset))
265
+ def get_multi_geometry_integrator(self):
266
+ """Get a `MultiGeometry` integrator suitable for use by this instance of
267
+ `IntegrationConfig`.
268
+
269
+ :return: A `MultiGeometry` integrator
270
+ :rtype: pyFAI.multi_geometry.MultiGeometry
271
+ """
272
+ poni_files = tuple([detector.poni_file for detector in self.detectors])
273
+ radial_range = (self.radial_min, self.radial_max)
274
+ azimuthal_range = (self.azimuthal_min, self.azimuthal_max)
275
+ return(get_multi_geometry_integrator(poni_files, self.radial_units, radial_range, azimuthal_range))
276
+ def get_azimuthally_integrated_data(self, spec_scans:BaseModel, scan_number:int, scan_step_index:int):
277
+ """Return azimuthally-integrated data for the scan step specified.
278
+
279
+ :param spec_scans: An instance of `SpecScans` containing the scan step requested.
280
+ :type spec_scans: SpecScans
281
+ :param scan_number: The number of the scan containing the scan step requested.
282
+ :type scan_number: int
283
+ :param scan_step_index: The index of the scan step requested.
284
+ :type scan_step_index: int
285
+ :return: A 1D array of azimuthally-integrated raw detector intensities.
286
+ :rtype: np.ndarray
287
+ """
288
+ detector_data = spec_scans.get_detector_data(self.detectors, scan_number, scan_step_index)
289
+ integrator = self.get_multi_geometry_integrator()
290
+ lst_mask = [detector.mask_array for detector in self.detectors]
291
+ result = integrator.integrate1d(detector_data, lst_mask=lst_mask, npt=self.radial_npt, error_model=self.error_model)
292
+ if result.sigma is None:
293
+ return(result.intensity)
294
+ else:
295
+ return(result.intensity, result.sigma)
296
+ def get_radially_integrated_data(self, spec_scans:BaseModel, scan_number:int, scan_step_index:int):
297
+ """Return radially-integrated data for the scan step specified.
298
+
299
+ :param spec_scans: An instance of `SpecScans` containing the scan step requested.
300
+ :type spec_scans: SpecScans
301
+ :param scan_number: The number of the scan containing the scan step requested.
302
+ :type scan_number: int
303
+ :param scan_step_index: The index of the scan step requested.
304
+ :type scan_step_index: int
305
+ :return: A 1D array of radially-integrated raw detector intensities.
306
+ :rtype: np.ndarray
307
+ """
308
+ # Handle idiosyncracies of azimuthal ranges in pyFAI
309
+ # Adjust chi ranges to get a continuous range of iintegrated data
310
+ chi_min, chi_max, chi_offset, chi_disc = self.get_azimuthal_adjustments()
311
+ # Perform radial integration on a detector-by-detector basis.
312
+ I_each_detector = []
313
+ variance_each_detector = []
314
+ integrators = self.get_azimuthal_integrators()
315
+ for i,(integrator,detector) in enumerate(zip(integrators,self.detectors)):
316
+ detector_data = spec_scans.get_detector_data([detector], scan_number, scan_step_index)[0]
317
+ result = integrator.integrate_radial(detector_data, self.azimuthal_npt,
318
+ unit=self.azimuthal_units, azimuth_range=(chi_min,chi_max),
319
+ radial_unit=self.radial_units, radial_range=(self.radial_min,self.radial_max),
320
+ mask=detector.mask_array) #, error_model=self.error_model)
321
+ I_each_detector.append(result.intensity)
322
+ if result.sigma is not None:
323
+ variance_each_detector.append(result.sigma**2)
324
+ # Add the individual detectors' integrated intensities together
325
+ I = np.nansum(I_each_detector, axis=0)
326
+ # Ignore data at values of chi for which there was no data
327
+ I = np.where(I==0, np.nan, I)
328
+ if len(I_each_detector) != len(variance_each_detector):
329
+ return(I)
330
+ else:
331
+ # Get the standard deviation of the summed detectors' intensities
332
+ sigma = np.sqrt(np.nansum(variance_each_detector, axis=0))
333
+ return(I, sigma)
334
+ def get_cake_integrated_data(self, spec_scans:BaseModel, scan_number:int, scan_step_index:int):
335
+ """Return cake-integrated data for the scan step specified.
336
+
337
+ :param spec_scans: An instance of `SpecScans` containing the scan step requested.
338
+ :type spec_scans: SpecScans
339
+ :param scan_number: The number of the scan containing the scan step requested.
340
+ :type scan_number: int
341
+ :param scan_step_index: The index of the scan step requested.
342
+ :type scan_step_index: int
343
+ :return: A 2D array of cake-integrated raw detector intensities.
344
+ :rtype: np.ndarray
345
+ """
346
+ detector_data = spec_scans.get_detector_data(self.detectors, scan_number, scan_step_index)
347
+ integrator = self.get_multi_geometry_integrator()
348
+ lst_mask = [detector.mask_array for detector in self.detectors]
349
+ result = integrator.integrate2d(detector_data, lst_mask=lst_mask,
350
+ npt_rad=self.radial_npt, npt_azim=self.azimuthal_npt,
351
+ method='bbox',
352
+ error_model=self.error_model)
353
+ if result.sigma is None:
354
+ return(result.intensity)
355
+ else:
356
+ return(result.intensity, result.sigma)
357
+ def get_integrated_data(self, spec_scans:BaseModel, scan_number:int, scan_step_index:int):
358
+ """Return integrated data for the scan step specified.
359
+
360
+ :param spec_scans: An instance of `SpecScans` containing the scan step requested.
361
+ :type spec_scans: SpecScans
362
+ :param scan_number: The number of the scan containing the scan step requested.
363
+ :type scan_number: int
364
+ :param scan_step_index: The index of the scan step requested.
365
+ :type scan_step_index: int
366
+ :return: An array of integrated raw detector intensities.
367
+ :rtype: np.ndarray
368
+ """
369
+ if self.integration_type == 'azimuthal':
370
+ return(self.get_azimuthally_integrated_data(spec_scans, scan_number, scan_step_index))
371
+ elif self.integration_type == 'radial':
372
+ return(self.get_radially_integrated_data(spec_scans, scan_number, scan_step_index))
373
+ elif self.integration_type == 'cake':
374
+ return(self.get_cake_integrated_data(spec_scans, scan_number, scan_step_index))
375
+
376
+ @property
377
+ def integrated_data_coordinates(self):
378
+ """
379
+ Return a dictionary of coordinate arrays for navigating the dimension(s)
380
+ of the integrated data produced by this instance of `IntegrationConfig`.
381
+
382
+ :return: A dictionary with either one or two keys: 'azimuthal' and/or
383
+ 'radial', each of which points to a 1-D `numpy` array of coordinate
384
+ values.
385
+ :rtype: dict[str,np.ndarray]
386
+ """
387
+ if self.integration_type == 'azimuthal':
388
+ return(get_integrated_data_coordinates(radial_range=(self.radial_min,self.radial_max),
389
+ radial_npt=self.radial_npt))
390
+ elif self.integration_type == 'radial':
391
+ return(get_integrated_data_coordinates(azimuthal_range=(self.azimuthal_min,self.azimuthal_max),
392
+ azimuthal_npt=self.azimuthal_npt))
393
+ elif self.integration_type == 'cake':
394
+ return(get_integrated_data_coordinates(radial_range=(self.radial_min,self.radial_max),
395
+ radial_npt=self.radial_npt,
396
+ azimuthal_range=(self.azimuthal_min,self.azimuthal_max),
397
+ azimuthal_npt=self.azimuthal_npt))
398
+ @property
399
+ def integrated_data_dims(self):
400
+ """Return a tuple of the coordinate labels for the integrated data
401
+ produced by this instance of `IntegrationConfig`.
402
+ """
403
+ directions = list(self.integrated_data_coordinates.keys())
404
+ dim_names = [getattr(self, f'{direction}_units') for direction in directions]
405
+ return(dim_names)
406
+ @property
407
+ def integrated_data_shape(self):
408
+ """Return a tuple representing the shape of the integrated data
409
+ produced by this instance of `IntegrationConfig` for a single scan step.
410
+ """
411
+ return(tuple([len(coordinate_values) for coordinate_name,coordinate_values in self.integrated_data_coordinates.items()]))
412
+
413
+ @cache
414
+ def get_azimuthal_adjustments(chi_min:float, chi_max:float):
415
+ """
416
+ Fix chi discontinuity at 180 degrees and return the adjusted chi range,
417
+ offset, and discontinuty.
418
+
419
+ If the discontinuity is crossed, obtain the offset to artificially rotate
420
+ detectors to achieve a continuous azimuthal integration range.
421
+
422
+ :param chi_min: The minimum value of the azimuthal range.
423
+ :type chi_min: float
424
+ :param chi_max: The maximum value of the azimuthal range.
425
+ :type chi_max: float
426
+ :return: The following four values: the adjusted minimum value of the
427
+ azimuthal range, the adjusted maximum value of the azimuthal range, the
428
+ value by which the chi angle was adjusted, the position of the chi
429
+ discontinuity.
430
+ """
431
+ # Fix chi discontinuity at 180 degrees for now.
432
+ chi_disc = 180
433
+ # If the discontinuity is crossed, artificially rotate the detectors to
434
+ # achieve a continuous azimuthal integration range
435
+ if chi_min < chi_disc and chi_max > chi_disc:
436
+ chi_offset = chi_max - chi_disc
437
+ else:
438
+ chi_offset = 0
439
+ return(chi_min-chi_offset, chi_max-chi_offset, chi_offset, chi_disc)
440
+ @cache
441
+ def get_azimuthal_integrators(poni_files:tuple, chi_offset=0):
442
+ """
443
+ Return a list of `AzimuthalIntegrator` objects generated from PONI files.
444
+
445
+ :param poni_files: Tuple of strings, each string being a path to a PONI file. : tuple
446
+ :type poni_files: tuple
447
+ :param chi_offset: The angle in degrees by which the `AzimuthalIntegrator` objects will be rotated, defaults to 0.
448
+ :type chi_offset: float, optional
449
+ :return: List of `AzimuthalIntegrator` objects
450
+ :rtype: list[pyFAI.azimuthalIntegrator.AzimuthalIntegrator]
451
+ """
452
+ ais = []
453
+ for poni_file in poni_files:
454
+ ai = copy.deepcopy(azimuthal_integrator(poni_file))
455
+ ai.rot3 += chi_offset * np.pi/180
456
+ ais.append(ai)
457
+ return(ais)
458
+ @cache
459
+ def get_multi_geometry_integrator(poni_files:tuple, radial_unit:str, radial_range:tuple, azimuthal_range:tuple):
460
+ """Return a `MultiGeometry` instance that can be used for azimuthal or cake
461
+ integration.
462
+
463
+ :param poni_files: Tuple of PONI files that describe the detectors to be
464
+ integrated.
465
+ :type poni_files: tuple
466
+ :param radial_unit: Unit to use for radial integration range.
467
+ :type radial_unit: str
468
+ :param radial_range: Tuple describing the range for radial integration.
469
+ :type radial_range: tuple[float,float]
470
+ :param azimuthal_range:Tuple describing the range for azimuthal integration.
471
+ :type azimuthal_range: tuple[float,float]
472
+ :return: `MultiGeometry` instance that can be used for azimuthal or cake
473
+ integration.
474
+ :rtype: pyFAI.multi_geometry.MultiGeometry
475
+ """
476
+ chi_min, chi_max, chi_offset, chi_disc = get_azimuthal_adjustments(*azimuthal_range)
477
+ ais = copy.deepcopy(get_azimuthal_integrators(poni_files, chi_offset=chi_offset))
478
+ multi_geometry = MultiGeometry(ais,
479
+ unit=radial_unit,
480
+ radial_range=radial_range,
481
+ azimuth_range=(chi_min,chi_max),
482
+ wavelength=sum([ai.wavelength for ai in ais])/len(ais),
483
+ chi_disc=chi_disc)
484
+ return(multi_geometry)
485
+ @cache
486
+ def get_integrated_data_coordinates(azimuthal_range:tuple=None, azimuthal_npt:int=None, radial_range:tuple=None, radial_npt:int=None):
487
+ """
488
+ Return a dictionary of coordinate arrays for the specified radial and/or
489
+ azimuthal integration ranges.
490
+
491
+ :param azimuthal_range: Tuple specifying the range of azimuthal angles over
492
+ which to generate coordinates, in the format (min, max), defaults to
493
+ None.
494
+ :type azimuthal_range: tuple[float,float], optional
495
+ :param azimuthal_npt: Number of azimuthal coordinate points to generate,
496
+ defaults to None.
497
+ :type azimuthal_npt: int, optional
498
+ :param radial_range: Tuple specifying the range of radial distances over
499
+ which to generate coordinates, in the format (min, max), defaults to
500
+ None.
501
+ :type radial_range: tuple[float,float], optional
502
+ :param radial_npt: Number of radial coordinate points to generate, defaults
503
+ to None.
504
+ :type radial_npt: int, optional
505
+ :return: A dictionary with either one or two keys: 'azimuthal' and/or
506
+ 'radial', each of which points to a 1-D `numpy` array of coordinate
507
+ values.
508
+ :rtype: dict[str,np.ndarray]
509
+ """
510
+ integrated_data_coordinates = {}
511
+ if azimuthal_range is not None and azimuthal_npt is not None:
512
+ integrated_data_coordinates['azimuthal'] = np.linspace(*azimuthal_range, azimuthal_npt)
513
+ if radial_range is not None and radial_npt is not None:
514
+ integrated_data_coordinates['radial'] = np.linspace(*radial_range, radial_npt)
515
+ return(integrated_data_coordinates)