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

CHAP/edd/processor.py CHANGED
@@ -26,13 +26,9 @@ class DiffractionVolumeLengthProcessor(Processor):
26
26
  length of the diffraction volume for an EDD setup.
27
27
  """
28
28
 
29
- def process(self,
30
- data,
31
- config=None,
32
- save_figures=False,
33
- inputdir='.',
34
- outputdir='.',
35
- interactive=False):
29
+ def process(
30
+ self, data, config=None, save_figures=False, inputdir='.',
31
+ outputdir='.', interactive=False):
36
32
  """Return the calculated value of the DV length.
37
33
 
38
34
  :param data: Input configuration for the raw scan data & DVL
@@ -43,17 +39,17 @@ class DiffractionVolumeLengthProcessor(Processor):
43
39
  `None`.
44
40
  :type config: dict, optional
45
41
  :param save_figures: Save .pngs of plots for checking inputs &
46
- outputs of this Processor, defaults to False.
42
+ outputs of this Processor, defaults to `False`.
47
43
  :type save_figures: bool, optional
48
44
  :param outputdir: Directory to which any output figures will
49
- be saved, defaults to '.'
45
+ be saved, defaults to `'.'`.
50
46
  :type outputdir: str, optional
51
47
  :param inputdir: Input directory, used only if files in the
52
48
  input configuration are not absolute paths,
53
- defaults to '.'.
49
+ defaults to `'.'`.
54
50
  :type inputdir: str, optional
55
51
  :param interactive: Allows for user interactions, defaults to
56
- False.
52
+ `False`.
57
53
  :type interactive: bool, optional
58
54
  :raises RuntimeError: Unable to get a valid DVL configuration.
59
55
  :return: Complete DVL configuraiton dictionary.
@@ -87,12 +83,9 @@ class DiffractionVolumeLengthProcessor(Processor):
87
83
 
88
84
  return dvl_config.dict()
89
85
 
90
- def measure_dvl(self,
91
- dvl_config,
92
- detector,
93
- save_figures=False,
94
- outputdir='.',
95
- interactive=False):
86
+ def measure_dvl(
87
+ self, dvl_config, detector, save_figures=False, outputdir='.',
88
+ interactive=False):
96
89
  """Return a measured value for the length of the diffraction
97
90
  volume. Use the iron foil raster scan data provided in
98
91
  `dvl_config` and fit a gaussian to the sum of all MCA channel
@@ -107,13 +100,13 @@ class DiffractionVolumeLengthProcessor(Processor):
107
100
  :type detector:
108
101
  CHAP.edd.models.MCAElementDiffractionVolumeLengthConfig
109
102
  :param save_figures: Save .pngs of plots for checking inputs &
110
- outputs of this Processor, defaults to False.
103
+ outputs of this Processor, defaults to `False`.
111
104
  :type save_figures: bool, optional
112
105
  :param outputdir: Directory to which any output figures will
113
- be saved, defaults to '.'.
106
+ be saved, defaults to `'.'`.
114
107
  :type outputdir: str, optional
115
108
  :param interactive: Allows for user interactions, defaults to
116
- False.
109
+ `False`.
117
110
  :type interactive: bool, optional
118
111
  :raises ValueError: No value provided for included bin ranges
119
112
  for the MCA detector element.
@@ -128,6 +121,7 @@ class DiffractionVolumeLengthProcessor(Processor):
128
121
  )
129
122
 
130
123
  # Get raw MCA data from raster scan
124
+ raise RuntimeError('DiffractionVolumeLengthProcessor not updated yet')
131
125
  mca_data = dvl_config.mca_data(detector)
132
126
 
133
127
  # Blank out data below bin 500 (~25keV) as well as the last bin
@@ -258,14 +252,10 @@ class DiffractionVolumeLengthProcessor(Processor):
258
252
 
259
253
  class LatticeParameterRefinementProcessor(Processor):
260
254
  """Processor to get a refined estimate for a sample's lattice
261
- parameters"""
262
- def process(self,
263
- data,
264
- config=None,
265
- save_figures=False,
266
- outputdir='.',
267
- inputdir='.',
268
- interactive=False):
255
+ parameters."""
256
+ def process(
257
+ self, data, config=None, save_figures=False, outputdir='.',
258
+ inputdir='.', interactive=False):
269
259
  """Given a strain analysis configuration, return a copy
270
260
  contining refined values for the materials' lattice
271
261
  parameters."""
@@ -304,6 +294,8 @@ class LatticeParameterRefinementProcessor(Processor):
304
294
  raise NotImplementedError(msg)
305
295
 
306
296
  # Collect the raw MCA data
297
+ raise RuntimeError(
298
+ 'LatticeParameterRefinementProcessor not updated yet')
307
299
  self.logger.debug(f'Reading data ...')
308
300
  mca_data = strain_analysis_config.mca_data()
309
301
  self.logger.debug(f'... done')
@@ -313,9 +305,7 @@ class LatticeParameterRefinementProcessor(Processor):
313
305
  else:
314
306
  mca_data_summed = np.mean(
315
307
  mca_data, axis=tuple(np.arange(1, mca_data.ndim-1)))
316
- effective_map_shape = mca_data.shape[1:-1]
317
308
  self.logger.debug(f'mca_data_summed.shape: {mca_data_summed.shape}')
318
- self.logger.debug(f'effective_map_shape: {effective_map_shape}')
319
309
 
320
310
  # Create the NXroot object
321
311
  nxroot = NXroot()
@@ -330,7 +320,8 @@ class LatticeParameterRefinementProcessor(Processor):
330
320
  nxsubentry[map_config.title] = MapProcessor.get_nxentry(map_config)
331
321
  nxsubentry[f'{map_config.title}_lat_par_refinement'] = NXprocess()
332
322
  nxprocess = nxsubentry[f'{map_config.title}_lat_par_refinement']
333
- nxprocess.strain_analysis_config = dumps(strain_analysis_config.dict())
323
+ nxprocess.strain_analysis_config = dumps(
324
+ strain_analysis_config.dict())
334
325
  nxprocess.calibration_config = dumps(calibration_config.dict())
335
326
 
336
327
  lattice_parameters = []
@@ -350,7 +341,8 @@ class LatticeParameterRefinementProcessor(Processor):
350
341
  nxentry.lat_par_output = NXsubentry()
351
342
  nxentry.lat_par_output.attrs['schema'] = 'yaml'
352
343
  nxentry.lat_par_output.attrs['filename'] = 'lattice_parameters.yaml'
353
- nxentry.lat_par_output.data = dumps(strain_analysis_config.dict())
344
+ nxentry.lat_par_output.data = dumps(
345
+ strain_analysis_config.dict())
354
346
 
355
347
  return nxroot
356
348
 
@@ -366,7 +358,7 @@ class LatticeParameterRefinementProcessor(Processor):
366
358
  Return the averaged value of the calculated lattice parameters
367
359
  across all spectra.
368
360
 
369
- :param strain_analysis_config: Strain analysis configuration
361
+ :param strain_analysis_config: Strain analysis configuration.
370
362
  :type strain_analysis_config:
371
363
  CHAP.edd.models.StrainAnalysisConfig
372
364
  :param calibration_config: Energy calibration configuration.
@@ -374,21 +366,21 @@ class LatticeParameterRefinementProcessor(Processor):
374
366
  :param detector: A single MCA detector element configuration.
375
367
  :type detector: CHAP.edd.models.MCAElementStrainAnalysisConfig
376
368
  :param mca_data: Raw specta for the current MCA detector.
377
- :type mca_data: np.ndarray
369
+ :type mca_data: numpy.ndarray
378
370
  :param mca_data_summed: Raw specta for the current MCA detector
379
371
  summed over all data point.
380
- :type mca_data_summed: np.ndarray
372
+ :type mca_data_summed: numpy.ndarray
381
373
  :param nxsubentry: NeXus subentry to store the detailed refined
382
374
  lattice parameters for each detector.
383
375
  :type nxsubentry: nexusformat.nexus.NXprocess
384
376
  :param interactive: Boolean to indicate whether interactive
385
- matplotlib figures should be presented
377
+ matplotlib figures should be presented.
386
378
  :type interactive: bool
387
379
  :param save_figures: Boolean to indicate whether figures
388
- indicating the selection should be saved
380
+ indicating the selection should be saved.
389
381
  :type save_figures: bool
390
382
  :param outputdir: Where to save figures (if `save_figures` is
391
- `True`)
383
+ `True`).
392
384
  :type outputdir: str
393
385
  :returns: List of refined lattice parameters for materials in
394
386
  `strain_analysis_config`
@@ -610,7 +602,8 @@ class LatticeParameterRefinementProcessor(Processor):
610
602
  a_uniform_mean = float(a_uniform.mean())
611
603
  d_unconstrained = get_peak_locations(
612
604
  unconstrained_fit_centers, detector.tth_calibrated)
613
- a_unconstrained = (Rs_map * d_unconstrained.flatten()).reshape(d_unconstrained.shape)
605
+ a_unconstrained = (
606
+ Rs_map * d_unconstrained.flatten()).reshape(d_unconstrained.shape)
614
607
  a_unconstrained = np.moveaxis(a_unconstrained, 0, -1)
615
608
  a_unconstrained_mean = float(a_unconstrained.mean())
616
609
  self.logger.warning(
@@ -715,19 +708,11 @@ class MCAEnergyCalibrationProcessor(Processor):
715
708
  the location of fluorescence peaks whenever possible, _not_
716
709
  diffraction peaks, as this Processor does not account for
717
710
  2&theta."""
718
- def process(self,
719
- data,
720
- config=None,
721
- centers_range=20,
722
- fwhm_min=None,
723
- fwhm_max=None,
724
- max_energy_kev=200.0,
725
- background=None,
726
- baseline=False,
727
- save_figures=False,
728
- interactive=False,
729
- inputdir='.',
730
- outputdir='.'):
711
+ def process(
712
+ self, data, config=None, centers_range=20, fwhm_min=None,
713
+ fwhm_max=None, max_energy_kev=200.0, background=None,
714
+ baseline=False, save_figures=False, interactive=False,
715
+ inputdir='.', outputdir='.'):
731
716
  """For each detector in the `MCAEnergyCalibrationConfig`
732
717
  provided with `data`, fit the specified peaks in the MCA
733
718
  spectrum specified. Using the difference between the provided
@@ -756,7 +741,7 @@ class MCAEnergyCalibrationProcessor(Processor):
756
741
  when performing the fit, defaults to `None`.
757
742
  :type fwhm_max: float, optional
758
743
  :param max_energy_kev: Maximum channel energy of the MCA in
759
- keV, defaults to 200.0.
744
+ keV, defaults to `200.0`.
760
745
  :type max_energy_kev: float, optional
761
746
  :param background: Background model for peak fitting.
762
747
  :type background: str, list[str], optional
@@ -780,14 +765,31 @@ class MCAEnergyCalibrationProcessor(Processor):
780
765
  version of the calibrated configuration.
781
766
  :rtype: dict
782
767
  """
768
+ # Third party modules
769
+ from nexusformat.nexus import (
770
+ NXentry,
771
+ NXroot,
772
+ )
773
+
783
774
  # Local modules
784
- from CHAP.edd.models import BaselineConfig
775
+ from CHAP.edd.models import (
776
+ BaselineConfig,
777
+ MCAElementCalibrationConfig,
778
+ )
785
779
  from CHAP.utils.general import (
786
780
  is_int,
787
781
  is_num,
788
782
  is_str_series,
789
783
  )
790
784
 
785
+ # Load the detector data
786
+ # FIX input a numpy and create/use NXobject to numpy proc
787
+ # FIX right now spec info is lost in output yaml, add to it?
788
+ nxroot = self.get_data(data, 'SpecReader')
789
+ if not isinstance(nxroot, NXroot):
790
+ raise RuntimeError('No valid NXroot data in input pipeline data')
791
+ nxentry = nxroot[nxroot.default]
792
+
791
793
  # Load the validated energy calibration configuration
792
794
  try:
793
795
  calibration_config = self.get_config(
@@ -805,6 +807,21 @@ class MCAEnergyCalibrationProcessor(Processor):
805
807
  except Exception as dict_exc:
806
808
  raise RuntimeError from dict_exc
807
809
 
810
+ # Validate the detector configuration
811
+ available_detector_indices = [
812
+ int(d) for d in nxentry.detector_names]
813
+ if calibration_config.detectors is None:
814
+ calibration_config.detectors = [
815
+ MCAElementCalibrationConfig(detector_name=d)
816
+ for d in nxentry.detector_names]
817
+ else:
818
+ for detector in deepcopy(calibration_config.detectors):
819
+ index = int(detector.detector_name)
820
+ if index not in available_detector_indices:
821
+ self.logger.warning(
822
+ f'Skipping detector {int} (no raw data)')
823
+ calibration_config.detectors.remove(detector)
824
+
808
825
  # Validate the fit index range
809
826
  if calibration_config.fit_index_ranges is None and not interactive:
810
827
  raise RuntimeError(
@@ -838,19 +855,54 @@ class MCAEnergyCalibrationProcessor(Processor):
838
855
  except Exception as dict_exc:
839
856
  raise RuntimeError from dict_exc
840
857
 
841
- # Calibrate detector channel energies based on fluorescence peaks.
858
+ # Collect and sum the detector data
859
+ mca_data = []
860
+ for scan_name in nxentry.spec_scans:
861
+ for scan_number, scan_data in nxentry.spec_scans[scan_name].items():
862
+ mca_data.append(scan_data.data.data.nxdata)
863
+ summed_detector_data = np.asarray(mca_data).sum(axis=(0,1))
864
+
865
+ # Get the detectors' num_bins parameter
842
866
  for detector in calibration_config.detectors:
867
+ if detector.num_bins is None:
868
+ detector.num_bins = summed_detector_data.shape[-1]
869
+
870
+ # Check each detector's include_energy_ranges field against the
871
+ # flux file, if available.
872
+ if calibration_config.flux_file is not None:
873
+ flux = np.loadtxt(flux_file)
874
+ flux_file_energies = flux[:,0]/1.e3
875
+ flux_e_min = flux_file_energies.min()
876
+ flux_e_max = flux_file_energies.max()
877
+ for detector in calibration_config.detectors:
878
+ for i, (det_e_min, det_e_max) in enumerate(
879
+ deepcopy(detector.include_energy_ranges)):
880
+ if det_e_min < flux_e_min or det_e_max > flux_e_max:
881
+ energy_range = [float(max(det_e_min, flux_e_min)),
882
+ float(min(det_e_max, flux_e_max))]
883
+ print(
884
+ f'WARNING: include_energy_ranges[{i}] out of range'
885
+ f' ({detector.include_energy_ranges[i]}): adjusted'
886
+ f' to {energy_range}')
887
+ detector.include_energy_ranges[i] = energy_range
888
+
889
+ # Calibrate detector channel energies based on fluorescence peaks
890
+ for detector in calibration_config.detectors:
891
+ index = available_detector_indices.index(
892
+ int(detector.detector_name))
843
893
  if background is not None:
844
894
  detector.background = background.copy()
845
895
  if baseline:
846
- detector.baseline = baseline.copy()
896
+ detector.baseline = baseline.model_copy()
847
897
  detector.energy_calibration_coeffs = self.calibrate(
848
- calibration_config, detector, centers_range, fwhm_min,
849
- fwhm_max, max_energy_kev, save_figures, interactive, outputdir)
898
+ calibration_config, detector, summed_detector_data[index],
899
+ centers_range, fwhm_min, fwhm_max, max_energy_kev,
900
+ save_figures, interactive, outputdir)
850
901
 
851
902
  return calibration_config.dict()
852
903
 
853
- def calibrate(self, calibration_config, detector, centers_range,
904
+ def calibrate(
905
+ self, calibration_config, detector, spectrum, centers_range,
854
906
  fwhm_min, fwhm_max, max_energy_kev, save_figures, interactive,
855
907
  outputdir):
856
908
  """Return energy_calibration_coeffs (a, b, and c) for
@@ -861,6 +913,8 @@ class MCAEnergyCalibrationProcessor(Processor):
861
913
  :type calibration_config: MCAEnergyCalibrationConfig
862
914
  :param detector: Configuration of the current detector.
863
915
  :type detector: MCAElementCalibrationConfig
916
+ :param spectrum: Summed MCA spectrum for the current detector.
917
+ :type spectrum: numpy.ndarray
864
918
  :param centers_range: Set boundaries on the peak centers in
865
919
  MCA channels when performing the fit. The min/max
866
920
  possible values for the peak centers will be the initial
@@ -873,7 +927,7 @@ class MCAEnergyCalibrationProcessor(Processor):
873
927
  when performing the fit, defaults to `None`.
874
928
  :type fwhm_max: float, optional
875
929
  :param max_energy_kev: Maximum channel energy of the MCA in
876
- keV, defaults to 200.0.
930
+ keV, defaults to `200.0`.
877
931
  :type max_energy_kev: float
878
932
  :param save_figures: Save .pngs of plots for checking inputs &
879
933
  outputs of this Processor.
@@ -904,7 +958,7 @@ class MCAEnergyCalibrationProcessor(Processor):
904
958
 
905
959
  self.logger.info(f'Calibrating detector {detector.detector_name}')
906
960
 
907
- spectrum = calibration_config.mca_data(detector)
961
+ # Get the MCA bin energies
908
962
  uncalibrated_energies = np.linspace(
909
963
  0, max_energy_kev, detector.num_bins)
910
964
  bins = np.arange(detector.num_bins, dtype=np.int16)
@@ -1266,22 +1320,13 @@ class MCATthCalibrationProcessor(Processor):
1266
1320
  energy calibration coefficients for an EDD experimental setup.
1267
1321
  """
1268
1322
 
1269
- def process(self,
1270
- data,
1271
- config=None,
1272
- tth_initial_guess=None,
1273
- include_energy_ranges=None,
1274
- calibration_method='iterate_tth',
1275
- quadratic_energy_calibration=False,
1276
- centers_range=20,
1277
- fwhm_min=None,
1278
- fwhm_max=None,
1279
- background=None,
1280
- baseline=False,
1281
- save_figures=False,
1282
- inputdir='.',
1283
- outputdir='.',
1284
- interactive=False):
1323
+ def process(
1324
+ self, data, config=None, tth_initial_guess=None,
1325
+ include_energy_ranges=None, calibration_method='iterate_tth',
1326
+ quadratic_energy_calibration=False, centers_range=20,
1327
+ fwhm_min=None, fwhm_max=None, background=None, baseline=False,
1328
+ save_figures=False, inputdir='.', outputdir='.',
1329
+ interactive=False):
1285
1330
  """Return the calibrated 2&theta value and the fine tuned
1286
1331
  energy calibration coefficients to convert MCA channel
1287
1332
  indices to MCA channel energies.
@@ -1290,8 +1335,8 @@ class MCATthCalibrationProcessor(Processor):
1290
1335
  procedure.
1291
1336
  :type data: list[PipelineData]
1292
1337
  :param config: Initialization parameters for an instance of
1293
- CHAP.edd.models.MCATthCalibrationConfig, defaults to
1294
- None.
1338
+ CHAP.edd.models.MCATthCalibrationConfig,
1339
+ defaults to `None`.
1295
1340
  :type config: dict, optional
1296
1341
  :param tth_initial_guess: Initial guess for 2&theta to supercede
1297
1342
  the values from the energy calibration detector cofiguration
@@ -1329,23 +1374,29 @@ class MCATthCalibrationProcessor(Processor):
1329
1374
  defaults to `False`.
1330
1375
  :type baseline: bool, BaselineConfig, optional
1331
1376
  :param save_figures: Save .pngs of plots for checking inputs &
1332
- outputs of this Processor, defaults to False.
1377
+ outputs of this Processor, defaults to `False`.
1333
1378
  :type save_figures: bool, optional
1334
1379
  :param outputdir: Directory to which any output figures will
1335
- be saved, defaults to '.'.
1380
+ be saved, defaults to `'.'`.
1336
1381
  :type outputdir: str, optional
1337
1382
  :param inputdir: Input directory, used only if files in the
1338
1383
  input configuration are not absolute paths,
1339
- defaults to '.'.
1384
+ defaults to `'.'`.
1340
1385
  :type inputdir: str, optional
1341
- :param interactive: Allows for user interactions, defaults to
1342
- False.
1386
+ :param interactive: Allows for user interactions,
1387
+ defaults to `False`.
1343
1388
  :type interactive: bool, optional
1344
1389
  :raises RuntimeError: Invalid or missing input configuration.
1345
1390
  :return: Original configuration with the tuned values for
1346
1391
  2&theta and the linear correction parameters added.
1347
1392
  :rtype: dict[str,float]
1348
1393
  """
1394
+ # Third party modules
1395
+ from nexusformat.nexus import (
1396
+ NXentry,
1397
+ NXroot,
1398
+ )
1399
+
1349
1400
  # Local modules
1350
1401
  from CHAP.edd.models import BaselineConfig
1351
1402
  from CHAP.utils.general import (
@@ -1353,6 +1404,15 @@ class MCATthCalibrationProcessor(Processor):
1353
1404
  is_str_series,
1354
1405
  )
1355
1406
 
1407
+ # Load the detector data
1408
+ # FIX input a numpy and create/use NXobject to numpy proc
1409
+ # FIX right now spec info is lost in output yaml, add to it?
1410
+ nxroot = self.get_data(data, 'SpecReader')
1411
+ if not isinstance(nxroot, NXroot):
1412
+ raise RuntimeError('No valid NXroot data in input pipeline data')
1413
+ nxentry = nxroot[nxroot.default]
1414
+
1415
+ # Load the validated 2&theta calibration configuration
1356
1416
  try:
1357
1417
  calibration_config = self.get_config(
1358
1418
  data, 'edd.models.MCATthCalibrationConfig',
@@ -1371,6 +1431,26 @@ class MCATthCalibrationProcessor(Processor):
1371
1431
  except Exception as dict_exc:
1372
1432
  raise RuntimeError from dict_exc
1373
1433
 
1434
+ # Validate the detector configuration
1435
+ if calibration_config.detectors is None:
1436
+ raise RuntimeError('No available calibrated detectors')
1437
+ available_detector_indices = [int(d) for d in nxentry.detector_names]
1438
+ detectors = []
1439
+ for detector in calibration_config.detectors:
1440
+ index = int(detector.detector_name)
1441
+ if index in available_detector_indices:
1442
+ detectors.append(detector)
1443
+ else:
1444
+ self.logger.warning(
1445
+ f'Skipping detector {name} (no energy calibration data)')
1446
+
1447
+ # Validate the fit index range
1448
+ if calibration_config.fit_index_ranges is None and not interactive:
1449
+ raise RuntimeError(
1450
+ 'If `fit_index_ranges` is not explicitly provided, '
1451
+ + self.__class__.__name__
1452
+ + ' must be run with `interactive=True`.')
1453
+
1374
1454
  # Validate the optional inputs
1375
1455
  if not is_int(centers_range, gt=0, log=False):
1376
1456
  RuntimeError(f'Invalid centers_range parameter ({centers_range})')
@@ -1392,10 +1472,41 @@ class MCATthCalibrationProcessor(Processor):
1392
1472
  except Exception as dict_exc:
1393
1473
  raise RuntimeError from dict_exc
1394
1474
 
1395
- self.logger.debug(f'In process: save_figures = {save_figures}; '
1396
- f'interactive = {interactive}')
1397
-
1398
- for detector in calibration_config.detectors:
1475
+ # Collect and sum the detector data
1476
+ mca_data = []
1477
+ for scan_name in nxentry.spec_scans:
1478
+ for scan_number, scan_data in nxentry.spec_scans[scan_name].items():
1479
+ mca_data.append(scan_data.data.data.nxdata)
1480
+ summed_detector_data = np.asarray(mca_data).sum(axis=(0,1))
1481
+
1482
+ # Get the detectors' num_bins parameter
1483
+ for detector in detectors:
1484
+ if detector.num_bins is None:
1485
+ detector.num_bins = summed_detector_data.shape[-1]
1486
+
1487
+ # Check each detector's include_energy_ranges field against the
1488
+ # flux file, if available.
1489
+ if calibration_config.flux_file is not None:
1490
+ flux = np.loadtxt(flux_file)
1491
+ flux_file_energies = flux[:,0]/1.e3
1492
+ flux_e_min = flux_file_energies.min()
1493
+ flux_e_max = flux_file_energies.max()
1494
+ for detector in detectors:
1495
+ for i, (det_e_min, det_e_max) in enumerate(
1496
+ deepcopy(detector.include_energy_ranges)):
1497
+ if det_e_min < flux_e_min or det_e_max > flux_e_max:
1498
+ energy_range = [float(max(det_e_min, flux_e_min)),
1499
+ float(min(det_e_max, flux_e_max))]
1500
+ print(
1501
+ f'WARNING: include_energy_ranges[{i}] out of range'
1502
+ f' ({detector.include_energy_ranges[i]}): adjusted'
1503
+ f' to {energy_range}')
1504
+ detector.include_energy_ranges[i] = energy_range
1505
+
1506
+ # Calibrate detector channel energies
1507
+ for detector in detectors:
1508
+ index = available_detector_indices.index(
1509
+ int(detector.detector_name))
1399
1510
  if tth_initial_guess is not None:
1400
1511
  detector.tth_initial_guess = tth_initial_guess
1401
1512
  if include_energy_ranges is not None:
@@ -1405,22 +1516,17 @@ class MCATthCalibrationProcessor(Processor):
1405
1516
  if baseline:
1406
1517
  detector.baseline = baseline
1407
1518
  self.calibrate(
1408
- calibration_config, detector, quadratic_energy_calibration,
1409
- centers_range, fwhm_min, fwhm_max, save_figures, interactive,
1410
- outputdir)
1519
+ calibration_config, detector, summed_detector_data[index],
1520
+ quadratic_energy_calibration, centers_range, fwhm_min,
1521
+ fwhm_max, save_figures, interactive, outputdir)
1411
1522
 
1412
1523
  return calibration_config.dict()
1413
1524
 
1414
- def calibrate(self,
1415
- calibration_config,
1416
- detector,
1417
- quadratic_energy_calibration=False,
1418
- centers_range=20,
1419
- fwhm_min=None,
1420
- fwhm_max=None,
1421
- save_figures=False,
1422
- interactive=False,
1423
- outputdir='.'):
1525
+ def calibrate(
1526
+ self, calibration_config, detector, spectrum,
1527
+ quadratic_energy_calibration=False, centers_range=20,
1528
+ fwhm_min=None, fwhm_max=None, save_figures=False,
1529
+ interactive=False, outputdir='.'):
1424
1530
  """Iteratively calibrate 2&theta by fitting selected peaks of
1425
1531
  an MCA spectrum until the computed strain is sufficiently
1426
1532
  small. Use the fitted peak locations to determine linear
@@ -1432,6 +1538,8 @@ class MCATthCalibrationProcessor(Processor):
1432
1538
  CHAP.edd.models.MCATthCalibrationConfig
1433
1539
  :param detector: A single MCA detector element configuration.
1434
1540
  :type detector: CHAP.edd.models.MCAElementCalibrationConfig
1541
+ :param spectrum: Summed MCA spectrum for the current detector.
1542
+ :type spectrum: numpy.ndarray
1435
1543
  :param quadratic_energy_calibration: Adds a quadratic term to
1436
1544
  the detector channel index to energy conversion, defaults
1437
1545
  to `False` (linear only).
@@ -1448,13 +1556,13 @@ class MCATthCalibrationProcessor(Processor):
1448
1556
  when performing the fit, defaults to `None`.
1449
1557
  :type fwhm_max: float, optional
1450
1558
  :param save_figures: Save .pngs of plots for checking inputs &
1451
- outputs of this Processor, defaults to False.
1559
+ outputs of this Processor, defaults to `False`.
1452
1560
  :type save_figures: bool, optional
1453
1561
  :param interactive: Allows for user interactions, defaults to
1454
- False.
1562
+ `False`.
1455
1563
  :type interactive: bool, optional
1456
1564
  :param outputdir: Directory to which any output figures will
1457
- be saved, defaults to '.'.
1565
+ be saved, defaults to `'.'`.
1458
1566
  :type outputdir: str, optional
1459
1567
  :raises ValueError: No value provided for included bin ranges
1460
1568
  or the fitted HKLs for the MCA detector element.
@@ -1485,14 +1593,13 @@ class MCATthCalibrationProcessor(Processor):
1485
1593
  hkls, ds = calibration_config.material.unique_hkls_ds(
1486
1594
  tth_tol=detector.hkl_tth_tol, tth_max=detector.tth_max)
1487
1595
 
1488
- # Collect raw MCA data of interest
1596
+ # Get the MCA bin energies
1489
1597
  mca_bin_energies = detector.energies
1490
- mca_data = calibration_config.mca_data(detector)
1491
1598
 
1492
1599
  # Blank out data below 25 keV as well as the last bin
1493
1600
  energy_mask = np.where(mca_bin_energies >= 25.0, 1, 0)
1494
1601
  energy_mask[-1] = 0
1495
- mca_data = mca_data*energy_mask
1602
+ spectrum = spectrum*energy_mask
1496
1603
 
1497
1604
  # Subtract the baseline
1498
1605
  if detector.baseline:
@@ -1506,13 +1613,13 @@ class MCATthCalibrationProcessor(Processor):
1506
1613
  else:
1507
1614
  filename = None
1508
1615
  baseline, baseline_config = ConstructBaseline.construct_baseline(
1509
- mca_data, mask=energy_mask, tol=detector.baseline.tol,
1616
+ spectrum, mask=energy_mask, tol=detector.baseline.tol,
1510
1617
  lam=detector.baseline.lam, max_iter=detector.baseline.max_iter,
1511
1618
  title=f'Baseline for detector {detector.detector_name}',
1512
1619
  xlabel='Energy (keV)', ylabel='Intensity (counts)',
1513
1620
  interactive=interactive, filename=filename)
1514
1621
 
1515
- mca_data -= baseline
1622
+ spectrum -= baseline
1516
1623
  detector.baseline.lam = baseline_config['lambda']
1517
1624
  detector.baseline.attrs['num_iter'] = baseline_config['num_iter']
1518
1625
  detector.baseline.attrs['error'] = baseline_config['error']
@@ -1525,7 +1632,7 @@ class MCATthCalibrationProcessor(Processor):
1525
1632
  else:
1526
1633
  filename = None
1527
1634
  tth_init = select_tth_initial_guess(
1528
- mca_bin_energies, mca_data, hkls, ds,
1635
+ mca_bin_energies, spectrum, hkls, ds,
1529
1636
  detector.tth_initial_guess, interactive, filename)
1530
1637
  detector.tth_initial_guess = tth_init
1531
1638
  self.logger.debug(f'tth_initial_guess = {detector.tth_initial_guess}')
@@ -1540,7 +1647,7 @@ class MCATthCalibrationProcessor(Processor):
1540
1647
  else:
1541
1648
  num_hkl_min = 1
1542
1649
  include_bin_ranges, hkl_indices = select_mask_and_hkls(
1543
- mca_bin_energies, mca_data, hkls, ds,
1650
+ mca_bin_energies, spectrum, hkls, ds,
1544
1651
  detector.tth_initial_guess,
1545
1652
  preselected_bin_ranges=detector.include_bin_ranges,
1546
1653
  num_hkl_min=num_hkl_min, detector_name=detector.detector_name,
@@ -1572,7 +1679,7 @@ class MCATthCalibrationProcessor(Processor):
1572
1679
  'the detector\'s MCA Tth Calibration Configuration or '
1573
1680
  're-run the pipeline with the --interactive flag.')
1574
1681
  mca_mask = detector.mca_mask()
1575
- mca_data_fit = mca_data[mca_mask]
1682
+ spectrum_fit = spectrum[mca_mask]
1576
1683
 
1577
1684
  # Correct raw MCA data for variable flux at different energies
1578
1685
  flux_correct = \
@@ -1580,7 +1687,7 @@ class MCATthCalibrationProcessor(Processor):
1580
1687
  if flux_correct is not None:
1581
1688
  mca_intensity_weights = flux_correct(
1582
1689
  mca_bin_energies[mca_mask])
1583
- mca_data_fit = mca_data_fit / mca_intensity_weights
1690
+ spectrum_fit = spectrum_fit / mca_intensity_weights
1584
1691
 
1585
1692
  # Get the fluorescence peak info
1586
1693
  e_xrf = calibration_config.peak_energies
@@ -1677,7 +1784,7 @@ class MCATthCalibrationProcessor(Processor):
1677
1784
  # Perform the fit
1678
1785
  fit = FitProcessor()
1679
1786
  result = fit.process(
1680
- NXdata(NXfield(mca_data_fit, 'y'),
1787
+ NXdata(NXfield(spectrum_fit, 'y'),
1681
1788
  NXfield(np.arange(detector.num_bins)[mca_mask], 'x')),
1682
1789
  {'parameters': parameters, 'models': models, 'method': 'trf'})
1683
1790
 
@@ -1747,7 +1854,7 @@ class MCATthCalibrationProcessor(Processor):
1747
1854
  'fwhm_min': fwhm_min, 'fwhm_max': fwhm_max})
1748
1855
  fit = FitProcessor()
1749
1856
  result = fit.process(
1750
- NXdata(NXfield(mca_data_fit, 'y'),
1857
+ NXdata(NXfield(spectrum_fit, 'y'),
1751
1858
  NXfield(mca_bins_fit, 'x')),
1752
1859
  {'models': models, 'method': 'trf'})
1753
1860
 
@@ -1865,7 +1972,7 @@ class MCATthCalibrationProcessor(Processor):
1865
1972
  'fwhm_min': fwhm_min, 'fwhm_max': fwhm_max})
1866
1973
  fit = FitProcessor()
1867
1974
  result = fit.process(
1868
- NXdata(NXfield(mca_data_fit, 'y'),
1975
+ NXdata(NXfield(spectrum_fit, 'y'),
1869
1976
  NXfield(mca_bins_fit, 'x')),
1870
1977
  {'models': models, 'method': 'trf'})
1871
1978
 
@@ -1885,7 +1992,7 @@ class MCATthCalibrationProcessor(Processor):
1885
1992
  # for the fluorescense peaks and Bragg's law for the Bragg
1886
1993
  # peaks for given material properties and a freely
1887
1994
  # adjustable 2&theta angle and MCA energy axis calibration
1888
- norm = mca_data_fit.max()
1995
+ norm = spectrum_fit.max()
1889
1996
  pars_init = [tth_init, b_init, c_init]
1890
1997
  for amp, sig in zip(amplitudes_init, sigmas_init):
1891
1998
  pars_init += [amp/norm, sig]
@@ -1910,7 +2017,7 @@ class MCATthCalibrationProcessor(Processor):
1910
2017
  result = minimize(
1911
2018
  cost_function_combined, pars_init,
1912
2019
  args=(
1913
- mca_bins_fit, mca_data_fit/norm,
2020
+ mca_bins_fit, spectrum_fit/norm,
1914
2021
  quadratic_energy_calibration, ds_fit,
1915
2022
  indices_unconstrained, e_xrf),
1916
2023
  method='Nelder-Mead', bounds=bounds)
@@ -1936,7 +2043,7 @@ class MCATthCalibrationProcessor(Processor):
1936
2043
  best_fit_uniform += gaussian(
1937
2044
  mca_bins_fit, a_fit, b_fit, c_fit, amplitudes_fit[i],
1938
2045
  sigmas_fit[i], e_peak)
1939
- residual_uniform = mca_data_fit - best_fit_uniform
2046
+ residual_uniform = spectrum_fit - best_fit_uniform
1940
2047
  e_bragg_uniform = e_bragg_fit
1941
2048
  strain_uniform = 0.0
1942
2049
 
@@ -1978,7 +2085,7 @@ class MCATthCalibrationProcessor(Processor):
1978
2085
  fit = FitProcessor()
1979
2086
  uniform_fit = fit.process(
1980
2087
  NXdata(
1981
- NXfield(mca_data_fit, 'y'),
2088
+ NXfield(spectrum_fit, 'y'),
1982
2089
  NXfield(mca_bin_energies_fit, 'x')),
1983
2090
  {'models': models, 'method': 'trf'})
1984
2091
 
@@ -2102,7 +2209,7 @@ class MCATthCalibrationProcessor(Processor):
2102
2209
  'fwhm_min': fwhm_min, 'fwhm_max': fwhm_max})
2103
2210
  fit = FitProcessor()
2104
2211
  result = fit.process(
2105
- NXdata(NXfield(mca_data_fit, 'y'),
2212
+ NXdata(NXfield(spectrum_fit, 'y'),
2106
2213
  NXfield(mca_energies_fit, 'x')),
2107
2214
  {'models': models, 'method': 'trf'})
2108
2215
  best_fit_unconstrained = result.best_fit
@@ -2134,11 +2241,11 @@ class MCATthCalibrationProcessor(Processor):
2134
2241
  transform=axs[0,0].get_xaxis_transform())
2135
2242
  if flux_correct is None:
2136
2243
  axs[0,0].plot(
2137
- mca_energies_fit, mca_data_fit, marker='.', c='C2', ms=3,
2244
+ mca_energies_fit, spectrum_fit, marker='.', c='C2', ms=3,
2138
2245
  ls='', label='MCA data')
2139
2246
  else:
2140
2247
  axs[0,0].plot(
2141
- mca_energies_fit, mca_data_fit, marker='.', c='C2', ms=3,
2248
+ mca_energies_fit, spectrum_fit, marker='.', c='C2', ms=3,
2142
2249
  ls='', label='Flux-corrected MCA data')
2143
2250
  if calibration_method == 'iterate_tth':
2144
2251
  label_unconstrained = 'Unconstrained'
@@ -2257,13 +2364,9 @@ class MCADataProcessor(Processor):
2257
2364
  transformed according to the results of a energy/tth calibration.
2258
2365
  """
2259
2366
 
2260
- def process(self,
2261
- data,
2262
- config=None,
2263
- save_figures=False,
2264
- inputdir='.',
2265
- outputdir='.',
2266
- interactive=False):
2367
+ def process(
2368
+ self, data, config=None, save_figures=False, inputdir='.',
2369
+ outputdir='.', interactive=False):
2267
2370
  """Process configurations for a map and MCA detector(s), and
2268
2371
  return the calibrated MCA data collected over the map.
2269
2372
 
@@ -2358,9 +2461,10 @@ class MCADataProcessor(Processor):
2358
2461
  class MCACalibratedDataPlotter(Processor):
2359
2462
  """Convenience Processor for quickly visualizing calibrated MCA
2360
2463
  data from a single scan. Returns None!"""
2361
- def process(self, data, spec_file, scan_number, scan_step_index=None,
2362
- material=None, save_figures=False, interactive=False,
2363
- outputdir='.'):
2464
+ def process(
2465
+ self, data, spec_file, scan_number, scan_step_index=None,
2466
+ material=None, save_figures=False, interactive=False,
2467
+ outputdir='.'):
2364
2468
  """Show a maplotlib figure of the MCA data fom the scan
2365
2469
  provided on a calibrated energy axis. If `scan_step_index` is
2366
2470
  None, a plot of the sum of all spectra across the whole scan
@@ -2372,7 +2476,8 @@ class MCACalibratedDataPlotter(Processor):
2372
2476
  :type spec_file: str
2373
2477
  :param scan_number: Scan number of interest.
2374
2478
  :type scan_number: int
2375
- :param scan_step_index: Scan step index of interest, defaults to None.
2479
+ :param scan_step_index: Scan step index of interest,
2480
+ defaults to `None`.
2376
2481
  :type scan_step_index: int, optional
2377
2482
  :param material: Material parameters to plot HKLs for.
2378
2483
  :type material: dict
@@ -2391,7 +2496,7 @@ class MCACalibratedDataPlotter(Processor):
2391
2496
  import matplotlib.pyplot as plt
2392
2497
 
2393
2498
  # Local modules
2394
- from CHAP.utils.scanparsers import SMBMCAScanParser as ScanParser
2499
+ from chess_scanparsers import SMBMCAScanParser as ScanParser
2395
2500
 
2396
2501
  if material is not None:
2397
2502
  self.logger.warning('Plotting HKL lines is not supported yet.')
@@ -2418,20 +2523,20 @@ class MCACalibratedDataPlotter(Processor):
2418
2523
  ax.set_ylabel('Intenstiy (a.u)')
2419
2524
  for detector in calibration_config.detectors:
2420
2525
  if scan_step_index is None:
2421
- mca_data = np.sum(
2526
+ spectrum = np.sum(
2422
2527
  scanparser.get_all_detector_data(detector.detector_name),
2423
2528
  axis=0)
2424
2529
  else:
2425
- mca_data = scanparser.get_detector_data(
2530
+ spectrum = scanparser.get_detector_data(
2426
2531
  detector.detector_name, scan_step_index=scan_step_index)
2427
- ax.plot(detector.energies, mca_data,
2532
+ ax.plot(detector.energies, spectrum,
2428
2533
  label=f'Detector {detector.detector_name}')
2429
2534
  ax.legend()
2430
2535
  if interactive:
2431
2536
  plt.show()
2432
2537
  if save_figures:
2433
2538
  fig.savefig(os.path.join(
2434
- outputdir, f'mca_data_{scanparser.scan_title}'))
2539
+ outputdir, f'spectrum_{scanparser.scan_title}'))
2435
2540
  plt.close()
2436
2541
  return None
2437
2542
 
@@ -2440,41 +2545,50 @@ class StrainAnalysisProcessor(Processor):
2440
2545
  """Processor that takes a map of MCA data and returns a map of
2441
2546
  sample strains
2442
2547
  """
2443
- def process(self,
2444
- data,
2445
- config=None,
2446
- find_peaks=False,
2447
- save_figures=False,
2448
- inputdir='.',
2449
- outputdir='.',
2450
- interactive=False):
2548
+ def __init__(self):
2549
+ super().__init__()
2550
+ self._save_figures = False
2551
+ self._inputdir = '.'
2552
+ self._outputdir = '.'
2553
+ self._interactive = False
2554
+ self._detectors = []
2555
+ self._detector_indices = []
2556
+ self._nxdata = None
2557
+
2558
+ def process(
2559
+ self, data, config=None, find_peaks=False, skip_animation=False,
2560
+ save_figures=False, inputdir='.', outputdir='.',
2561
+ interactive=False):
2451
2562
  """Return strain analysis maps & associated metadata in an NXprocess.
2452
2563
 
2453
2564
  :param data: Input data containing configurations for a map,
2454
2565
  completed energy/tth calibration, and parameters for strain
2455
- analysis
2566
+ analysis.
2456
2567
  :type data: list[PipelineData]
2457
2568
  :param config: Initialization parameters for an instance of
2458
2569
  CHAP.edd.models.StrainAnalysisConfig, defaults to
2459
- None.
2570
+ `None`.
2460
2571
  :type config: dict, optional
2461
2572
  :param find_peaks: Exclude peaks where the average spectrum
2462
2573
  is below the `rel_height_cutoff` (in the detector
2463
2574
  configuration) cutoff relative to the maximum value of the
2464
2575
  average spectrum, defaults to `False`.
2465
2576
  :type find_peaks: bool, optional
2577
+ :param skip_animation: Skip the animation and plotting of
2578
+ the strain analysis fits, defaults to `False`.
2579
+ :type skip_animation: bool, optional
2466
2580
  :param save_figures: Save .pngs of plots for checking inputs &
2467
- outputs of this Processor, defaults to False.
2581
+ outputs of this Processor, defaults to `False`.
2468
2582
  :type save_figures: bool, optional
2469
- :param outputdir: Directory to which any output figures will
2470
- be saved, defaults to '.'.
2471
- :type outputdir: str, optional
2472
2583
  :param inputdir: Input directory, used only if files in the
2473
2584
  input configuration are not absolute paths,
2474
- defaults to '.'.
2585
+ defaults to `'.'`.
2475
2586
  :type inputdir: str, optional
2587
+ :param outputdir: Directory to which any output figures will
2588
+ be saved, defaults to `'.'`.
2589
+ :type outputdir: str, optional
2476
2590
  :param interactive: Allows for user interactions, defaults to
2477
- False.
2591
+ `False`.
2478
2592
  :type interactive: bool, optional
2479
2593
  :raises RuntimeError: Unable to get a valid strain analysis
2480
2594
  configuration.
@@ -2484,52 +2598,105 @@ class StrainAnalysisProcessor(Processor):
2484
2598
  :rtype: nexusformat.nexus.NXprocess
2485
2599
 
2486
2600
  """
2487
- # Get required configuration models from input data
2488
- calibration_config = self.get_config(
2489
- data, 'edd.models.MCATthCalibrationConfig', inputdir=inputdir)
2601
+ # Third party modules
2602
+ from nexusformat.nexus import (
2603
+ NXentry,
2604
+ NXroot,
2605
+ )
2606
+
2607
+ self._save_figures = save_figures
2608
+ self._outputdir = outputdir
2609
+ self._interactive = interactive
2610
+
2611
+ # Load the detector data
2612
+ try:
2613
+ nxentry = self.get_data(data, 'MapProcessor')
2614
+ if not isinstance(nxentry, NXentry):
2615
+ raise RuntimeError(
2616
+ 'No valid NXentry data in MapProcessor pipeline data')
2617
+ except:
2618
+ try:
2619
+ try:
2620
+ nxroot = self.get_data(data, 'NexusReader')
2621
+ except:
2622
+ nxroot = self.get_data(data, 'NexusWriter')
2623
+ if not isinstance(nxroot, NXroot):
2624
+ raise RuntimeError(
2625
+ 'No valid NXroot data in NexusWriter pipeline data')
2626
+ nxentry = nxroot[nxroot.default]
2627
+ if not isinstance(nxentry, NXentry):
2628
+ raise RuntimeError(
2629
+ 'No valid NXentry data in NexusWriter pipeline data')
2630
+ except:
2631
+ raise RuntimeError(
2632
+ 'No valid detector data in input pipeline data')
2633
+
2634
+ # Load the validated calibration and strain analysis configuration
2490
2635
  try:
2491
2636
  strain_analysis_config = self.get_config(
2492
2637
  data, 'edd.models.StrainAnalysisConfig', inputdir=inputdir)
2493
2638
  except Exception as data_exc:
2494
- # Local modules
2495
- from CHAP.edd.models import StrainAnalysisConfig
2496
-
2497
2639
  self.logger.info('No valid strain analysis config in input '
2498
- + 'pipeline data, using config parameter instead')
2640
+ 'pipeline data, using config parameter instead')
2499
2641
  try:
2642
+ # Local modules
2643
+ from CHAP.edd.models import StrainAnalysisConfig
2644
+
2500
2645
  strain_analysis_config = StrainAnalysisConfig(
2501
2646
  **config, inputdir=inputdir)
2502
2647
  except Exception as dict_exc:
2503
2648
  raise RuntimeError from dict_exc
2504
2649
 
2505
- nxroot = self.get_nxroot(
2506
- strain_analysis_config.map_config,
2507
- calibration_config,
2508
- strain_analysis_config,
2509
- find_peaks=find_peaks,
2510
- save_figures=save_figures,
2511
- outputdir=outputdir,
2512
- interactive=interactive)
2513
- self.logger.debug(nxroot.tree)
2514
-
2515
- return nxroot
2516
-
2517
- def get_nxroot(self,
2518
- map_config,
2519
- calibration_config,
2520
- strain_analysis_config,
2521
- find_peaks=False,
2522
- save_figures=False,
2523
- outputdir='.',
2524
- interactive=False):
2525
- """Return NXroot containing strain maps.
2526
-
2527
-
2528
- :param map_config: The map configuration.
2529
- :type map_config: CHAP.common.models.map.MapConfig
2530
- :param calibration_config: The calibration configuration.
2531
- :type calibration_config:
2532
- 'CHAP.edd.models.MCATthCalibrationConfig'
2650
+ # Validate the detector configuration and load, validate and
2651
+ # add the calibration info to the detectors
2652
+ calibration_config = self.get_config(
2653
+ data, 'edd.models.MCATthCalibrationConfig', inputdir=inputdir)
2654
+ calibration_detector_indices = [
2655
+ int(d.detector_name) for d in calibration_config.detectors]
2656
+ if 'detector_names' in nxentry:
2657
+ available_detector_indices = [
2658
+ int(d) for d in nxentry.detector_names]
2659
+ else:
2660
+ available_detector_indices = calibration_detector_indices
2661
+ if strain_analysis_config.detectors is None:
2662
+ # Local modules:
2663
+ from CHAP.edd.models import MCAElementStrainAnalysisConfig
2664
+
2665
+ strain_analysis_config.detectors = [
2666
+ MCAElementStrainAnalysisConfig(**dict(d))
2667
+ for d in calibration_config.detectors
2668
+ if int(d.detector_name) in available_detector_indices]
2669
+ for detector in deepcopy(strain_analysis_config.detectors):
2670
+ index = int(detector.detector_name)
2671
+ if index not in available_detector_indices:
2672
+ self.logger.warning(
2673
+ f'Skipping detector {index} (no raw data)')
2674
+ strain_analysis_config.detectors.remove(detector)
2675
+ elif index in calibration_detector_indices:
2676
+ self._detectors.append(detector)
2677
+ self._detector_indices.append(
2678
+ available_detector_indices.index(index))
2679
+ calibration = [
2680
+ d for d in calibration_config.detectors
2681
+ if d.detector_name == detector.detector_name][0]
2682
+ detector.add_calibration(calibration)
2683
+ else:
2684
+ self.logger.warning(f'Skipping detector {index} '
2685
+ '(no energy/tth calibration data)')
2686
+ if not self._detectors:
2687
+ raise ValueError('Unable to match an available calibrated '
2688
+ 'detector for the strain analysis')
2689
+
2690
+ return self.strain_analysis(
2691
+ nxentry, strain_analysis_config, find_peaks, skip_animation)
2692
+
2693
+ def strain_analysis(
2694
+ self, nxentry, strain_analysis_config, find_peaks, skip_animation):
2695
+ """Return NXroot containing the strain maps.
2696
+
2697
+ :param nxentry: The strain analysis map, including the raw
2698
+ detector data.
2699
+ :type nxentry: nexusformat.nexus.NXentry
2533
2700
  :param strain_analysis_config: Strain analysis processing
2534
2701
  configuration.
2535
2702
  :type strain_analysis_config:
@@ -2539,19 +2706,14 @@ class StrainAnalysisProcessor(Processor):
2539
2706
  configuration) cutoff relative to the maximum value of the
2540
2707
  average spectrum, defaults to `False`.
2541
2708
  :type find_peaks: bool, optional
2542
- :param save_figures: Save .pngs of plots for checking inputs &
2543
- outputs of this Processor, defaults to False.
2544
- :type save_figures: bool, optional
2545
- :param outputdir: Directory to which any output figures will
2546
- be saved, defaults to '.'.
2547
- :type outputdir: str, optional
2548
- :param interactive: Allows for user interactions, defaults to
2549
- False.
2550
- :type interactive: bool, optional
2551
- :return: NXroot containing strain maps.
2709
+ :param skip_animation: Skip the animation and plotting of
2710
+ the strain analysis fits, defaults to `False`.
2711
+ :type skip_animation: bool, optional
2712
+ :return: The strain maps.
2552
2713
  :rtype: nexusformat.nexus.NXroot
2553
2714
  """
2554
2715
  # Third party modules
2716
+ from json import loads
2555
2717
  from nexusformat.nexus import (
2556
2718
  NXcollection,
2557
2719
  NXdata,
@@ -2561,87 +2723,91 @@ class StrainAnalysisProcessor(Processor):
2561
2723
  NXprocess,
2562
2724
  NXroot,
2563
2725
  )
2726
+ from nexusformat.nexus.tree import NXlinkfield
2564
2727
 
2565
2728
  # Local modules
2566
2729
  from CHAP.common import MapProcessor
2730
+ from CHAP.common.models.map import MapConfig
2567
2731
  from CHAP.edd.utils import (
2568
2732
  get_peak_locations,
2733
+ get_spectra_fits,
2569
2734
  get_unique_hkls_ds,
2570
- get_spectra_fits
2571
2735
  )
2572
- if interactive or save_figures:
2573
- from CHAP.edd.utils import (
2574
- select_material_params,
2575
- select_mask_and_hkls,
2576
- )
2577
2736
 
2578
- def linkdims(nxgroup, field_dims=[], oversampling_axis={}):
2579
- if isinstance(field_dims, dict):
2580
- field_dims = [field_dims]
2581
- if map_config.map_type == 'structured':
2582
- axes = deepcopy(map_config.dims)
2583
- for dims in field_dims:
2584
- axes.append(dims['axes'])
2585
- else:
2586
- axes = ['map_index']
2587
- for dims in field_dims:
2588
- axes.append(dims['axes'])
2589
- nxgroup.attrs[f'map_index_indices'] = 0
2590
- for dim in map_config.dims:
2737
+ def linkdims(
2738
+ nxgroup, nxdata_source, field_dims=[], oversampling_axis={}):
2739
+ source_axes = nxdata_source.axes
2740
+ if isinstance(source_axes, str):
2741
+ source_axes = [source_axes]
2742
+ axes = []
2743
+ for dim in source_axes:
2744
+ axes.append(dim)
2591
2745
  if dim in oversampling_axis:
2592
2746
  bin_name = dim.replace('fly_', 'bin_')
2593
2747
  axes[axes.index(dim)] = bin_name
2594
2748
  nxgroup[bin_name] = NXfield(
2595
2749
  value=oversampling_axis[dim],
2596
- units=nxentry.data[dim].units,
2750
+ units=nxdata_source[dim].units,
2597
2751
  attrs={
2598
2752
  'long_name':
2599
- f'oversampled {nxentry.data[dim].long_name}',
2600
- 'data_type': nxentry.data[dim].data_type,
2601
- 'local_name':
2602
- f'oversampled {nxentry.data[dim].local_name}'})
2753
+ f'oversampled {nxdata_source[dim].long_name}',
2754
+ 'data_type': nxdata_source[dim].data_type,
2755
+ 'local_name':
2756
+ f'oversampled {nxdata_source[dim].local_name}'})
2603
2757
  else:
2604
- nxgroup.makelink(nxentry.data[dim])
2605
- if f'{dim}_indices' in nxentry.data.attrs:
2758
+ if isinstance(nxdata_source[dim], NXlinkfield):
2759
+ nxgroup[dim] = nxdata_source[dim]
2760
+ else:
2761
+ nxgroup.makelink(nxdata_source[dim])
2762
+ if f'{dim}_indices' in nxdata_source.attrs:
2606
2763
  nxgroup.attrs[f'{dim}_indices'] = \
2607
- nxentry.data.attrs[f'{dim}_indices']
2608
- nxgroup.attrs['axes'] = axes
2764
+ nxdata_source.attrs[f'{dim}_indices']
2609
2765
  for dims in field_dims:
2610
- nxgroup.attrs[f'{dims["axes"]}_indices'] = dims['index']
2766
+ axes.append(dims['axis'])
2767
+ nxgroup.attrs[f'{dims["axis"]}_indices'] = dims['index']
2768
+ nxgroup.attrs['axes'] = axes
2611
2769
 
2612
- if not interactive and not strain_analysis_config.materials:
2770
+ if not self._interactive and not strain_analysis_config.materials:
2613
2771
  raise ValueError(
2614
2772
  'No material provided. Provide a material in the '
2615
2773
  'StrainAnalysis Configuration, or re-run the pipeline with '
2616
2774
  'the --interactive flag.')
2617
2775
 
2776
+ # Get the map configuration
2777
+ map_config = MapConfig(**loads(str(nxentry.map_config)))
2778
+
2618
2779
  # Create the NXroot object
2619
2780
  nxroot = NXroot()
2620
- nxroot[map_config.title] = MapProcessor.get_nxentry(map_config)
2621
- nxentry = nxroot[map_config.title]
2781
+ nxroot[map_config.title] = nxentry
2622
2782
  nxroot[f'{map_config.title}_strainanalysis'] = NXprocess()
2623
2783
  nxprocess = nxroot[f'{map_config.title}_strainanalysis']
2624
- nxprocess.strain_analysis_config = dumps(strain_analysis_config.dict())
2784
+ nxprocess.strain_analysis_config = dumps(
2785
+ strain_analysis_config.dict())
2625
2786
 
2626
2787
  # Setup plottable data group
2627
- nxprocess.data = NXdata()
2628
- nxprocess.default = 'data'
2629
- nxdata = nxprocess.data
2630
- linkdims(nxdata)
2631
2788
 
2632
2789
  # Collect the raw MCA data
2633
- self.logger.debug(f'Reading data ...')
2634
- mca_data = strain_analysis_config.mca_data()
2635
- self.logger.debug(f'... done')
2636
- self.logger.debug(f'mca_data.shape: {mca_data.shape}')
2637
- if mca_data.ndim == 2:
2638
- mca_data_summed = mca_data
2790
+ if (strain_analysis_config.sum_axes
2791
+ and map_config.attrs['scan_type'] > 2):
2792
+ # FIX? Could make this a processor
2793
+ mca_data = self._get_sum_axes_data(
2794
+ nxentry[nxentry.default],
2795
+ map_config.attrs.get('fly_axis_labels', [])).astype(np.float64)
2796
+ nxprocess.data = self._nxdata
2797
+ nxdata = nxprocess.data
2639
2798
  else:
2640
- mca_data_summed = np.mean(
2641
- mca_data, axis=tuple(np.arange(1, mca_data.ndim-1)))
2642
- effective_map_shape = mca_data.shape[1:-1]
2643
- self.logger.debug(f'mca_data_summed.shape: {mca_data_summed.shape}')
2644
- self.logger.debug(f'effective_map_shape: {effective_map_shape}')
2799
+ mca_data = np.asarray(
2800
+ nxentry[nxentry.default].nxsignal[:,self._detector_indices,:],
2801
+ dtype=np.float64)
2802
+ nxprocess.data = NXdata()
2803
+ self._nxdata = nxprocess.data
2804
+ linkdims(self._nxdata, nxentry.data)
2805
+ self._nxdata.makelink(nxentry.data['detector_data'])
2806
+ self._nxdata.attrs['signal'] = 'detector_data'
2807
+ nxprocess.default = 'data'
2808
+ self.logger.debug(f'mca_data.shape: {mca_data.shape}')
2809
+ mca_data_mean = np.mean(mca_data, axis=0)
2810
+ self.logger.debug(f'mca_data_mean.shape: {mca_data_mean.shape}')
2645
2811
 
2646
2812
  # Check for oversampling axis and create the binned coordinates
2647
2813
  oversampling_axis = {}
@@ -2650,6 +2816,7 @@ class StrainAnalysisProcessor(Processor):
2650
2816
  # Local modules
2651
2817
  from CHAP.utils.general import rolling_average
2652
2818
 
2819
+ raise RuntimeError('oversampling needs testing')
2653
2820
  fly_axis = map_config.attrs.get('fly_axis_labels')[0]
2654
2821
  oversampling = strain_analysis_config.oversampling
2655
2822
  oversampling_axis[fly_axis] = rolling_average(
@@ -2661,129 +2828,27 @@ class StrainAnalysisProcessor(Processor):
2661
2828
  num=oversampling.get('num'),
2662
2829
  mode=oversampling.get('mode', 'valid'))
2663
2830
 
2664
- # Loop over the detectors to adjust the material properties
2665
- # and the mask and HKLs used in the strain analysis
2666
- baselines = []
2667
- for i, detector in enumerate(strain_analysis_config.detectors):
2831
+ # Get the energy masks
2832
+ energy_masks = self._get_energy_and_masks()
2668
2833
 
2669
- # Get and add the calibration info to the detector
2670
- calibration = [
2671
- d for d in calibration_config.detectors \
2672
- if d.detector_name == detector.detector_name][0]
2673
- detector.add_calibration(calibration)
2674
-
2675
- # Get the MCA bin energies
2676
- mca_bin_energies = detector.energies
2834
+ # Get and subtract the detector baselines
2835
+ baselines = self._get_baselines(mca_data_mean, energy_masks)
2836
+ if baselines:
2837
+ baselines = np.asarray(baselines)
2838
+ mca_data_mean -= baselines
2839
+ mca_data -= baselines
2677
2840
 
2678
- # Blank out data below 25 keV as well as the last bin
2679
- energy_mask = np.where(mca_bin_energies >= 25.0, 1, 0)
2680
- energy_mask[-1] = 0
2841
+ # Adjust the material properties
2842
+ self._adjust_material_props(
2843
+ mca_data_mean, strain_analysis_config.materials, energy_masks)
2681
2844
 
2682
- # Subtract the baseline
2683
- if detector.baseline:
2684
- # Local modules
2685
- from CHAP.edd.models import BaselineConfig
2686
- from CHAP.common.processor import ConstructBaseline
2687
-
2688
- if isinstance(detector.baseline, bool):
2689
- detector.baseline = BaselineConfig()
2690
- if save_figures:
2691
- filename = os.path.join(
2692
- outputdir,
2693
- f'{detector.detector_name}_strainanalysis_'
2694
- 'baseline.png')
2695
- else:
2696
- filename = None
2697
- baseline, baseline_config = \
2698
- ConstructBaseline.construct_baseline(
2699
- mca_data_summed[i], mask=energy_mask,
2700
- tol=detector.baseline.tol, lam=detector.baseline.lam,
2701
- max_iter=detector.baseline.max_iter,
2702
- title=
2703
- f'Baseline for detector {detector.detector_name}',
2704
- xlabel='Energy (keV)', ylabel='Intensity (counts)',
2705
- interactive=interactive, filename=filename)
2706
-
2707
- mca_data_summed[i] -= baseline
2708
- baselines.append(baseline)
2709
- detector.baseline.lam = baseline_config['lambda']
2710
- detector.baseline.attrs['num_iter'] = \
2711
- baseline_config['num_iter']
2712
- detector.baseline.attrs['error'] = baseline_config['error']
2713
-
2714
- # Interactively adjust the material properties based on the
2715
- # first detector calibration information and/or save figure
2716
- # ASK: extend to multiple detectors?
2717
- if not i and (interactive or save_figures):
2718
-
2719
- tth = detector.tth_calibrated
2720
- if save_figures:
2721
- filename = os.path.join(
2722
- outputdir,
2723
- f'{detector.detector_name}_strainanalysis_'
2724
- 'material_config.png')
2725
- else:
2726
- filename = None
2727
- strain_analysis_config.materials = select_material_params(
2728
- mca_bin_energies, mca_data_summed[i]*energy_mask, tth,
2729
- preselected_materials=strain_analysis_config.materials,
2730
- label='Sum of all spectra in the map',
2731
- interactive=interactive, filename=filename)
2732
- self.logger.debug(
2733
- f'materials: {strain_analysis_config.materials}')
2734
-
2735
- # Mask during calibration
2736
- calibration_bin_ranges = calibration.include_bin_ranges
2737
-
2738
- # Get the unique HKLs and lattice spacings for the strain
2739
- # analysis materials
2740
- hkls, ds = get_unique_hkls_ds(
2741
- strain_analysis_config.materials,
2742
- tth_tol=detector.hkl_tth_tol,
2743
- tth_max=detector.tth_max)
2744
-
2745
- # Interactively adjust the mask and HKLs used in the
2746
- # strain analysis
2747
- if save_figures:
2748
- filename = os.path.join(
2749
- outputdir,
2750
- f'{detector.detector_name}_strainanalysis_'
2751
- 'fit_mask_hkls.png')
2752
- else:
2753
- filename = None
2754
- include_bin_ranges, hkl_indices = \
2755
- select_mask_and_hkls(
2756
- mca_bin_energies, mca_data_summed[i]*energy_mask,
2757
- hkls, ds, detector.tth_calibrated,
2758
- preselected_bin_ranges=detector.include_bin_ranges,
2759
- preselected_hkl_indices=detector.hkl_indices,
2760
- detector_name=detector.detector_name,
2761
- ref_map=mca_data[i]*energy_mask,
2762
- calibration_bin_ranges=calibration_bin_ranges,
2763
- label='Sum of all spectra in the map',
2764
- interactive=interactive, filename=filename)
2765
- detector.include_energy_ranges = \
2766
- detector.get_include_energy_ranges(include_bin_ranges)
2767
- detector.hkl_indices = hkl_indices
2768
- self.logger.debug(
2769
- f'include_energy_ranges for detector {detector.detector_name}:'
2770
- f' {detector.include_energy_ranges}')
2771
- self.logger.debug(
2772
- f'hkl_indices for detector {detector.detector_name}:'
2773
- f' {detector.hkl_indices}')
2774
- if not detector.include_energy_ranges:
2775
- raise ValueError(
2776
- 'No value provided for include_energy_ranges. '
2777
- 'Provide them in the MCA Tth Calibration Configuration, '
2778
- 'or re-run the pipeline with the --interactive flag.')
2779
- if not detector.hkl_indices:
2780
- raise ValueError(
2781
- 'No value provided for hkl_indices. Provide them in '
2782
- 'the detector\'s MCA Tth Calibration Configuration, or'
2783
- ' re-run the pipeline with the --interactive flag.')
2845
+ # Get the mask and HKLs used in the strain analysis
2846
+ self._get_mask_hkls(
2847
+ mca_data, mca_data_mean, strain_analysis_config.materials,
2848
+ energy_masks)
2784
2849
 
2785
2850
  # Loop over the detectors to perform the strain analysis
2786
- for i, detector in enumerate(strain_analysis_config.detectors):
2851
+ for index, detector in enumerate(self._detectors):
2787
2852
 
2788
2853
  self.logger.info(f'Analysing detector {detector.detector_name}')
2789
2854
 
@@ -2807,44 +2872,32 @@ class StrainAnalysisProcessor(Processor):
2807
2872
  nxdetector.data = NXdata()
2808
2873
  det_nxdata = nxdetector.data
2809
2874
  linkdims(
2810
- det_nxdata,
2811
- {'axes': 'energy', 'index': len(effective_map_shape)},
2875
+ det_nxdata, self._nxdata,
2876
+ [{'axis': 'energy', 'index': mca_data.ndim-1}],
2812
2877
  oversampling_axis=oversampling_axis)
2813
2878
  mask = detector.mca_mask()
2814
2879
  energies = mca_bin_energies[mask]
2815
2880
  det_nxdata.energy = NXfield(value=energies, attrs={'units': 'keV'})
2816
- det_nxdata.intensity = NXfield(
2817
- dtype=np.float64,
2818
- shape=(*effective_map_shape, len(energies)),
2819
- attrs={'units': 'counts'})
2820
2881
  det_nxdata.tth = NXfield(
2821
2882
  dtype=np.float64,
2822
- shape=effective_map_shape,
2823
- attrs={'units':'degrees', 'long_name': '2\u03B8 (degrees)'}
2824
- )
2883
+ shape=(mca_data.shape[0]),
2884
+ attrs={'units':'degrees', 'long_name': '2\u03B8 (degrees)'})
2825
2885
  det_nxdata.uniform_microstrain = NXfield(
2826
2886
  dtype=np.float64,
2827
- shape=effective_map_shape,
2828
- attrs={'long_name':
2829
- 'Strain from uniform fit(\u03BC\u03B5)'})
2887
+ shape=(mca_data.shape[0]),
2888
+ attrs={'long_name': 'Strain from uniform fit(\u03BC\u03B5)'})
2830
2889
  det_nxdata.unconstrained_microstrain = NXfield(
2831
2890
  dtype=np.float64,
2832
- shape=effective_map_shape,
2833
- attrs={'long_name':
2891
+ shape=(mca_data.shape[0]),
2892
+ attrs={'long_name':
2834
2893
  'Strain from unconstrained fit(\u03BC\u03B5)'})
2835
2894
 
2836
- # Gather detector data
2837
- self.logger.debug(
2838
- f'Gathering detector data for {detector.detector_name}')
2839
- for map_index in np.ndindex(effective_map_shape):
2840
- if baselines:
2841
- det_nxdata.intensity[map_index] = \
2842
- (mca_data[i][map_index]-baselines[i]).astype(
2843
- np.float64)[mask]
2844
- else:
2845
- det_nxdata.intensity[map_index] = \
2846
- mca_data[i][map_index].astype(np.float64)[mask]
2847
- det_nxdata.summed_intensity = det_nxdata.intensity.sum(axis=-1)
2895
+ # Add detector data
2896
+ det_nxdata.intensity = NXfield(
2897
+ value=np.asarray([mca_data[i,index,:].astype(np.float64)[mask]
2898
+ for i in range(mca_data.shape[0])]),
2899
+ attrs={'units': 'counts'})
2900
+ det_nxdata.summed_intensity = det_nxdata.intensity.sum(axis=0)
2848
2901
 
2849
2902
  # Perform strain analysis
2850
2903
  self.logger.debug(
@@ -2868,9 +2921,9 @@ class StrainAnalysisProcessor(Processor):
2868
2921
  from CHAP.utils.general import index_nearest
2869
2922
 
2870
2923
  peaks = find_peaks_scipy(
2871
- mca_data_summed[i],
2924
+ mca_data_mean[index],
2872
2925
  height=(detector.rel_height_cutoff
2873
- * mca_data_summed[i][mask].max()),
2926
+ * mca_data_mean[index][mask].max()),
2874
2927
  width=5)
2875
2928
  heights = peaks[1]['peak_heights']
2876
2929
  widths = peaks[1]['widths']
@@ -2900,7 +2953,7 @@ class StrainAnalysisProcessor(Processor):
2900
2953
  continue
2901
2954
 
2902
2955
  # Perform the fit
2903
- self.logger.debug(f'Fitting {detector.detector_name} ...')
2956
+ self.logger.debug(f'Fitting detector {detector.detector_name} ...')
2904
2957
  (uniform_fit_centers, uniform_fit_centers_errors,
2905
2958
  uniform_fit_amplitudes, uniform_fit_amplitudes_errors,
2906
2959
  uniform_fit_sigmas, uniform_fit_sigmas_errors,
@@ -2924,8 +2977,8 @@ class StrainAnalysisProcessor(Processor):
2924
2977
  fit_nxgroup.results = NXdata()
2925
2978
  fit_nxdata = fit_nxgroup.results
2926
2979
  linkdims(
2927
- fit_nxdata,
2928
- {'axes': 'energy', 'index': len(map_config.shape)},
2980
+ fit_nxdata, self._nxdata,
2981
+ [{'axis': 'energy', 'index': mca_data.ndim-1}],
2929
2982
  oversampling_axis=oversampling_axis)
2930
2983
  fit_nxdata.makelink(det_nxdata.energy)
2931
2984
  fit_nxdata.best_fit = uniform_best_fit
@@ -2936,7 +2989,7 @@ class StrainAnalysisProcessor(Processor):
2936
2989
  # Peak-by-peak results
2937
2990
  # fit_nxgroup.fit_hkl_centers = NXdata()
2938
2991
  # fit_nxdata = fit_nxgroup.fit_hkl_centers
2939
- # linkdims(fit_nxdata)
2992
+ # linkdims(fit_nxdata, self._nxdata)
2940
2993
  for (hkl, center_guess, centers_fit, centers_error,
2941
2994
  amplitudes_fit, amplitudes_error, sigmas_fit,
2942
2995
  sigmas_error) in zip(
@@ -2952,7 +3005,7 @@ class StrainAnalysisProcessor(Processor):
2952
3005
  'keV'
2953
3006
  # Report HKL peak centers
2954
3007
  fit_nxgroup[hkl_name].centers = NXdata()
2955
- linkdims(fit_nxgroup[hkl_name].centers)
3008
+ linkdims(fit_nxgroup[hkl_name].centers, self._nxdata)
2956
3009
  fit_nxgroup[hkl_name].centers.values = NXfield(
2957
3010
  value=centers_fit, attrs={'units': 'keV'})
2958
3011
  fit_nxgroup[hkl_name].centers.errors = NXfield(
@@ -2962,7 +3015,7 @@ class StrainAnalysisProcessor(Processor):
2962
3015
  # fit_nxgroup[f'{hkl_name}/centers/values'], name=hkl_name)
2963
3016
  # Report HKL peak amplitudes
2964
3017
  fit_nxgroup[hkl_name].amplitudes = NXdata()
2965
- linkdims(fit_nxgroup[hkl_name].amplitudes)
3018
+ linkdims(fit_nxgroup[hkl_name].amplitudes, self._nxdata)
2966
3019
  fit_nxgroup[hkl_name].amplitudes.values = NXfield(
2967
3020
  value=amplitudes_fit, attrs={'units': 'counts'})
2968
3021
  fit_nxgroup[hkl_name].amplitudes.errors = NXfield(
@@ -2970,62 +3023,61 @@ class StrainAnalysisProcessor(Processor):
2970
3023
  fit_nxgroup[hkl_name].amplitudes.attrs['signal'] = 'values'
2971
3024
  # Report HKL peak FWHM
2972
3025
  fit_nxgroup[hkl_name].sigmas = NXdata()
2973
- linkdims(fit_nxgroup[hkl_name].sigmas)
3026
+ linkdims(fit_nxgroup[hkl_name].sigmas, self._nxdata)
2974
3027
  fit_nxgroup[hkl_name].sigmas.values = NXfield(
2975
3028
  value=sigmas_fit, attrs={'units': 'keV'})
2976
3029
  fit_nxgroup[hkl_name].sigmas.errors = NXfield(
2977
3030
  value=sigmas_error)
2978
3031
  fit_nxgroup[hkl_name].sigmas.attrs['signal'] = 'values'
2979
3032
 
2980
- if interactive or save_figures:
3033
+ if ((self._interactive or self._save_figures)
3034
+ and not skip_animation):
2981
3035
  # Third party modules
2982
3036
  import matplotlib.animation as animation
2983
3037
  import matplotlib.pyplot as plt
2984
3038
 
2985
- if save_figures:
3039
+ if self._save_figures:
2986
3040
  path = os.path.join(
2987
- outputdir,
3041
+ self._outputdir,
2988
3042
  f'{detector.detector_name}_strainanalysis_'
2989
3043
  'unconstrained_fits')
2990
3044
  if not os.path.isdir(path):
2991
3045
  os.mkdir(path)
2992
3046
 
2993
3047
  def animate(i):
2994
- map_index = np.unravel_index(i, effective_map_shape)
2995
- norm = det_nxdata.intensity.nxdata[map_index].max()
2996
- intensity.set_ydata(
2997
- det_nxdata.intensity.nxdata[map_index] / norm)
2998
- best_fit.set_ydata(
2999
- unconstrained_best_fit[map_index] / norm)
3048
+ norm = det_nxdata.intensity.nxdata[i].max()
3049
+ intensity.set_ydata(det_nxdata.intensity.nxdata[i] / norm)
3050
+ best_fit.set_ydata(unconstrained_best_fit[i] / norm)
3051
+ axes = self._nxdata.attrs['axes']
3052
+ if isinstance(axes, str):
3053
+ axes = [axes]
3000
3054
  index.set_text('\n'.join(
3001
3055
  [f'norm = {int(norm)}'] +
3002
3056
  ['relative norm = '
3003
3057
  f'{(norm / det_nxdata.intensity.max()):.5f}'] +
3004
- [f'{k}[{i}] = {v}'
3005
- for k, v in map_config.get_coords(map_index).items()]))
3006
- if save_figures:
3058
+ [f'{dim}[{i}] = {self._nxdata[dim][i]}'
3059
+ for dim in axes]))
3060
+ if self._save_figures:
3007
3061
  plt.savefig(os.path.join(
3008
3062
  path, f'frame_{str(i).zfill(num_digit)}.png'))
3009
3063
  return intensity, best_fit, index
3010
3064
 
3011
3065
  fig, ax = plt.subplots()
3012
- effective_map_shape
3013
- map_index = np.unravel_index(0, effective_map_shape)
3014
3066
  data_normalized = (
3015
- det_nxdata.intensity.nxdata[map_index]
3016
- / det_nxdata.intensity.nxdata[map_index].max())
3067
+ det_nxdata.intensity.nxdata[0]
3068
+ / det_nxdata.intensity.nxdata[0].max())
3017
3069
  intensity, = ax.plot(
3018
3070
  energies, data_normalized, 'b.', label='data')
3019
- if unconstrained_best_fit[map_index].max():
3071
+ if unconstrained_best_fit[0].max():
3020
3072
  fit_normalized = (
3021
- unconstrained_best_fit[map_index]
3022
- / unconstrained_best_fit[map_index].max())
3073
+ unconstrained_best_fit[0]
3074
+ / unconstrained_best_fit[0].max())
3023
3075
  else:
3024
- fit_normalized = unconstrained_best_fit[map_index]
3076
+ fit_normalized = unconstrained_best_fit[0]
3025
3077
  best_fit, = ax.plot(
3026
3078
  energies, fit_normalized, 'k-', label='fit')
3027
3079
  # residual, = ax.plot(
3028
- # energies, unconstrained_residuals[map_index], 'r-',
3080
+ # energies, unconstrained_residuals[0], 'r-',
3029
3081
  # label='residual')
3030
3082
  ax.set(
3031
3083
  title='Unconstrained fits',
@@ -3038,7 +3090,7 @@ class StrainAnalysisProcessor(Processor):
3038
3090
  num_frame = int(det_nxdata.intensity.nxdata.size
3039
3091
  / det_nxdata.intensity.nxdata.shape[-1])
3040
3092
  num_digit = len(str(num_frame))
3041
- if not save_figures:
3093
+ if not self._save_figures:
3042
3094
  ani = animation.FuncAnimation(
3043
3095
  fig, animate,
3044
3096
  frames=int(det_nxdata.intensity.nxdata.size
@@ -3066,18 +3118,18 @@ class StrainAnalysisProcessor(Processor):
3066
3118
  plt.gcf(), frames, interval=1000, blit=True,
3067
3119
  repeat=False)
3068
3120
 
3069
- if interactive:
3121
+ if self._interactive:
3070
3122
  plt.show()
3071
3123
 
3072
- if save_figures:
3124
+ if self._save_figures:
3073
3125
  path = os.path.join(
3074
- outputdir,
3126
+ self._outputdir,
3075
3127
  f'{detector.detector_name}_strainanalysis_'
3076
3128
  'unconstrained_fits.gif')
3077
3129
  ani.save(path)
3078
3130
  plt.close()
3079
3131
 
3080
- tth_map = detector.get_tth_map(effective_map_shape)
3132
+ tth_map = detector.get_tth_map((mca_data.shape[0],))
3081
3133
  det_nxdata.tth.nxdata = tth_map
3082
3134
  nominal_centers = np.asarray(
3083
3135
  [get_peak_locations(d0, tth_map)
@@ -3101,8 +3153,8 @@ class StrainAnalysisProcessor(Processor):
3101
3153
  fit_nxgroup.results = NXdata()
3102
3154
  fit_nxdata = fit_nxgroup.results
3103
3155
  linkdims(
3104
- fit_nxdata,
3105
- {'axes': 'energy', 'index': len(map_config.shape)},
3156
+ fit_nxdata, self._nxdata,
3157
+ [{'axis': 'energy', 'index': mca_data.ndim-1}],
3106
3158
  oversampling_axis=oversampling_axis)
3107
3159
  fit_nxdata.makelink(det_nxdata.energy)
3108
3160
  fit_nxdata.best_fit= unconstrained_best_fit
@@ -3113,7 +3165,7 @@ class StrainAnalysisProcessor(Processor):
3113
3165
  # Peak-by-peak results
3114
3166
  fit_nxgroup.fit_hkl_centers = NXdata()
3115
3167
  fit_nxdata = fit_nxgroup.fit_hkl_centers
3116
- linkdims(fit_nxdata)
3168
+ linkdims(fit_nxdata, self._nxdata)
3117
3169
  for (hkl, center_guesses, centers_fit, centers_error,
3118
3170
  amplitudes_fit, amplitudes_error, sigmas_fit,
3119
3171
  sigmas_error) in zip(
@@ -3127,7 +3179,7 @@ class StrainAnalysisProcessor(Processor):
3127
3179
  fit_nxgroup[hkl_name] = NXparameters()
3128
3180
  # Report initial guesses HKL peak centers
3129
3181
  fit_nxgroup[hkl_name].center_initial_guess = NXdata()
3130
- linkdims(fit_nxgroup[hkl_name].center_initial_guess)
3182
+ linkdims(fit_nxgroup[hkl_name].center_initial_guess, self._nxdata)
3131
3183
  fit_nxgroup[hkl_name].center_initial_guess.makelink(
3132
3184
  nxdetector.uniform_fit[f'{hkl_name}/centers/values'],
3133
3185
  name='values')
@@ -3135,7 +3187,7 @@ class StrainAnalysisProcessor(Processor):
3135
3187
  'values'
3136
3188
  # Report HKL peak centers
3137
3189
  fit_nxgroup[hkl_name].centers = NXdata()
3138
- linkdims(fit_nxgroup[hkl_name].centers)
3190
+ linkdims(fit_nxgroup[hkl_name].centers, self._nxdata)
3139
3191
  fit_nxgroup[hkl_name].centers.values = NXfield(
3140
3192
  value=centers_fit, attrs={'units': 'keV'})
3141
3193
  fit_nxgroup[hkl_name].centers.errors = NXfield(
@@ -3145,7 +3197,7 @@ class StrainAnalysisProcessor(Processor):
3145
3197
  fit_nxgroup[hkl_name].centers.attrs['signal'] = 'values'
3146
3198
  # Report HKL peak amplitudes
3147
3199
  fit_nxgroup[hkl_name].amplitudes = NXdata()
3148
- linkdims(fit_nxgroup[hkl_name].amplitudes)
3200
+ linkdims(fit_nxgroup[hkl_name].amplitudes, self._nxdata)
3149
3201
  fit_nxgroup[hkl_name].amplitudes.values = NXfield(
3150
3202
  value=amplitudes_fit, attrs={'units': 'counts'})
3151
3203
  fit_nxgroup[hkl_name].amplitudes.errors = NXfield(
@@ -3153,7 +3205,7 @@ class StrainAnalysisProcessor(Processor):
3153
3205
  fit_nxgroup[hkl_name].amplitudes.attrs['signal'] = 'values'
3154
3206
  # Report HKL peak sigmas
3155
3207
  fit_nxgroup[hkl_name].sigmas = NXdata()
3156
- linkdims(fit_nxgroup[hkl_name].sigmas)
3208
+ linkdims(fit_nxgroup[hkl_name].sigmas, self._nxdata)
3157
3209
  fit_nxgroup[hkl_name].sigmas.values = NXfield(
3158
3210
  value=sigmas_fit, attrs={'units': 'keV'})
3159
3211
  fit_nxgroup[hkl_name].sigmas.errors = NXfield(
@@ -3162,25 +3214,164 @@ class StrainAnalysisProcessor(Processor):
3162
3214
 
3163
3215
  return nxroot
3164
3216
 
3217
+ def _get_sum_axes_data(self, nxdata, sum_axes):
3218
+ """Get the raw MCA data collected by the scan averaged over the
3219
+ sum_axes.
3220
+ """
3221
+ # Third party modules
3222
+ from nexusformat.nexus import (
3223
+ NXdata,
3224
+ NXfield,
3225
+ )
3165
3226
 
3166
- class CreateStrainAnalysisConfigProcessor(Processor):
3167
- """Processor that takes a basics stain analysis config file
3168
- (the old style configuration without the map_config) and the output
3169
- of EddMapReader and returns the old style stain analysis
3170
- configuration.
3171
- """
3172
- def process(self, data, inputdir='.'):
3227
+ mca_data = np.asarray(
3228
+ nxdata.nxsignal[:,self._detector_indices,:])
3229
+ axes = [nxdata[axis] for axis in nxdata.axes if axis not in sum_axes]
3230
+ unique_points = []
3231
+ sum_indices = []
3232
+ for i in range(mca_data.shape[0]):
3233
+ point = [float(nxdata[axis.nxname][i]) for axis in axes]
3234
+ try:
3235
+ sum_indices[unique_points.index(point)].append(i)
3236
+ except:
3237
+ unique_points.append(point)
3238
+ sum_indices.append([i])
3239
+ mean_mca_data = np.empty((len(unique_points), *mca_data.shape[1:]))
3240
+ for i in range(len(unique_points)):
3241
+ mean_mca_data[i] = np.mean(mca_data[sum_indices[i],:,:], axis=0)
3242
+ self._nxdata = NXdata(
3243
+ NXfield(mean_mca_data, 'detector_data'),
3244
+ tuple([
3245
+ NXfield(
3246
+ [p[i] for p in unique_points], axis.nxname,
3247
+ attrs=axis.attrs)
3248
+ for i, axis in enumerate(axes)]))
3249
+ return mean_mca_data
3250
+
3251
+ def _get_energy_and_masks(self):
3252
+ """Get the energy mask by blanking out data below 25 keV as
3253
+ well as that in the last bin.
3254
+ """
3255
+ energy_masks = []
3256
+ for detector in self._detectors:
3257
+ energy_mask = np.where(detector.energies >= 25.0, 1, 0)
3258
+ energy_mask[-1] = 0
3259
+ energy_masks.append(energy_mask)
3260
+ return energy_masks
3261
+
3262
+ def _get_baselines(self, mca_data_mean, energy_masks):
3263
+ """Get the detector baselines."""
3173
3264
  # Local modules
3174
- from CHAP.common.models.map import MapConfig
3265
+ from CHAP.edd.models import BaselineConfig
3266
+ from CHAP.common.processor import ConstructBaseline
3175
3267
 
3176
- map_config = self.get_config(
3177
- data, 'common.models.map.MapConfig', inputdir=inputdir)
3178
- config = self.get_config(
3179
- data, 'edd.models.StrainAnalysisConfig', inputdir=inputdir)
3180
- config.map_config = map_config
3268
+ baselines = []
3269
+ for index, detector in enumerate(self._detectors):
3270
+ if detector.baseline:
3271
+ if isinstance(detector.baseline, bool):
3272
+ detector.baseline = BaselineConfig()
3273
+ if self._save_figures:
3274
+ filename = os.path.join(
3275
+ self._outputdir,
3276
+ f'{detector.detector_name}_strainanalysis_'
3277
+ 'baseline.png')
3278
+ else:
3279
+ filename = None
3280
+ baseline, baseline_config = \
3281
+ ConstructBaseline.construct_baseline(
3282
+ mca_data_mean[index], mask=energy_masks[index],
3283
+ tol=detector.baseline.tol, lam=detector.baseline.lam,
3284
+ max_iter=detector.baseline.max_iter,
3285
+ title=
3286
+ f'Baseline for detector {detector.detector_name}',
3287
+ xlabel='Energy (keV)', ylabel='Intensity (counts)',
3288
+ interactive=self._interactive, filename=filename)
3289
+
3290
+ baselines.append(baseline)
3291
+ detector.baseline.lam = baseline_config['lambda']
3292
+ detector.baseline.attrs['num_iter'] = \
3293
+ baseline_config['num_iter']
3294
+ detector.baseline.attrs['error'] = baseline_config['error']
3295
+ return baselines
3296
+
3297
+ def _adjust_material_props(self, mca_data_mean, materials, energy_masks):
3298
+ """Adjust the material properties."""
3299
+ # Local modules
3300
+ from CHAP.edd.utils import select_material_params
3301
+
3302
+ # ASK: extend to multiple detectors?
3303
+ detector = self._detectors[0]
3304
+ tth = detector.tth_calibrated
3305
+ if self._save_figures:
3306
+ filename = os.path.join(
3307
+ self._outputdir,
3308
+ f'{detector.detector_name}_strainanalysis_'
3309
+ 'material_config.png')
3310
+ else:
3311
+ filename = None
3312
+ materials = select_material_params(
3313
+ detector.energies, mca_data_mean[0]*energy_masks[0],
3314
+ tth, label='Sum of all spectra in the map',
3315
+ preselected_materials=materials, interactive=self._interactive,
3316
+ filename=filename)
3317
+ self.logger.debug(f'materials: {materials}')
3318
+
3319
+ def _get_mask_hkls(self, mca_data, mca_data_mean, materials, energy_masks):
3320
+ """Get the mask and HKLs used in the strain analysis."""
3321
+ # Local modules
3322
+ from CHAP.edd.utils import (
3323
+ get_unique_hkls_ds,
3324
+ select_mask_and_hkls,
3325
+ )
3326
+
3327
+ for index, detector in enumerate(self._detectors):
3181
3328
 
3182
- return config
3329
+ # Get the unique HKLs and lattice spacings for the strain
3330
+ # analysis materials
3331
+ hkls, ds = get_unique_hkls_ds(
3332
+ materials, tth_tol=detector.hkl_tth_tol,
3333
+ tth_max=detector.tth_max)
3183
3334
 
3335
+ # Interactively adjust the mask and HKLs used in the
3336
+ # strain analysis
3337
+ if self._save_figures:
3338
+ filename = os.path.join(
3339
+ self._outputdir,
3340
+ f'{detector.detector_name}_strainanalysis_'
3341
+ 'fit_mask_hkls.png')
3342
+ else:
3343
+ filename = None
3344
+ include_bin_ranges, hkl_indices = \
3345
+ select_mask_and_hkls(
3346
+ detector.energies,
3347
+ mca_data_mean[index]*energy_masks[index],
3348
+ hkls, ds, detector.tth_calibrated,
3349
+ preselected_bin_ranges=detector.include_bin_ranges,
3350
+ preselected_hkl_indices=detector.hkl_indices,
3351
+ detector_name=detector.detector_name,
3352
+ ref_map=mca_data[:,index,:]*energy_masks[index],
3353
+ calibration_bin_ranges=detector.calibration_bin_ranges,
3354
+ label='Sum of all spectra in the map',
3355
+ interactive=self._interactive, filename=filename)
3356
+ detector.include_energy_ranges = \
3357
+ detector.get_include_energy_ranges(include_bin_ranges)
3358
+ detector.hkl_indices = hkl_indices
3359
+ self.logger.debug(
3360
+ f'include_energy_ranges for detector {detector.detector_name}:'
3361
+ f' {detector.include_energy_ranges}')
3362
+ self.logger.debug(
3363
+ f'hkl_indices for detector {detector.detector_name}:'
3364
+ f' {detector.hkl_indices}')
3365
+ if not detector.include_energy_ranges:
3366
+ raise ValueError(
3367
+ 'No value provided for include_energy_ranges. '
3368
+ 'Provide them in the MCA Tth Calibration Configuration, '
3369
+ 'or re-run the pipeline with the --interactive flag.')
3370
+ if not detector.hkl_indices:
3371
+ raise ValueError(
3372
+ 'No value provided for hkl_indices. Provide them in '
3373
+ 'the detector\'s MCA Tth Calibration Configuration, or'
3374
+ ' re-run the pipeline with the --interactive flag.')
3184
3375
 
3185
3376
  if __name__ == '__main__':
3186
3377
  # Local modules