def add_theta(self): """Add theta at the end of a scan. """ log.info('add theta') full_file_name = self.epics_pvs['FPFullFileName'].get(as_string=True) if os.path.exists(full_file_name): try: with h5py.File(full_file_name, "a") as f: if self.theta is not None: unique_ids = f['/defaults/NDArrayUniqueId'] hdf_location = f['/defaults/HDF5FrameLocation'] total_dark_fields = self.num_dark_fields * ( (self.dark_field_mode in ('Start', 'Both')) + (self.dark_field_mode in ('End', 'Both'))) total_flat_fields = self.num_flat_fields * ( (self.flat_field_mode in ('Start', 'Both')) + (self.flat_field_mode in ('End', 'Both'))) proj_ids = unique_ids[hdf_location[:] == b'/exchange/data'] flat_ids = unique_ids[hdf_location[:] == b'/exchange/data_white'] dark_ids = unique_ids[hdf_location[:] == b'/exchange/data_dark'] # create theta dataset in hdf5 file if len(proj_ids) > 0: theta_ds = f.create_dataset( '/exchange/theta', (len(proj_ids), )) theta_ds[:] = self.theta[proj_ids - proj_ids[0]] # warnings that data is missing if len(proj_ids) != len(self.theta): log.warning( f'There are {len(self.theta) - len(proj_ids)} missing data frames' ) missed_ids = [ ele for ele in range(len(self.theta)) if ele not in proj_ids - proj_ids[0] ] missed_theta = self.theta[missed_ids] # log.warning(f'Missed ids: {list(missed_ids)}') log.warning(f'Missed theta: {list(missed_theta)}') if len(flat_ids) != total_flat_fields: log.warning( f'There are {total_flat_fields - len(flat_ids)} missing flat field frames' ) if (len(dark_ids) != total_dark_fields): log.warning( f'There are {total_dark_fields - len(dark_ids)} missing dark field frames' ) except: log.error('Add theta: Failed accessing: %s', full_file_name) traceback.print_exc(file=sys.stdout) else: log.error('Failed adding theta. %s file does not exist', full_file_name)
def fly_scan(self): """Control of Sample X position """ if(abs(self.epics_pvs['SampleInX'].value-self.epics_pvs['SampleX'].value)>1e-4): log.error('SampleInX is not the same as current SampleTopX') self.epics_pvs['ScanStatus'].put('SampleX error') self.epics_pvs['StartScan'].put(0) return super().fly_scan()
def run(fname): try: # Form auto-complete for scan out = subprocess.Popen(['tomo', 'scan', '-h'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) stdout, _ = out.communicate() stdout = str(stdout) cmdscan = [] parscan = [] st = stdout.find("optional") while (1): st = stdout.find('--', st) end = min(stdout.find(' ', st), stdout.find('\\n', st)) if (st < 0): break cmdscan.append(stdout[st:end]) st = stdout.find('(default:', end) st1 = stdout.find('--', end) if (st1 > st or st1 < 0): end = stdout.find(')', st) parscan.append(stdout[st + 9:end].replace(" ", "").replace( "\\n", "")) else: parscan.append("") st = end # Create bash file fid = open(fname, 'w') fid.write( '#/usr/bin/env bash\n _tomo()\n{\n\tlocal cur prev opts\n\tCOMPREPLY=()\n\tcur="${COMP_WORDS[COMP_CWORD]}"\n\tprev="${COMP_WORDS[COMP_CWORD-1]}"\n' ) # check all tomo scan fid.write('\tif [[ ${COMP_WORDS[1]} == "scan" ]] ; then\n') fid.write('\t\topts="') fid.write(' '.join(cmdscan)) fid.write('"\n') fid.write( '\t\tCOMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )\n\tfi\n') for k in range(len(cmdscan)): fid.write('\tif [[ ${prev} == "') fid.write(cmdscan[k]) fid.write('" ]] ; then\n') fid.write('\t\topts="') fid.write(parscan[k]) fid.write('"\n') fid.write( '\t\tCOMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )\n\t\treturn 0\n\tfi\n' ) fid.write('}\n') fid.write('complete -F _tomo tomo') fid.close() except: log.error('Autocomplete file was not created')
def create_remote_directory(remote_server, remote_dir): cmd = 'mkdir -p ' + remote_dir try: log.info(' *** creating remote directory %s' % (remote_dir)) subprocess.check_call(['ssh', '-t', remote_server, cmd]) log.info(' *** creating remote directory %s: Done!' % (remote_dir)) return 0 except subprocess.CalledProcessError as e: log.error(' *** Error while creating remote directory. Error code: %d' % (e.returncode)) return -1
def check_remote_directory(remote_server, remote_dir): try: rcmd = 'ls ' + remote_dir # rcmd is the command used to check if the remote directory exists subprocess.check_call(['ssh', '-t', remote_server, rcmd], stderr=open(os.devnull, 'wb'), stdout=open(os.devnull, 'wb')) log.warning(' *** remote directory %s exists' % (remote_dir)) return 0 except subprocess.CalledProcessError as e: log.warning(' *** remote directory %s does not exist' % (remote_dir)) if e.returncode == 2: return e.returncode else: log.error(' *** Unknown error code returned: %d' % (e.returncode)) return -1
def check_pvs_connected(self): """Checks whether all EPICS PVs are connected. Returns ------- bool True if all PVs are connected, otherwise False. """ all_connected = True for key in self.epics_pvs: if not self.epics_pvs[key].connected: log.error('PV %s is not connected', self.epics_pvs[key].pvname) all_connected = False return all_connected
def begin_scan(self): """Performs the operations needed at the very start of a scan. This does the following: - Calls the base class method. - Set the HDF plugin. """ log.info('begin scan') # Call the base class method super().begin_scan() # Set angles for the interlaced scan if (self.epics_pvs['InterlacedScan'].get(as_string=True) == 'Yes'): interlacedfilename = self.epics_pvs['InterlacedFileName'].get( as_string=True) try: self.theta = np.load(interlacedfilename) # update angles and number of frames to be captured and collected self.total_images -= self.num_angles - len(self.theta) self.num_angles = len(self.theta) # print some information about angles stheta = np.sort(self.theta) log.info('file with interlaced angles %s', interlacedfilename) log.info('loaded %d interlaced angles', self.num_angles) log.info('min angle %f', stheta[0]) log.info('max angle %f', stheta[-1]) log.info('min distance between neigbouring sorted angles %f', np.amin(np.abs(stheta[1::2] - stheta[::2]))) log.info('max distance between neigbouring sorted angles %f', np.amax(np.abs(stheta[1::2] - stheta[::2]))) except: log.error('%s file with interlaced angles is not valid', interlacedfilename) self.theta = [] self.abort_scan() return else: self.theta = self.rotation_start + np.arange( self.num_angles) * self.rotation_step self.epics_pvs['FPNumCapture'].put(self.total_images, wait=True) self.epics_pvs['FPCapture'].put('Capture')
def set_trigger_mode(self, trigger_mode, num_images): """Sets the trigger mode SIS3820 and the camera. Parameters ---------- trigger_mode : str Choices are: "FreeRun", "Internal", or "PSOExternal" num_images : int Number of images to collect. Ignored if trigger_mode="FreeRun". This is used to set the ``NumImages`` PV of the camera. """ camera_model = self.epics_pvs['CamModel'].get(as_string=True) if (camera_model == 'Grasshopper3 GS3-U3-51S5M'): self.set_trigger_mode_grasshopper(trigger_mode, num_images) else: log.error('Camera is not supported') exit(1)
def wait_pv(self, epics_pv, wait_val, timeout=np.inf, delta_t=0.01): """Wait on a pv to be a value until max_timeout (default forever) delay for pv to change """ time.sleep(delta_t) start_time = time.time() while time.time() - start_time < timeout: pv_val = epics_pv.get() if isinstance(pv_val, float): if abs(pv_val - wait_val) < EPSILON: return True if pv_val == wait_val: return True time.sleep(delta_t) else: log.error(' *** ERROR: PV TIMEOUT ***') log.error(' *** wait_pv(%s, %d, %5.2f reached max timeout. Return False', epics_pv.pvname, wait_val, timeout) return False
def add_theta(self): """Add theta at the end of a scan. """ log.info('add theta') self.theta = np.linspace(self.rotation_start, self.rotation_stop, self.num_angles) full_file_name = self.epics_pvs['FPFullFileName'].get(as_string=True) file_name_path = Path(full_file_name) if os.path.exists(full_file_name): try: f = h5py.File(full_file_name, "a") with f: try: if self.theta is not None: theta_ds = f.create_dataset('/exchange/theta', data=self.theta) except: log.error('Add theta: Failed accessing: %s', full_file_name) traceback.print_exc(file=sys.stdout) except OSError: log.error('Add theta aborted') else: log.error('Failed adding theta. %s file does not exist', full_file_name)
def add_theta(self): """Add theta at the end of a scan. """ log.info('add theta') full_file_name = self.epics_pvs['FPFullFileName'].get(as_string=True) if os.path.exists(full_file_name): try: f = h5py.File(full_file_name, "a") with f: try: if self.theta is not None: # theta_ds = f.create_dataset('/exchange/theta', (len(self.theta),)) # theta_ds[:] = self.theta[:] unique_ids = f['/defaults/NDArrayUniqueId'] shift_start = int(self.num_dark_fields > 0 and (self.dark_field_mode in ('Start', 'Both')))+ \ int(self.num_flat_fields > 0 and (self.flat_field_mode in ('Start', 'Both'))) # find beginnings of sorted subarrays # for [1,2,1,3,1,2,3,4,1,2] returns 0,2,4,8 ids_list = [ 0, *np.where(unique_ids[1:] - unique_ids[:-1] < 0)[0] + 1 ] # extract projection ids if (len(ids_list[shift_start:]) == 1): proj_ids = unique_ids[ids_list[shift_start]:] else: proj_ids = unique_ids[ids_list[shift_start]: ids_list[shift_start + 1]] # subtract first id proj_ids -= proj_ids[0] # create theta dataset in hdf5 file theta_ds = f.create_dataset( '/exchange/theta', (len(proj_ids), )) theta_ds[:] = self.theta[proj_ids] if (len(proj_ids) != len(self.theta)): log.warning('There are %d missing frames', len(self.theta) - len(proj_ids)) missed_ids = [ ele for ele in range(len(self.theta)) if ele not in proj_ids ] missed_theta = self.theta[missed_ids] log.warning(f'Missed ids: {list(missed_ids)}') log.warning( f'Missed theta: {list(missed_theta)}') except: log.error('Add theta: Failed accessing: %s', full_file_name) traceback.print_exc(file=sys.stdout) except OSError: log.error('Add theta aborted: %s not closed', full_file_name) else: log.error('Failed adding theta. %s file does not exist', full_file_name)
def end_scan(self): """Performs the operations needed at the very end of a scan. This does the following: - Calls ``save_configuration()``. - Put the camera back in "FreeRun" mode and acquiring so the user sees live images. - Sets the speed of the rotation stage back to the maximum value. - Calls ``move_sample_in()``. - Calls the base class method. """ log.info('end scan') # Save the configuration # Strip the extension from the FullFileName and add .config full_file_name = self.epics_pvs['FPFullFileName'].get(as_string=True) log.info('data save location: %s', full_file_name) config_file_root = os.path.splitext(full_file_name)[0] try: self.save_configuration(config_file_root + '.config') except FileNotFoundError: log.error('config file write error') self.epics_pvs['ScanStatus'].put('Config File Write Error') # Put the camera back in FreeRun mode and acquiring self.set_trigger_mode('FreeRun', 1) # Set the rotation speed to maximum self.epics_pvs['RotationSpeed'].put(self.max_rotation_speed) self.cleanup_PSO() # Move the sample in. Could be out if scan was aborted while taking flat fields self.move_sample_in() # Call the base class method super().end_scan()
def fly_scan(self): """Performs the operations for a tomography fly scan, i.e. with continuous rotation. This base class method does the following: - Moves the rotation motor to position defined by the ``RotationStart`` PV. - Calls ``begin_scan()`` - If the ``DarkFieldMode`` PV is 'Start' or 'Both' calls ``collect_dark_fields()`` - If the ``FlatFieldMode`` PV is 'Start' or 'Both' calls ``collect_flat_fields()`` - Calls ``collect_projections()`` - If the ``FlatFieldMode`` PV is 'End' or 'Both' calls ``collect_flat_fields()`` - If the ``DarkFieldMode`` PV is 'End' or 'Both' calls ``collect_dark_fields()`` - Calls ``end_scan`` If there is either CameraTimeoutError exception or ScanAbortError exception during the scan, it jumps immediate to calling ``end_scan()`` and returns. It is not expected that most derived classes will need to override this method, but they are free to do so if required. """ try: # Prepare for scan self.begin_scan() self.epics_pvs['ScanStatus'].put('Moving rotation axis to start') # Move the rotation to the start self.epics_pvs['Rotation'].put(self.rotation_start, wait=True, timeout=600) # Collect the pre-scan dark fields if required if (self.num_dark_fields > 0) and (self.dark_field_mode in ('Start', 'Both')): self.collect_dark_fields() # Collect the pre-scan flat fields if required if (self.num_flat_fields > 0) and (self.flat_field_mode in ('Start', 'Both')): self.collect_flat_fields() # Collect the projections self.collect_projections() # Collect the post-scan flat fields if required if (self.num_flat_fields > 0) and (self.flat_field_mode in ('End', 'Both')): self.collect_flat_fields() # Collect the post-scan dark fields if required if (self.num_dark_fields > 0) and (self.dark_field_mode in ('End', 'Both')): self.collect_dark_fields() except ScanAbortError: log.error('Scan aborted') except CameraTimeoutError: log.error('Camera timeout') except FileOverwriteError: log.error('File overwrite aborted') #Make sure we do cleanup tasks from the end of the scan finally: self.end_scan()
def program_helical_motion(self): """Computes the speed and magnitude from SampleY motion for helical scans. """ y_pixels_per_rotation = self.epics_pvs['PixelsYPer360Deg'].get() pixel_size = self.epics_pvs['ImagePixelSize'].get() / 1000. angle_range = self.epics_pvs['RotationStep'].get() * ( self.epics_pvs['NumAngles'].get() - 1) rotation_speed = self.epics_pvs['RotationSpeed'].get() speed_Y = np.abs(y_pixels_per_rotation * pixel_size / 360. * rotation_speed) end_Y = (self.epics_pvs['SampleY'].get() + angle_range / 360. * y_pixels_per_rotation * pixel_size) #Arbitrarily add 5 s to the y motion to account for timing imperfections end_Y += np.sign(y_pixels_per_rotation) * speed_Y * 5.0 #Check limits if end_Y > self.epics_pvs['SampleYHLM'].get( ) or end_Y < self.epics_pvs['SampleYLLM'].get(): log.error( 'helical: Y range would fall outside SampleY motor limits') self.epics_pvs['AbortScan'].put(1) self.epics_pvs['SampleYSpeed'].put(speed_Y) self.epics_pvs['SampleY'].put(end_Y)
def wait_pv(self, epics_pv, wait_val, timeout=-1): """Wait on a pv to be a value until max_timeout (default forever) delay for pv to change """ time.sleep(.01) start_time = time.time() while True: pv_val = epics_pv.get() if isinstance(pv_val, float): if abs(pv_val - wait_val) < EPSILON: return True if pv_val != wait_val: if timeout > -1: current_time = time.time() diff_time = current_time - start_time if diff_time >= timeout: log.error(' *** ERROR: DROPPED IMAGES ***') log.error(' *** wait_pv(%s, %d, %5.2f reached max timeout. Return False', epics_pv.pvname, wait_val, timeout) return False time.sleep(.01) else: return True
def begin_scan(self): """Performs the operations needed at the very start of a scan. This base class method does the following: - Sets the status string in the ``ScanStatus`` PV. - Stops the camera acquisition. - Calls ``set_exposure_time()`` - Copies the ``FilePath`` and ``FileName`` PVs to the areaDetector file plugin. - Sets class variables with the important scan parameters - Checks whether the file that will be saved by the file plugin already exists. If it does, and if the OverwriteWarning PV is 'Yes' then it opens a dialog box asking the user if they want to overwrite the file. If they answer 'No' then a FileOverwriteError exception is raised. It is expected that most derived classes will override this method. In most cases they should first call this base class method, and then perform any beamline-specific operations. """ self.scan_is_running = True self.epics_pvs['ScanStatus'].put('Beginning scan') # Stop the camera since it could be in free-run mode self.epics_pvs['CamAcquire'].put(0, wait=True) # Set the exposure time self.set_exposure_time() # Set the file path, file name and file number self.epics_pvs['FPFilePath'].put(self.epics_pvs['FilePath'].value) self.epics_pvs['FPFileName'].put(self.epics_pvs['FileName'].value) # Copy the current values of scan parameters into class variables self.exposure_time = self.epics_pvs['ExposureTime'].value self.rotation_start = self.epics_pvs['RotationStart'].value self.rotation_step = self.epics_pvs['RotationStep'].value self.num_angles = self.epics_pvs['NumAngles'].value self.rotation_stop = self.rotation_start + (self.num_angles * self.rotation_step) self.rotation_resolution = self.epics_pvs['RotationResolution'].value self.max_rotation_speed = self.epics_pvs['RotationMaxSpeed'].value self.return_rotation = self.epics_pvs['ReturnRotation'].get( as_string=True) self.num_dark_fields = self.epics_pvs['NumDarkFields'].value self.dark_field_mode = self.epics_pvs['DarkFieldMode'].get( as_string=True) self.num_flat_fields = self.epics_pvs['NumFlatFields'].value self.flat_field_mode = self.epics_pvs['FlatFieldMode'].get( as_string=True) self.file_path_rbv = self.epics_pvs['FPFilePathRBV'].get( as_string=True) self.file_name_rbv = self.epics_pvs['FPFileNameRBV'].get( as_string=True) self.file_number = self.epics_pvs['FPFileNumber'].value self.file_template = self.epics_pvs['FPFileTemplate'].get( as_string=True) self.total_images = self.num_angles if self.dark_field_mode != 'None': self.total_images += self.num_dark_fields if self.dark_field_mode == 'Both': self.total_images += self.num_dark_fields if self.flat_field_mode != 'None': self.total_images += self.num_flat_fields if self.flat_field_mode == 'Both': self.total_images += self.num_flat_fields if self.epics_pvs['OverwriteWarning'].get(as_string=True) == 'Yes': # Make sure there is not already a file by this name try: file_name = self.file_template % ( self.file_path_rbv, self.file_name_rbv, self.file_number) except: try: file_name = self.file_template % (self.file_path_rbv, self.file_name_rbv) except: try: file_name = self.file_template % (self.file_path_rbv) except: log.error("File name template: %s not supported", self.file_template) raise TypeError if os.path.exists(file_name): reply = pymsgbox.confirm( 'File ' + file_name + ' exists. Overwrite?', 'Overwrite file', ['Yes', 'No']) if reply == 'No': raise FileOverwriteError
def stream_sync(self): """Synchronize new angular step and exposure with rotation speed. Brodcast new array of angles for streaming reconstruction - cleanup PSO - save last unique ID of projection - program pso (base part) - change the JOG speed wrt exposure and angular step - initiate DATAACQ in aerotech for saving encoder values - ARM PSO - wait exposure time to acquire 1 projection - read encoder value for the projection unique_id + 1 - convert encoder value to the angle, form array of angles - broadcast array of angles for streaming """ log.info('stream sync') if self.epics_pvs['StreamMessage'].get(as_string=True)=='Done' and self.epics_pvs['StreamScanType'].get(as_string=True)!='backforth' and \ (self.exposure_time!=self.epics_pvs['ExposureTime'].value or self.rotation_step!=self.epics_pvs['RotationStep'].value): self.exposure_time = self.epics_pvs['ExposureTime'].value self.rotation_step = self.epics_pvs['RotationStep'].value #compute new counts per step # Compute the actual delta to keep each interval an integer number of encoder counts encoder_multiply = float( self.epics_pvs['PSOCountsPerRotation'].get()) / 360. raw_delta_encoder_counts = self.rotation_step * encoder_multiply delta_encoder_counts = round(raw_delta_encoder_counts) if abs(raw_delta_encoder_counts - delta_encoder_counts) > 1e-4: log.warning( ' *** *** *** Requested scan would have used a non-integer number of encoder counts.' ) log.warning( ' *** *** *** Calculated # of encoder counts per step = {0:9.4f}' .format(raw_delta_encoder_counts)) log.warning(' *** *** *** Instead, using {0:d}'.format( delta_encoder_counts)) self.epics_pvs['PSOEncoderCountsPerStep'].put(delta_encoder_counts) self.cleanup_PSO() # wait until last projection is acquired time.sleep(self.exposure_time + 0.1) # save last unique id of the projection projid = self.epics_pvs['CamNumImagesCounter'].get() pso_command = self.epics_pvs['PSOCommand.BOUT'] pso_model = self.epics_pvs['PSOControllerModel'].get( as_string=True) pso_axis = self.epics_pvs['PSOAxisName'].get(as_string=True) pso_input = int( self.epics_pvs['PSOEncoderInput'].get(as_string=True)) overall_sense, user_direction = self._compute_senses() # Make sure the PSO control is off pso_command.put('PSOCONTROL %s RESET' % pso_axis, wait=True, timeout=10.0) # Set the output to occur from the I/O terminal on the controller if (pso_model == 'Ensemble'): pso_command.put('PSOOUTPUT %s CONTROL 1' % pso_axis, wait=True, timeout=10.0) elif (pso_model == 'A3200'): pso_command.put('PSOOUTPUT %s CONTROL 0 1' % pso_axis, wait=True, timeout=10.0) # Set the pulse width. The total width and active width are the same, since this is a single pulse. pulse_width = self.epics_pvs['PSOPulseWidth'].get() pso_command.put('PSOPULSE %s TIME %f,%f' % (pso_axis, pulse_width, pulse_width), wait=True, timeout=10.0) # Set the pulses to only occur in a specific window pso_command.put('PSOOUTPUT %s PULSE WINDOW MASK' % pso_axis, wait=True, timeout=10.0) # Set which encoder we will use. 3 = the MXH (encoder multiplier) input, which is what we generally want pso_command.put('PSOTRACK %s INPUT %d' % (pso_axis, pso_input), wait=True, timeout=10.0) # Set the distance between pulses. Do this in encoder counts. pso_command.put( 'PSODISTANCE %s FIXED %d' % (pso_axis, int(np.abs(self.epics_pvs['PSOEncoderCountsPerStep'].get()))), wait=True, timeout=10.0) # Which encoder is being used to calculate whether we are in the window. 1 for single axis pso_command.put('PSOWINDOW %s 1 INPUT %d' % (pso_axis, pso_input), wait=True, timeout=10.0) # Calculate window function parameters. Must be in encoder counts, and is # referenced from the stage location where we arm the PSO. We are at that point now. # We want pulses to start at start - delta/2, end at end + delta/2. range_start = -round( np.abs(self.epics_pvs['PSOEncoderCountsPerStep'].get()) / 2) * overall_sense range_length = np.abs(self.epics_pvs['PSOEncoderCountsPerStep']. get()) * self.num_angles # The start of the PSO window must be < end. Handle this. if overall_sense > 0: window_start = range_start window_end = window_start + range_length else: window_end = range_start window_start = window_end - range_length pso_command.put('PSOWINDOW %s 1 RANGE %d,%d' % (pso_axis, window_start - 5, window_end + 5), wait=True, timeout=10.0) self.epics_pvs['CamAcquireTime'].put(self.exposure_time, wait=True, timeout=10.0) time_per_angle = self.compute_frame_time() # compute acceleration time accelJog_time = np.abs( (self.motor_speed - self.rotation_step / time_per_angle)) / self.control_pvs['RotationAccelJog'].value # recompute velocity self.motor_speed = self.rotation_step / time_per_angle self.control_pvs['RotationSpeedJog'].put(self.motor_speed, wait=True) pso_command.put('PROGRAM RUN 1, "dataacqoff.bcx"', wait=True, timeout=10.0) pso_command.put('PROGRAM RUN 1, "dataacqon.bcx"', wait=True, timeout=10.0) # wait acceleration log.warning(f'wait {accelJog_time+1} for acceleration') time.sleep(accelJog_time + 1) # Arm the PSO log.warning( f'ARM PSO and wait until the first projection is acquired') pso_command.put('PSOCONTROL %s ARM' % pso_axis, wait=True, timeout=10.0) # wait while 1 projection is acquired time.sleep(self.exposure_time + 0.4) pso_command.put('PROGRAM RUN 1, "dataacqread.bcx"', wait=True, timeout=10.0) # need some delay here time.sleep(0.1) pso_command.put('IGLOBAL(0)', wait=True, timeout=10.0) time.sleep(0.1) reply = self.epics_pvs['PSOCommand.BINP'].get(as_string=True) time.sleep(0.1) if reply: encoder_angle = int(reply[1:]) self.rotation_start = self.control_pvs[ 'RotationOFF'].value + encoder_angle * self.epics_pvs[ 'RotationEResolution'].value self.epics_pvs['FirstProjid'].put(projid + 1, wait=True) self.compute_positions_PSO() # Assign the fly scan angular position to theta[] self.theta = self.rotation_start + np.arange( self.num_angles) * self.rotation_step self.pva_stream_theta['value'] = self.theta.astype('float32') self.pva_stream_theta['sizex'] = len(self.theta) log.info( f'Angle {self.theta[0]} corresponds to unique ID {projid+1}' ) else: log.error('PSO didnt return encoder value') else: log.info('stream sync ignore') self.epics_pvs['StreamSync'].put('Done', wait=True)
def compute_frame_time(self): """Computes the time to collect and readout an image from the camera. This method is used to compute the time between triggers to the camera. This can be used, for example, to configure a trigger generator or to compute the speed of the rotation stage. The calculation is camera specific. The result can depend on the actual exposure time of the camera, and on a variety of camera configuration settings (pixel binning, pixel bit depth, video mode, etc.) The current version only supports the Point Grey Grasshopper3 GS3-U3-23S6M. The logic for additional cameras should be added to this function in the future if the camera is expected to be used at more than one beamline. If the camera is only to be used at a single beamline then the code should be added to this method in the derived class Returns ------- float The frame time, which is the minimum time allowed between triggers for the value of the ``ExposureTime`` PV. """ # The readout time of the camera depends on the model, and things like the # PixelFormat, VideoMode, etc. # The measured times in ms with 100 microsecond exposure time and 1000 frames # without dropping camera_model = self.epics_pvs['CamModel'].get(as_string=True) pixel_format = self.epics_pvs['CamPixelFormat'].get(as_string=True) readout = None if camera_model == 'Grasshopper3 GS3-U3-23S6M': video_mode = self.epics_pvs['CamVideoMode'].get(as_string=True) readout_times = { 'Mono8': { 'Mode0': 6.2, 'Mode1': 6.2, 'Mode5': 6.2, 'Mode7': 7.9 }, 'Mono12Packed': { 'Mode0': 9.2, 'Mode1': 6.2, 'Mode5': 6.2, 'Mode7': 11.5 }, 'Mono16': { 'Mode0': 12.2, 'Mode1': 6.2, 'Mode5': 6.2, 'Mode7': 12.2 } } readout = readout_times[pixel_format][video_mode] / 1000. if camera_model == 'Oryx ORX-10G-51S5M': # video_mode = self.epics_pvs['CamVideoMode'].get(as_string=True) readout_times = { 'Mono8': 6.18, 'Mono12Packed': 8.20, 'Mono16': 12.34 } readout = readout_times[pixel_format] / 1000. if readout is None: log.error( 'Unsupported combination of camera model, pixel format and video mode: %s %s %s', camera_model, pixel_format, video_mode) return 0 # We need to use the actual exposure time that the camera is using, not the requested time exposure = self.epics_pvs['CamAcquireTimeRBV'].value # Add some extra time to exposure time for margin. These values are empirical. if exposure > 2.3: frame_time = exposure + .01 elif exposure > 1.0: frame_time = exposure + .002 else: frame_time = exposure + .001 # If the time is less than the readout time then use the readout time if frame_time < readout: frame_time = readout return frame_time
def __init__(self, pv_files, macros): self.scan_is_running = False self.config_pvs = {} self.control_pvs = {} self.pv_prefixes = {} # These variables are set in begin_scan(). # They are used to prevent reading PVs repeatedly, and so that if the users changes # a PV during the scan it won't mess things up. self.exposure_time = None self.rotation_start = None self.rotation_step = None self.rotation_stop = None self.rotation_resolution = None self.max_rotation_speed = None self.return_rotation = None self.num_angles = None self.num_dark_fields = None self.dark_field_mode = None self.num_flat_fields = None self.flat_field_mode = None self.total_images = None self.file_path_rbv = None self.file_name_rbv = None self.file_number = None self.file_template = None if not isinstance(pv_files, list): pv_files = [pv_files] for pv_file in pv_files: self.read_pv_file(pv_file, macros) if 'Rotation' not in self.control_pvs: log.error('RotationPVName must be present in autoSettingsFile') sys.exit() if 'Camera' not in self.pv_prefixes: log.error('CameraPVPrefix must be present in autoSettingsFile') sys.exit() if 'FilePlugin' not in self.pv_prefixes: log.error('FilePluginPVPrefix must be present in autoSettingsFile') sys.exit() #Define PVs we will need from the rotation motor, which is on another IOC rotation_pv_name = self.control_pvs['Rotation'].pvname self.control_pvs['RotationSpeed'] = PV(rotation_pv_name + '.VELO') self.control_pvs['RotationMaxSpeed'] = PV(rotation_pv_name + '.VMAX') self.control_pvs['RotationResolution'] = PV(rotation_pv_name + '.MRES') self.control_pvs['RotationSet'] = PV(rotation_pv_name + '.SET') self.control_pvs['RotationStop'] = PV(rotation_pv_name + '.STOP') self.control_pvs['RotationDmov'] = PV(rotation_pv_name + '.DMOV') self.control_pvs['RotationDirection'] = PV(rotation_pv_name + '.DIR') #Define PVs from the camera IOC that we will need prefix = self.pv_prefixes['Camera'] camera_prefix = prefix + 'cam1:' self.control_pvs['CamManufacturer'] = PV(camera_prefix + 'Manufacturer_RBV') self.control_pvs['CamModel'] = PV(camera_prefix + 'Model_RBV') self.control_pvs['CamAcquire'] = PV(camera_prefix + 'Acquire') self.control_pvs['CamAcquireBusy'] = PV(camera_prefix + 'AcquireBusy') self.control_pvs['CamImageMode'] = PV(camera_prefix + 'ImageMode') self.control_pvs['CamTriggerMode'] = PV(camera_prefix + 'TriggerMode') self.control_pvs['CamNumImages'] = PV(camera_prefix + 'NumImages') self.control_pvs['CamNumImagesCounter'] = PV(camera_prefix + 'NumImagesCounter_RBV') self.control_pvs['CamAcquireTime'] = PV(camera_prefix + 'AcquireTime') self.control_pvs['CamAcquireTimeRBV'] = PV(camera_prefix + 'AcquireTime_RBV') self.control_pvs['CamBinX'] = PV(camera_prefix + 'BinX') self.control_pvs['CamBinY'] = PV(camera_prefix + 'BinY') self.control_pvs['CamWaitForPlugins'] = PV(camera_prefix + 'WaitForPlugins') self.control_pvs['PortNameRBV'] = PV(camera_prefix + 'PortName_RBV') # If this is a Point Grey camera then assume we are running ADSpinnaker # and create some PVs specific to that driver manufacturer = self.control_pvs['CamManufacturer'].get(as_string=True) model = self.control_pvs['CamModel'].get(as_string=True) if (manufacturer.find('Point Grey') != -1) or (manufacturer.find('FLIR') != -1): self.control_pvs['CamExposureMode'] = PV(camera_prefix + 'ExposureMode') self.control_pvs['CamTriggerOverlap'] = PV(camera_prefix + 'TriggerOverlap') self.control_pvs['CamPixelFormat'] = PV(camera_prefix + 'PixelFormat') self.control_pvs['CamArrayCallbacks'] = PV(camera_prefix + 'ArrayCallbacks') self.control_pvs['CamFrameRateEnable'] = PV(camera_prefix + 'FrameRateEnable') self.control_pvs['CamTriggerSource'] = PV(camera_prefix + 'TriggerSource') if model.find('Grasshopper3') != -1: self.control_pvs['CamVideoMode'] = PV(camera_prefix + 'GC_VideoMode_RBV') # Set some initial PV values self.control_pvs['CamWaitForPlugins'].put('Yes') self.control_pvs['StartScan'].put(0) prefix = self.pv_prefixes['FilePlugin'] self.control_pvs['FPNDArrayPort'] = PV(prefix + 'NDArrayPort') self.control_pvs['FPFileWriteMode'] = PV(prefix + 'FileWriteMode') self.control_pvs['FPNumCapture'] = PV(prefix + 'NumCapture') self.control_pvs['FPNumCaptured'] = PV(prefix + 'NumCaptured_RBV') self.control_pvs['FPCapture'] = PV(prefix + 'Capture') self.control_pvs['FPCaptureRBV'] = PV(prefix + 'Capture_RBV') self.control_pvs['FPFilePath'] = PV(prefix + 'FilePath') self.control_pvs['FPFilePathRBV'] = PV(prefix + 'FilePath_RBV') self.control_pvs['FPFilePathExists'] = PV(prefix + 'FilePathExists_RBV') self.control_pvs['FPFileName'] = PV(prefix + 'FileName') self.control_pvs['FPFileNameRBV'] = PV(prefix + 'FileName_RBV') self.control_pvs['FPFileNumber'] = PV(prefix + 'FileNumber') self.control_pvs['FPAutoIncrement'] = PV(prefix + 'AutoIncrement') self.control_pvs['FPFileTemplate'] = PV(prefix + 'FileTemplate') self.control_pvs['FPFullFileName'] = PV(prefix + 'FullFileName_RBV') self.control_pvs['FPAutoSave'] = PV(prefix + 'AutoSave') self.control_pvs['FPEnableCallbacks'] = PV(prefix + 'EnableCallbacks') # Set some initial PV values file_path = self.config_pvs['FilePath'].get(as_string=True) self.control_pvs['FPFilePath'].put(file_path) file_name = self.config_pvs['FileName'].get(as_string=True) self.control_pvs['FPFileName'].put(file_name) self.control_pvs['FPAutoSave'].put('No') self.control_pvs['FPFileWriteMode'].put('Stream') self.control_pvs['FPEnableCallbacks'].put('Enable') #Define PVs from the MCS or PSO that live on another IOC if 'MCS' in self.pv_prefixes: prefix = self.pv_prefixes['MCS'] self.control_pvs['MCSEraseStart'] = PV(prefix + 'EraseStart') self.control_pvs['MCSStopAll'] = PV(prefix + 'StopAll') self.control_pvs['MCSPrescale'] = PV(prefix + 'Prescale') self.control_pvs['MCSDwell'] = PV(prefix + 'Dwell') self.control_pvs['MCSLNEOutputWidth'] = PV(prefix + 'LNEOutputWidth') self.control_pvs['MCSChannelAdvance'] = PV(prefix + 'ChannelAdvance') self.control_pvs['MCSMaxChannels'] = PV(prefix + 'MaxChannels') self.control_pvs['MCSNuseAll'] = PV(prefix + 'NuseAll') if 'PSO' in self.pv_prefixes: prefix = self.pv_prefixes['PSO'] self.control_pvs['PSOscanDelta'] = PV(prefix + 'scanDelta') self.control_pvs['PSOstartPos'] = PV(prefix + 'startPos') self.control_pvs['PSOendPos'] = PV(prefix + 'endPos') self.control_pvs['PSOslewSpeed'] = PV(prefix + 'slewSpeed') self.control_pvs['PSOtaxi'] = PV(prefix + 'taxi') self.control_pvs['PSOfly'] = PV(prefix + 'fly') self.control_pvs['PSOscanControl'] = PV(prefix + 'scanControl') self.control_pvs['PSOcalcProjections'] = PV(prefix + 'numTriggers') self.control_pvs['ThetaArray'] = PV(prefix + 'motorPos.AVAL') if 'PvaPlugin' in self.pv_prefixes: prefix = self.pv_prefixes['PvaPlugin'] self.control_pvs['PVANDArrayPort'] = PV(prefix + 'NDArrayPort') self.control_pvs['PVAEnableCallbacks'] = PV(prefix + 'EnableCallbacks') if 'RoiPlugin' in self.pv_prefixes: prefix = self.pv_prefixes['RoiPlugin'] self.control_pvs['ROINDArrayPort'] = PV(prefix + 'NDArrayPort') self.control_pvs['ROIScale'] = PV(prefix + 'Scale') self.control_pvs['ROIBinX'] = PV(prefix + 'BinX') self.control_pvs['ROIBinY'] = PV(prefix + 'BinY') self.control_pvs['ROIEnableCallbacks'] = PV(prefix + 'EnableCallbacks') if 'CbPlugin' in self.pv_prefixes: prefix = self.pv_prefixes['CbPlugin'] self.control_pvs['CBPortNameRBV'] = PV(prefix + 'PortName_RBV') self.control_pvs['CBNDArrayPort'] = PV(prefix + 'NDArrayPort') self.control_pvs['CBPreCount'] = PV(prefix + 'PreCount') self.control_pvs['CBPostCount'] = PV(prefix + 'PostCount') self.control_pvs['CBCapture'] = PV(prefix + 'Capture') self.control_pvs['CBCaptureRBV'] = PV(prefix + 'Capture_RBV') self.control_pvs['CBTrigger'] = PV(prefix + 'Trigger') self.control_pvs['CBTriggerRBV'] = PV(prefix + 'Trigger_RBV') self.control_pvs['CBCurrentQtyRBV'] = PV(prefix + 'CurrentQty_RBV') self.control_pvs['CBEnableCallbacks'] = PV(prefix + 'EnableCallbacks') self.control_pvs['CBStatusMessage'] = PV(prefix + 'StatusMessage') self.epics_pvs = {**self.config_pvs, **self.control_pvs} # Wait 1 second for all PVs to connect time.sleep(1) self.check_pvs_connected() # Setting the pva servers to broadcast dark and flat fields if 'PvaStream' in self.pv_prefixes: prefix = self.pv_prefixes['PvaStream'] self.pva_stream_dark = pvaccess.PvObject({ 'value': [pvaccess.pvaccess.ScalarType.FLOAT], 'sizex': pvaccess.pvaccess.ScalarType.INT, 'sizey': pvaccess.pvaccess.ScalarType.INT }) self.pva_server_dark = pvaccess.PvaServer(prefix + 'dark', self.pva_stream_dark) self.pva_stream_flat = pvaccess.PvObject({ 'value': [pvaccess.pvaccess.ScalarType.FLOAT], 'sizex': pvaccess.pvaccess.ScalarType.INT, 'sizey': pvaccess.pvaccess.ScalarType.INT }) self.pva_server_flat = pvaccess.PvaServer(prefix + 'flat', self.pva_stream_flat) # Configure callbacks on a few PVs for epics_pv in ('MoveSampleIn', 'MoveSampleOut', 'StartScan', 'AbortScan', 'ExposureTime', 'FilePath', 'FPFilePathExists'): self.epics_pvs[epics_pv].add_callback(self.pv_callback) # Synchronize the FilePathExists PV self.copy_file_path_exists() # Set ^C interrupt to abort the scan signal.signal(signal.SIGINT, self.signal_handler) # Start the watchdog timer thread thread = threading.Thread(target=self.reset_watchdog, args=(), daemon=True) thread.start()