import logging,sys,os,socket,time from math import ceil _log=logging.getLogger(__name__) class DataCollectionAbort(Exception): """Raised when acquisition configuration is invalid and the current scan should abort.""" LASER_DELAY_PV='SLAAR03-LTIM-PDLY:DELAYNS_SLOW' # fine laser delay in ns, negative: laser before the X-rays MOTION_KEYS=('cnt', 'scale', 'dwell', 'tmove', 'twait', 'ssz', 'smv', 'sdelay', 'use_trf') # recorded motion parameters def motion_delay_pulses(mode, param): ''' FEL pulses from the trigger (laser/droplet) to the X-ray pulse on the same aperture, as defined by the CTA sequences in Jungfrau.config: mode 1,3,4: 0 (same pulse) mode 5: twait//tmove mode 6: ssz[0]*ssz[1] (nominal: probe pass in the same aperture order as the pump pass) mode 7: ssz[0]*ssz[1]*(twait//tmove+1)+twait//tmove (tmove, twait in ms for mode 5 and 7: shapepath uses round((tmove+twait)/fel_per), its 'shots' doc of mode 7 is wrong) raises KeyError/ValueError/TypeError if a needed parameter is missing or invalid ''' mode=int(mode) if mode in (5, 7): tmove=param['tmove'] wait=int(param['twait']//tmove) if tmove else 0 if mode==5: return wait sx, sy=param['ssz'] return int(sx)*int(sy)*(wait+1)+wait if mode==6: sx, sy=param['ssz'] return int(sx)*int(sy) return 0 def motion_tmove(mode, fel_per_ms): 'move time in ms for mode 5 and 7, fixed to one FEL pulse (the CTA sequences assume it), else None' try: if int(mode) not in (5, 7): return None except (TypeError, ValueError): return None return int(fel_per_ms) if float(fel_per_ms).is_integer() else fel_per_ms # 10 not 10.0 in the parameters def motion_run_pulses(mode, param, count): ''' FEL pulses of a collection (length of the CTA sequences in Jungfrau.config x repetitions), count: grid count (nx, ny) returns (pulses, warnings): pulses is None if the collection would abort (the warnings tell why) ''' mode=int(mode) nx, ny=map(int, count) if mode==4: return nx*ny, [] wait=0 if mode in (5, 7): tmove=param['tmove'] wait=int(param['twait']//tmove) if tmove else 0 if mode==5: return nx*ny*(wait+1), [] sx, sy=map(int, param['ssz']) warn=[] if sx<=0 or sx%2: warn.append(f'ssz[0] ({sx}) must be even and positive') if sx>0 and nx%sx: warn.append(f'ssz[0] ({sx}) must divide the chip columns ({nx})') if mode==6 and sy!=ny: warn.append(f'mode 6: ssz[1] ({sy}) must be the chip rows ({ny})') if mode==7 and sy>0 and ny%sy: warn.append(f'ssz[1] ({sy}) must divide the chip rows ({ny})') if warn: return None, warn grp=sx*sy if mode==6: return (nx//sx)*(ny//sy)*(2*grp+sx-2), [] return (ny//sy)*((nx//sx)*(2*grp*(wait+1)+sx-2)+sy), [] # mode 7 def total_delay_ns(pulses, fel_per_ms, laser_delay_ns): '''laser to X-ray delay in ns (negative: laser before the X-rays)''' return laser_delay_ns-pulses*fel_per_ms*1e6 def _plain(v): 'numpy values/arrays and tuples to plain python (json header)' if hasattr(v, 'tolist'): return v.tolist() if isinstance(v, (tuple, list)): return [_plain(x) for x in v] return v logging.getLogger('matplotlib').setLevel(logging.INFO) if socket.gethostname()=='ganymede': base=os.path.abspath(os.path.dirname(__file__)) sys.path.insert(0, os.path.abspath(os.path.join(base,'../PBSwissMX/python'))) sys.path.insert(0, os.path.abspath(os.path.join(base,'../../PBTools'))) else: base=os.path.abspath(os.path.dirname(__file__)) sys.path.insert(0, os.path.join(base,'PBTools')) sys.path.insert(0, os.path.join(base,'PBSwissMX/python')) sys.path.insert(0, os.path.expanduser('/sf/cristallina/applications/slic/slic-package')) sys.path.insert(0, os.path.expanduser('/sf/cristallina/applications/mx/swissmx_tools/jfj')) from PyQt5.QtWidgets import (QApplication,) from app_config import AppCfg #settings, option, toggle_option import epics from pbtools.misc.pp_comm import PPComm from pbtools.misc.gather import Gather import shapepath try: from slic.core.acquisition import SFAcquisition from slic.core.acquisition.broker.tools import get_endstation from slic.devices.timing.events import CTASequencer from ctadaq import CTAAcquisition from jfjoch_device import JFJ except ImportError as e: _log.warning(f'import failed: {e}') class Shutter: def __init__(self,mode=1): self._mode=mode @property def sync_flag(self): # read at each open/close: the deltatau sync_flag can be changed in the preferences while the app runs return QApplication.instance()._cfg.value(AppCfg.DT_MISC)['sync_flag'] def open(self): mode=self._mode if mode==0: _log.info('open simulated shutter') elif mode==1: # open laser shutter #epics.caput("SLAAR31-LPSYS-ESC:LHX1_SHUT_OPEN", 1) '''think this shutter is no longer used''' if epics.caget("SLAAR31-LPSYS-ESCIP23:ONOFF.RVAL") == 1: epics.caput("SLAAR31-LPSYS-ESCOP0:ONOFF", 1) _log.info('laser shutter opened') if self.sync_flag==0: #if using cta, sets pulse_picker output to follow pulser 0 # open fast shutter epics.caput("SARES30-LTIM01-EVR0:RearUniv0_SOURCE", 0) epics.caput("SARES30-LTIM01-EVR0:RearUniv0_SOURCE2", 0) epics.caput("SARES30-LTIM01-EVR0:RearUniv0_SNUMPD", 0) epics.caput("SARES30-LTIM01-EVR0:RearUniv0_SNUMPD2", 0) epics.caput("SARES30-LTIM01-EVR0:RearUniv0-Ena-SP", 1) _log.info('fast shutter opened') def close(self): mode=self._mode if mode==0: _log.info('close simulated shutter') elif mode==1: # close laser shutter if epics.caget("SLAAR31-LPSYS-ESCIP23:ONOFF.RVAL") == 1: epics.caput("SLAAR31-LPSYS-ESCOP2:ONOFF", 1) _log.info('laser shutter closed') #epics.caput("SLAAR31-LPSYS-ESC:LHX1_SHUT_CLOSE", 1) '''think this shutter is no longer used''' # close fast shutter epics.caput("SARES30-LTIM01-EVR0:RearUniv0-Ena-SP", 0) if self.sync_flag==0: #if using cta, sets pulse_picker output back to high low, could do this every time? epics.caput("SARES30-LTIM01-EVR0:RearUniv0_SOURCE", 3) epics.caput("SARES30-LTIM01-EVR0:RearUniv0_SOURCE2", 4) epics.caput("SARES30-LTIM01-EVR0:RearUniv0_SNUMPD", 0) epics.caput("SARES30-LTIM01-EVR0:RearUniv0_SNUMPD2", 0) _log.info('fast shutter closed') class Deltatau: def __init__(self,sim=False): app=QApplication.instance() cfg=app._cfg host=cfg.value(AppCfg.DT_HOST) # sim=False;host='localhost:10001:10002' # this only moves motors during acquisition if sim: self._comm=comm=None self._gather=gather=None else: hpp=host.split(':') param={'host':hpp[0]} if len(hpp)>1: param['port']=int(hpp[1]) if len(hpp)>2: param['fast_gather_port']=int(hpp[2]) _log.info(' -> ssh-tunneling PPComm({host}:{port} {host}:{fast_gather_port})'.format(**param)) try: self._comm=comm=PPComm(**param,timeout=2.0) self._gather=gather=Gather(comm) except (socket.timeout,socket.gaierror) as e: _log.critical(f'can not connect to deltatau:"{host}" -> {e}') self._shapepath=sp=shapepath.ShapePath(comm, gather, verbose=0xff, sync_mode=1, sync_flag=3) class Jungfrau: def __init__(self,sim=False): # setup slic parameters if sim: self._sim=True return try: self._pv_pulse_id=epics.PV('SAR-EXPMX-EVR0:RX-PULSEID') self._pv_pulse_id.connect() except NameError as e: _log.critical(f'Jungfrau not connected: {e}') self.n_pulses_run = None self.image_pulse_mask = None self.images_per_cycle = None self.pulses_per_cycle = None self.motion_mode = None self.block_size_to_slic = None def _pulse_count_for_images(self, n_images): if self.images_per_cycle is None or self.pulses_per_cycle is None: raise ValueError("Pulse/image mapping not configured") full_cycles, remainder = divmod(n_images, self.images_per_cycle) pulses = full_cycles * self.pulses_per_cycle if remainder == 0: return pulses count = 0 for idx, is_image in enumerate(self.image_pulse_mask): if is_image: count += 1 if count == remainder: return pulses + idx + 1 raise ValueError( f"Could not map {remainder} images into pulse mask of length {len(self.image_pulse_mask)}" ) def _pulse_blocks_for_jf_files(self, jf_block_size): """Return either a constant raw pulse block size or a list of per-file pulse counts. For mode 6/7 the raw pulse block size can vary between JF files because the image-to-pulse mapping is not one-to-one. This method preserves exact Jungfrau file boundaries by splitting pulses at the image counts used by JFJ. """ total_images = self.images if total_images is None: raise ValueError("Total image count is not configured") if jf_block_size <= 0: raise ValueError("JF block size must be positive") file_sizes = [] full_files, remainder = divmod(total_images, jf_block_size) file_sizes.extend([jf_block_size] * full_files) if remainder: file_sizes.append(remainder) boundaries = [] current_images = 0 for file_size in file_sizes: current_images += file_size boundaries.append(self._pulse_count_for_images(current_images)) block_sizes = [boundaries[0]] for prev, curr in zip(boundaries, boundaries[1:]): block_sizes.append(curr - prev) if len(set(block_sizes)) == 1: return block_sizes[0] return block_sizes def config(self,**kwargs): if getattr(self,'_sim',False): _log.info(f'simulated') return app=QApplication.instance() #temproary fix, couldnt access these in function, maybe the bt above needs to be self.detectors ... etc cfg=app._cfg det = cfg.value(AppCfg.DAQ_DET) #print("det", repr(det)) detectors = [det] if det.get("name") else None bs_channels = cfg.value(AppCfg.DAQ_BS_CH) pv_channels = cfg.value(AppCfg.DAQ_PV_CH) loc=cfg.value(AppCfg.DAQ_LOC) run=cfg.value(AppCfg.DAQ_RUN) dt_misc = cfg.value(AppCfg.DT_MISC) code_gen=kwargs.get('code_gen',0) sync_mode=dt_misc['sync_mode'] sync_flag=dt_misc['sync_flag'] self.n_pulses_run = None self.image_pulse_mask = None self.images_per_cycle = None self.pulses_per_cycle = None self.block_size_to_slic = None self.block_size_to_jungfrau = None # Store mode-specific parameters for block size calculation self.mode_wait_pulses = None self.mode_ssz = None self.mode_apperture_group_size = None self.mode_number_of_apperture_groups = None #print(kwargs) motion_mode = kwargs['mode'] self.motion_mode = motion_mode # recorded in the JFJ header (acquire): what actually runs, not what the main page shows motion_param={k:_plain(kwargs[k]) for k in MOTION_KEYS if k in kwargs} try: delay_pulses=motion_delay_pulses(motion_mode, kwargs) except (KeyError, ValueError, TypeError) as e: _log.warning(f'delay pulses not defined for mode {motion_mode}: {e!r}') delay_pulses=None self.scan_meta={'chip_type':kwargs.get('chip_type', ''), 'motion_mode':int(motion_mode), 'motion_param':motion_param, 'delay_pulses':delay_pulses} _log.info(f'scan meta: {self.scan_meta}') if loc['jungfraujoch']: try: self.jfj = JFJ("http://sf-daq-2:8080") self.detectors=None _log.info(f'JungFrauJoch connected') except NameError as e: self.jfj = None _log.critical(f'JungfrauJoch not connected: {e}') is_scan_step=False # JFJ comment block # if loc['jungfraujoch']: # self.jfj = None # self.detectors = None # _log.warning('JFJ bypassed - running in simulated connected mode') # print('JFJ bypassed') # else: # self.jfj = None # _log.info(f'JungFrauJoch is not in use') # print('JungFrauJoch not in use') # JFJ comment block #if sync_flag==0: if motion_mode == 1 or motion_mode == 3: #grid_cnt=kwargs['grid']['count'] repetitions=int(kwargs['points'][:, 0].max())+1 #grid_cnt[0] #'x' or number of columns # if mode = 1 or 3 cta_multiplier=int(kwargs['points'][:, 1].max())+1#grid_cnt[1] #'y' or number of appertures in a column/number of rows elif motion_mode == 4 or motion_mode == 5 or motion_mode == 6 or motion_mode == 7: grid_cnt=kwargs['grid']['count'] repetitions=grid_cnt[0] #'x' or number of columns # if mode = 1 or 3 cta_multiplier=grid_cnt[1] #'y' or number of appertures in a column/number of rows number_of_appertures = repetitions*cta_multiplier cta=CTASequencer("SAR-CCTA-ESC") cta.cfg.repetitions = repetitions self.images=repetitions*cta_multiplier on_off_ratio = int(run['num_of_on_to_1_off'])+1 if motion_mode==5: if kwargs['tmove']==0: wait_pulses=0 else: wait_pulses=int(kwargs['twait']//kwargs['tmove']) # int: used to repeat lists self.mode_wait_pulses = wait_pulses #xray_seq=[0,]*wait_pulses+[1] # multiplier is proportional to wait_time i.e. 10 ms = 1, 20 ms =2, 30 ms =3. #xray_sequence=[0,]*wait_pulses+[1] #cta.seq[200]=xray_sequence*cta_multiplier # x-ray_shutter #cta.seq[214] = [1,] + [0,] * (len(xray_sequence * cta_multiplier) -1) #trigger_sequence = [1,] + [0,] * wait_pulses + [0,] * wait_pulses + [0] #cta.seq[215]=trigger_sequence*(cta_multiplier//2) # laser_shutter #image_label_sequence = [0,] * wait_pulses + [1] + [0,] * wait_pulses + [0] #cta.seq[216]=image_label_sequence*(cta_multiplier//2) #cta.seq[219] = xray_sequence * cta_multiplier #on_off_ratio = 2 #1 on to 1 off which means 2 in the whole sequence xray_sequence = [0,] * wait_pulses + [1,] cta.seq[200] = xray_sequence * cta_multiplier # x-ray_shutter cta.seq[214] = [1,] + [0,] * (len(xray_sequence * cta_multiplier) -1) #start motion if run["triggered"]: trigger_on = [1,] + [0,] * wait_pulses trigger_off = [0,] + [0,] * wait_pulses trigger_sequence = trigger_off + trigger_on * (on_off_ratio-1) image_on = [0,] * wait_pulses + [1,] image_label_sequence = [0,] * wait_pulses + [0,] + image_on * (on_off_ratio-1) cta.seq[215] = trigger_sequence * int(cta_multiplier/on_off_ratio) # trigger (laser_shutter or droplet ejector) cta.seq[216] = image_label_sequence * int(cta_multiplier/on_off_ratio) # image label (on or off) else: no_trigger_sequence = [0,] + [0,] * wait_pulses cta.seq[215] = no_trigger_sequence * cta_multiplier # x-ray_shutter # trigger (laser_shutter or droplet ejector) cta.seq[216] = no_trigger_sequence * cta_multiplier # x-ray_shutter # image label (on or off)1 cta.seq[219] = xray_sequence * cta_multiplier # detector trigger self.n_pulses_run = len(cta.seq[200])*repetitions #print(on_off_ratio,cta_multiplier,int(cta_multiplier/on_off_ratio),len(cta.seq[200]),len(cta.seq[214]),len(cta.seq[215]),len(cta.seq[216]),len(cta.seq[219])) #self.motion_mode = 5 elif motion_mode==6: ssz = kwargs['ssz'] #smv = kwargs['smv'] grid_cnt=kwargs['grid']['count'] repetitions=grid_cnt[0] #'x' or number of columns # if mode = 1 or 3 cta_multiplier=grid_cnt[1] #'y' or number of appertures in a column/number of rows if ssz[0] <= 0 or ssz[0] % 2 != 0: message = f"Mode 6 requires ssz[0] to be even and positive, got {ssz[0]}" _log.warning(message) raise DataCollectionAbort(message) if repetitions % ssz[0] != 0: message = f"Mode 6 requires repetitions ({repetitions}) to be divisible by ssz[0] ({ssz[0]})" _log.warning(message) raise DataCollectionAbort(message) if ssz[1] != cta_multiplier: message = f"Mode 6 requires ssz[1] == vertical chip length ({cta_multiplier}), got {ssz[1]}" _log.warning(message) raise DataCollectionAbort(message) smv = ssz[0]-1 #max(ssz[0]-1, ssz[1]) commented out for fast camera tests, need a better work around for non-equal jumps in the CTA or motion script, currently avoid! apperture_group_size = ssz[0] * ssz[1] #number_of_apperture_groups = number_of_appertures // apperture_group_size number_of_apperture_groups=(repetitions//ssz[0])*(cta_multiplier//ssz[1]) # Store mode 6 parameters for block size calculation self.mode_ssz = ssz self.mode_apperture_group_size = apperture_group_size self.mode_number_of_apperture_groups = number_of_apperture_groups _log.debug(f"number_of_apperture_groups = {number_of_apperture_groups}") #print(f"actually want 162//8 * {162//7} (or 162//8 * 162//7) which = 460, so 460*8*7=25760") transition_pulses = smv #on_off_ratio = 2 #1 on to 1 off which means 2 in the whole sequence xray_sequence = [0,] * apperture_group_size + [1,] * apperture_group_size + [0,] * (transition_pulses - 1) cta.seq[200] = xray_sequence cta.seq[214] = [1,] + [0,] * (apperture_group_size -1) + [0,] * apperture_group_size + [0,] * (transition_pulses - 1) #start motion if run["triggered"]: if on_off_ratio > apperture_group_size: on_off_ratio = apperture_group_size on_off_sequence = [1,] * (on_off_ratio - 1) + [0,] cta.seq[215] = on_off_sequence * (apperture_group_size//on_off_ratio) + [0,] * apperture_group_size + [0,] * (transition_pulses -1) # Trigger 1:1 repeat_sequence_laser_closed = [1, ] + [0,] * (apperture_group_size - 1) repeat_sequence_laser_open = [1,] + [0,] * (apperture_group_size - 1) tranisition_sequence_laser_shut = [0,] * (transition_pulses - 1) #close shutter after final point before starting move...? cta.seq[213] = repeat_sequence_laser_open + repeat_sequence_laser_closed + tranisition_sequence_laser_shut # Trigger 1:1 cta.seq[216] = [0,] * apperture_group_size + on_off_sequence * (apperture_group_size//on_off_ratio) + [0,] * (transition_pulses - 1) # Label image light dark 1:1 #change back to 1,0 for normal on off measurements else: cta.seq[215] = [0] * apperture_group_size * 2 + [0,] * (transition_pulses - 1) # trigger (laser_shutter or droplet ejector) cta.seq[216] = [0] * apperture_group_size * 2 + [0,] * (transition_pulses -1) # image label (on or off)1 cta.seq[219] = xray_sequence # detector trigger cta.cfg.repetitions = number_of_apperture_groups # number of repeitions is now number of times we repeat the loop self.images=number_of_apperture_groups*apperture_group_size _log.info(f"mode 6: length of sequence={len(cta.seq[200])} apperture in group={apperture_group_size} " f"number_of_apperture_groups={number_of_apperture_groups}") #print(f"n_pulses = {self.n_pulses_run} and n_appertures and images = {number_of_appertures}") elif motion_mode==7: ssz = kwargs['ssz'] self.mode_ssz = ssz if ssz[0] <= 0 or ssz[0] % 2 != 0: message = f"Mode 7 requires ssz[0] to be even and positive, got {ssz[0]}" _log.warning(message) raise DataCollectionAbort(message) if repetitions % ssz[0] != 0: message = f"Mode 7 requires repetitions ({repetitions}) to be divisible by ssz[0] ({ssz[0]})" _log.warning(message) raise DataCollectionAbort(message) if cta_multiplier % ssz[1] != 0: message = f"Mode 7 requires cta_multiplier ({cta_multiplier}) to be divisible by ssz[1] ({ssz[1]})" _log.warning(message) raise DataCollectionAbort(message) if kwargs['tmove']==0: wait_pulses=0 else: wait_pulses=int(kwargs['twait']//kwargs['tmove']) # int: used to repeat lists self.mode_wait_pulses = wait_pulses apperture_group_size = ssz[0]*ssz[1] sections_per_row = repetitions//ssz[0] number_of_rows = cta_multiplier//ssz[1] number_of_apperture_groups = sections_per_row*number_of_rows # Store mode 7 parameters for block size calculation self.mode_apperture_group_size = apperture_group_size self.mode_number_of_apperture_groups = number_of_apperture_groups # matches shapepath.py mode-7 defaults: tsx=ssz[0]-1 (section-to-section, same row), # tsy=ssz[1] (row-to-row) - the pvt time (in shots) each transition move is queued with. section_transition_pulses = ssz[0]-1 row_transition_pulses = ssz[1] aperture_sequence = [0,]*wait_pulses+[1,] xray_return_sequence = aperture_sequence*apperture_group_size xray_dark_sequence = [0,]*len(xray_return_sequence) section_transition_sequence = [0,]*(section_transition_pulses-1) row_transition_sequence = [0,]*row_transition_pulses # one section: dark (pump pass) + aperture pattern (return pass) + gap to next section; # one row: sections_per_row sections + gap to next row; repeated number_of_rows times # by the CTA sequencer directly, same pattern as mode 6. section_xray_sequence = xray_dark_sequence+xray_return_sequence+section_transition_sequence cta.seq[200] = section_xray_sequence*sections_per_row+row_transition_sequence cta.seq[219] = cta.seq[200] if run["triggered"]: if on_off_ratio > apperture_group_size: on_off_ratio = apperture_group_size droplet_sequence = ([1,]+[0,]*wait_pulses)*(on_off_ratio-1) no_droplet_sequence = [0,]+[0,]*wait_pulses trigger_dark_sequence = (droplet_sequence+no_droplet_sequence)*(apperture_group_size//on_off_ratio) trigger_return_sequence = [0,]*len(xray_return_sequence) section_trigger_sequence = trigger_dark_sequence+trigger_return_sequence+section_transition_sequence cta.seq[215] = section_trigger_sequence*sections_per_row+row_transition_sequence image_on_sequence = ([0,]*wait_pulses+[1,])*(on_off_ratio-1) image_off_sequence = [0,]*wait_pulses+[0,] image_return_sequence = (image_on_sequence+image_off_sequence)*(apperture_group_size//on_off_ratio) image_dark_sequence = [0,]*len(xray_return_sequence) section_image_sequence = image_dark_sequence+image_return_sequence+section_transition_sequence cta.seq[216] = section_image_sequence*sections_per_row+row_transition_sequence else: no_trigger_sequence = [0,]*len(cta.seq[200]) cta.seq[215] = no_trigger_sequence cta.seq[216] = no_trigger_sequence cta.seq[214] = [1,]+[0,]*(len(cta.seq[200])-1) cta.cfg.repetitions = number_of_rows self.images = number_of_apperture_groups*apperture_group_size _log.info(f"mode 7: apperture_group_size={apperture_group_size} sections_per_row={sections_per_row} " f"number_of_rows={number_of_rows} number_of_apperture_groups={number_of_apperture_groups} " f"section_transition_pulses={section_transition_pulses} row_transition_pulses={row_transition_pulses} " f"wait_pulses={wait_pulses} length of one row sequence={len(cta.seq[200])}") else: _log.info('not mode 5 or 6') #need to add on off ratio # no extra rows 1:1 #self.motion_mode = 4 cta.seq[214]=[1,]+[0,]*(cta_multiplier-1) #start motion cta.seq[200]=[1,]*cta_multiplier #uncomment me for normal operation #cta.seq[200]=[1,0,]*(cta_multiplier//2) # x-ray_shutter cta.seq[219]=[1,]*cta_multiplier #trigger detector if run["triggered"]: #cta.seq[215]=[1,0,]*(cta_multiplier//2) # Trigger 1:1 _log.info('Oi John, you left the light on! From Martin') cta.seq[215]=[1,]*cta_multiplier # shutter always open _log.info('event 215 is on for every pulse in mode 4, and 216 is assuming light:dark is 1:1') cta.seq[216]=[1,0,]*(cta_multiplier//2) # Label image light dark 1:1 #change back to 1,0 for normal on off measurements self.n_pulses_run = len(cta.seq[200])*repetitions cta.seq.upload() try: # -------------------------------------------------------- # detector exposure topology # -------------------------------------------------------- self.image_pulse_mask = [ int(x) == 1 for x in cta.seq[219] ] self.images_per_cycle = sum( self.image_pulse_mask ) self.pulses_per_cycle = len( self.image_pulse_mask ) if self.images_per_cycle == 0: raise ValueError( "No detector images found in cta.seq[219]" ) jf_block_size = run['block_size'] # -------------------------------------------------------- # total dataset pulse count and block size mapping # -------------------------------------------------------- self.n_pulses_run = self._pulse_count_for_images(self.images) block_sizes = self._pulse_blocks_for_jf_files(jf_block_size) self.block_size_to_slic = block_sizes if motion_mode == 4: # For continuous acquisition, image pulses and raw pulses are one-to-one. self.block_size_to_slic = jf_block_size _log.debug( f"Mode {motion_mode}: exact BS pulse blocks for {jf_block_size} JF images = {self.block_size_to_slic}, " f"images_per_cycle={self.images_per_cycle}, pulses_per_cycle={self.pulses_per_cycle}, " f"n_pulses_run={self.n_pulses_run}" ) _log.debug( f"pulse/image mapping: motion_mode={motion_mode}, " f"images_per_cycle={self.images_per_cycle}, " f"pulses_per_cycle={self.pulses_per_cycle}, " f"jf_block_size={jf_block_size}, " f"block_size_to_slic={self.block_size_to_slic}, " f"n_pulses_run={self.n_pulses_run}" ) _log.info( f"motion_mode={motion_mode} images={self.images} " f"images_per_cycle={self.images_per_cycle} pulses_per_cycle={self.pulses_per_cycle} " f"n_pulses_run={self.n_pulses_run} block_size_to_slic={self.block_size_to_slic}" ) except Exception as e: _log.warning( f'failed to build pulse/image mapping: {e}' ) self.image_pulse_mask = None self.images_per_cycle = None self.pulses_per_cycle = None self.block_size_to_slic = None self._daq=CTAAcquisition(cta, loc['end_station'], loc['p_group'], default_detectors=detectors, default_channels=bs_channels, default_pvs=pv_channels, rate_multiplicator=1, append_user_tag_to_data_dir=True)#, timeout=30) #else: # self._daq=SFAcquisition( # loc['end_station'], loc['p_group'], # default_detectors=detectors, default_channels=bs_channels, default_pvs=pv_channels, # rate_multiplicator=1, append_user_tag_to_data_dir=True) def acquire(self, n_pulses, wait=False): if getattr(self,'_sim',False): _log.info(f'simulated') return try: daq=self._daq except AttributeError: _log.info(f'simulated') return app=QApplication.instance() cfg=app._cfg run=cfg.value(AppCfg.DAQ_RUN) loc=cfg.value(AppCfg.DAQ_LOC) data_proc=cfg.value(AppCfg.DATA_PROC) daq_sample=cfg.value(AppCfg.DAQ_SAMPLE) daq_uc=cfg.value(AppCfg.DAQ_UC) try: self._pulse_id_start=int(self._pv_pulse_id.value) except TypeError as e: _log.warning(f'failed to get _pulse_id_start: {e}') if self.n_pulses_run: n_pulses_run=self.n_pulses_run _log.info(f'self.n_pulses_run {self.n_pulses_run} is equal to n_pulses_run {n_pulses_run}') _log.info(f'number of images to JFJ is {self.images}') else: n_pulses_run = n_pulses + run['padding'] _log.info(f'self.n_pulses_run {self.n_pulses_run} is not equal to n_pulses_run {n_pulses_run}') #n_pulses_run*=2 # comment me out please when not using 10 ms wait (for stop and go) images_per_file = run['block_size'] sample_name = daq_sample['protein'] #protein_name prefix = run['prefix'] user_tag = f'{sample_name}_{prefix}' if self.jfj: #daq_uc={"a":70,"b":70,"c":70,"alpha":90,"beta":90,"gamma":90} unit_cell = {"a":daq_uc['a'],"b":daq_uc['b'], "c":daq_uc['c'],"alpha":daq_uc['alpha'], "beta":daq_uc['beta'], "gamma":daq_uc['gamma']} geometry_file_location = loc['geometry_file_location'] trigger_event = loc['event_trigger'] trigger_flag = run['triggered'] laser_energy = run['laser_pulse_energy'] dt_misc = cfg.value(AppCfg.DT_MISC) fel_per = dt_misc['fel_per'] try: laser_delay_ns = float(epics.caget(LASER_DELAY_PV, timeout=1.0)) except (TypeError, ValueError): _log.warning(f'laser delay: no value from {LASER_DELAY_PV}') laser_delay_ns = None scan_meta = getattr(self, 'scan_meta', {}) delay_pulses = scan_meta.get('delay_pulses') if delay_pulses is None or laser_delay_ns is None: trigger_delay = None else: trigger_delay = total_delay_ns(delay_pulses, fel_per, laser_delay_ns) _log.info(f'delay: {delay_pulses} pulses x {fel_per} ms + laser {laser_delay_ns} ns = {trigger_delay} ns') transmission_ATT53 = epics.caget("SARFE10-OATT053:UsrRec.TD") transmission_ATT150 = epics.caget("SAROP31-OATA150:UsrRec.TD") transmission = transmission_ATT53*transmission_ATT150 detector_distance_mm = round(epics.caget("SAR-EXPMX:MOT_DET_Z.RBV"),2) run_number = self._daq.client.next_run() is_scan_step = True pgroup=loc['p_group'] try: endstation = get_endstation() _log.info(f'got endstation through slic:{endstation}') except: endstation = loc['end_station'] #'cristallina' #need cdoe form slic to direct this _log.info(f'got endstation through gui:{endstation}') jfj_file_prefix = f'sf/{endstation}/data/{pgroup}/raw/run{run_number:04}-{user_tag}/data/acq' self.jfj_file_prefix = jfj_file_prefix header_appendix = {} for key in data_proc.keys(): value = data_proc[key] header_appendix[f'{key}']=value header_appendix['geometry_file_location'] = geometry_file_location header_appendix['trigger_flag'] = trigger_flag header_appendix['trigger_delay'] = trigger_delay # total delay in ns (same as total_delay_ns) header_appendix['trigger_event'] = trigger_event header_appendix['laser_pulse_energy'] = laser_energy header_appendix['chip_type'] = scan_meta.get('chip_type', '') header_appendix['motion_mode'] = scan_meta.get('motion_mode') header_appendix['motion_param'] = scan_meta.get('motion_param', {}) header_appendix['fel_per_ms'] = fel_per header_appendix['delay_pulses'] = delay_pulses header_appendix['laser_delay_ns'] = laser_delay_ns header_appendix['total_delay_ns'] = trigger_delay header_appendix['file_prefix'] = f'run{run_number:04}-{user_tag}' try: self.jfj.acquire( beam_x_pxl = loc['beam_x'], beam_y_pxl = loc['beam_y'], detector_distance_mm = detector_distance_mm, incident_energy_keV = loc['incident_energy_kev'], transmission = transmission, sample_name = sample_name, run_number = run_number, file_prefix = jfj_file_prefix, experiment_group = pgroup, ntrigger = self.images, images_per_file = images_per_file, unit_cell = unit_cell, space_group_number = daq_uc['space_group_number'], header_appendix = header_appendix ) except Exception as e: _log.warning('error in priming JFJoch continuing, if not debugging motion be warned JFJoch not on or an error') _log.error(f'JFJoch exception rasied as: {e}') _log.info('JFJ primed') else: is_scan_step=False if self.block_size_to_slic: block_size = self.block_size_to_slic _log.info(f'using exact pulse/image mapping: block_size_to_slic={block_size}') else: block_size = run['block_size'] _log.info(f'using simple block size={block_size}') if type(self._daq) is CTAAcquisition: if self.block_size_to_slic: self._daq.acquire( user_tag, n_pulses=n_pulses_run, n_block_size=self.block_size_to_slic, wait=False, cell_name=run['cell_name'], is_scan_step=is_scan_step ) else: self._daq.acquire( user_tag, n_pulses=max( n_pulses_run, block_size ), n_block_size=block_size, wait=False, cell_name=run['cell_name'], is_scan_step=is_scan_step ) else: self._daq.acquire( user_tag, n_pulses=max(n_pulses_run, block_size), n_repeat=ceil(n_pulses_run/block_size), wait=False, cell_name=run['cell_name'], is_scan_step=is_scan_step ) cfg.setValue(AppCfg.DAQ_RUN,run) is_scan_step=False def stop_triggers(self): ''' called on abort (also from the GUI thread): stop the CTA sequence (detector, x-ray shutter and laser triggers) and end the BS/PV data retrieval at the current pulse id. The shutters are closed separately by Shutter.close() ''' if getattr(self,'_sim',False): return daq=getattr(self,'_daq',None) if daq is None: return try: stop_pid=int(self._pv_pulse_id.get()) except Exception as e: stop_pid=None _log.warning(f'abort: failed to get pulse id, no further data retrieval: {e}') if hasattr(daq,'abort'): # CTAAcquisition _log.warning(f'abort: stop CTA sequence, retrieve data up to pulse id {stop_pid}') daq.abort(stop_pid) else: task=getattr(daq,'current_task',None) if task is not None and task.stopper: _log.warning('abort: stop DAQ task') task.stopper() def cancel(self, timeout=60.): ''' called from the collection worker after an abort: cancel JFJ and wait (max. timeout seconds each) for the data retrieval to finish and JFJ to be idle again ''' if getattr(self,'_sim',False): return jfj=getattr(self,'jfj',None) if jfj: try: jfj.api.cancel_post() _log.warning('abort: JFJ cancel sent') except Exception as e: _log.error(f'abort: JFJ cancel failed: {e}') task=getattr(getattr(self,'_daq',None),'current_task',None) if task is not None: task.thread.join(timeout) if task.thread.is_alive(): _log.error(f'abort: data retrieval still running after {timeout}s') else: _log.info('abort: data retrieval finished') if jfj: t_end=time.time()+timeout status=None while time.time()