ChessAnalysisPipeline 0.0.13__py3-none-any.whl → 0.0.15__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
@@ -2,6 +2,38 @@
2
2
 
3
3
  # -*- coding: utf-8 -*-
4
4
 
5
+ """Parsing data from certain CHESS SPEC scans is supported by a family
6
+ of classes derived from the base class, `ScanParser` (defined
7
+ below). An instance of `ScanParser` represents a single SPEC scan --
8
+ each instance is initialized with the name of a specific spec file and
9
+ integer scan number. Access to certain data collected by that scan
10
+ (counter data, positioner values, scan shape, detector data, etc.) are
11
+ made available through the properties and methods of that object.
12
+
13
+ `ScanParser` is just an incomplete abstraction -- one should not
14
+ declare or work with an instance of `ScanParser` directly. Instead,
15
+ one must find the appropriate concrete subclass to use for the
16
+ particular type of scan one wishes to parse, then declare an instance
17
+ of that specific class to begin accessing data from that scan.
18
+
19
+ Basic usage examples:
20
+ 1. Print out the position of a the SPEC positioner motor with mnemonic
21
+ `'mne0'` for a SAXS/WAXS scan collected at FMB:
22
+ ```python
23
+ from CHAP.utils.scanparsers import FMBSAXSWAXSScanParser
24
+ sp = FMBSAXSWAXSScanParser('/path/to/fmb/saxswaxs/spec/file', 1)
25
+ print(sp.get_spec_positioner_value('mne0'))
26
+ ```
27
+ 1. Store all the detector data collected by the detector with prefix
28
+ `'det'` over a rotation series collected at SMB in the variable
29
+ `data`:
30
+ ```python
31
+ from CHAP.utils.scanparsers import SMBRotationScanParser
32
+ sp = SMBRotationScanParser('/path/to/smb/rotation/spec/file', 1)
33
+ data = sp.get_detector_data('det')
34
+ ```
35
+ """
36
+
5
37
  # System modules
6
38
  from csv import reader
7
39
  from fnmatch import filter as fnmatch_filter
@@ -15,6 +47,9 @@ import numpy as np
15
47
  from pyspec.file.spec import FileSpec
16
48
  from pyspec.file.tiff import TiffFile
17
49
 
50
+ @cache
51
+ def filespec(spec_file_name):
52
+ return FileSpec(spec_file_name)
18
53
 
19
54
  class ScanParser:
20
55
  """Partial implementation of a class representing a SPEC scan and
@@ -55,6 +90,7 @@ class ScanParser:
55
90
  and len(scanparser.spec_args) == 5):
56
91
  self._rams4_args = scanparser.spec_args
57
92
 
93
+ def __repr_(self):
58
94
  return (f'{self.__class__.__name__}'
59
95
  f'({self.spec_file_name}, {self.scan_number}) '
60
96
  f'-- {self.spec_command}')
@@ -64,7 +100,7 @@ class ScanParser:
64
100
  # NB This FileSpec instance is not stored as a private
65
101
  # attribute because it cannot be pickled (and therefore could
66
102
  # cause problems for parallel code that uses ScanParsers).
67
- return FileSpec(self.spec_file_name)
103
+ return filespec(self.spec_file_name)
68
104
 
69
105
  @property
70
106
  def scan_path(self):
@@ -300,6 +336,7 @@ class SMBScanParser(ScanParser):
300
336
 
301
337
  self._pars = None
302
338
  self._par_file_pattern = f'*-*-{self.scan_name}'
339
+ self._par_file = None
303
340
 
304
341
  def get_scan_name(self):
305
342
  return os.path.basename(self.scan_path)
@@ -323,6 +360,10 @@ class SMBScanParser(ScanParser):
323
360
  json_files = fnmatch_filter(
324
361
  os.listdir(self.scan_path),
325
362
  f'{self._par_file_pattern}.json')
363
+ if not json_files:
364
+ json_files = fnmatch_filter(
365
+ os.listdir(self.scan_path),
366
+ f'*.json')
326
367
  if len(json_files) != 1:
327
368
  raise RuntimeError(f'{self.scan_title}: cannot find the '
328
369
  '.json file to decode the .par file')
@@ -336,15 +377,20 @@ class SMBScanParser(ScanParser):
336
377
  raise RuntimeError(f'{self.scan_title}: cannot find scan pars '
337
378
  'without a "SCAN_N" column in the par file')
338
379
 
339
- par_files = fnmatch_filter(
340
- os.listdir(self.scan_path),
341
- f'{self._par_file_pattern}.par')
342
- if len(par_files) != 1:
343
- raise RuntimeError(f'{self.scan_title}: cannot find the .par '
344
- 'file for this scan directory')
380
+ if getattr(self, '_par_file'):
381
+ par_file = self._par_file
382
+ else:
383
+ par_files = fnmatch_filter(
384
+ os.listdir(self.scan_path),
385
+ f'{self._par_file_pattern}.par')
386
+ if len(par_files) != 1:
387
+ raise RuntimeError(f'{self.scan_title}: cannot find the .par '
388
+ 'file for this scan directory')
389
+ par_file = os.path.join(self.scan_path, par_files[0])
390
+ self._par_file = par_file
345
391
  par_dict = None
346
- with open(os.path.join(self.scan_path, par_files[0])) as par_file:
347
- par_reader = reader(par_file, delimiter=' ')
392
+ with open(par_file) as f:
393
+ par_reader = reader(f, delimiter=' ')
348
394
  for row in par_reader:
349
395
  if len(row) == len(par_col_names):
350
396
  row_scann = int(row[scann_col_idx])
@@ -362,22 +408,21 @@ class SMBScanParser(ScanParser):
362
408
  except:
363
409
  pass
364
410
  par_dict[par_col_name] = par_value
365
-
366
411
  if par_dict is None:
367
412
  raise RuntimeError(f'{self.scan_title}: could not find scan pars '
368
413
  f'for scan number {self.scan_number}')
369
414
  return par_dict
370
415
 
371
416
  def get_counter_gain(self, counter_name):
372
- """Return the gain of a counter as recorded in the comments of
373
- a scan in a SPEC file converted to nA/V.
417
+ """Return the gain of a counter as recorded in the user lines
418
+ of a scan in a SPEC file converted to nA/V.
374
419
 
375
420
  :param counter_name: the name of the counter
376
421
  :type counter_name: str
377
422
  :rtype: str
378
423
  """
379
424
  counter_gain = None
380
- for comment in self.spec_scan.comments:
425
+ for comment in self.spec_scan.comments + self.spec_scan.user_lines:
381
426
  match = re.search(
382
427
  f'{counter_name} gain: ' # start of counter gain comments
383
428
  '(?P<gain_value>\d+) ' # gain numerical value
@@ -388,6 +433,7 @@ class SMBScanParser(ScanParser):
388
433
  gain_scalar = 1 if unit_prefix == 'n' \
389
434
  else 1e3 if unit_prefix == 'u' else 1e6
390
435
  counter_gain = f'{float(match["gain_value"])*gain_scalar} nA/V'
436
+ break
391
437
 
392
438
  if counter_gain is None:
393
439
  raise RuntimeError(f'{self.scan_title}: could not get gain for '
@@ -404,6 +450,7 @@ class LinearScanParser(ScanParser):
404
450
 
405
451
  self._spec_scan_motor_mnes = None
406
452
  self._spec_scan_motor_vals = None
453
+ self._spec_scan_motor_vals_relative = None
407
454
  self._spec_scan_shape = None
408
455
  self._spec_scan_dwell = None
409
456
 
@@ -416,9 +463,17 @@ class LinearScanParser(ScanParser):
416
463
  @property
417
464
  def spec_scan_motor_vals(self):
418
465
  if self._spec_scan_motor_vals is None:
419
- self._spec_scan_motor_vals = self.get_spec_scan_motor_vals()
466
+ self._spec_scan_motor_vals = self.get_spec_scan_motor_vals(
467
+ relative=False)
420
468
  return self._spec_scan_motor_vals
421
469
 
470
+ @property
471
+ def spec_scan_motor_vals_relative(self):
472
+ if self._spec_scan_motor_vals_relative is None:
473
+ self._spec_scan_motor_vals_relative = \
474
+ self.get_spec_scan_motor_vals(relative=True)
475
+ return self._spec_scan_motor_vals_relative
476
+
422
477
  @property
423
478
  def spec_scan_shape(self):
424
479
  if self._spec_scan_shape is None:
@@ -442,13 +497,17 @@ class LinearScanParser(ScanParser):
442
497
  """
443
498
  raise NotImplementedError
444
499
 
445
- def get_spec_scan_motor_vals(self):
500
+ def get_spec_scan_motor_vals(self, relative=False):
446
501
  """Return the values visited by each of the scanned motors. If
447
502
  there is more than one motor scanned (in a "flymesh" scan, for
448
503
  example), the order of motor values in the returned tuple will
449
504
  go from the fastest moving motor's values first to the slowest
450
505
  moving motor's values last.
451
506
 
507
+ :param relative: If `True`, return scanned motor positions
508
+ *relative* to the scanned motors' positions before the scan
509
+ started, defaults to False.
510
+ :type relative: bool, optional
452
511
  :rtype: tuple
453
512
  """
454
513
  raise NotImplementedError
@@ -531,7 +590,7 @@ class FMBLinearScanParser(LinearScanParser, FMBScanParser):
531
590
  """
532
591
 
533
592
  def get_spec_scan_motor_mnes(self):
534
- if self.spec_macro == 'flymesh':
593
+ if self.spec_macro in ('flymesh', 'mesh', 'flydmesh', 'dmesh'):
535
594
  m1_mne = self.spec_args[0]
536
595
  try:
537
596
  # Try post-summer-2022 format
@@ -543,15 +602,15 @@ class FMBLinearScanParser(LinearScanParser, FMBScanParser):
543
602
  m2_mne_i = 5
544
603
  m2_mne = self.spec_args[m2_mne_i]
545
604
  return (m1_mne, m2_mne)
546
- if self.spec_macro in ('flyscan', 'ascan'):
605
+ if self.spec_macro in ('flyscan', 'ascan', 'flydscan', 'dscan'):
547
606
  return (self.spec_args[0],)
548
607
  if self.spec_macro in ('tseries', 'loopscan'):
549
608
  return ('Time',)
550
609
  raise RuntimeError(f'{self.scan_title}: cannot determine scan motors '
551
610
  f'for scans of type {self.spec_macro}')
552
611
 
553
- def get_spec_scan_motor_vals(self):
554
- if self.spec_macro == 'flymesh':
612
+ def get_spec_scan_motor_vals(self, relative=False):
613
+ if self.spec_macro in ('flymesh', 'mesh', 'flydmesh', 'dmesh'):
555
614
  m1_start = float(self.spec_args[1])
556
615
  m1_end = float(self.spec_args[2])
557
616
  m1_npt = int(self.spec_args[3]) + 1
@@ -572,19 +631,27 @@ class FMBLinearScanParser(LinearScanParser, FMBScanParser):
572
631
  m2_npt = int(self.spec_args[m2_nint_i]) + 1
573
632
  fast_mot_vals = np.linspace(m1_start, m1_end, m1_npt)
574
633
  slow_mot_vals = np.linspace(m2_start, m2_end, m2_npt)
634
+ if relative:
635
+ fast_mot_vals -= self.get_spec_positioner_value(
636
+ self.spec_scan_motor_mnes[0])
637
+ slow_mot_vals -= self.get_spec_positioner_value(
638
+ self.spec_scan_motor_mnes[1])
575
639
  return (fast_mot_vals, slow_mot_vals)
576
- if self.spec_macro in ('flyscan', 'ascan'):
640
+ if self.spec_macro in ('flyscan', 'ascan', 'flydscan', 'dscan'):
577
641
  mot_vals = np.linspace(float(self.spec_args[1]),
578
642
  float(self.spec_args[2]),
579
643
  int(self.spec_args[3])+1)
644
+ if relative:
645
+ mot_vals -= self.get_spec_positioner_value(
646
+ self.spec_scan_motor_mnes[0])
580
647
  return (mot_vals,)
581
648
  if self.spec_macro in ('tseries', 'loopscan'):
582
- return self.spec_scan.data[:,0]
649
+ return (self.spec_scan.data[:,0],)
583
650
  raise RuntimeError(f'{self.scan_title}: cannot determine scan motors '
584
651
  f'for scans of type {self.spec_macro}')
585
652
 
586
653
  def get_spec_scan_shape(self):
587
- if self.spec_macro == 'flymesh':
654
+ if self.spec_macro in ('flymesh', 'mesh', 'flydmesh', 'dmesh'):
588
655
  fast_mot_npts = int(self.spec_args[3]) + 1
589
656
  try:
590
657
  # Try post-summer-2022 format
@@ -596,16 +663,16 @@ class FMBLinearScanParser(LinearScanParser, FMBScanParser):
596
663
  m2_nint_i = 8
597
664
  slow_mot_npts = int(self.spec_args[m2_nint_i]) + 1
598
665
  return (fast_mot_npts, slow_mot_npts)
599
- if self.spec_macro in ('flyscan', 'ascan'):
666
+ if self.spec_macro in ('flyscan', 'ascan', 'flydscan', 'dscan'):
600
667
  mot_npts = int(self.spec_args[3])+1
601
668
  return (mot_npts,)
602
669
  if self.spec_macro in ('tseries', 'loopscan'):
603
- return len(np.array(self.spec_scan.data[:,0]))
670
+ return (len(np.array(self.spec_scan.data[:,0])),)
604
671
  raise RuntimeError(f'{self.scan_title}: cannot determine scan shape '
605
672
  f'for scans of type {self.spec_macro}')
606
673
 
607
674
  def get_spec_scan_dwell(self):
608
- if self.macro == 'flymesh':
675
+ if self.macro in ('flymesh', 'mesh', 'flydmesh', 'dmesh'):
609
676
  try:
610
677
  # Try post-summer-2022 format
611
678
  dwell = float(self.spec_args[4])
@@ -613,7 +680,7 @@ class FMBLinearScanParser(LinearScanParser, FMBScanParser):
613
680
  # Accommodate pre-summer-2022 format
614
681
  dwell = float(self.spec_args[8])
615
682
  return dwell
616
- if self.spec_macro in ('flyscan', 'ascan'):
683
+ if self.spec_macro in ('flyscan', 'ascan', 'flydscan', 'dscan'):
617
684
  return float(self.spec_args[4])
618
685
  if self.spec_macro in ('tseries', 'loopscan'):
619
686
  return float(self.spec_args[1])
@@ -656,14 +723,28 @@ class FMBSAXSWAXSScanParser(FMBLinearScanParser):
656
723
  def get_detector_data_file(self, detector_prefix, scan_step_index:int):
657
724
  detector_files = list_fmb_saxswaxs_detector_files(
658
725
  self.detector_data_path, detector_prefix)
659
- return os.path.join(
660
- self.detector_data_path, detector_files[scan_step_index])
726
+ if len(detector_files) == self.spec_scan_npts:
727
+ return os.path.join(
728
+ self.detector_data_path, detector_files[scan_step_index])
729
+ else:
730
+ scan_step = self.get_scan_step(scan_step_index)
731
+ for f in detector_files:
732
+ filename, _ = os.path.splitext(f)
733
+ file_indices = tuple(
734
+ [int(i) for i in \
735
+ filename.split('_')[-len(self.spec_scan_shape):]])
736
+ if file_indices == scan_step:
737
+ return os.path.join(self.detector_data_path, f)
738
+ raise RuntimeError(
739
+ 'Could not find a matching detector data file for detector '
740
+ + f'{detector_prefix} at scan step index {scan_step_index}')
661
741
 
662
742
  def get_detector_data(self, detector_prefix, scan_step_index:int):
743
+ import fabio
663
744
  image_file = self.get_detector_data_file(detector_prefix,
664
745
  scan_step_index)
665
- with TiffFile(image_file) as tiff_file:
666
- image_data = tiff_file.asarray()
746
+ with fabio.open(image_file) as det_file:
747
+ image_data = det_file.data
667
748
  return image_data
668
749
 
669
750
 
@@ -704,7 +785,15 @@ class SMBLinearScanParser(LinearScanParser, SMBScanParser):
704
785
  """
705
786
 
706
787
  def get_spec_scan_dwell(self):
707
- if self.spec_macro in ('flymesh', 'flyscan', 'ascan'):
788
+ if self.spec_macro in ('flymesh', 'mesh', 'flydmesh', 'dmesh'):
789
+ try:
790
+ # Try post-summer-2022 format
791
+ dwell = float(self.spec_args[4])
792
+ except:
793
+ # Accommodate pre-summer-2022 format
794
+ dwell = float(self.spec_args[8])
795
+ return dwell
796
+ if self.spec_macro in ('flyscan', 'ascan', 'flydscan', 'dscan'):
708
797
  return float(self.spec_args[4])
709
798
  if self.spec_macro == 'tseries':
710
799
  return float(self.spec_args[1])
@@ -714,55 +803,87 @@ class SMBLinearScanParser(LinearScanParser, SMBScanParser):
714
803
  f'for scans of type {self.spec_macro}')
715
804
 
716
805
  def get_spec_scan_motor_mnes(self):
717
- if self.spec_macro == 'flymesh':
718
- return (self.spec_args[0], self.spec_args[5])
719
- if self.spec_macro in ('flyscan', 'ascan'):
806
+ if self.spec_macro in ('flymesh', 'mesh', 'flydmesh', 'dmesh'):
807
+ m1_mne = self.spec_args[0]
808
+ try:
809
+ # Try post-summer-2022 format
810
+ dwell = float(self.spec_args[4])
811
+ except:
812
+ # Accommodate pre-summer-2022 format
813
+ m2_mne_i = 4
814
+ else:
815
+ m2_mne_i = 5
816
+ m2_mne = self.spec_args[m2_mne_i]
817
+ return (m1_mne, m2_mne)
818
+ if self.spec_macro in ('flyscan', 'ascan', 'flydscan', 'dscan'):
720
819
  return (self.spec_args[0],)
721
820
  if self.spec_macro in ('tseries', 'loopscan'):
722
821
  return ('Time',)
723
822
  raise RuntimeError(f'{self.scan_title}: cannot determine scan motors '
724
823
  f'for scans of type {self.spec_macro}')
725
824
 
726
- def get_spec_scan_motor_vals(self):
727
- if self.spec_macro == 'flymesh':
728
- fast_mot_vals = np.linspace(float(self.spec_args[1]),
729
- float(self.spec_args[2]),
730
- int(self.spec_args[3])+1)
731
- slow_mot_vals = np.linspace(float(self.spec_args[6]),
732
- float(self.spec_args[7]),
733
- int(self.spec_args[8])+1)
825
+ def get_spec_scan_motor_vals(self, relative=False):
826
+ if self.spec_macro in ('flymesh', 'mesh', 'flydmesh', 'dmesh'):
827
+ m1_start = float(self.spec_args[1])
828
+ m1_end = float(self.spec_args[2])
829
+ m1_npt = int(self.spec_args[3]) + 1
830
+ try:
831
+ # Try post-summer-2022 format
832
+ dwell = float(self.spec_args[4])
833
+ except:
834
+ # Accommodate pre-summer-2022 format
835
+ m2_start_i = 5
836
+ m2_end_i = 6
837
+ m2_nint_i = 7
838
+ else:
839
+ m2_start_i = 6
840
+ m2_end_i = 7
841
+ m2_nint_i = 8
842
+ m2_start = float(self.spec_args[m2_start_i])
843
+ m2_end = float(self.spec_args[m2_end_i])
844
+ m2_npt = int(self.spec_args[m2_nint_i]) + 1
845
+ fast_mot_vals = np.linspace(m1_start, m1_end, m1_npt)
846
+ slow_mot_vals = np.linspace(m2_start, m2_end, m2_npt)
847
+ if relative:
848
+ fast_mot_vals -= self.spec_positioner_values[
849
+ self.spec_scan_motor_mnes[0]]
850
+ slow_mot_vals -= self.spec_positioner_values[
851
+ self.spec_scan_motor_mnes[1]]
734
852
  return (fast_mot_vals, slow_mot_vals)
735
- if self.spec_macro in ('flyscan', 'ascan'):
853
+ if self.spec_macro in ('flyscan', 'ascan', 'flydscan', 'dscan'):
736
854
  mot_vals = np.linspace(float(self.spec_args[1]),
737
855
  float(self.spec_args[2]),
738
856
  int(self.spec_args[3])+1)
857
+ if relative:
858
+ mot_vals -= self.get_spec_positioner_value(
859
+ self.spec_scan_motor_mnes[0])
739
860
  return (mot_vals,)
740
861
  if self.spec_macro in ('tseries', 'loopscan'):
741
- return self.spec_scan.data[:,0]
862
+ return (self.spec_scan.data[:,0],)
742
863
  raise RuntimeError(f'{self.scan_title}: cannot determine scan motors '
743
864
  f'for scans of type {self.spec_macro}')
744
865
 
745
866
  def get_spec_scan_shape(self):
746
- if self.spec_macro == 'flymesh':
747
- fast_mot_npts = int(self.spec_args[3])+1
748
- slow_mot_npts = int(self.spec_args[8])+1
867
+ if self.spec_macro in ('flymesh', 'mesh', 'flydmesh', 'dmesh'):
868
+ fast_mot_npts = int(self.spec_args[3]) + 1
869
+ try:
870
+ # Try post-summer-2022 format
871
+ dwell = float(self.spec_args[4])
872
+ except:
873
+ # Accommodate pre-summer-2022 format
874
+ m2_nint_i = 7
875
+ else:
876
+ m2_nint_i = 8
877
+ slow_mot_npts = int(self.spec_args[m2_nint_i]) + 1
749
878
  return (fast_mot_npts, slow_mot_npts)
750
- if self.spec_macro in ('flyscan', 'ascan'):
879
+ if self.spec_macro in ('flyscan', 'ascan', 'flydscan', 'dscan'):
751
880
  mot_npts = int(self.spec_args[3])+1
752
881
  return (mot_npts,)
753
882
  if self.spec_macro in ('tseries', 'loopscan'):
754
- return len(np.array(self.spec_scan.data[:,0]))
883
+ return (len(np.array(self.spec_scan.data[:,0])),)
755
884
  raise RuntimeError(f'{self.scan_title}: cannot determine scan shape '
756
885
  f'for scans of type {self.spec_macro}')
757
886
 
758
- def get_spec_scan_dwell(self):
759
- if self.spec_macro == 'flymesh':
760
- return float(self.spec_args[4])
761
- if self.spec_macro in ('flyscan', 'ascan'):
762
- return float(self.spec_args[-1])
763
- raise RuntimeError(f'{self.scan_title}: cannot determine dwell time '
764
- f'for scans of type {self.spec_macro}')
765
-
766
887
  def get_detector_data_path(self):
767
888
  return os.path.join(self.scan_path, str(self.scan_number))
768
889
 
@@ -963,12 +1084,14 @@ class SMBRotationScanParser(RotationScanParser, SMBScanParser):
963
1084
  with the typical tomography setup at SMB.
964
1085
  """
965
1086
 
966
- def __init__(self, spec_file_name, scan_number):
1087
+ def __init__(self, spec_file_name, scan_number, par_file=None):
967
1088
  self._scan_type = None
968
1089
  super().__init__(spec_file_name, scan_number)
969
1090
 
970
1091
  self._katefix = 0 # RV remove when no longer needed
971
1092
  self._par_file_pattern = f'id*-*tomo*-{self.scan_name}'
1093
+ if par_file is not None:
1094
+ self._par_file = par_file
972
1095
 
973
1096
  @property
974
1097
  def scan_type(self):
@@ -1038,7 +1161,8 @@ class SMBRotationScanParser(RotationScanParser, SMBScanParser):
1038
1161
  if os.path.isfile(file_name_full):
1039
1162
  return file_name_full
1040
1163
  raise RuntimeError(f'{self.scan_title}: could not find detector image '
1041
- f'file for scan step ({scan_step_index})')
1164
+ f'file ({file_name_full}) for scan step '
1165
+ f'({scan_step_index})')
1042
1166
 
1043
1167
  def get_detector_data(self, detector_prefix, scan_step_index=None):
1044
1168
  if scan_step_index is None:
@@ -1090,10 +1214,101 @@ class SMBMCAScanParser(MCAScanParser, SMBLinearScanParser):
1090
1214
  """Concrete implementation of a class representing a scan taken
1091
1215
  with the typical EDD setup at SMB or FAST.
1092
1216
  """
1217
+ detector_data_formats = ('spec', 'h5')
1218
+ def __init__(self, spec_file_name, scan_number, detector_data_format=None):
1219
+ """Constructor for SMBMCAScnaParser.
1220
+
1221
+ :param spec_file: Path to scan's SPEC file
1222
+ :type spec_file: str
1223
+ :param scan_number: Number of the SPEC scan
1224
+ :type scan_number: int
1225
+ :param detector_data_format: Format of the MCA data collected,
1226
+ defaults to None
1227
+ :type detector_data_format: Optional[Literal["spec", "h5"]]
1228
+ """
1229
+ super().__init__(spec_file_name, scan_number)
1093
1230
 
1094
- def get_detector_num_bins(self, detector_prefix):
1095
- with open(self.get_detector_data_file(detector_prefix)) \
1096
- as detector_file:
1231
+ self.detector_data_format = None
1232
+ if detector_data_format is None:
1233
+ self.init_detector_data_format()
1234
+ else:
1235
+ if detector_data_format.lower() in self.detector_data_formats:
1236
+ self.detector_data_format = detector_data_format.lower()
1237
+ else:
1238
+ raise ValueError(
1239
+ 'Unrecognized value for detector_data_format: '
1240
+ + f'{detector_data_format}. Allowed values are: '
1241
+ + ', '.join(self.detector_data_formats))
1242
+
1243
+ def get_spec_scan_motor_vals(self, relative=True):
1244
+ if not relative:
1245
+ # The scanned motor's recorded position in the spec.log
1246
+ # file's "#P" lines does not always give the right offset
1247
+ # to use to obtain absolute motor postions from relative
1248
+ # motor positions (or relative from actual). Sometimes,
1249
+ # the labx/y/z/ometotal value from the scan's .par file is
1250
+ # the quantity for the offset that _should_ be used, but
1251
+ # there is currently no consistent way to determine when
1252
+ # to use the labx/y/z/ometotal .par file value and when to
1253
+ # use the spec file "#P" lines value. Because the relative
1254
+ # motor values are the only ones currently used in EDD
1255
+ # workflows, obtain them from relevant values available in
1256
+ # the .par file, and defer implementation for absolute
1257
+ # motor postions to later.
1258
+ return super().get_spec_scan_motor_vals(relative=True)
1259
+ # raise NotImplementedError('Only relative motor values are available.')
1260
+ if self.spec_macro in ('flymesh', 'mesh', 'flydmesh', 'dmesh'):
1261
+ mot_vals_axis0 = np.linspace(self.pars['fly_axis0_start'],
1262
+ self.pars['fly_axis0_end'],
1263
+ self.pars['fly_axis0_npts'])
1264
+ mot_vals_axis1 = np.linspace(self.pars['fly_axis1_start'],
1265
+ self.pars['fly_axis1_end'],
1266
+ self.pars['fly_axis1_npts'])
1267
+ return (mot_vals_axis0, mot_vals_axis1)
1268
+ if self.spec_macro in ('flyscan', 'ascan', 'flydscan', 'dscan'):
1269
+ mot_vals = np.linspace(self.pars['fly_axis0_start'],
1270
+ self.pars['fly_axis0_end'],
1271
+ self.pars['fly_axis0_npts'])
1272
+ return (mot_vals,)
1273
+ if self.spec_macro in ('tseries', 'loopscan'):
1274
+ return (self.spec_scan.data[:,0],)
1275
+ raise RuntimeError(f'{self.scan_title}: cannot determine scan motors '
1276
+ f'for scans of type {self.spec_macro}')
1277
+
1278
+ def init_detector_data_format(self):
1279
+ """Determine and set a value for the instance variable
1280
+ `detector_data_format` based on the presence / absence of
1281
+ detector data files of different formats conventionally
1282
+ associated with this scan. Also set the corresponding
1283
+ appropriate value for `_detector_data_path`.
1284
+ """
1285
+ try:
1286
+ self._detector_data_path = self.scan_path
1287
+ detector_file = self.get_detector_data_file_spec()
1288
+ except OSError:
1289
+ try:
1290
+ self._detector_data_path = os.path.join(
1291
+ self.scan_path, str(self.scan_number), 'edd')
1292
+ detector_file = self.get_detector_data_file_h5()
1293
+ except OSError:
1294
+ raise RuntimeError(
1295
+ f"{self.scan_title}: Can't determine detector data format")
1296
+ else:
1297
+ self.detector_data_format = 'h5'
1298
+ else:
1299
+ self.detector_data_format = 'spec'
1300
+
1301
+ def get_detector_data_path(self):
1302
+ raise NotImplementedError
1303
+
1304
+ def get_detector_num_bins(self, element_index=0):
1305
+ if self.detector_data_format == 'spec':
1306
+ return self.get_detector_num_bins_spec()
1307
+ elif self.detector_data_format == 'h5':
1308
+ return self.get_detector_num_bins_h5(element_index)
1309
+
1310
+ def get_detector_num_bins_spec(self):
1311
+ with open(self.get_detector_data_file_spec()) as detector_file:
1097
1312
  lines = detector_file.readlines()
1098
1313
  for line in lines:
1099
1314
  if line.startswith('#@CHANN'):
@@ -1103,33 +1318,101 @@ class SMBMCAScanParser(MCAScanParser, SMBLinearScanParser):
1103
1318
  return int(number_saved)
1104
1319
  except:
1105
1320
  continue
1106
- raise RuntimeError(f'{self.scan_title}: could not find num_bins for '
1107
- f'detector {detector_prefix}')
1108
-
1109
- def get_detector_data_path(self):
1110
- return self.scan_path
1321
+ raise RuntimeError(f'{self.scan_title}: could not find num_bins')
1111
1322
 
1112
- def get_detector_data_file(self, detector_prefix, scan_step_index=0):
1323
+ def get_detector_num_bins_h5(self, element_index):
1324
+ from h5py import File
1325
+ detector_file = self.get_detector_data_file_h5()
1326
+ with File(detector_file) as h5_file:
1327
+ dset_shape = h5_file['/entry/data/data'].shape
1328
+ return dset_shape[-1]
1329
+
1330
+ def get_detector_data_file(self, scan_step_index=0):
1331
+ if self.detector_data_format == 'spec':
1332
+ return self.get_detector_data_file_spec()
1333
+ elif self.detector_data_format == 'h5':
1334
+ return self.get_detector_data_file_h5(
1335
+ scan_step_index=scan_step_index)
1336
+
1337
+ def get_detector_data_file_spec(self):
1338
+ """Return the filename (full absolute path) to the file
1339
+ containing spec-formatted MCA data for this scan.
1340
+ """
1113
1341
  file_name = f'spec.log.scan{self.scan_number}.mca1.mca'
1114
1342
  file_name_full = os.path.join(self.detector_data_path, file_name)
1115
1343
  if os.path.isfile(file_name_full):
1116
1344
  return file_name_full
1117
- raise RuntimeError(
1118
- f'{self.scan_title}: could not find detector image file')
1345
+ raise OSError(
1346
+ '{self.scan_title}: could not find detector image file'
1347
+ )
1348
+
1349
+ def get_detector_data_file_h5(self, scan_step_index=0):
1350
+ """Return the filename (full absolute path) to the file
1351
+ containing h5-formatted MCA data for this scan.
1352
+
1353
+ :param scan_step_index:
1354
+ """
1355
+ scan_step = self.get_scan_step(scan_step_index)
1356
+ if len(self.spec_scan_shape) == 1:
1357
+ filename_index = 0
1358
+ elif len(self.spec_scan_shape) == 2:
1359
+ scan_step = self.get_scan_step(scan_step_index)
1360
+ filename_index = scan_step[0]
1361
+ else:
1362
+ raise NotImplementedError(
1363
+ 'Cannot find detector file for scans with dimension > 2')
1364
+ file_name = list_smb_mca_detector_files_h5(
1365
+ self.detector_data_path)[filename_index]
1366
+ file_name_full = os.path.join(self.detector_data_path, file_name)
1367
+ if os.path.isfile(file_name_full):
1368
+ return file_name_full
1369
+ raise OSError(
1370
+ '{self.scan_title}: could not find detector image file'
1371
+ )
1119
1372
 
1120
- def get_all_detector_data(self, detector_prefix):
1373
+
1374
+ def get_all_detector_data(self, detector):
1375
+ """Return a 2D array of all MCA spectra collected in this scan
1376
+ by the detector element indicated with `detector`.
1377
+
1378
+ :param detector: For detector data collected in SPEC format,
1379
+ this is the detector prefix as it appears in the spec MCA
1380
+ data file. For detector data collected in H5 format, this
1381
+ is the index of a particular detector element.
1382
+ :type detector: Union[str, int]
1383
+ :rtype: numpy.ndarray
1384
+ """
1385
+ if self.detector_data_format == 'spec':
1386
+ return self.get_all_detector_data_spec(detector)
1387
+ elif self.detector_data_format == 'h5':
1388
+ try:
1389
+ element_index = int(detector)
1390
+ except:
1391
+ raise TypeError(f'{detector} is not an integer element index')
1392
+ return self.get_all_detector_data_h5(element_index)
1393
+
1394
+ def get_all_detector_data_spec(self, detector_prefix):
1395
+ """Return a 2D array of all MCA spectra collected by a
1396
+ detector in the spec MCA file format during the scan.
1397
+
1398
+ :param detector_prefix: Detector name at is appears in the
1399
+ spec MCA file.
1400
+ :type detector_prefix: str
1401
+ :returns: 2D array of MCA spectra
1402
+ :rtype: numpy.ndarray
1403
+ """
1121
1404
  # This should be easy with pyspec, but there are bugs in
1122
1405
  # pyspec for MCA data..... or is the 'bug' from a nonstandard
1123
1406
  # implementation of some macro on our end? According to spec
1124
1407
  # manual and pyspec code, mca data should always begin w/ '@A'
1125
- # In example scans, it begins with '@mca1' instead
1408
+ # In example scans, it begins with '@{detector_prefix}'
1409
+ # instead
1126
1410
  data = []
1127
1411
 
1128
- with open(self.get_detector_data_file(detector_prefix)) \
1129
- as detector_file:
1412
+ with open(self.get_detector_data_file_spec()) as detector_file:
1130
1413
  lines = [line.strip("\\\n") for line in detector_file.readlines()]
1131
1414
 
1132
- num_bins = self.get_detector_num_bins(detector_prefix)
1415
+ num_bins = self.get_detector_num_bins()
1133
1416
 
1134
1417
  counter = 0
1135
1418
  for line in lines:
@@ -1156,6 +1439,106 @@ class SMBMCAScanParser(MCAScanParser, SMBLinearScanParser):
1156
1439
 
1157
1440
  return np.array(data)
1158
1441
 
1159
- def get_detector_data(self, detector_prefix, scan_step_index:int):
1160
- detector_data = self.get_all_detector_data(detector_prefix)
1442
+ def get_all_detector_data_h5(self, element_index):
1443
+ """Return a 2D array of all MCA spectra collected by a
1444
+ detector in the h5 file format during the scan.
1445
+
1446
+ :param element_index: The index of a particualr MCA element to
1447
+ return data for.
1448
+ :type element_index: int
1449
+ :returns: 2D array of MCA spectra
1450
+ :rtype: numpy.ndarray
1451
+ """
1452
+ detector_data = np.empty(
1453
+ (self.spec_scan_npts,
1454
+ self.get_detector_num_bins_h5(element_index)))
1455
+ detector_files = list_smb_mca_detector_files_h5(
1456
+ self.detector_data_path)
1457
+ for i, detector_file in enumerate(detector_files):
1458
+ full_filename = os.path.join(
1459
+ self.detector_data_path, detector_file)
1460
+ element_data = get_all_mca_data_h5(
1461
+ full_filename)[:,element_index,:]
1462
+ i_0 = i * self.spec_scan_shape[0]
1463
+ if len(self.spec_scan_shape) == 2:
1464
+ i_f = i_0 + self.spec_scan_shape[0]
1465
+ else:
1466
+ i_f = self.spec_scan_npts
1467
+ detector_data[i_0:i_f] = element_data
1468
+ return detector_data
1469
+
1470
+ def get_detector_data(self, detector, scan_step_index:int):
1471
+ """Return a single MCA spectrum for the detector indicated.
1472
+
1473
+ :param detector: If this scan collected MCA data in "spec"
1474
+ format, this is the detector prefix as it appears in the
1475
+ spec MCA data file. If this scan collected data in .h5
1476
+ format, this is the index of the detector element of
1477
+ interest.:type detector: typing.Union[str, int]
1478
+ :param scan_step_index: Index of the scan step to return the
1479
+ spectrum from.
1480
+ :type scan_step_index: int
1481
+ :returns: A single MCA spectrum
1482
+ :rtype: numpy.ndarray
1483
+ """
1484
+ detector_data = self.get_all_detector_data(detector)
1161
1485
  return detector_data[scan_step_index]
1486
+
1487
+ @cache
1488
+ def list_smb_mca_detector_files_h5(detector_data_path):
1489
+ """Return a sorted list of all *.hdf5 files in a directory
1490
+
1491
+ :param detector_data_path: Directory to return *.hdf5 files from
1492
+ :type detector_data_path: str
1493
+ :returns: Sorted list of detector data filenames
1494
+ :rtype: list[str]
1495
+ """
1496
+ return sorted(
1497
+ [f for f in os.listdir(detector_data_path) if f.endswith('.hdf5')])
1498
+
1499
+ @cache
1500
+ def get_all_mca_data_h5(filename):
1501
+ """Return all data from all elements from an MCA data file
1502
+
1503
+ :param filename: Name of the MCA h5 data file
1504
+ :type filename: str
1505
+ :returns: 3D array of MCA spectra where the first axis is scan
1506
+ step, second index is detector element, third index is channel
1507
+ energy.
1508
+ :rtype: numpy.ndarray
1509
+ """
1510
+ import os
1511
+
1512
+ from h5py import File
1513
+ import numpy as np
1514
+
1515
+ with File(filename) as h5_file:
1516
+ data = h5_file['/entry/data/data'][:]
1517
+
1518
+ # Prior to 2023-12-12, there was an issue where the XPS23 detector
1519
+ # was capturing one or two frames of all 0s at the start of the
1520
+ # dataset in every hdf5 file. In both cases, there is only ONE
1521
+ # extra frame of data relative to the number of frames that should
1522
+ # be there (based on the number of points in the spec scan). If
1523
+ # one frame of all 0s is present: skip it and deliver only the
1524
+ # real data. If two frames of all 0s are present: detector data
1525
+ # will be missing for the LAST step in the scan. Skip the first
1526
+ # two frames of all 0s in the hdf5 dataset, then add a frame of
1527
+ # fake data (all 0-s) to the end of that real data so that the
1528
+ # number of detector data frames matches the number of points in
1529
+ # the spec scan.
1530
+ check_zeros_before = 1702357200
1531
+ file_mtime = os.path.getmtime(filename)
1532
+ if file_mtime <= check_zeros_before:
1533
+ if not np.any(data[0]):
1534
+ # If present, remove first frame of blank data
1535
+ print('Warning: removing blank first frame of detector data')
1536
+ data = data[1:]
1537
+ if not np.any(data[0]):
1538
+ # If present, shift second frame of blank data to the
1539
+ # end
1540
+ print('Warning: shifting second frame of blank detector data '
1541
+ + 'to the end of the scan')
1542
+ data = np.concatenate((data[1:], np.asarray([data[0]])))
1543
+
1544
+ return data