def slit_scan_fiducialize(slits, yag, x_width=0.01, y_width=0.01, samples=10, filters=None, centroid='detector_stats2_centroid_y'): """ Assists beam alignment by setting the slits to a w,h and checking, returning the centroid position. Parameters ---------- slits : pcdsdevices.slits.Slits Ophyd slits object from pcdsdevices.slits.Slits yag : pcdsdevices.pim.PIM Detector to fidicuialize. This plan assumes the detector is stated and inserted x_width : float x dimensions of the gap in the slits. EGU: mm y_width : float y dimensions of the gap in the slits. EGU: mm samples : int Returned measurements are averages over multiple samples. samples arg determines the number of samples to average over for returned data filters : dict, optional Key, callable pairs of event keys and single input functions that evaluate to True or False. For more infromation see :meth:`.apply_filters` centroid : str, optional Key to gather centroid information Returns ------- float return centroid position in pixel space, single axis """ # Set slits and imager, wait together group = str(uuid.uuid4()) yield from abs_set(yag, "IN", group=group) yield from abs_set(slits, x_width, group=group) yield from plan_wait(group=group) #Collect data from yags yag_measurements = yield from measure_average([yag], num=samples, filters=filters) #Extract centroid positions from yag_measurments dict centroid = yag_measurements[field_prepend(centroid, yag)] return centroid
def delay_scan(daq, vitara, delay, start, stop, num, *args, return_to_start=True, delay_const=1, **kwargs): """Performs a delay scan using the vitara phase shifter and a delay stage, keeping the delay between them fixed. Parameters ---------- daq : Daq DAQ instance to use. Must be running and allocated for the scan to work. vitara : Vitara Vitara phase shifter to use for the scan. delay : Motor Delay stage to use for the scan. num : int number of steps return_to_start : bool, optional Return the vitara and the delay stage to their starting positions. delay_const : float, optional Scale the delay move delta by this amount. """ # Get the initial positions of the delay stage and vitara delay_init = delay.position vitara_init = vitara.position # Find the difference between the starting and and ending points of the # vitara relative to where it currently is vitara_start_diff = start - vitara_init vitara_stop_diff = stop - vitara_init # Use the vitara position differences to find the delay stage starting and # ending positions delay_start = delay_init - delay_const * (vitara_start_diff * c) / 2 delay_stop = delay_init - delay_const * (vitara_stop_diff * c) / 2 # Run the underlying plan try: yield from a2_daq_scan(daq, num, vitara, start, stop, delay, delay_start, delay_stop, *args, **kwargs) # Move back to the initial positions if specified finally: if return_to_start: print("Returning vitara and delay stage to initial positions.") group = str(uuid.uuid4()) for dev, pos in zip([vitara, delay], [vitara_init, delay_init]): yield from abs_set(dev, pos, group=group) yield from plan_wait(group=group)
def wrapper(*args, **kwargs): # Get the initial positions of all the inputted devices initial_positions = {dev: dev.position for dev in devices} try: return (yield from func(*args, **kwargs)) finally: # Start returning all the devices to their initial positions if perform: group = short_uid('set') for dev, pos in initial_positions.items(): yield from abs_set(dev, pos, group=group) # Wait for all the moves to finish if they haven't already yield from plan_wait(group=group)
def prep_img_motors(n_mot, img_motors, prev_out=True, tail_in=True, timeout=None): """ Plan to prepare image motors for taking data. Moves the correct imagers in and waits for them to be ready. Parameters ---------- n_mot: int Index of the motor in img_motors that we need to take data with. img_motors: list of OphydObject OphydObjects to move in or out. These objects need to have .set methods that accept the strings "IN" and "OUT", reacting appropriately, and need to accept a "timeout" kwarg that allows their status to be set as done after a timeout. These should be ordered by increasing distance to the source. prev_out: bool, optional If True, pull out imagers closer to the source than the one we need to use. Default True. (True if imager blocks beam) tail_in: bool, optional If True, put in imagers after this one, to be ready for later. If False, don't touch them. We won't wait for the tail motors to move in. timeout: number, optional Only wait for this many seconds before moving on. Returns ------- ok: bool True if the wait succeeded, False otherwise. """ start_time = time.time() prev_img_mot = str(uuid.uuid4()) ok = True try: for i, mot in enumerate(img_motors): if i < n_mot and prev_out: if timeout is None: yield from abs_set(mot, "OUT", group=prev_img_mot) else: yield from abs_set(mot, "OUT", group=prev_img_mot, timeout=timeout) elif i == n_mot: if timeout is None: yield from abs_set(mot, "IN", group=prev_img_mot) else: yield from abs_set(mot, "IN", group=prev_img_mot, timeout=timeout) elif tail_in: yield from abs_set(mot, "IN") yield from plan_wait(group=prev_img_mot) except FailedStatus: ok = False if ok and timeout is not None: ok = time.time() - start_time < timeout if ok: logger.debug("prep_img_motors completed successfully") else: logger.debug("prep_img_motors exitted with timeout") return ok
def iterwalk(detectors, motors, goals, starts=None, first_steps=1, gradients=None, detector_fields='centroid_x', motor_fields='alpha', tolerances=20, system=None, averages=1, overshoot=0, max_walks=None, timeout=None, recovery_plan=None, filters=None, tol_scaling=None): """ Iteratively adjust a system of detectors and motors where each motor primarily affects the reading of a single detector but also affects the other axes parasitically. This is a Bluesky plan, but it does not have start run or end run decorators so it can be included inside of other plans. It yields a checkpoint message before adjusting the detector positions and before executing a walk substep. All list arguments that expect one entry per detector must be the same length as the detectors list. If any optional list arguments are provided as single values instead of as iterables, iterwalk will interpret it as "use this value for every detector". Parameters ---------- detectors: list of objects These are your axes of motion, which must implement both the Bluesky reader interface and the setter interface. The set method must accept the "IN" and "OUT" arguments to move the device in and out. It is assumed that detectors earlier in the list block detectors later in the list. motors: list of objects These are your axes of motion, which must implement both the Bluesky reader interface and the setter interface. goals: list of numbers These are the scalar readbacks expected from the detectors at the desired positions. starts: list of numbers, optional If provided, these are the nominal positions to move the motors to before starting to align. first_steps: list of numbers, optional. This is how far the motors will be moved in an initial probe step as we gauge the detector's response. This argument is ignored if ``gradients`` is provided. gradients: list of numbers, optional If provided, this is a guess at the ratio between motor move and change in detector readback. This will be used to make a very good first guess at the first step towards the goal pixel. This should be in units of detector/motor detector_fields: list of strings, optional For each detector, this is the field we're walking to. motor_fields: list of strings, optional For each motor, this is the field we're using along with the detector_field to build our linear regression. This should be the readback with nominally the same value as the position field. tolerances: list of numbers, optional If our detector readbacks are within these tolerances of the goal positions, we'll treat the goal as reached. system: list of readable objects, optional Other system parameters that we would like to read during the walk. averages: list of numbers, optional For each detector, this is the number of shots to average before settling on a reading. overshoot: number, optional The percent to overshoot at each goal step. For these parasitic systems, over or undershooting can allow convergence to occur faster. An overshoot of 0 is no overshoot, an overshoot of 0.1 is a 10% overshoot, an overshoot of -0.2 is a 20% undershoot, etc. max_walks: int, optional The number of sets of walks to try before giving up on the alignment. E.g. if max_walks is 3, we'll move each motor/detector pair 3 times in series before giving up. timeout: number, optional The maximum time to allow for the alignment before aborting. The timeout is checked after each walk step. recovery_plan: plan, optional A backup plan to run when no there is no readback on the detectors. This plan should expect a complete description of the situation in the form of all arguments given to iterwalk, except for recovery_plan. Note that these arguments are listified before being passed to recovery_plan. It will also be passed ``index``, which is the index in all of the list arguments that is currently active. Also note that there is no requirement to use all of the provided arguments. A good practice is to have recovery_plan accept and ignore kwargs while explicitly including desired keywords to use. filters: list of dictionaries Each entry in this list should be a valid input to the filters argument in the lower functions, such as walk_to_pixel and measure_average. tol_scaling: list of floats or Nones, optional Constants for adaptive tolerances. Nones or omitting this argument reverts to fixed tolerance. For early walks with a starting point far from the goal, tolerance for the walk is set at current_dist/tol_scaling instead of the set tolerance. Scaling ends when calculated tolerance < targeted tolerance. """ num = len(detectors) # Listify most optional arguments goals = as_list(goals, num) starts = as_list(starts, num) first_steps = as_list(first_steps, num, float) gradients = as_list(gradients, num) detector_fields = as_list(detector_fields, num) motor_fields = as_list(motor_fields, num) tolerances = as_list(tolerances, num) system = as_list(system) averages = as_list(averages, num) filters = as_list(filters, num) tol_scaling = as_list(tol_scaling, num) logger.debug("iterwalk aligning %s to %s on %s", motors, goals, detectors) # Debug counters mirror_walks = 0 yag_cycles = 0 recoveries = 0 # Set up end conditions n_steps = 0 start_time = time.time() models = [None] * num finished = [False] * num done_pos = [0] * num selected_tol = [None] * num moving_to_nominal = False group = str(uuid.uuid4()) for mot in motors: try: position = mot.nominal_position except AttributeError: continue if position is not None: yield from abs_set(mot, mot.nominal_position, group=group) moving_to_nominal = True if moving_to_nominal: yield from plan_wait(group=group) while True: index = 0 while index < num: try: # Before each walk, check the global timeout. if timeout is not None and time.time() - start_time > timeout: raise RuntimeError("Iterwalk has timed out after %s s", time.time() - start_time) logger.debug("putting imager in") ok = (yield from prep_img_motors(index, detectors, timeout=15)) yag_cycles += 1 # Be loud if the yags fail to move! Operator should know! if not ok: err = "Detector motion timed out!" logger.error(err) raise RuntimeError(err) # Choose a start position for the first move if it was given if n_steps == 0 and starts[index] is not None: firstpos = starts[index] else: firstpos = None # Give higher-level a chance to recover or suspend yield from checkpoint() # Set up the system to not include the redundant objects full_system = copy(system) try: full_system.remove(motors[index]) except ValueError: pass try: full_system.remove(detectors[index]) except ValueError: pass # Set flag for needing recovery before walking recover_pre_walk = True original_position = motors[index].position # Check if we're already done logger.debug("measure_average on det=%s, mot=%s, sys=%s", detectors[index], motors[index], full_system) avgs = (yield from measure_average( [detectors[index], motors[index]] + full_system, num=averages[index], filters=filters[index])) pos = avgs[field_prepend(detector_fields[index], detectors[index])] logger.debug("recieved %s from measure_average on %s", pos, detectors[index]) if abs(pos - goals[index]) < tolerances[index]: logger.info("Beam was aligned on %s without a move", detectors[index].name) finished[index] = True done_pos[index] = pos if all(finished): logger.debug("beam aligned on all yags") break # Increment index before restarting loop index += 1 continue else: # If any of the detectors were wrong, reset finished flags logger.debug("reset alignment flags before move") finished = [False] * num # Modify goal to use overshoot if index == 0: goal = goals[index] else: goal = (goals[index] - pos) * (1 + overshoot) + pos # Calculate adaptive tolerance - otherwise use static tolerance if tol_scaling[index] != None: selected_tol[index] = \ abs(pos-goals[index]) / tol_scaling[index] if selected_tol[index] < tolerances[index]: selected_tol[index] = tolerances[index] else: selected_tol[index] = tolerances[index] # Clear flag for needing recovery before walking recover_pre_walk = False # Core walk logger.info(('Starting walk of {} pixels on {} using {}' ''.format(abs(pos - goal), detectors[index].name, motors[index].name))) logger.debug(('Starting walk from {} to {} on {} using {}' ''.format(pos, goal, detectors[index].name, motors[index].name))) logger.debug("selected tolerance: {}".format( selected_tol[index])) pos, models[index] = (yield from walk_to_pixel( detectors[index], motors[index], goal, filters=filters[index], start=firstpos, gradient=gradients[index], target_fields=[ detector_fields[index], motor_fields[index] ], first_step=first_steps[index], tolerance=selected_tol[index], system=full_system, average=averages[index], max_steps=10)) if models[index]: try: gradients[index] = models[index].result.values['slope'] logger.debug( "Found equation of ({}, {}) between " "linear fit of {} to {}" "".format(gradients[index], models[index].result.values['intercept'], motors[index].name, detectors[index].name)) except Exception as e: logger.warning(e) logger.warning("Unable to find gradient of " "linear fit of {} to {}" "".format(motors[index].name, detectors[index].name)) logger.debug("Walk reached pos %s on %s", pos, detectors[index].name) mirror_walks += 1 # Be loud if the walk fails to reach the pixel! if abs(pos - goal) > selected_tol[index]: err = "walk_to_pixel failed to reach the goal" logger.error(err) raise RuntimeError(err) finished[index] = True done_pos[index] = pos # Increment index before restarting loop index += 1 except FilterCountError as err: if recovery_plan is None: logger.error("No recovery plan, not attempting to recover") raise # If we had to recover before walk_to_pixel, # get a fallback position for if the recovery fails if recover_pre_walk: try: fallback_pos = motors[index].nominal_position except AttributeError: fallback_pos = None # Explicitly check again in case nominal_position is None if fallback_pos is None: fallback_pos = motors[index].position else: # If we are recovering after walk_to_pixel, don't bother # with the recovery plan. Just move back and adjust # parameters. logger.info(("Bad state reached during walk_to_pixel. " "Undoing walk_to_pixel...")) yield from mv(motors[index], original_position) # Reset the finished flag finished = [False] * num # Cut our step parameters in half, because they were # probably too big logger.info("Lowering initial step parameters...") if gradients[index] is not None: gradients[index] = gradients[index] * 2 if first_steps[index] is not None: first_steps[index] = first_steps[index] / -2 continue ok = yield from recovery_plan(detectors=detectors, motors=motors, goals=goals, starts=starts, first_steps=first_steps, gradients=gradients, detector_fields=detector_fields, motor_fields=motor_fields, tolerances=tolerances, system=system, averages=averages, overshoot=overshoot, max_walks=max_walks, timeout=timeout, filters=filters, index=index) # Reset the finished tag because we moved something finished = [False] * num recoveries += 1 # If recovery failed, move to nominal and switch to next device if not ok: logger.info(("Recover failed, using fallback pos and " "trying next device alignment.")) yield from mv(motors[index], fallback_pos) index += 1 # Try again continue if all(finished): break # After each set of walks, check if we've exceeded max_walks n_steps += 1 if max_walks is not None and n_steps > max_walks: logger.info("Iterwalk has reached the max_walks limit") break txt = 'Finished in %.2fs after %s mirror walks, %s yag cycles, and %s ' txt += 'recoveries.\n' txt += 'Aligned to %s\n' txt += 'Goals were %s\n' txt += 'Deltas are %s\n' txt += 'Mirror positions are %s' logger.info(txt, time.time() - start_time, mirror_walks, yag_cycles, recoveries, done_pos, goals, [d - g for g, d in zip(goals, done_pos)], [m.position for m in motors])