def __init__(self, positioner, data_processor, reader, writer=None, before_measurement_executor=None, after_measurement_executor=None, initialization_executor=None, finalization_executor=None, data_validator=None, settings=None, before_move_executor=None, after_move_executor=None): """ Initialize scanner. :param positioner: Positioner should provide a generator to get the positions to move to. :param writer: Object that implements the write(position) method and sets the positions. :param data_processor: How to store and handle the data. :param reader: Object that implements the read() method to return data to the data_processor. :param before_measurement_executor: Callbacks executor that executed before measurements. :param after_measurement_executor: Callbacks executor that executed after measurements. :param before_move_executor: Callbacks executor that executes before each move. :param after_move_executor: Callbacks executor that executes after each move. """ self.positioner = positioner self.writer = writer self.data_processor = data_processor self.reader = reader self.before_measurement_executor = before_measurement_executor self.after_measurement_executor = after_measurement_executor self.initialization_executor = initialization_executor self.finalization_executor = finalization_executor self.settings = settings or scan_settings() self.before_move_executor = before_move_executor self.after_move_executor = after_move_executor # If no data validator is provided, data is always valid. self.data_validator = data_validator or (lambda position, data: True) self._user_abort_scan_flag = False self._user_pause_scan_flag = False self._status = STATUS_INITIALIZED
def test_bsread_positioner(self): config.bs_connection_mode = "pull" config.bs_default_host = "localhost" config.bs_default_port = 9999 settling_time = 0.5 n_messages = 10 positioner = BsreadPositioner(n_messages) readables = ["bs://CAMERA1:X"] settings = scan_settings(settling_time=settling_time) time.sleep(1) # settings = scan_settings(bs_read_filter=mock_filter) result = scan(positioner=positioner, readables=readables, settings=settings) first_message_offset = result[0][0] # Expect to receive all messages from bsread from a pulse_id on; [[11], [12], [13], ...] expected_results = [[index + first_message_offset] for index in range(n_messages)] self.assertListEqual(result, expected_results)
def test_mulitple_messages_on_same_position(self): # DO NOT INCLUDE IN README - default. config.bs_connection_mode = "pull" config.bs_default_host = "localhost" config.bs_default_port = 9999 config.bs_default_missing_property_value = Exception # Lets acquire 10 messages from the stream. n_images = 5 n_measurements = 3 positioner = StaticPositioner(n_images) settings = scan_settings(n_measurements=n_measurements) # Read camera X and Y value. readables = [bs_property("CAMERA1:X"), bs_property("CAMERA1:Y")] # The result will have 10 consecutive, valid, messages from the stream. result = scan(positioner=positioner, readables=readables, settings=settings) self.assertEqual(n_images, len(result)) for position_index in range(n_images): first = result[position_index][0] for measurement_index in range(1, n_measurements): self.assertNotEqual(first, result[position_index][measurement_index])
def test_bs_read_filter(self): config.bs_connection_mode = "pull" config.bs_default_host = "localhost" config.bs_default_port = 9999 n_images = 10 positioner = StaticPositioner(n_images) readables = ["bs://CAMERA1:X"] # Count how many messages passed. def mock_filter(message): if message: nonlocal filter_pass filter_pass += 1 return True filter_pass = 0 settings = scan_settings(bs_read_filter=mock_filter) # settings = scan_settings(bs_read_filter=mock_filter) result = scan(positioner=positioner, readables=readables, settings=settings) # The length should still be the same - the filter just throws away messages we do not want. self.assertEqual(len(result), n_images) self.assertTrue(filter_pass >= n_images, "The filter passed less then the received messages.")
def test_bsread_timestamp_read(self): config.bs_connection_mode = "pull" config.bs_default_host = "localhost" config.bs_default_port = 9999 settling_time = 0.2 messages_expected_factor = settling_time / MOCK_SENDER_INTERVAL n_images = 10 positioner = StaticPositioner(n_images) readables = ["bs://CAMERA1:X"] settings = scan_settings(settling_time=settling_time) # settings = scan_settings(bs_read_filter=mock_filter) result = scan(positioner=positioner, readables=readables, settings=settings) # For messages_expected_factor=4; [[4], [8], [12], ...] expected_results = [[x] for x in [(index + 1) * messages_expected_factor for index in range(n_images)]] self.assertListEqual(result, expected_results)
def test_progress_monitor(self): current_index = [] total_positions = 0 current_percentage = [] def progress(current_position, max_position): current_index.append(current_position) current_percentage.append(100 * (current_position / max_position)) nonlocal total_positions total_positions = max_position positions = [1, 2, 3, 4, 5] positioner = VectorPositioner(positions) writables = epics_pv("PYSCAN:TEST:MOTOR1:SET", "PYSCAN:TEST:MOTOR1:GET") readables = epics_pv("PYSCAN:TEST:OBS1") settings = scan_settings(progress_callback=progress) scan(positioner, readables, writables, settings=settings) self.assertEqual( len(positions) + 1, len(current_index), "The number of reported positions is wrong.") self.assertEqual(total_positions, 5, "The number of total positions is wrong.") self.assertEqual(current_index, [0, 1, 2, 3, 4, 5], "The reported percentage is wrong.") self.assertEqual(current_percentage, [0, 20, 40, 60, 80, 100], "The reported percentage is wrong.")
def test_same_condition_multiple_times(self): config.bs_connection_mode = "pull" config.bs_default_host = "localhost" config.bs_default_port = 9999 positioner = StaticPositioner(5) readables = [bs_property("CAMERA1:X")] conditions = [ bs_condition("CAMERA1:VALID", 10, operation=ConditionComparison.EQUAL), bs_condition("CAMERA1:VALID", 10, operation=ConditionComparison.EQUAL) ] result = scan(positioner=positioner, readables=readables, conditions=conditions, settings=scan_settings(measurement_interval=0.25, n_measurements=1)) self.assertTrue(all(x[0] == i + 1 for i, x in enumerate(result)), "The result is wrong.")
def test_multiple_conditions(self): config.bs_connection_mode = "pull" config.bs_default_host = "localhost" config.bs_default_port = 9999 positioner = StaticPositioner(5) initialization = [action_set_epics_pv("PYSCAN:TEST:VALID1", 778)] readables = [bs_property("CAMERA1:X")] conditions = [ bs_condition("CAMERA1:VALID", 10, operation=ConditionComparison.EQUAL), bs_condition("CAMERA1:VALID", 11, operation=ConditionComparison.LOWER), epics_condition("PYSCAN:TEST:VALID1", 778, operation=ConditionComparison.LOWER_OR_EQUAL), epics_condition("PYSCAN:TEST:VALID1", 779, operation=ConditionComparison.LOWER) ] result = scan(positioner=positioner, readables=readables, conditions=conditions, initialization=initialization, settings=scan_settings(measurement_interval=0.25, n_measurements=1)) self.assertTrue(all(x[0] == i + 1 for i, x in enumerate(result)), "The result is wrong.")
def test_actions(self): positions = [[1, 1], [2, 2]] positioner = VectorPositioner(positions) writables = [ epics_pv("PYSCAN:TEST:MOTOR1:SET", "PYSCAN:TEST:MOTOR1:GET"), epics_pv("PYSCAN:TEST:MOTOR2:SET", "PYSCAN:TEST:MOTOR2:GET") ] readables = [epics_pv("PYSCAN:TEST:OBS1")] # MOTOR1 initial values should be -11, MOTOR2 -22. cached_initial_values["PYSCAN:TEST:MOTOR1:SET"] = -11 cached_initial_values["PYSCAN:TEST:MOTOR2:SET"] = -22 initialization = [action_set_epics_pv("PYSCAN:TEST:OBS1", -33)] finalization = [action_restore(writables)] result = scan(positioner=positioner, readables=readables, writables=writables, initialization=initialization, finalization=finalization, settings=scan_settings(measurement_interval=0.25, n_measurements=1)) self.assertEqual(pv_cache["PYSCAN:TEST:MOTOR1:SET"][0].value, -11, "Finalization did not restore original value.") self.assertEqual(pv_cache["PYSCAN:TEST:MOTOR2:SET"][0].value, -22, "Finalization did not restore original value.") self.assertEqual(result[0][0], -33, "Initialization action did not work.")
def test_bsread_positioner_multi_measurements(self): with self.assertRaisesRegex( ValueError, "When using BsreadPositioner the maximum number of n_measurements = 1" ): scan(readables=[epics_pv("something")], positioner=BsreadPositioner(10), settings=scan_settings(n_measurements=2))
def test_mixed_sources(self): config.bs_connection_mode = "pull" config.bs_default_host = "localhost" config.bs_default_port = 9999 positions = [[1, 1], [2, 2], [3, 3], [4, 4]] positioner = VectorPositioner(positions) writables = [ epics_pv("PYSCAN:TEST:MOTOR1:SET", "PYSCAN:TEST:MOTOR1:GET"), epics_pv("PYSCAN:TEST:MOTOR2:SET", "PYSCAN:TEST:MOTOR2:GET") ] readables = [ bs_property("CAMERA1:X"), bs_property("CAMERA1:Y"), epics_pv("PYSCAN:TEST:OBS1") ] conditions = [ epics_condition("PYSCAN:TEST:VALID1", 10), bs_condition("CAMERA1:VALID", 10) ] initialization = [ action_set_epics_pv("PYSCAN:TEST:PRE1:SET", 1, "PYSCAN:TEST:PRE1:GET") ] finalization = [ action_set_epics_pv("PYSCAN:TEST:PRE1:SET", 0, "PYSCAN:TEST:PRE1:GET"), action_restore(writables) ] result = scan(positioner=positioner, readables=readables, writables=writables, conditions=conditions, initialization=initialization, finalization=finalization, settings=scan_settings(measurement_interval=0.25, n_measurements=1)) self.assertEqual(len(result), len(positions), "Not the expected number of results.") # The first 2 attributes are from bs_read, they should be equal to the pulse ID processed. self.assertTrue(all(x[0] == x[1] and x[2] == 1 for x in result), "The result is wrong.")
def _generate_scan_parameters(relative, writables, latency): # If the scan is relative we collect the initial writables offset, and restore the state at the end of the scan. offsets = None finalization_action = [] if relative: pv_names = [x.pv_name for x in convert_to_list(writables) or []] reader = EPICS_READER(pv_names) offsets = reader.read() reader.close() finalization_action.append(action_restore(writables)) settings = scan_settings(settling_time=latency) return offsets, finalization_action, settings
def test_bsread_dal_cache_invalidation(self): config.bs_connection_mode = "pull" config.bs_default_host = "localhost" config.bs_default_port = 9999 settling_time = 0.15 n_messages = 10 positioner = BsreadPositioner(n_messages) readables = ["bs://CAMERA1:X"] counter = 0 def odd_condition(message): nonlocal counter counter += 1 if counter % 2 == 0: return True return False conditions = [ function_condition(odd_condition, action=ConditionAction.Retry) ] settings = scan_settings(settling_time=settling_time) time.sleep(1) # settings = scan_settings(bs_read_filter=mock_filter) result = scan(positioner=positioner, readables=readables, conditions=conditions, settings=settings) first_message_offset = result[0][0] # Expect to receive all odd messages from bsread from a pulse_id on; [[10], [12], [14], ...] expected_results = [[first_message_offset + (2 * index)] for index in range(n_messages)] self.assertListEqual(result, expected_results)
def scanner(positioner, readables, writables=None, conditions=None, before_read=None, after_read=None, initialization=None, finalization=None, settings=None, data_processor=None, before_move=None, after_move=None): # Allow a list or a single value to be passed. Initialize None values. writables = convert_input(convert_to_list(writables) or []) readables = convert_input(convert_to_list(readables) or []) conditions = convert_conditions(convert_to_list(conditions) or []) before_read = convert_to_list(before_read) or [] after_read = convert_to_list(after_read) or [] before_move = convert_to_list(before_move) or [] after_move = convert_to_list(after_move) or [] initialization = convert_to_list(initialization) or [] finalization = convert_to_list(finalization) or [] settings = settings or scan_settings() # TODO: Ugly. The scanner should not depend on a particular positioner implementation. if isinstance(positioner, BsreadPositioner) and settings.n_measurements > 1: raise ValueError( "When using BsreadPositioner the maximum number of n_measurements = 1." ) bs_reader = _initialize_bs_dal(readables, conditions, settings.bs_read_filter, positioner) epics_writer, epics_pv_reader, epics_condition_reader = _initialize_epics_dal( writables, readables, conditions, settings) function_writer, function_reader, function_condition = _initialize_function_dal( writables, readables, conditions) writables_order = [type(writable) for writable in writables] # Write function needs to merge PV and function proxy data. def write_data(positions): positions = convert_to_list(positions) pv_values = [ x for x, source in zip(positions, writables_order) if source == EPICS_PV ] function_values = [ x for x, source in zip(positions, writables_order) if source == FUNCTION_VALUE ] if epics_writer: epics_writer.set_and_match(pv_values) if function_writer: function_writer.write(function_values) # Order of value sources, needed to reconstruct the correct order of the result. readables_order = [type(readable) for readable in readables] # Read function needs to merge BS, PV, and function proxy data. def read_data(current_position_index, retry=False): _logger.debug("Reading data for position index %s." % current_position_index) bs_values = iter( bs_reader.read(current_position_index, retry) if bs_reader else []) epics_values = iter( epics_pv_reader.read(current_position_index ) if epics_pv_reader else []) function_values = iter( function_reader.read(current_position_index ) if function_reader else []) # Interleave the values correctly. result = [] for source in readables_order: if source == BS_PROPERTY: next_result = next(bs_values) elif source == EPICS_PV: next_result = next(epics_values) elif source == FUNCTION_VALUE: next_result = next(function_values) else: raise ValueError("Unknown type of readable %s used." % source) # We flatten the result, whenever possible. if isinstance(next_result, list) and source != FUNCTION_VALUE: result.extend(next_result) else: result.append(next_result) return result # Order of value sources, needed to reconstruct the correct order of the result. conditions_order = [type(condition) for condition in conditions] # Validate function needs to validate both BS, PV, and function proxy data. def validate_data(current_position_index, data): _logger.debug("Reading data for position index %s." % current_position_index) bs_values = iter( bs_reader.read_cached_conditions() if bs_reader else []) epics_values = iter( epics_condition_reader.read(current_position_index ) if epics_condition_reader else []) function_values = iter( function_condition.read(current_position_index ) if function_condition else []) for index, source in enumerate(conditions_order): if source == BS_CONDITION: value = next(bs_values) elif source == EPICS_CONDITION: value = next(epics_values) elif source == FUNCTION_CONDITION: value = next(function_values) else: raise ValueError("Unknown type of condition %s used." % source) value_valid = False # Function conditions are self contained. if source == FUNCTION_CONDITION: if value: value_valid = True else: expected_value = conditions[index].value tolerance = conditions[index].tolerance operation = conditions[index].operation if compare_channel_value(value, expected_value, tolerance, operation): value_valid = True if not value_valid: if conditions[index].action == ConditionAction.Retry: return False if source == FUNCTION_CONDITION: raise ValueError("Function condition %s returned False." % conditions[index].identifier) else: raise ValueError( "Condition %s failed, expected value %s, actual value %s, " "tolerance %s, operation %s." % (conditions[index].identifier, conditions[index].value, value, conditions[index].tolerance, conditions[index].operation)) return True if not data_processor: data_processor = DATA_PROCESSOR() # Before acquisition hook. before_measurement_executor = None if before_read: before_measurement_executor = ACTION_EXECUTOR(before_read).execute # After acquisition hook. after_measurement_executor = None if after_read: after_measurement_executor = ACTION_EXECUTOR(after_read).execute # Executor before each move. before_move_executor = None if before_move: before_move_executor = ACTION_EXECUTOR(before_move).execute # Executor after each move. after_move_executor = None if after_move: after_move_executor = ACTION_EXECUTOR(after_move).execute # Initialization (before move to first position) hook. initialization_executor = None if initialization: initialization_executor = ACTION_EXECUTOR(initialization).execute # Finalization (after last acquisition AND on error) hook. finalization_executor = None if finalization: finalization_executor = ACTION_EXECUTOR(finalization).execute scanner = Scanner(positioner=positioner, data_processor=data_processor, reader=read_data, writer=write_data, before_measurement_executor=before_measurement_executor, after_measurement_executor=after_measurement_executor, initialization_executor=initialization_executor, finalization_executor=finalization_executor, data_validator=validate_data, settings=settings, before_move_executor=before_move_executor, after_move_executor=after_move_executor) return scanner
def execute_scan(self): after_executor = self.get_action_executor("In-loopPostAction") # Wrap the post action executor to update the number of completed scans. def progress_after_executor(scanner_instance, data): # Execute other post actions. after_executor(scanner_instance) # Update progress. self.n_done_measurements += 1 self.ProgDisp.Progress = 100.0 * (self.n_done_measurements / self.n_total_positions) def prepare_monitors(reader): # If there are no monitors defined we have nothing to validate. if not self.dimensions[-1]["Monitor"]: return None def validate_monitors(position, data): monitor_values = reader.read() combined_data = zip(self.dimensions[-1]['Monitor'], self.dimensions[-1]['MonitorValue'], self.dimensions[-1]['MonitorTolerance'], self.dimensions[-1]['MonitorAction'], self.dimensions[-1]['MonitorTimeout'], monitor_values) for pv, expected_value, tolerance, action, timeout, value in combined_data: # Monitor value does not match. if not compare_channel_value(value, expected_value, tolerance): if action == "Abort": raise ValueError( "Monitor %s, expected value %s, tolerance %s, has value %s. Aborting." % (pv, expected_value, tolerance, value)) elif action == "WaitAndAbort": return False else: raise ValueError( "MonitorAction %s, on PV %s, is not supported." % (pv, action)) return True return validate_monitors # Setup scan settings. settings = scan_settings( settling_time=self.dimensions[-1]["KnobWaitingExtra"], n_measurements=self.dimensions[-1]["NumberOfMeasurements"], measurement_interval=self.dimensions[-1]["Waiting"]) data_processor = PyScanDataProcessor( self.outdict, n_readbacks=self.n_readbacks, n_validations=self.n_validations, n_observables=self.n_observables, n_measurements=settings.n_measurements) self.scanner = Scanner( positioner=self.get_positioner(), data_processor=data_processor, reader=self.epics_dal.get_group(READ_GROUP).read, writer=self.epics_dal.get_group(WRITE_GROUP).set_and_match, before_measurement_executor=self.get_action_executor( "In-loopPreAction"), after_measurement_executor=progress_after_executor, initialization_executor=self.get_action_executor("PreAction"), finalization_executor=self.get_action_executor("PostAction"), data_validator=prepare_monitors( self.epics_dal.get_group(MONITOR_GROUP)), settings=settings) self.outdict.update(self.scanner.discrete_scan())