ChessAnalysisPipeline 0.0.12__py3-none-any.whl → 0.0.14__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/utils/scanparsers.py CHANGED
@@ -15,6 +15,9 @@ import numpy as np
15
15
  from pyspec.file.spec import FileSpec
16
16
  from pyspec.file.tiff import TiffFile
17
17
 
18
+ @cache
19
+ def filespec(spec_file_name):
20
+ return FileSpec(spec_file_name)
18
21
 
19
22
  class ScanParser:
20
23
  """Partial implementation of a class representing a SPEC scan and
@@ -49,7 +52,12 @@ class ScanParser:
49
52
 
50
53
  self._detector_data_path = None
51
54
 
52
- def __repr__(self):
55
+ if isinstance(self, FMBRotationScanParser) and scan_number > 1:
56
+ scanparser = FMBRotationScanParser(spec_file_name, scan_number-1)
57
+ if (scanparser.spec_macro in ('rams4_step_ome', 'rams4_fly_ome')
58
+ and len(scanparser.spec_args) == 5):
59
+ self._rams4_args = scanparser.spec_args
60
+
53
61
  return (f'{self.__class__.__name__}'
54
62
  f'({self.spec_file_name}, {self.scan_number}) '
55
63
  f'-- {self.spec_command}')
@@ -59,7 +67,7 @@ class ScanParser:
59
67
  # NB This FileSpec instance is not stored as a private
60
68
  # attribute because it cannot be pickled (and therefore could
61
69
  # cause problems for parallel code that uses ScanParsers).
62
- return FileSpec(self.spec_file_name)
70
+ return filespec(self.spec_file_name)
63
71
 
64
72
  @property
65
73
  def scan_path(self):
@@ -318,6 +326,10 @@ class SMBScanParser(ScanParser):
318
326
  json_files = fnmatch_filter(
319
327
  os.listdir(self.scan_path),
320
328
  f'{self._par_file_pattern}.json')
329
+ if not json_files:
330
+ json_files = fnmatch_filter(
331
+ os.listdir(self.scan_path),
332
+ f'*.json')
321
333
  if len(json_files) != 1:
322
334
  raise RuntimeError(f'{self.scan_title}: cannot find the '
323
335
  '.json file to decode the .par file')
@@ -331,15 +343,19 @@ class SMBScanParser(ScanParser):
331
343
  raise RuntimeError(f'{self.scan_title}: cannot find scan pars '
332
344
  'without a "SCAN_N" column in the par file')
333
345
 
334
- par_files = fnmatch_filter(
335
- os.listdir(self.scan_path),
336
- f'{self._par_file_pattern}.par')
337
- if len(par_files) != 1:
338
- raise RuntimeError(f'{self.scan_title}: cannot find the .par '
339
- 'file for this scan directory')
346
+ if hasattr(self, '_par_file'):
347
+ par_file = self._par_file
348
+ else:
349
+ par_files = fnmatch_filter(
350
+ os.listdir(self.scan_path),
351
+ f'{self._par_file_pattern}.par')
352
+ if len(par_files) != 1:
353
+ raise RuntimeError(f'{self.scan_title}: cannot find the .par '
354
+ 'file for this scan directory')
355
+ par_file = os.path.join(self.scan_path, par_files[0])
340
356
  par_dict = None
341
- with open(os.path.join(self.scan_path, par_files[0])) as par_file:
342
- par_reader = reader(par_file, delimiter=' ')
357
+ with open(par_file) as f:
358
+ par_reader = reader(f, delimiter=' ')
343
359
  for row in par_reader:
344
360
  if len(row) == len(par_col_names):
345
361
  row_scann = int(row[scann_col_idx])
@@ -526,8 +542,18 @@ class FMBLinearScanParser(LinearScanParser, FMBScanParser):
526
542
  """
527
543
 
528
544
  def get_spec_scan_motor_mnes(self):
529
- if self.spec_macro == 'flymesh':
530
- return (self.spec_args[0], self.spec_args[5])
545
+ if self.spec_macro in ('flymesh', 'mesh', 'flydmesh', 'dmesh'):
546
+ m1_mne = self.spec_args[0]
547
+ try:
548
+ # Try post-summer-2022 format
549
+ dwell = float(self.spec_args[4])
550
+ except:
551
+ # Accommodate pre-summer-2022 format
552
+ m2_mne_i = 4
553
+ else:
554
+ m2_mne_i = 5
555
+ m2_mne = self.spec_args[m2_mne_i]
556
+ return (m1_mne, m2_mne)
531
557
  if self.spec_macro in ('flyscan', 'ascan'):
532
558
  return (self.spec_args[0],)
533
559
  if self.spec_macro in ('tseries', 'loopscan'):
@@ -536,13 +562,27 @@ class FMBLinearScanParser(LinearScanParser, FMBScanParser):
536
562
  f'for scans of type {self.spec_macro}')
537
563
 
538
564
  def get_spec_scan_motor_vals(self):
539
- if self.spec_macro == 'flymesh':
540
- fast_mot_vals = np.linspace(float(self.spec_args[1]),
541
- float(self.spec_args[2]),
542
- int(self.spec_args[3])+1)
543
- slow_mot_vals = np.linspace(float(self.spec_args[6]),
544
- float(self.spec_args[7]),
545
- int(self.spec_args[8])+1)
565
+ if self.spec_macro in ('flymesh', 'mesh', 'flydmesh', 'dmesh'):
566
+ m1_start = float(self.spec_args[1])
567
+ m1_end = float(self.spec_args[2])
568
+ m1_npt = int(self.spec_args[3]) + 1
569
+ try:
570
+ # Try post-summer-2022 format
571
+ dwell = float(self.spec_args[4])
572
+ except:
573
+ # Accommodate pre-summer-2022 format
574
+ m2_start_i = 5
575
+ m2_end_i = 6
576
+ m2_nint_i = 7
577
+ else:
578
+ m2_start_i = 6
579
+ m2_end_i = 7
580
+ m2_nint_i = 8
581
+ m2_start = float(self.spec_args[m2_start_i])
582
+ m2_end = float(self.spec_args[m2_end_i])
583
+ m2_npt = int(self.spec_args[m2_nint_i]) + 1
584
+ fast_mot_vals = np.linspace(m1_start, m1_end, m1_npt)
585
+ slow_mot_vals = np.linspace(m2_start, m2_end, m2_npt)
546
586
  return (fast_mot_vals, slow_mot_vals)
547
587
  if self.spec_macro in ('flyscan', 'ascan'):
548
588
  mot_vals = np.linspace(float(self.spec_args[1]),
@@ -555,9 +595,17 @@ class FMBLinearScanParser(LinearScanParser, FMBScanParser):
555
595
  f'for scans of type {self.spec_macro}')
556
596
 
557
597
  def get_spec_scan_shape(self):
558
- if self.spec_macro == 'flymesh':
559
- fast_mot_npts = int(self.spec_args[3])+1
560
- slow_mot_npts = int(self.spec_args[8])+1
598
+ if self.spec_macro in ('flymesh', 'mesh', 'flydmesh', 'dmesh'):
599
+ fast_mot_npts = int(self.spec_args[3]) + 1
600
+ try:
601
+ # Try post-summer-2022 format
602
+ dwell = float(self.spec_args[4])
603
+ except:
604
+ # Accommodate pre-summer-2022 format
605
+ m2_nint_i = 7
606
+ else:
607
+ m2_nint_i = 8
608
+ slow_mot_npts = int(self.spec_args[m2_nint_i]) + 1
561
609
  return (fast_mot_npts, slow_mot_npts)
562
610
  if self.spec_macro in ('flyscan', 'ascan'):
563
611
  mot_npts = int(self.spec_args[3])+1
@@ -568,7 +616,15 @@ class FMBLinearScanParser(LinearScanParser, FMBScanParser):
568
616
  f'for scans of type {self.spec_macro}')
569
617
 
570
618
  def get_spec_scan_dwell(self):
571
- if self.spec_macro in ('flymesh', 'flyscan', 'ascan'):
619
+ if self.macro in ('flymesh', 'mesh', 'flydmesh', 'dmesh'):
620
+ try:
621
+ # Try post-summer-2022 format
622
+ dwell = float(self.spec_args[4])
623
+ except:
624
+ # Accommodate pre-summer-2022 format
625
+ dwell = float(self.spec_args[8])
626
+ return dwell
627
+ if self.spec_macro in ('flyscan', 'ascan'):
572
628
  return float(self.spec_args[4])
573
629
  if self.spec_macro in ('tseries', 'loopscan'):
574
630
  return float(self.spec_args[1])
@@ -609,8 +665,10 @@ class FMBSAXSWAXSScanParser(FMBLinearScanParser):
609
665
  return f'{self.scan_name}_{self.scan_number:03d}'
610
666
 
611
667
  def get_detector_data_file(self, detector_prefix, scan_step_index:int):
612
- detector_files = list_fmb_saxswaxs_detector_files(self.detector_data_path, detector_prefix)
613
- return os.path.join(self.detector_data_path, detector_files[scan_step_index])
668
+ detector_files = list_fmb_saxswaxs_detector_files(
669
+ self.detector_data_path, detector_prefix)
670
+ return os.path.join(
671
+ self.detector_data_path, detector_files[scan_step_index])
614
672
 
615
673
  def get_detector_data(self, detector_prefix, scan_step_index:int):
616
674
  image_file = self.get_detector_data_file(detector_prefix,
@@ -657,7 +715,15 @@ class SMBLinearScanParser(LinearScanParser, SMBScanParser):
657
715
  """
658
716
 
659
717
  def get_spec_scan_dwell(self):
660
- if self.spec_macro in ('flymesh', 'flyscan', 'ascan'):
718
+ if self.spec_macro in ('flymesh', 'mesh'):
719
+ try:
720
+ # Try post-summer-2022 format
721
+ dwell = float(self.spec_args[4])
722
+ except:
723
+ # Accommodate pre-summer-2022 format
724
+ dwell = float(self.spec_args[8])
725
+ return dwell
726
+ if self.spec_macro in ('flyscan', 'ascan'):
661
727
  return float(self.spec_args[4])
662
728
  if self.spec_macro == 'tseries':
663
729
  return float(self.spec_args[1])
@@ -667,8 +733,18 @@ class SMBLinearScanParser(LinearScanParser, SMBScanParser):
667
733
  f'for scans of type {self.spec_macro}')
668
734
 
669
735
  def get_spec_scan_motor_mnes(self):
670
- if self.spec_macro == 'flymesh':
671
- return (self.spec_args[0], self.spec_args[5])
736
+ if self.spec_macro in ('flymesh', 'mesh'):
737
+ m1_mne = self.spec_args[0]
738
+ try:
739
+ # Try post-summer-2022 format
740
+ dwell = float(self.spec_args[4])
741
+ except:
742
+ # Accommodate pre-summer-2022 format
743
+ m2_mne_i = 4
744
+ else:
745
+ m2_mne_i = 5
746
+ m2_mne = self.spec_args[m2_mne_i]
747
+ return (m1_mne, m2_mne)
672
748
  if self.spec_macro in ('flyscan', 'ascan'):
673
749
  return (self.spec_args[0],)
674
750
  if self.spec_macro in ('tseries', 'loopscan'):
@@ -677,13 +753,27 @@ class SMBLinearScanParser(LinearScanParser, SMBScanParser):
677
753
  f'for scans of type {self.spec_macro}')
678
754
 
679
755
  def get_spec_scan_motor_vals(self):
680
- if self.spec_macro == 'flymesh':
681
- fast_mot_vals = np.linspace(float(self.spec_args[1]),
682
- float(self.spec_args[2]),
683
- int(self.spec_args[3])+1)
684
- slow_mot_vals = np.linspace(float(self.spec_args[6]),
685
- float(self.spec_args[7]),
686
- int(self.spec_args[8])+1)
756
+ if self.spec_macro in ('flymesh', 'mesh'):
757
+ m1_start = float(self.spec_args[1])
758
+ m1_end = float(self.spec_args[2])
759
+ m1_npt = int(self.spec_args[3]) + 1
760
+ try:
761
+ # Try post-summer-2022 format
762
+ dwell = float(self.spec_args[4])
763
+ except:
764
+ # Accommodate pre-summer-2022 format
765
+ m2_start_i = 5
766
+ m2_end_i = 6
767
+ m2_nint_i = 7
768
+ else:
769
+ m2_start_i = 6
770
+ m2_end_i = 7
771
+ m2_nint_i = 8
772
+ m2_start = float(self.spec_args[m2_start_i])
773
+ m2_end = float(self.spec_args[m2_end_i])
774
+ m2_npt = int(self.spec_args[m2_nint_i]) + 1
775
+ fast_mot_vals = np.linspace(m1_start, m1_end, m1_npt)
776
+ slow_mot_vals = np.linspace(m2_start, m2_end, m2_npt)
687
777
  return (fast_mot_vals, slow_mot_vals)
688
778
  if self.spec_macro in ('flyscan', 'ascan'):
689
779
  mot_vals = np.linspace(float(self.spec_args[1]),
@@ -691,31 +781,31 @@ class SMBLinearScanParser(LinearScanParser, SMBScanParser):
691
781
  int(self.spec_args[3])+1)
692
782
  return (mot_vals,)
693
783
  if self.spec_macro in ('tseries', 'loopscan'):
694
- return self.spec_scan.data[:,0]
784
+ return (self.spec_scan.data[:,0],)
695
785
  raise RuntimeError(f'{self.scan_title}: cannot determine scan motors '
696
786
  f'for scans of type {self.spec_macro}')
697
787
 
698
788
  def get_spec_scan_shape(self):
699
- if self.spec_macro == 'flymesh':
700
- fast_mot_npts = int(self.spec_args[3])+1
701
- slow_mot_npts = int(self.spec_args[8])+1
789
+ if self.spec_macro in ('flymesh', 'mesh'):
790
+ fast_mot_npts = int(self.spec_args[3]) + 1
791
+ try:
792
+ # Try post-summer-2022 format
793
+ dwell = float(self.spec_args[4])
794
+ except:
795
+ # Accommodate pre-summer-2022 format
796
+ m2_nint_i = 7
797
+ else:
798
+ m2_nint_i = 8
799
+ slow_mot_npts = int(self.spec_args[m2_nint_i]) + 1
702
800
  return (fast_mot_npts, slow_mot_npts)
703
801
  if self.spec_macro in ('flyscan', 'ascan'):
704
802
  mot_npts = int(self.spec_args[3])+1
705
803
  return (mot_npts,)
706
804
  if self.spec_macro in ('tseries', 'loopscan'):
707
- return len(np.array(self.spec_scan.data[:,0]))
805
+ return (len(np.array(self.spec_scan.data[:,0])),)
708
806
  raise RuntimeError(f'{self.scan_title}: cannot determine scan shape '
709
807
  f'for scans of type {self.spec_macro}')
710
808
 
711
- def get_spec_scan_dwell(self):
712
- if self.spec_macro == 'flymesh':
713
- return float(self.spec_args[4])
714
- if self.spec_macro in ('flyscan', 'ascan'):
715
- return float(self.spec_args[-1])
716
- raise RuntimeError(f'{self.scan_title}: cannot determine dwell time '
717
- f'for scans of type {self.spec_macro}')
718
-
719
809
  def get_detector_data_path(self):
720
810
  return os.path.join(self.scan_path, str(self.scan_number))
721
811
 
@@ -793,7 +883,17 @@ class FMBRotationScanParser(RotationScanParser, FMBScanParser):
793
883
  with the typical tomography setup at FMB.
794
884
  """
795
885
 
886
+ def get_spec_scan_data(self):
887
+ spec_scan_data = super().get_spec_scan_data()
888
+ if hasattr(self, '_rams4_args'):
889
+ spec_scan_data['theta'] = np.linspace(
890
+ float(self._rams4_args[0]), float(self._rams4_args[1]),
891
+ 1+int(self._rams4_args[2]))
892
+ return spec_scan_data
893
+
796
894
  def get_spec_scan_npts(self):
895
+ if hasattr(self, '_rams4_args'):
896
+ return 1+int(self._rams4_args[2])
797
897
  if self.spec_macro == 'flyscan':
798
898
  if len(self.spec_args) == 2:
799
899
  return 1+int(self.spec_args[0])
@@ -802,12 +902,12 @@ class FMBRotationScanParser(RotationScanParser, FMBScanParser):
802
902
  raise RuntimeError(f'{self.scan_title}: cannot obtain number of '
803
903
  f'points from {self.spec_macro} with arguments '
804
904
  f'{self.spec_args}')
805
- elif self.spec_macro == 'ascan':
806
- if len(self.spec_args) == 5:
807
- return int(self.spec_args[3])
808
- raise RuntimeError(f'{self.scan_title}: cannot obtain number of '
809
- f'points from {self.spec_macro} with arguments '
810
- f'{self.spec_args}')
905
+ # if self.spec_macro == 'ascan':
906
+ # if len(self.spec_args) == 5:
907
+ # return int(self.spec_args[3])
908
+ # raise RuntimeError(f'{self.scan_title}: cannot obtain number of '
909
+ # f'points from {self.spec_macro} with arguments '
910
+ # f'{self.spec_args}')
811
911
  raise RuntimeError(f'{self.scan_title}: cannot determine rotation '
812
912
  f' angles for scans of type {self.spec_macro}')
813
913
 
@@ -815,21 +915,10 @@ class FMBRotationScanParser(RotationScanParser, FMBScanParser):
815
915
  return 0
816
916
 
817
917
  def get_starting_image_offset(self):
918
+ if hasattr(self, '_rams4_args'):
919
+ return int(self.spec_args[0]) - self.spec_scan_npts
818
920
  if self.spec_macro == 'flyscan':
819
- # if len(self.spec_args) == 2:
820
- # return 1
821
- # if len(self.spec_args) == 5:
822
- # return 1
823
921
  return 1
824
- # raise RuntimeError(f'{self.scan_title}: cannot obtain starting '
825
- # f'image offset {self.spec_macro} with arguments'
826
- # f' {self.spec_args}')
827
- # elif self.spec_macro == 'ascan':
828
- # if len(self.spec_args) == 5:
829
- # return 0
830
- # raise RuntimeError(f'{self.scan_title}: cannot obtain starting '
831
- # f'image offset {self.spec_macro} with arguments'
832
- # f' {self.spec_args}')
833
922
  raise RuntimeError(f'{self.scan_title}: cannot determine starting '
834
923
  f'image offset for scans of type {self.spec_macro}')
835
924
 
@@ -855,8 +944,6 @@ class FMBRotationScanParser(RotationScanParser, FMBScanParser):
855
944
  if scan_step_index is None:
856
945
  detector_data = h5_file['/entry/instrument/detector/data'][
857
946
  self.starting_image_offset:]
858
- # sum_det = list(np.sum(detector_data, (1,2)))
859
- # print(f'\n\nsum scanparser ({len(sum_det)}):\n{sum_det}')
860
947
  elif isinstance(scan_step_index, int):
861
948
  detector_data = h5_file['/entry/instrument/detector/data'][
862
949
  self.starting_image_offset+scan_step_index]
@@ -873,10 +960,8 @@ class FMBRotationScanParser(RotationScanParser, FMBScanParser):
873
960
  def get_detector_data(self, detector_prefix, scan_step_index=None):
874
961
  try:
875
962
  # Detector files in h5 format
876
- # print('data in h5 file')
877
963
  detector_data = self.get_all_detector_data_in_file(
878
964
  detector_prefix, scan_step_index)
879
- # print(f'detector_data {detector_prefix} {scan_step_index}:\n{detector_data.shape}')
880
965
  except:
881
966
  # Detector files in tiff format
882
967
  if scan_step_index is None:
@@ -921,12 +1006,14 @@ class SMBRotationScanParser(RotationScanParser, SMBScanParser):
921
1006
  with the typical tomography setup at SMB.
922
1007
  """
923
1008
 
924
- def __init__(self, spec_file_name, scan_number):
1009
+ def __init__(self, spec_file_name, scan_number, par_file=None):
925
1010
  self._scan_type = None
926
1011
  super().__init__(spec_file_name, scan_number)
927
1012
 
928
1013
  self._katefix = 0 # RV remove when no longer needed
929
1014
  self._par_file_pattern = f'id*-*tomo*-{self.scan_name}'
1015
+ if par_file is not None:
1016
+ self._par_file = par_file
930
1017
 
931
1018
  @property
932
1019
  def scan_type(self):
@@ -996,22 +1083,20 @@ class SMBRotationScanParser(RotationScanParser, SMBScanParser):
996
1083
  if os.path.isfile(file_name_full):
997
1084
  return file_name_full
998
1085
  raise RuntimeError(f'{self.scan_title}: could not find detector image '
999
- f'file for scan step ({scan_step_index})')
1086
+ f'file ({file_name_full}) for scan step '
1087
+ f'({scan_step_index})')
1000
1088
 
1001
1089
  def get_detector_data(self, detector_prefix, scan_step_index=None):
1002
- # print(f'\n\nin get_detector_data: {detector_prefix} {scan_step_index}')
1003
1090
  if scan_step_index is None:
1004
1091
  detector_data = []
1005
1092
  for index in range(self.spec_scan_npts):
1006
1093
  detector_data.append(
1007
1094
  self.get_detector_data(detector_prefix, index))
1008
1095
  detector_data = np.asarray(detector_data)
1009
- # print(f'detector_data shape {type(detector_data)} {detector_data.shape}:\n{detector_data}')
1010
1096
  elif isinstance(scan_step_index, int):
1011
1097
  image_file = self.get_detector_data_file(scan_step_index)
1012
1098
  with TiffFile(image_file) as tiff_file:
1013
1099
  detector_data = tiff_file.asarray()
1014
- # print(f'\t{scan_step_index} {image_file} {np.sum(np.asarray(detector_data))}')
1015
1100
  elif (isinstance(scan_step_index, (list, tuple))
1016
1101
  and len(scan_step_index) == 2):
1017
1102
  detector_data = []
@@ -1051,10 +1136,66 @@ class SMBMCAScanParser(MCAScanParser, SMBLinearScanParser):
1051
1136
  """Concrete implementation of a class representing a scan taken
1052
1137
  with the typical EDD setup at SMB or FAST.
1053
1138
  """
1139
+ detector_data_formats = ('spec', 'h5')
1140
+ def __init__(self, spec_file_name, scan_number, detector_data_format=None):
1141
+ """Constructor for SMBMCAScnaParser.
1142
+
1143
+ :param spec_file: Path to scan's SPEC file
1144
+ :type spec_file: str
1145
+ :param scan_number: Number of the SPEC scan
1146
+ :type scan_number: int
1147
+ :param detector_data_format: Format of the MCA data collected,
1148
+ defaults to None
1149
+ :type detector_data_format: Optional[Literal["spec", "h5"]]
1150
+ """
1151
+ super().__init__(spec_file_name, scan_number)
1054
1152
 
1055
- def get_detector_num_bins(self, detector_prefix):
1056
- with open(self.get_detector_data_file(detector_prefix)) \
1057
- as detector_file:
1153
+ self.detector_data_format = None
1154
+ if detector_data_format is None:
1155
+ self.init_detector_data_format()
1156
+ else:
1157
+ if detector_data_format.lower() in self.detector_data_formats:
1158
+ self.detector_data_format = detector_data_format.lower()
1159
+ else:
1160
+ raise ValueError(
1161
+ 'Unrecognized value for detector_data_format: '
1162
+ + f'{detector_data_format}. Allowed values are: '
1163
+ + ', '.join(self.detector_data_formats))
1164
+
1165
+ def init_detector_data_format(self):
1166
+ """Determine and set a value for the instance variable
1167
+ `detector_data_format` based on the presence / absence of
1168
+ detector data files of different formats conventionally
1169
+ associated with this scan. Also set the corresponding
1170
+ appropriate value for `_detector_data_path`.
1171
+ """
1172
+ try:
1173
+ self._detector_data_path = self.scan_path
1174
+ detector_file = self.get_detector_data_file_spec()
1175
+ except OSError:
1176
+ try:
1177
+ self._detector_data_path = os.path.join(
1178
+ self.scan_path, str(self.scan_number), 'edd')
1179
+ detector_file = self.get_detector_data_file_h5()
1180
+ except OSError:
1181
+ raise RuntimeError(
1182
+ f"{self.scan_title}: Can't determine detector data format")
1183
+ else:
1184
+ self.detector_data_format = 'h5'
1185
+ else:
1186
+ self.detector_data_format = 'spec'
1187
+
1188
+ def get_detector_data_path(self):
1189
+ raise NotImplementedError
1190
+
1191
+ def get_detector_num_bins(self, element_index=0):
1192
+ if self.detector_data_format == 'spec':
1193
+ return self.get_detector_num_bins_spec()
1194
+ elif self.detector_data_format == 'h5':
1195
+ return self.get_detector_num_bins_h5(element_index)
1196
+
1197
+ def get_detector_num_bins_spec(self):
1198
+ with open(self.get_detector_data_file_spec()) as detector_file:
1058
1199
  lines = detector_file.readlines()
1059
1200
  for line in lines:
1060
1201
  if line.startswith('#@CHANN'):
@@ -1064,33 +1205,101 @@ class SMBMCAScanParser(MCAScanParser, SMBLinearScanParser):
1064
1205
  return int(number_saved)
1065
1206
  except:
1066
1207
  continue
1067
- raise RuntimeError(f'{self.scan_title}: could not find num_bins for '
1068
- f'detector {detector_prefix}')
1069
-
1070
- def get_detector_data_path(self):
1071
- return self.scan_path
1208
+ raise RuntimeError(f'{self.scan_title}: could not find num_bins')
1072
1209
 
1073
- def get_detector_data_file(self, detector_prefix, scan_step_index=0):
1210
+ def get_detector_num_bins_h5(self, element_index):
1211
+ from h5py import File
1212
+ detector_file = self.get_detector_data_file_h5()
1213
+ with File(detector_file) as h5_file:
1214
+ dset_shape = h5_file['/entry/data/data'].shape
1215
+ return dset_shape[-1]
1216
+
1217
+ def get_detector_data_file(self, scan_step_index=0):
1218
+ if self.detector_data_format == 'spec':
1219
+ return self.get_detector_data_file_spec()
1220
+ elif self.detector_data_format == 'h5':
1221
+ return self.get_detector_data_file_h5(
1222
+ scan_step_index=scan_step_index)
1223
+
1224
+ def get_detector_data_file_spec(self):
1225
+ """Return the filename (full absolute path) to the file
1226
+ containing spec-formatted MCA data for this scan.
1227
+ """
1074
1228
  file_name = f'spec.log.scan{self.scan_number}.mca1.mca'
1075
1229
  file_name_full = os.path.join(self.detector_data_path, file_name)
1076
1230
  if os.path.isfile(file_name_full):
1077
1231
  return file_name_full
1078
- raise RuntimeError(
1079
- f'{self.scan_title}: could not find detector image file')
1232
+ raise OSError(
1233
+ '{self.scan_title}: could not find detector image file'
1234
+ )
1235
+
1236
+ def get_detector_data_file_h5(self, scan_step_index=0):
1237
+ """Return the filename (full absolute path) to the file
1238
+ containing h5-formatted MCA data for this scan.
1239
+
1240
+ :param scan_step_index:
1241
+ """
1242
+ scan_step = self.get_scan_step(scan_step_index)
1243
+ if len(self.spec_scan_shape) == 1:
1244
+ filename_index = 0
1245
+ elif len(self.spec_scan_shape) == 2:
1246
+ scan_step = self.get_scan_step(scan_step_index)
1247
+ filename_index = scan_step[0]
1248
+ else:
1249
+ raise NotImplementedError(
1250
+ 'Cannot find detector file for scans with dimension > 2')
1251
+ file_name = list_smb_mca_detector_files_h5(
1252
+ self.detector_data_path)[filename_index]
1253
+ file_name_full = os.path.join(self.detector_data_path, file_name)
1254
+ if os.path.isfile(file_name_full):
1255
+ return file_name_full
1256
+ raise OSError(
1257
+ '{self.scan_title}: could not find detector image file'
1258
+ )
1259
+
1080
1260
 
1081
- def get_all_detector_data(self, detector_prefix):
1261
+ def get_all_detector_data(self, detector):
1262
+ """Return a 2D array of all MCA spectra collected in this scan
1263
+ by the detector element indicated with `detector`.
1264
+
1265
+ :param detector: For detector data collected in SPEC format,
1266
+ this is the detector prefix as it appears in the spec MCA
1267
+ data file. For detector data collected in H5 format, this
1268
+ is the index of a particular detector element.
1269
+ :type detector: Union[str, int]
1270
+ :rtype: numpy.ndarray
1271
+ """
1272
+ if self.detector_data_format == 'spec':
1273
+ return self.get_all_detector_data_spec(detector)
1274
+ elif self.detector_data_format == 'h5':
1275
+ try:
1276
+ element_index = int(detector)
1277
+ except:
1278
+ raise TypeError(f'{detector} is not an integer element index')
1279
+ return self.get_all_detector_data_h5(element_index)
1280
+
1281
+ def get_all_detector_data_spec(self, detector_prefix):
1282
+ """Return a 2D array of all MCA spectra collected by a
1283
+ detector in the spec MCA file format during the scan.
1284
+
1285
+ :param detector_prefix: Detector name at is appears in the
1286
+ spec MCA file.
1287
+ :type detector_prefix: str
1288
+ :returns: 2D array of MCA spectra
1289
+ :rtype: numpy.ndarray
1290
+ """
1082
1291
  # This should be easy with pyspec, but there are bugs in
1083
1292
  # pyspec for MCA data..... or is the 'bug' from a nonstandard
1084
1293
  # implementation of some macro on our end? According to spec
1085
1294
  # manual and pyspec code, mca data should always begin w/ '@A'
1086
- # In example scans, it begins with '@mca1' instead
1295
+ # In example scans, it begins with '@{detector_prefix}'
1296
+ # instead
1087
1297
  data = []
1088
1298
 
1089
- with open(self.get_detector_data_file(detector_prefix)) \
1090
- as detector_file:
1299
+ with open(self.get_detector_data_file_spec()) as detector_file:
1091
1300
  lines = [line.strip("\\\n") for line in detector_file.readlines()]
1092
1301
 
1093
- num_bins = self.get_detector_num_bins(detector_prefix)
1302
+ num_bins = self.get_detector_num_bins()
1094
1303
 
1095
1304
  counter = 0
1096
1305
  for line in lines:
@@ -1117,6 +1326,106 @@ class SMBMCAScanParser(MCAScanParser, SMBLinearScanParser):
1117
1326
 
1118
1327
  return np.array(data)
1119
1328
 
1120
- def get_detector_data(self, detector_prefix, scan_step_index:int):
1121
- detector_data = self.get_all_detector_data(detector_prefix)
1329
+ def get_all_detector_data_h5(self, element_index):
1330
+ """Return a 2D array of all MCA spectra collected by a
1331
+ detector in the h5 file format during the scan.
1332
+
1333
+ :param element_index: The index of a particualr MCA element to
1334
+ return data for.
1335
+ :type element_index: int
1336
+ :returns: 2D array of MCA spectra
1337
+ :rtype: numpy.ndarray
1338
+ """
1339
+ detector_data = np.empty(
1340
+ (self.spec_scan_npts,
1341
+ self.get_detector_num_bins_h5(element_index)))
1342
+ detector_files = list_smb_mca_detector_files_h5(
1343
+ self.detector_data_path)
1344
+ for i, detector_file in enumerate(detector_files):
1345
+ full_filename = os.path.join(
1346
+ self.detector_data_path, detector_file)
1347
+ element_data = get_all_mca_data_h5(
1348
+ full_filename)[:,element_index,:]
1349
+ i_0 = i * self.spec_scan_shape[0]
1350
+ if len(self.spec_scan_shape) == 2:
1351
+ i_f = i_0 + self.spec_scan_shape[1]
1352
+ else:
1353
+ i_f = self.spec_scan_npts
1354
+ detector_data[i_0:i_f] = element_data
1355
+ return detector_data
1356
+
1357
+ def get_detector_data(self, detector, scan_step_index:int):
1358
+ """Return a single MCA spectrum for the detector indicated.
1359
+
1360
+ :param detector: If this scan collected MCA data in "spec"
1361
+ format, this is the detector prefix as it appears in the
1362
+ spec MCA data file. If this scan collected data in .h5
1363
+ format, this is the index of the detector element of
1364
+ interest.:type detector: typing.Union[str, int]
1365
+ :param scan_step_index: Index of the scan step to return the
1366
+ spectrum from.
1367
+ :type scan_step_index: int
1368
+ :returns: A single MCA spectrum
1369
+ :rtype: numpy.ndarray
1370
+ """
1371
+ detector_data = self.get_all_detector_data(detector)
1122
1372
  return detector_data[scan_step_index]
1373
+
1374
+ @cache
1375
+ def list_smb_mca_detector_files_h5(detector_data_path):
1376
+ """Return a sorted list of all *.hdf5 files in a directory
1377
+
1378
+ :param detector_data_path: Directory to return *.hdf5 files from
1379
+ :type detector_data_path: str
1380
+ :returns: Sorted list of detector data filenames
1381
+ :rtype: list[str]
1382
+ """
1383
+ return sorted(
1384
+ [f for f in os.listdir(detector_data_path) if f.endswith('.hdf5')])
1385
+
1386
+ @cache
1387
+ def get_all_mca_data_h5(filename):
1388
+ """Return all data from all elements from an MCA data file
1389
+
1390
+ :param filename: Name of the MCA h5 data file
1391
+ :type filename: str
1392
+ :returns: 3D array of MCA spectra where the first axis is scan
1393
+ step, second index is detector element, third index is channel
1394
+ energy.
1395
+ :rtype: numpy.ndarray
1396
+ """
1397
+ import os
1398
+
1399
+ from h5py import File
1400
+ import numpy as np
1401
+
1402
+ with File(filename) as h5_file:
1403
+ data = h5_file['/entry/data/data'][:]
1404
+
1405
+ # Prior to 2023-12-12, there was an issue where the XPS23 detector
1406
+ # was capturing one or two frames of all 0s at the start of the
1407
+ # dataset in every hdf5 file. In both cases, there is only ONE
1408
+ # extra frame of data relative to the number of frames that should
1409
+ # be there (based on the number of points in the spec scan). If
1410
+ # one frame of all 0s is present: skip it and deliver only the
1411
+ # real data. If two frames of all 0s are present: detector data
1412
+ # will be missing for the LAST step in the scan. Skip the first
1413
+ # two frames of all 0s in the hdf5 dataset, then add a frame of
1414
+ # fake data (all 0-s) to the end of that real data so that the
1415
+ # number of detector data frames matches the number of points in
1416
+ # the spec scan.
1417
+ check_zeros_before = 1702357200
1418
+ file_mtime = os.path.getmtime(filename)
1419
+ if file_mtime <= check_zeros_before:
1420
+ if not np.any(data[0]):
1421
+ # If present, remove first frame of blank data
1422
+ print('Warning: removing blank first frame of detector data')
1423
+ data = data[1:]
1424
+ if not np.any(data[0]):
1425
+ # If present, shift second frame of blank data to the
1426
+ # end
1427
+ print('Warning: shifting second frame of blank detector data '
1428
+ + 'to the end of the scan')
1429
+ data = np.concatenate((data[1:], np.asarray([data[0]])))
1430
+
1431
+ return data