def on_validate( self, generator: scanning.hooks.AGenerator, part_info: scanning.hooks.APartInfo, detectors: ADetectorTable, ) -> Optional[scanning.hooks.UParameterTweakInfos]: # Check the primary generator is static self._check_generator_is_static(generator.generators[0]) # Calculate the time that should be spent at each position ( time_at_diffraction_position, time_at_imaging_position, ) = self._get_time_at_positions(part_info, detectors) # Now calculate how long one cycle should take cycle_duration = self._calculate_cycle_duration( time_at_diffraction_position, time_at_imaging_position) # See if we need to tweak the generator if generator.duration != cycle_duration: # Return the generator with our cycle duration self.log.debug( f"{self.name}: tweaking generator duration from {generator.duration} " f"to {cycle_duration}") serialized = generator.to_dict() new_generator = CompoundGenerator.from_dict(serialized) new_generator.duration = cycle_duration return scanning.infos.ParameterTweakInfo("generator", new_generator) else: return None
def calculate_generator_duration( self, context: scanning.hooks.AContext, generator: scanning.hooks.AGenerator, part_info: scanning.hooks.APartInfo, motion_axes: List[str], ) -> Optional[float]: """ Calculate a generator duration based on the generator and axes we are moving """ child = context.block_view(self.mri) layout_table = child.layout.value # Check if we are moving axes in this scan if motion_axes: # TODO: what happens if axes are not mapped due to the current config on # the brick not matching the target config for the scan? try: axis_mapping = cs_axis_mapping(context, layout_table, motion_axes) except AssertionError: # We can't check the axes as they are not mapped, so don't tweak the # generator and just return self.log.debug( f"{self.name}: can't guess generator duration during " f"validate as axis mappings are not loaded or missing.") return None # Step scans and fly scans behave differently generator.prepare() if generator.continuous and generator.size > 1: # Estimate the duration for fly scans using the distance between the # first two points and max velocities of participating axes first_point = generator.get_point(0) second_point = generator.get_point(1) return self.calculate_duration_from_first_two_points( axis_mapping, first_point, second_point, ) else: # Step scans have turnarounds at each point so can use this value min_turnaround = get_min_turnaround(part_info) return min_turnaround.time else: # Not moving axes so just return time of one tick return TICK_S
def on_validate( self, context: scanning.hooks.AContext, generator: scanning.hooks.AGenerator, axesToMove: scanning.hooks.AAxesToMove, part_info: scanning.hooks.APartInfo, ) -> scanning.hooks.UParameterTweakInfos: child = context.block_view(self.mri) # Check that we can move all the requested axes available = set(child.layout.value.name) motion_axes = get_motion_axes(generator, axesToMove) assert available.issuperset( motion_axes ), "Some of the requested axes %s are not on the motor list %s" % ( list(axesToMove), sorted(available), ) # Find the duration duration = generator.duration assert duration >= 0.0, f"{self.name}: negative duration is not supported" # Check if we should guess the duration if duration == 0.0: # We need to tweak the duration if we are going to take part if self.taking_part_in_scan(part_info, motion_axes): duration = self.calculate_generator_duration( context, generator, part_info, motion_axes) # We may have not been able to tweak duration if axis mappings are # missing. if not duration: return None else: return None # If GPIO is demanded for every point we need to align to the servo # cycle trigger = get_motion_trigger(part_info) if trigger == scanning.infos.MotionTrigger.EVERY_POINT: servo_freq = child.servoFrequency() duration = self.get_aligned_duration_with_servo_frequency( servo_freq, duration) # Check if the duration was tweaked and return if duration != generator.duration: self.log.debug( f"{self.name}: tweaking duration from {generator.duration} to " f"{duration}") serialized = generator.to_dict() new_generator = CompoundGenerator.from_dict(serialized) new_generator.duration = duration return scanning.infos.ParameterTweakInfo("generator", new_generator) else: return None
def on_validate( self, generator: scanning.hooks.AGenerator ) -> scanning.hooks.UParameterTweakInfos: duration = generator.duration if duration == 0.0: # We need to tweak the duration serialized = generator.to_dict() new_generator = CompoundGenerator.from_dict(serialized) # Set the duration to 2 clock cycles new_generator.duration = 2 * TICK return scanning.infos.ParameterTweakInfo("generator", new_generator) else: assert ( duration > 0 ), f"Generator duration of {duration} must be > 0 to signify fixed exposure" return None
def on_validate( self, generator: scanning.hooks.AGenerator ) -> scanning.hooks.UParameterTweakInfos: duration = generator.duration if duration == 0.0: # Set the duration for 2 samples (1 live 1 dead) serialized = generator.to_dict() new_generator = CompoundGenerator.from_dict(serialized) new_generator.duration = 2 / self.sample_freq return scanning.infos.ParameterTweakInfo("generator", new_generator) else: assert ( duration > 0 ), f"Generator duration of {duration} must be > 0 to signify fixed exposure" assert (self._number_of_adc_samples(duration) > 0 ), f"Generator duration of {duration} gives < 1 ADC sample" return None
def on_validate( self, context: scanning.hooks.AContext, generator: scanning.hooks.AGenerator, axesToMove: scanning.hooks.AAxesToMove, part_info: scanning.hooks.APartInfo, ) -> scanning.hooks.UParameterTweakInfos: child = context.block_view(self.mri) # Check that we can move all the requested axes available = set(child.layout.value.name) motion_axes = get_motion_axes(generator, axesToMove) assert available.issuperset( motion_axes ), "Some of the requested axes %s are not on the motor list %s" % ( list(axesToMove), sorted(available), ) # If GPIO not demanded for every point we don't need to align to the # servo cycle trigger = get_motion_trigger(part_info) if trigger != scanning.infos.MotionTrigger.EVERY_POINT: return None # Find the duration assert generator.duration > 0, "Can only do fixed duration at the moment" servo_freq = child.servoFrequency() # convert half an exposure to multiple of servo ticks, rounding down ticks = np.floor(servo_freq * 0.5 * generator.duration) if not np.isclose(servo_freq, 3200): # + 0.002 for some observed jitter in the servo frequency if I10 # isn't a whole number of 1/4 us move timer ticks # (any frequency apart from 3.2 kHz) ticks += 0.002 # convert to integer number of microseconds, rounding up micros = np.ceil(ticks / servo_freq * 1e6) # back to duration duration = 2 * float(micros) / 1e6 if duration != generator.duration: serialized = generator.to_dict() new_generator = CompoundGenerator.from_dict(serialized) new_generator.duration = duration return scanning.infos.ParameterTweakInfo("generator", new_generator) else: return None
def on_validate( self, context: scanning.hooks.AContext, generator: scanning.hooks.AGenerator, exposure: scanning.hooks.AExposure = 0.0, frames_per_step: AFramesPerStep = 1, ) -> scanning.hooks.UParameterTweakInfos: # Get the duration per frame duration = generator.duration assert ( duration >= 0 ), f"Generator duration of {duration} must be >= 0 to signify fixed exposure" # As the Andor runnable block does not have an ExposureDeadTimePart, we handle # the case where we have been given an exposure time here if exposure > 0: # Grab the current value of the readout time and hope that the parameters # will not change before we actually run configure... child = context.block_view(self.mri) driver_readout_time = child.andorReadoutTime.value # Add the exposure time and multiply up to get the total generator duration duration_per_frame = exposure + driver_readout_time # Check if we need to guess the duration if duration == 0.0: serialized = generator.to_dict() new_generator = CompoundGenerator.from_dict(serialized) # Multiply the duration per frame up duration_per_point = duration_per_frame * frames_per_step new_generator.duration = duration_per_frame * frames_per_step self.log.debug( f"{self.name}: tweaking generator duration from " f"{generator.duration} to {duration_per_point}") return scanning.hooks.ParameterTweakInfo( "generator", new_generator) # Otherwise we just want to check if we can achieve the exposure expected else: assert duration_per_frame <= duration, ( f"{self.name}: cannot achieve exposure of {exposure} with per frame" f" duration of {duration}") return None # Otherwise just let the DetectorDriverPart validate for us else: return super().on_validate(context, generator, frames_per_step=frames_per_step)
def on_configure( self, context: scanning.hooks.AContext, completed_steps: scanning.hooks.ACompletedSteps, steps_to_do: scanning.hooks.AStepsToDo, # The following were passed from user calling configure() generator: scanning.hooks.AGenerator, axesToMove: scanning.hooks.AAxesToMove, exceptionStep: AExceptionStep = 0, ) -> None: child = context.block_view(self.mri) # Store the generator and place we need to start self._generator = generator self._completed_steps = completed_steps self._steps_to_do = steps_to_do self._exception_step = exceptionStep self._axes_to_move = axesToMove self._movers = {axis: MaybeMover(child, axis) for axis in axesToMove} # Move to start (instantly) first_point = generator.get_point(completed_steps) fs: List[Future] = [] for axis, mover in self._movers.items(): mover.maybe_move_async(fs, first_point.lower[axis]) context.wait_all_futures(fs)
def on_validate( self, context: scanning.hooks.AContext, generator: scanning.hooks.AGenerator, frames_per_step: ADetectorFramesPerStep = 1, ) -> scanning.hooks.UParameterTweakInfos: # Check if we have a minimum acquire period if self.required_version is not None: child = context.block_view(self.mri) check_driver_version(child.driverVersion.value, self.required_version) if self.min_acquire_period > 0.0: duration = generator.duration # Check if we need to guess the generator duration if duration == 0.0: # Use the minimum acquire period as an estimate of readout time. We # also need to multiple by frames_per_step as the DetectorChildPart # divides the generator down to the duration for a single detector # frame. duration = self.min_acquire_period * frames_per_step serialized = generator.to_dict() new_generator = CompoundGenerator.from_dict(serialized) new_generator.duration = duration self.log.debug( f"{self.name}: tweaking generator duration from " f"{generator.duration} to {duration}") return scanning.hooks.ParameterTweakInfo( "generator", new_generator) # Otherwise check the provided duration is long enough else: assert generator.duration >= self.min_acquire_period, ( f"Duration {generator.duration} per frame is less than minimum " f"acquire period {self.min_acquire_period}s") return None return None
def on_configure( self, context: scanning.hooks.AContext, completed_steps: scanning.hooks.ACompletedSteps, steps_to_do: scanning.hooks.AStepsToDo, part_info: scanning.hooks.APartInfo, generator: scanning.hooks.AGenerator, axesToMove: scanning.hooks.AAxesToMove, ) -> None: # Double the number of cycles to get rotations static_axis = generator.generators[0] assert isinstance(static_axis, StaticPointGenerator ), "Static Point Generator not configured correctly" static_axis = StaticPointGenerator(size=static_axis.size * 2) steps_to_do *= 2 # Create a linear scan axis (proper rotation) selector_axis = LineGenerator(self.selectorAxis, "deg", self.tomoAngle, self.diffAngle, 1, alternate=True) axesToMove = [self.selectorAxis] def get_minturnaround(): # See if there is a minimum turnaround infos = scanning.infos.MinTurnaroundInfo.filter_values(part_info) if infos: assert ( len(infos) == 1 ), "Expected 0 or 1 MinTurnaroundInfos, got %d" % len(infos) min_turnaround = max(MIN_TIME, infos[0].gap) min_interval = infos[0].interval else: min_turnaround = MIN_TIME min_interval = MIN_INTERVAL return min_turnaround, min_interval # Calculate the exposure time min_turnaround = get_minturnaround()[0] cycle_duration = generator.duration exposure_time = cycle_duration / 2 - self.move_time if exposure_time < min_turnaround: exposure_time = min_turnaround new_generator = CompoundGenerator( [static_axis, selector_axis], [], [], duration=self.move_time, continuous=True, delay_after=exposure_time, ) new_generator.prepare() # Reduce the exposure of the camera/detector generator.duration = exposure_time super().on_configure(context, completed_steps, steps_to_do, part_info, new_generator, axesToMove)