class MezfliprTests(unittest.TestCase): def setUp(self): self._lewis, self._ioc = get_running_lewis_and_ioc( EMULATOR_NAME, DEVICE_PREFIX) self.ca = ChannelAccess(device_prefix=DEVICE_PREFIX) def test_WHEN_ioc_is_started_THEN_ioc_is_not_disabled(self): self.ca.assert_that_pv_is("DISABLE", "COMMS ENABLED") @skip_if_recsim("Disconnection simulation not implemented in recsim") def test_GIVEN_device_is_connected_THEN_can_read_id(self): self.ca.assert_that_pv_is("ID", "Flipper Control") self.ca.assert_that_pv_alarm_is("ID", self.ca.Alarms.NONE) @parameterized.expand(["ANALYSER", "POLARISER"]) def test_GIVEN_amplitude_is_set_THEN_amplitude_can_be_read_back( self, flipper): for val in [0., 0.12, 2.99]: # Amplitude should be limited to 3A self.ca.assert_setting_setpoint_sets_readback( val, readback_pv="{}:AMPLITUDE".format(flipper)) @parameterized.expand(["ANALYSER", "POLARISER"]) def test_GIVEN_compensation_is_set_THEN_compensation_can_be_read_back( self, flipper): for val in [0., 0.12, 5000.5]: self.ca.assert_setting_setpoint_sets_readback( val, readback_pv="{}:COMPENSATION".format(flipper)) @parameterized.expand(["ANALYSER", "POLARISER"]) def test_GIVEN_dt_is_set_THEN_dt_can_be_read_back(self, flipper): for val in [0., -0.12, -5000.5]: # DT only accepts negative values. self.ca.assert_setting_setpoint_sets_readback( val, readback_pv="{}:DT".format(flipper)) @parameterized.expand(["ANALYSER", "POLARISER"]) def test_GIVEN_constant_is_set_THEN_constant_can_be_read_back( self, flipper): for val in [0., 0.12, 5000.5]: self.ca.assert_setting_setpoint_sets_readback( val, readback_pv="{}:CONSTANT".format(flipper)) @parameterized.expand(["ANALYSER", "POLARISER"]) def test_GIVEN_constant_is_set_THEN_constant_can_be_read_back( self, flipper): for filename in [r"C:\a.txt", r"C:\b.txt"]: self.ca.assert_setting_setpoint_sets_readback( filename, readback_pv="{}:FILENAME".format(flipper)) @parameterized.expand([ "Analyser Off Pol. Off", "Analyser Off Pol. On", "Analyser On Pol. Off", "Analyser On Pol. On", ]) def test_GIVEN_toggle_state_is_set_THEN_toggle_state_can_be_read_back( self, toggle_state): self.ca.set_pv_value("TOGGLE:SP", toggle_state) self.ca.assert_that_pv_is("TOGGLE", toggle_state) self.ca.assert_that_pv_alarm_is("TOGGLE", self.ca.Alarms.NONE)
class CurrentTests(unittest.TestCase): # These current testing values are uncalibrated values from the DAQ lying between 0 and 10. current_values = [0, 1.33333, 5e1, 10e-3, 10] current_values_which_give_alarms = [10, 11] def setUp(self): self.ca = ChannelAccess(20, device_prefix=DEVICE_PREFIX) shared_setup(self.ca) self._simulate_current(0) def _simulate_current(self, current): curr_array = [current] * 1000 self.ca.set_pv_value("DAQ:CURR:WV:SIM", curr_array) @parameterized.expand(parameterized_list(current_values)) def test_that_GIVEN_current_value_THEN_calibrated_current_readback_changes( self, _, value): # GIVEN self._simulate_current(value) self.ca.assert_that_pv_is_number("CURR", value * DAQ_CURR_READ_SCALE_FACTOR, MARGIN_OF_ERROR) @parameterized.expand(parameterized_list(current_values_which_give_alarms)) def test_that_WHEN_current_is_out_of_range_THEN_alarm_raised( self, _, value): # WHEN self._simulate_current(value) # THEN self.ca.assert_that_pv_alarm_is("CURR", ChannelAccess.Alarms.MAJOR)
class CP2800StatusTests(unittest.TestCase): def setUp(self): _, self._ioc = get_running_lewis_and_ioc(None, DEVICE_PREFIX) self.assertIsNotNone(self._ioc) self.ca = ChannelAccess(device_prefix=DEVICE_PREFIX) def test_GIVEN_compressor_elapsed_minutes_THEN_alarm_correct(self): tests = [ (-1, self.ca.Alarms.MAJOR), (0, self.ca.Alarms.NONE), (1, self.ca.Alarms.NONE), (500001, self.ca.Alarms.MINOR), (1000001, self.ca.Alarms.MAJOR), ] for test in tests: elapsed_time, expected_alarm = test self.ca.set_pv_value("SIM:ELAPSED", elapsed_time) self.ca.assert_that_pv_alarm_is("ELAPSED", expected_alarm, 10) def test_GIVEN_compressor_state_on_or_off_THEN_readback_correct(self): states = [(1, "On"), (0, "Off")] for state in states: send_val, expected_response = state self.ca.set_pv_value("SIM:POWER", send_val) self.ca.assert_that_pv_is("POWER", expected_response) def test_GIVEN_error_value_THEN_readback_correct(self): self.ca.set_pv_value("SIM:ERR", 1) self.ca.assert_that_pv_is("ERR", 1, timeout=10) def test_GIVEN_negative_error_value_THEN_alarm_correct(self): self.ca.set_pv_value("SIM:ERR", -1) self.ca.assert_that_pv_alarm_is("ERR", self.ca.Alarms.MAJOR, timeout=10)
class Mk3chopperTests(unittest.TestCase): # Remote access modes LOCAL = "LOCAL" REMOTE = "REMOTE" COMP_MODE_PV = "COMP:MODE" def setUp(self): self._ioc = IOCRegister.get_running(DEVICE_PREFIX) # Comp mode is on a slow 10s read. Needs a longer timeout than default self.ca = ChannelAccess(device_prefix=DEVICE_PREFIX, default_timeout=30) self.ca.assert_that_pv_exists(self.COMP_MODE_PV) def test_WHEN_ioc_is_in_remote_mode_THEN_it_has_no_alarm(self): # In RECSIM, switch to remote explicitly. DEVSIM can only have remote mode so no switch needed if IOCRegister.uses_rec_sim: # Bug in CA channel. Reports invalid alarm severity if you set enum directly self.ca.set_pv_value("SIM:{}.VAL".format(self.COMP_MODE_PV), 1) self.ca.assert_that_pv_is(self.COMP_MODE_PV, self.REMOTE) self.ca.assert_that_pv_alarm_is(self.COMP_MODE_PV, ChannelAccess.Alarms.NONE) @skip_if_devsim("Can't switch to local mode in DEVSIM") def test_WHEN_ioc_is_in_local_mode_THEN_it_has_a_major_alarm(self): # Bug in CA channel. Reports invalid alarm severity if you set enum directly self.ca.set_pv_value("SIM:{}.VAL".format(self.COMP_MODE_PV), 0) self.ca.assert_that_pv_is(self.COMP_MODE_PV, self.LOCAL) self.ca.assert_that_pv_alarm_is(self.COMP_MODE_PV, ChannelAccess.Alarms.MAJOR)
class FlipprpsTests(unittest.TestCase): """ Tests for the Flipprps IOC. """ def setUp(self): self._lewis, self._ioc = get_running_lewis_and_ioc( "flipprps", DEVICE_PREFIX) self.ca = ChannelAccess(device_prefix=DEVICE_PREFIX) self._lewis.backdoor_set_on_device("connected", True) @skip_if_recsim("Lewis backdoor commands not available in RecSim") def test_SET_polarity(self): self.ca.set_pv_value("POLARITY", "Down") self._lewis.assert_that_emulator_value_is("polarity", 0) self.ca.set_pv_value("POLARITY", "Up") self._lewis.assert_that_emulator_value_is("polarity", 1) def test_GET_id(self): self.ca.assert_that_pv_is("ID", "Flipper") @skip_if_recsim("Lewis backdoor commands not available in RecSim") def test_GIVEN_device_not_connected_THEN_id_is_in_alarm(self): self._lewis.backdoor_set_on_device("connected", False) self.ca.assert_that_pv_alarm_is("ID", self.ca.Alarms.INVALID, 20) @skip_if_recsim("Lewis backdoor commands not available in RecSim") def test_GIVEN_device_not_connected_THEN_polarity_raises_timeout_alarm_after_set( self): self._lewis.backdoor_set_on_device("connected", False) self.ca.set_pv_value("POLARITY", "Up") self.ca.assert_that_pv_alarm_is("POLARITY", self.ca.Alarms.INVALID)
class DAQmxTests(object): """ General tests for the DAQmx. """ def setUp(self): self.emulator, self._ioc = get_running_lewis_and_ioc( DEVICE_PREFIX, DEVICE_PREFIX) self.ca = ChannelAccess(device_prefix=DEVICE_PREFIX) def test_WHEN_emulator_disconnected_THEN_data_in_alarm_and_valid_on_reconnect( self): self.ca.assert_that_pv_alarm_is_not("DATA", ChannelAccess.Alarms.INVALID) self.emulator.disconnect_device() self.ca.assert_that_pv_alarm_is("DATA", ChannelAccess.Alarms.INVALID) # Check we don't get excessive numbers of messages if we stay disconnected for 15s (up to 15 messages seems # reasonable - 1 per second on average) with assert_log_messages(self._ioc, number_of_messages=15): sleep(15) # Double-check we are still in alarm self.ca.assert_that_pv_alarm_is("DATA", ChannelAccess.Alarms.INVALID) self.emulator.reconnect_device() self.ca.assert_that_pv_alarm_is_not("DATA", ChannelAccess.Alarms.INVALID, timeout=5) self.ca.assert_that_pv_value_is_changing("DATA", 1)
class Tpg300Tests(unittest.TestCase): """ Tests for the TPG300. """ def setUp(self): self._lewis, self._ioc = get_running_lewis_and_ioc("tpg300", DEVICE_PREFIX) self.ca = ChannelAccess(20, device_prefix=DEVICE_PREFIX) def tearDown(self): self._connect_emulator() def _set_pressure(self, expected_pressure, channel): prop = "pressure_{}".format(channel.lower()) pv = "SIM:PRESSURE" self._lewis.backdoor_set_on_device(prop, expected_pressure) self._ioc.set_simulated_value(pv, expected_pressure) def _set_units(self, unit): self._lewis.backdoor_run_function_on_device("backdoor_set_unit", [unit.value]) self._ioc.set_simulated_value("SIM:UNITS", unit.name) def _connect_emulator(self): self._lewis.backdoor_run_function_on_device("connect") def _disconnect_emulator(self): self._lewis.backdoor_run_function_on_device("disconnect") def test_that_GIVEN_a_connected_emulator_WHEN_ioc_started_THEN_ioc_is_not_disabled(self): self.ca.assert_that_pv_is("DISABLE", "COMMS ENABLED") def test_that_GIVEN_a_connected_emulator_WHEN_units_are_set_THEN_unit_is_the_same_as_backdoor(self): for unit in Units: self._set_units(unit) expected_unit = unit.name self.ca.assert_that_pv_is("UNITS", expected_unit) def test_that_GIVEN_a_connected_emulator_and_pressure_value_WHEN_set_pressure_is_set_THEN_the_ioc_is_updated(self): for expected_pressure, channel in product(TEST_PRESSURES, CHANNELS): pv = "PRESSURE_{}".format(channel) self._set_pressure(expected_pressure, channel) self.ca.assert_that_pv_is(pv, expected_pressure) @skip_if_recsim("Recsim is unable to simulate a disconnected device") def test_that_GIVEN_a_disconnected_emulator_WHEN_getting_pressure_THEN_INVALID_alarm_shows(self): self._disconnect_emulator() for channel in CHANNELS: pv = "PRESSURE_{}".format(channel) self.ca.assert_that_pv_alarm_is(pv, self.ca.Alarms.INVALID)
class ChannelTests(unittest.TestCase): def setUp(self): self._lewis, self._ioc = get_running_lewis_and_ioc( "keithley_2700", DEVICE_PREFIX) self.ca = ChannelAccess(default_timeout=30, device_prefix=DEVICE_PREFIX) self.ca.assert_that_pv_exists("IDN") self.ca.set_pv_value("BUFF:CLEAR:SP", "") self.ca.assert_that_pv_is("BUFF:AUTOCLEAR", "ON") if not IOCRegister.uses_rec_sim: self._lewis.backdoor_set_on_device("simulate_readings", False) @unstable_test() @skip_if_recsim("Cannot use lewis backdoor in recsim") def test_GIVEN_empty_buffer_WHEN_reading_inserted_THEN_channel_PVs_get_correct_values( self): _reset_drift_channels(self) reading = "+1386.05,+4000,101" expected_values = { 'read': 1386.05, 'time': 4000, 'temp101': 47.424, 'drift': 0, } # GIVEN self.ca.set_pv_value("BUFF:CLEAR:SP", "") # WHEN _insert_reading(self, [reading]) # THEN self.ca.assert_that_pv_is_number("CHNL:101:READ", expected_values['read'], tolerance=READ_TOLERANCE) self.ca.assert_that_pv_is_number("CHNL:101:TIME", expected_values['time'], tolerance=TIME_TOLERANCE) self.ca.assert_that_pv_is_number("CHNL:101:TEMP", expected_values['temp101'], tolerance=TEMP_TOLERANCE) self.ca.assert_that_pv_is_number("CHNL:101:DRIFT", expected_values['drift'], tolerance=DRIFT_TOLERANCE) @skip_if_recsim("Alarm invalid in recsim") def test_GIVEN_temperature_out_of_bounds_THEN_alarm_is_major(self): #GIVEN reading = "+939,+10,101" _insert_reading(self, [reading]) #THEN self.ca.assert_that_pv_alarm_is("CHNL:101:TEMP:CHECK", self.ca.Alarms.MAJOR)
class WbvalveTests(unittest.TestCase): """ Tests for the Wbvalve IOC. """ def setUp(self): self._lewis, self._ioc = get_running_lewis_and_ioc( emulator_name, DEVICE_PREFIX) self.ca = ChannelAccess(device_prefix=DEVICE_PREFIX) self._lewis.backdoor_run_function_on_device('reset') @parameterized.expand(['J1', 'J2']) @skip_if_recsim("Recsim behaviour not defined") def test_GIVEN_an_ioc_WHEN_set_valve_to_wb1on_THEN_status_is_wb1on( self, expected_value): self.ca.assert_setting_setpoint_sets_readback(expected_value, 'POS') @skip_if_recsim("Can not test disconnection in rec sim") def test_GIVEN_device_not_connected_WHEN_get_status_THEN_alarm(self): self._lewis.backdoor_set_on_device('connected', False) self.ca.assert_that_pv_alarm_is('POS', ChannelAccess.Alarms.INVALID)
class Sans2dVacuumTankTest(unittest.TestCase): """ Tests for the SANS2D vacuum tank, based on a FINS PLC. """ def setUp(self): self._ioc = IOCRegister.get_running("FINS_01") self.ca = ChannelAccess(device_prefix=ioc_prefix) @parameterized.expand(parameterized_list([-5, 0, 3, 5, 7, 9, 16])) def test_WHEN_set_tank_status_to_unknown_value_THEN_error_status( self, _, status_rval): self.ca.set_pv_value("SIM:TANK:STATUS", status_rval) self.ca.assert_that_pv_is("TANK:STATUS", "ERROR: STATUS UNKNOWN") self.ca.assert_that_pv_alarm_is("TANK:STATUS", "MAJOR") @parameterized.expand([(1, "ATMOSPHERE"), (2, "VAC DOWN"), (4, "AT VACUUM"), (8, "VENTING")]) def test_WHEN_set_tank_status_to_known_value_THEN_no_error( self, status_rval, status_val): self.ca.set_pv_value("SIM:TANK:STATUS", status_rval) self.ca.assert_that_pv_is("TANK:STATUS", status_val) self.ca.assert_that_pv_alarm_is("TANK:STATUS", "NO_ALARM")
class Knrk6Tests(unittest.TestCase): """ Tests for the Knrk6 IOC. """ def setUp(self): self._lewis, self._ioc = get_running_lewis_and_ioc( DEVICE_NAME, DEVICE_PREFIX) self.ca = ChannelAccess(device_prefix=DEVICE_PREFIX) self.ca.assert_that_pv_exists("POSITION", timeout=30) self._lewis.backdoor_run_function_on_device("reset") def test_GIVEN_home_position_THEN_home_position_returned(self): expected_value = 1 self.ca.set_pv_value("POSITION:SP", expected_value) self.ca.assert_that_pv_is("POSITION", expected_value, timeout=5) def test_GIVEN_set_position_THEN_moved_to_correct_position(self): expected_value = 6 self.ca.set_pv_value("POSITION:SP", expected_value) self.ca.assert_that_pv_is("POSITION", expected_value, timeout=5) @skip_if_recsim("Unable to use lewis backdoor in RECSIM") def test_GIVEN_device_not_connected_WHEN_get_error_THEN_alarm(self): self._lewis.backdoor_set_on_device('connected', False) self.ca.assert_that_pv_alarm_is('POSITION', ChannelAccess.Alarms.INVALID, timeout=5) @skip_if_recsim("Unable to use lewis backdoor in RECSIM") def test_GIVEN_an_input_error_WHEN_open_file_THEN_error_str_returned(self): self._lewis.backdoor_set_on_device("input_correct", False) expected_value = "Position change slow" self.ca.set_pv_value("POSITION:SP", 5) self.ca.assert_that_pv_is("ERROR", expected_value, timeout=5)
class MecfrfTests(unittest.TestCase): def setUp(self): self._lewis, self._ioc = get_running_lewis_and_ioc( _EMULATOR_NAME, DEVICE_PREFIX) self.ca = ChannelAccess(device_prefix=DEVICE_PREFIX, default_timeout=30) self._lewis.backdoor_set_on_device("connected", True) self._lewis.backdoor_set_on_device("corrupted_messages", False) def test_WHEN_device_is_started_THEN_it_is_not_disabled(self): self.ca.assert_that_pv_is("DISABLE", "COMMS ENABLED") @parameterized.expand( parameterized_list(itertools.product(SENSORS, TEST_LENGTHS))) @skip_if_recsim("Uses Lewis backdoor") def test_WHEN_value_is_written_to_emulator_THEN_record_updates( self, _, sensor, length): self._lewis.backdoor_set_on_device("sensor{}".format(sensor), length * RAW_READING_SCALING) self.ca.assert_that_pv_is("SENSOR{}".format(sensor), length) self.ca.assert_that_pv_alarm_is("SENSOR{}".format(sensor), self.ca.Alarms.NONE) @skip_if_recsim("Uses Lewis backdoor") def test_WHEN_emulator_sends_corrupt_packets_THEN_records_go_into_alarm( self): with self.ca.assert_pv_processed("_RESET_CONNECTION"): self._lewis.backdoor_set_on_device("corrupted_messages", True) self.ca.assert_that_pv_is("_GETTING_INVALID_MESSAGES", 1) self._lewis.backdoor_set_on_device("corrupted_messages", False) self.ca.assert_that_pv_is("_GETTING_INVALID_MESSAGES", 0) @parameterized.expand(parameterized_list(SENSORS)) @skip_if_recsim("Uses Lewis backdoor") def test_WHEN_emulator_disconnected_THEN_records_go_into_alarm( self, _, sensor): self.ca.assert_that_pv_is("_READINGS_OUTDATED", "No") self.ca.assert_that_pv_alarm_is("SENSOR{}".format(sensor), self.ca.Alarms.NONE) self._lewis.backdoor_set_on_device("connected", False) self.ca.assert_that_pv_is("_READINGS_OUTDATED", "Yes") self.ca.assert_that_pv_alarm_is("SENSOR{}".format(sensor), self.ca.Alarms.INVALID)
class ZeroFieldMagFieldTests(unittest.TestCase): def setUp(self): self._ioc = IOCRegister.get_running(DEVICE_PREFIX) self.ca = ChannelAccess(device_prefix=DEVICE_PREFIX) self.ca.assert_that_pv_exists("DISABLE", timeout=30) self.write_offset(0) self.ca.set_pv_value("RANGE", 1.0, sleep_after_set=0.0) self.write_simulated_field_values(ZERO_FIELD) self.write_simulated_alarm_level(self.ca.Alarms.NONE) self.ca.process_pv("TAKEDATA") def write_offset(self, offset): """ Writes offset values for all three IOC components Args: offset: float, the offset to be written to the IOC Returns: None """ for axis in AXES.keys(): self.ca.set_pv_value("OFFSET:{}".format(axis), offset, sleep_after_set=0.0) def write_sensor_matrix(self, sensor_matrix): """ Writes the provided sensor matrix to the relevant PVs Args: sensor_matrix: 3x3 numpy ndarray containing the values to use as the fixed sensor matrix. Returns: None """ assert sensor_matrix.shape == (SENSOR_MATRIX_SIZE, SENSOR_MATRIX_SIZE) for i in range(SENSOR_MATRIX_SIZE): for j in range(SENSOR_MATRIX_SIZE): self.ca.set_pv_value(SENSOR_MATRIX_PVS.format(row=i+1, column=j+1), sensor_matrix[i, j], sleep_after_set=0.0) def apply_offset_to_field(self, simulated_field, offset): """ Applies offset to the simulated measured field Args: simulated_field: dict with 'X', 'Y' and 'Z' keys. Values are the corresponding simulated field values offset: float, The offset to subtract from the input data. Applies same offset to all fields Returns: offset_applied_field: dict with 'X', 'Y', 'Z' keys. Values are offset-subtracted simulated_field values """ offset_applied_field = {} for axis in AXES.keys(): offset_applied_field[axis] = simulated_field[axis] - offset return offset_applied_field def write_simulated_field_values(self, simulated_field): """ Writes the given simulated field values to the IOC. Also asserts that the value has been taken up by the '_RAW' PV. We need to do this because the '_RAW' PVs are on SCAN = .1 second in RECSIM, so some time is taken between writing the SIM field and it being available in the '_RAW' PV. Args: simulated_field: dict with 'X', 'Y' and 'Z' keys. Values are the corresponding simulated field values Returns: None """ for component in AXES.keys(): self.ca.set_pv_value("SIM:DAQ:{}".format(component), simulated_field[component], sleep_after_set=0.0) self.ca.assert_that_pv_is_number("DAQ:{}:_RAW".format(component), simulated_field[component]) def apply_offset_and_matrix_multiplication(self, simulated_field, offset, sensor_matrix): """ Applies trasformation between raw or 'measured' field to 'corrected' field. Subtracts the offset from the input (raw) data, then matrix multiplies by the sensor matrix. Args: simulated_field: dict with keys matching AXES (X, Y and Z). Values are the simulated field values offset: float, The Offset to subtract from the input data. Applies same offset to all fields sensor_matrix: 3x3 numpy ndarray containing the values to use as the fixed sensor matrix. Returns: corrected_field_vals: 3-element array containing corrected X, Y and Z field values """ offset_input_field = self.apply_offset_to_field(simulated_field, offset) corrected_field_vals = np.matmul(sensor_matrix, np.array([offset_input_field["X"], offset_input_field["Y"], offset_input_field["Z"]])) return corrected_field_vals def get_overload_range_value(self): """ Returns the maximum value an input field can have before the magnetometer is overloaded """ return self.ca.get_pv_value("RANGE") * 4.5 def write_simulated_alarm_level(self, level): """ Writes to the SIML field of the RAW data pvs. This sets the severity level of the three pvs to level. Waits for the SEVR fields of the RAW data pvs to update before returning. Args: level: Class attribute of ChannelAccess.Alarms (e.g. ca.Alarms.NONE). The severity level to set to the PV """ for axis in AXES.keys(): self.ca.set_pv_value("DAQ:{}:_RAW.SIMS".format(axis), level, sleep_after_set=0.0) # Wait for the raw PVs to process for axis in AXES.keys(): self.ca.assert_that_pv_alarm_is("DAQ:{}:_RAW".format(axis), level) @parameterized.expand(parameterized_list(itertools.product(AXES.keys(), FIELD_STRENGTHS))) def test_GIVEN_field_offset_THEN_field_strength_read_back_with_offset_applied(self, _, hw_axis, field_strength): # GIVEN self.write_offset(OFFSET) field = {"X": 0, "Y": 0, "Z": 0} field[hw_axis] = field_strength self.write_simulated_field_values(field) self.ca.set_pv_value("SIM:DAQ:{}".format(hw_axis), field_strength, sleep_after_set=0.0) # WHEN self.ca.process_pv("TAKEDATA") # THEN self.ca.assert_that_pv_is_number("APPLYOFFSET:{}".format(hw_axis), field_strength-OFFSET) def test_GIVEN_offset_corrected_field_WHEN_sensor_matrix_is_identity_THEN_input_field_returned_by_matrix_multiplier(self): offset_corrected_field = {"X": 1.1, "Y": 2.2, "Z": 3.3} # GIVEN self.write_simulated_field_values(offset_corrected_field) # WHEN self.write_sensor_matrix(np.identity(3)) self.ca.process_pv("TAKEDATA") # THEN for hw_axis in AXES.keys(): expected_value = offset_corrected_field[hw_axis] self.ca.assert_that_pv_is_number("CORRECTEDFIELD:{}".format(hw_axis), expected_value, tolerance=0.1*abs(expected_value)) self.ca.assert_that_pv_alarm_is("CORRECTEDFIELD:{}".format(hw_axis), self.ca.Alarms.NONE) @parameterized.expand(parameterized_list(['X', 'Y', 'Z'])) def test_GIVEN_sensor_matrix_with_only_one_nonzero_row_THEN_corrected_field_has_component_in_correct_dimension(self, _, hw_axis): input_field = {"X": 1.1, "Y": 2.2, "Z": 3.3} self.write_simulated_field_values(input_field) # GIVEN sensor_matrix = np.zeros((3, 3)) # Set one row to one if hw_axis == "X": sensor_matrix[0, :] = 1 elif hw_axis == "Y": sensor_matrix[1, :] = 1 elif hw_axis == "Z": sensor_matrix[2, :] = 1 # WHEN self.write_sensor_matrix(sensor_matrix) self.ca.process_pv("TAKEDATA") # THEN for component in AXES.keys(): if component == hw_axis: expected_value = sum(input_field.values()) else: expected_value = 0 self.ca.assert_that_pv_is_number("CORRECTEDFIELD:{}".format(component), expected_value) def test_GIVEN_test_input_field_strengths_WHEN_corrections_applied_THEN_corrected_fields_agree_with_labview(self): # GIVEN input_field = {"X": 11.1, "Y": 22.2, "Z": 33.3} input_offsets = {"X": -8.19e-1, "Y": 3.45e-1, "Z": -6.7e-1} sensor_matrix = np.array([-1.17e-1, 7.36e-2, -2e-1, -3.41e-1, -2.15e-1, -3e-1, -2.3e-1, -4e-2, 1e-1]).reshape(3, 3) self.write_simulated_field_values(input_field) self.write_sensor_matrix(sensor_matrix) for axis in input_offsets.keys(): self.ca.set_pv_value("OFFSET:{}".format(axis), input_offsets[axis], sleep_after_set=0.0) # WHEN self.ca.process_pv("TAKEDATA") # THEN labview_result = {"X": -6.58, "Y": -18.9542, "Z": -0.21857} for component in AXES.keys(): self.ca.assert_that_pv_is_number("CORRECTEDFIELD:{}".format(component), labview_result[component], tolerance=1e-4) def test_GIVEN_measured_data_WHEN_corrections_applied_THEN_field_magnitude_read_back(self): # GIVEN input_field = {"X": 2.2, "Y": 3.3, "Z": 4.4} sensor_matrix = np.array([-1.17e-1, 7.36e-2, -2e-1, -3.41e-1, -2.15e-1, -3e-1, -2.3e-1, -4e-2, 1e-1]).reshape(3, 3) self.write_simulated_field_values(input_field) self.write_offset(OFFSET) self.write_sensor_matrix(sensor_matrix) # WHEN self.ca.process_pv("TAKEDATA") # THEN expected_field_vals = self.apply_offset_and_matrix_multiplication(input_field, OFFSET, sensor_matrix) expected_magnitude = np.linalg.norm(expected_field_vals) self.ca.assert_that_pv_is_number("FIELDSTRENGTH", expected_magnitude, tolerance=0.1*expected_magnitude, timeout=30) def test_WHEN_takedata_alias_processed_THEN_all_magnetometer_axes_read_and_processed(self): # GIVEN test_field = {"X": 1.1, "Y": 2.2, "Z": 3.3} self.write_simulated_field_values(test_field) for component in AXES.keys(): self.ca.assert_that_pv_is_not_number("DAQ:{}".format(component), test_field[component]) # WHEN self.ca.process_pv("TAKEDATA") # THEN for component in AXES.keys(): self.ca.assert_that_pv_is_number("DAQ:{}".format(component), test_field[component], tolerance=0.1*test_field[component]) @parameterized.expand(parameterized_list(FIELD_STRENGTHS)) def test_GIVEN_magnetometer_scaling_factor_WHEN_data_read_THEN_inputs_scaled_by_factor(self, _, factor): # GIVEN self.ca.set_pv_value("RANGE", factor, sleep_after_set=0.0) test_field = {"X": 1.1, "Y": 2.2, "Z": 3.3} self.write_simulated_field_values(test_field) self.ca.process_pv("TAKEDATA") # THEN for component in AXES.keys(): self.ca.assert_that_pv_is_number("MEASURED:{}".format(component), test_field[component]*factor) @parameterized.expand(parameterized_list(AXES.keys())) def test_GIVEN_measured_field_too_high_THEN_overload_pv_reads_true_and_is_in_alarm(self, _, axis): # GIVEN test_field = { "X": 1.1, "Y": 1.1, "Z": 1.1 } test_field[axis] = self.ca.get_pv_value("RANGE") * 4.5 + 1.0 # WHEN self.write_simulated_field_values(test_field) self.ca.process_pv("TAKEDATA") # THEN self.ca.assert_that_pv_is("OVERLOAD", "OVERLOADED") self.ca.assert_that_pv_alarm_is("OVERLOAD", self.ca.Alarms.MAJOR) def test_GIVEN_measured_field_in_range_THEN_overload_pv_reads_false_and_not_in_alarm(self): # GIVEN test_value = self.get_overload_range_value() - 1.0 test_field = { "X": test_value, "Y": test_value, "Z": test_value } # WHEN self.write_simulated_field_values(test_field) self.ca.process_pv("TAKEDATA") # THEN self.ca.assert_that_pv_is("OVERLOAD", "NORMAL") self.ca.assert_that_pv_alarm_is("OVERLOAD", self.ca.Alarms.NONE) def test_GIVEN_field_overloaded_THEN_output_PVs_in_major_alarm(self): # GIVEN overload_value = self.get_overload_range_value() + 1.0 test_field = { "X": overload_value, "Y": overload_value, "Z": overload_value } self.write_simulated_field_values(test_field) self.ca.process_pv("TAKEDATA") # THEN self.ca.assert_that_pv_alarm_is("FIELDSTRENGTH", self.ca.Alarms.MAJOR) for axis in AXES.keys(): self.ca.assert_that_pv_alarm_is("CORRECTEDFIELD:{}".format(axis), self.ca.Alarms.MAJOR) @parameterized.expand(parameterized_list(itertools.product([ChannelAccess.Alarms.INVALID, ChannelAccess.Alarms.MAJOR, ChannelAccess.Alarms.MAJOR], PVS_WHICH_USE_DAQ_DATA))) def test_GIVEN_raw_daq_pvs_in_alarm_WHEN_PVs_processed_THEN_alarm_copied_to_downstream_pvs(self, _, alarm, pv): # GIVEN self.ca.assert_that_pv_alarm_is("{}.SEVR".format(pv), self.ca.Alarms.NONE) self.write_simulated_alarm_level(alarm) self.ca.process_pv("TAKEDATA") # THEN self.ca.assert_that_pv_alarm_is("{}.SEVR".format(pv), alarm) @parameterized.expand(parameterized_list(AXES.keys())) def test_GIVEN_smoothing_samples_WHEN_setting_field_THEN_average_field_is_given(self, _, axis): number_samples = 10 field_number = 100 pv = "DAQ:{}".format(axis) with self._ioc.start_with_macros({"NUM_SAMPLES": number_samples}, pv_to_wait_for=pv): field = {"X": 0, "Y": 0, "Z": 0} self.write_simulated_field_values(field) for i in range(1, number_samples+1): self.ca.process_pv("TAKEDATA") self.ca.process_pv("TAKEDATA") # make sure the field is 0 self.ca.assert_that_pv_is_number(pv, 0) # Change the field number field[axis] = field_number self.write_simulated_field_values(field) # every sample check the average has been processed correctly for i in range(1, number_samples+1): self.ca.process_pv("TAKEDATA") # assert that after every TAKEDATA the average has gone up by the field_number divided by the sample # number self.ca.assert_that_pv_is_number(pv, (field_number // number_samples) * i) # Check the final value stays the same self.ca.process_pv("TAKEDATA") self.ca.assert_that_pv_is_number(pv, field_number)
class InstronStressRigTests(unittest.TestCase): """ Tests for the Instron IOC. """ def _change_channel(self, name): # Setpoint is zero-indexed self.ca.set_pv_value("CHANNEL:SP", CHANNELS[name] - 1) self.ca.assert_that_pv_is("CHANNEL.RVAL", CHANNELS[name]) self.ca.assert_that_pv_alarm_is("CHANNEL", self.ca.Alarms.NONE) self.ca.assert_that_pv_is("CHANNEL:GUI", name) self.ca.assert_that_pv_alarm_is("CHANNEL:GUI", self.ca.Alarms.NONE) def setUp(self): self._lewis, self._ioc = get_running_lewis_and_ioc( "instron_stress_rig", DEVICE_PREFIX) self.ca = ChannelAccess(15, device_prefix=DEVICE_PREFIX) self.ca.assert_that_pv_exists("CHANNEL", timeout=30) # Can't use lewis backdoor commands in recsim # All of the below commands apply to devsim only. if not IOCRegister.uses_rec_sim: # Reinitialize the emulator state self._lewis.backdoor_command(["device", "reset"]) self._lewis.backdoor_set_on_device("status", 7680) for index, chan_type2 in enumerate((3, 2, 4)): self._lewis.backdoor_command([ "device", "set_channel_param", str(index + 1), "channel_type", str(chan_type2) ]) self.ca.assert_that_pv_is("CHANNEL:SP.ZRST", "Position") self._change_channel("Position") # Ensure the rig is stopped self._lewis.backdoor_command(["device", "movement_type", "0"]) self.ca.assert_that_pv_is("GOING", "NO") # Ensure stress area and strain length are sensible values (i.e. not zero) self._lewis.backdoor_command( ["device", "set_channel_param", "2", "area", "10"]) self._lewis.backdoor_command( ["device", "set_channel_param", "3", "length", "10"]) self.ca.assert_that_pv_is_number("STRESS:AREA", 10, tolerance=0.001) self.ca.assert_that_pv_is_number("STRAIN:LENGTH", 10, tolerance=0.001) # Ensure that all the scales are sensible values (i.e. not zero) for index, channel in enumerate(POS_STRESS_STRAIN, 1): self._lewis.backdoor_command( ["device", "set_channel_param", str(index), "scale", "10"]) self.ca.assert_that_pv_is_number(channel + ":SCALE", 10, tolerance=0.001) # Always set the waveform generator to run for lots of cycles so it only stops if we want it to self.ca.set_pv_value(quart_prefixed("CYCLE:SP"), LOTS_OF_CYCLES) @skip_if_recsim("In rec sim we can not set the code easily") def test_WHEN_the_rig_has_no_error_THEN_the_status_is_ok(self): self._lewis.backdoor_set_on_device("status", 7680) self.ca.assert_that_pv_is("STAT:DISP", "System OK") self.ca.assert_that_pv_alarm_is("STAT:DISP", ChannelAccess.Alarms.NONE) @skip_if_recsim("In rec sim we can not set the code easily") def test_WHEN_the_rig_has_other_no_error_THEN_the_status_is_ok(self): self._lewis.backdoor_set_on_device("status", 0) self.ca.assert_that_pv_is("STAT:DISP", "System OK") self.ca.assert_that_pv_alarm_is("STAT:DISP", ChannelAccess.Alarms.NONE) @skip_if_recsim("In rec sim we can not set the code easily") def test_WHEN_the_rig_has_error_THEN_the_status_is_emergency_stop_pushed( self): code_and_errors = [([0, 1, 1, 0], "Emergency stop pushed"), ([0, 0, 0, 1], "Travel limit exceeded"), ([0, 0, 1, 0], "Power amplifier too hot"), ([0, 1, 1, 0], "Emergency stop pushed"), ([0, 1, 0, 1], "Invalid status from rig"), ([0, 1, 0, 0], "Invalid status from rig"), ([0, 1, 1, 0], "Emergency stop pushed"), ([0, 1, 1, 1], "Oil too hot"), ([1, 0, 0, 0], "Oil level too low"), ([1, 0, 0, 1], "Motor too hot"), ([1, 0, 1, 0], "Oil pressure too high"), ([1, 0, 1, 1], "Oil pressure too low"), ([1, 1, 0, 0], "Manifold/pump blocked"), ([1, 1, 0, 1], "Oil level going too low"), ([1, 1, 1, 0], "Manifold low pressure")] for code, error in code_and_errors: code_val = code[0] * 2**12 code_val += code[1] * 2**11 code_val += code[2] * 2**10 code_val += code[3] * 2**9 self._lewis.backdoor_set_on_device("status", code_val) self.ca.assert_that_pv_is("STAT:DISP", error, msg="code set {0} = {code_val}".format( code, code_val=code_val)) self.ca.assert_that_pv_alarm_is("STAT:DISP", ChannelAccess.Alarms.MAJOR) def test_WHEN_the_rig_is_initialized_THEN_it_is_not_going(self): self.ca.assert_that_pv_is("GOING", "NO") def test_WHEN_the_rig_is_initialized_THEN_it_is_not_panic_stopping(self): self.ca.assert_that_pv_is("PANIC:SP", "READY") def test_WHEN_the_rig_is_initialized_THEN_it_is_not_stopping(self): self.ca.assert_that_pv_is("STOP:SP", "READY") def test_that_the_rig_is_not_normally_in_control_mode(self): self.ca.assert_that_pv_is("STOP:SP", "READY") def test_WHEN_init_sequence_run_THEN_waveform_ramp_is_set_the_status_is_ok( self): for chan in POS_STRESS_STRAIN: self.ca.set_pv_value("{0}:RAMP:WFTYP:SP".format(chan), 0) self.ca.set_pv_value("INIT", 1) for chan in POS_STRESS_STRAIN: self.ca.assert_that_pv_is("{0}:RAMP:WFTYP".format(chan), RAMP_WAVEFORM_TYPES[3]) def _switch_to_position_channel_and_change_setpoint(self): # It has to be big or the set point will be reached before the test completes _big_set_point = 999999999999 # Select position as control channel self._change_channel("Position") # Change the setpoint so that movement can be started self.ca.set_pv_value("POS:SP", _big_set_point) self.ca.assert_that_pv_is_number("POS:SP", _big_set_point, tolerance=1) self.ca.assert_that_pv_is_number("POS:SP:RBV", _big_set_point, tolerance=1) @skip_if_recsim("Dynamic behaviour not captured in RECSIM") def test_WHEN_going_and_then_stopping_THEN_going_pv_reflects_the_expected_state( self): self.ca.assert_that_pv_is("GOING", "NO") self._switch_to_position_channel_and_change_setpoint() self.ca.set_pv_value("MOVE:GO:SP", 1) self.ca.assert_that_pv_is("GOING", "YES") self.ca.set_pv_value("STOP:SP", 1) self.ca.assert_that_pv_is("GOING", "NO") self.ca.set_pv_value("STOP:SP", 0) @skip_if_recsim("Dynamic behaviour not captured in RECSIM") def test_WHEN_going_and_then_panic_stopping_THEN_going_pv_reflects_the_expected_state( self): self.ca.assert_that_pv_is("GOING", "NO") self._switch_to_position_channel_and_change_setpoint() self.ca.set_pv_value("MOVE:GO:SP", 1) self.ca.assert_that_pv_is("GOING", "YES") self.ca.set_pv_value("PANIC:SP", 1) self.ca.assert_that_pv_is("GOING", "NO") self.ca.set_pv_value("PANIC:SP", 0) @skip_if_recsim("In rec sim this test fails") def test_WHEN_arbitrary_command_Q22_is_sent_THEN_the_response_is_a_status_code( self): self.ca.set_pv_value("ARBITRARY:SP", "Q22") # Assert that the response to Q22 is a status code self.ca.assert_that_pv_is_within_range("ARBITRARY", min_value=0, max_value=65535) @skip_if_recsim("In rec sim this test fails") def test_WHEN_arbitrary_command_Q300_is_sent_THEN_the_response_is_a_number_between_1_and_3( self): self.ca.set_pv_value("ARBITRARY:SP", "Q300") # Assert that the response to Q300 is between 1 and 3 self.ca.assert_that_pv_is_within_range("ARBITRARY", min_value=1, max_value=3) @skip_if_recsim("In rec sim this test fails") def test_WHEN_arbitrary_command_C4_is_sent_THEN_Q4_gives_back_the_value_that_was_just_set( self): def _set_and_check(value): # Put the record into a non-alarm state. This is needed so that we can wait until the record is in alarm # later, when we do a command which (expectedly) puts the record into a timeout alarm. self.ca.set_pv_value("ARBITRARY:SP", "Q4,1") self.ca.assert_that_pv_alarm_is("ARBITRARY", self.ca.Alarms.NONE) self.ca.set_pv_value("ARBITRARY:SP", "C4,1," + str(value)) self.ca.assert_that_pv_is("ARBITRARY:SP", "C4,1," + str(value)) # No response from arbitrary command causes record to be TIMEOUT INVALID - this is expected. self.ca.assert_that_pv_alarm_is("ARBITRARY", self.ca.Alarms.INVALID) self.ca.set_pv_value("ARBITRARY:SP", "Q4,1") self.ca.assert_that_pv_is_number("ARBITRARY", value, tolerance=0.001, timeout=60) for v in [0, 1, 0]: _set_and_check(v) def test_WHEN_control_channel_is_requested_THEN_an_allowed_value_is_returned( self): self.ca.assert_that_pv_is_one_of("CHANNEL", CHANNELS.keys()) @skip_if_recsim("In rec sim this test fails") def test_WHEN_control_channel_setpoint_is_requested_THEN_it_is_one_of_the_allowed_values( self): self.ca.assert_that_pv_is_one_of("CHANNEL:SP", CHANNELS.keys()) @skip_if_recsim("In rec sim this test fails") def test_WHEN_the_control_channel_is_set_THEN_the_readback_contains_the_value_that_was_just_set( self): for channel in CHANNELS.keys(): # change channel function contains the relevant assertions. self._change_channel(channel) def test_WHEN_the_step_time_for_various_channels_is_set_as_an_integer_THEN_the_readback_contains_the_value_that_was_just_set( self): for chan, val in [("POS", 123), ("STRESS", 456), ("STRAIN", 789)]: pv_name = chan + ":STEP:TIME" self.ca.assert_setting_setpoint_sets_readback(val, pv_name) def test_WHEN_the_step_time_for_various_channels_is_set_as_a_float_THEN_the_readback_contains_the_value_that_was_just_set( self): for chan, val in [("POS", 111.111), ("STRESS", 222.222), ("STRAIN", 333.333)]: pv_name = chan + ":STEP:TIME" self.ca.assert_setting_setpoint_sets_readback(val, pv_name) def test_WHEN_the_ramp_waveform_for_a_channel_is_set_THEN_the_readback_contains_the_value_that_was_just_set( self): pv_name = "{0}:RAMP:WFTYP" for chan in POS_STRESS_STRAIN: for set_value, return_value in enumerate(RAMP_WAVEFORM_TYPES): self.ca.assert_setting_setpoint_sets_readback( set_value, pv_name.format(chan), expected_value=return_value) def test_WHEN_the_ramp_amplitude_for_a_channel_is_set_as_an_integer_THEN_the_readback_contains_the_value_that_was_just_set( self): for chan in POS_STRESS_STRAIN: for val in [0, 10, 1000, 1000000]: pv_name = chan + ":RAW:SP" pv_name_rbv = pv_name + ":RBV" self.ca.assert_setting_setpoint_sets_readback( val, readback_pv=pv_name_rbv, set_point_pv=pv_name) def test_WHEN_the_ramp_amplitude_for_a_channel_is_set_as_a_float_THEN_the_readback_contains_the_value_that_was_just_set( self): for chan in POS_STRESS_STRAIN: for val in [1.0, 5.5, 1.000001, 9.999999, 10000.1]: pv_name = chan + ":RAW:SP" pv_name_rbv = pv_name + ":RBV" self.ca.assert_setting_setpoint_sets_readback( val, readback_pv=pv_name_rbv, set_point_pv=pv_name) @skip_if_recsim("In rec sim this test fails") def test_WHEN_the_setpoint_for_a_channel_is_set_THEN_the_readback_contains_the_value_that_was_just_set( self): def _set_and_check(chan, value): self.ca.set_pv_value(chan + ":SP", value) self.ca.assert_that_pv_is_number(chan + ":SP", value, tolerance=0.001) self.ca.assert_that_pv_is_number(chan + ":SP:RBV", value, tolerance=0.05, timeout=30) for chan in POS_STRESS_STRAIN: for i in [1.0, 123.456, 555.555, 1000]: _set_and_check(chan, i) def test_WHEN_channel_tolerance_is_set_THEN_it_changes_limits_on_SP_RBV( self): for chan in POS_STRESS_STRAIN: sp_val = 1 self.ca.set_pv_value(chan + ":SP", sp_val) for val in [0.1, 1.0, 2.5]: pv_name = chan + ":TOLERANCE" pv_name_high = chan + ":SP:RBV.HIGH" pv_name_low = chan + ":SP:RBV.LOW" self.ca.assert_setting_setpoint_sets_readback( val, readback_pv=pv_name_high, set_point_pv=pv_name, expected_value=val + sp_val, expected_alarm=None) self.ca.assert_setting_setpoint_sets_readback( val, readback_pv=pv_name_low, set_point_pv=pv_name, expected_value=sp_val - val, expected_alarm=None) @skip_if_recsim("Alarms not properly emulated in recsim") def test_GIVEN_a_big_tolerance_WHEN_the_setpoint_is_set_THEN_the_setpoint_has_no_alarms( self): def _set_and_check(chan, value): self.ca.set_pv_value(chan + ":SP", value) self.ca.set_pv_value(chan + ":TOLERANCE", 9999) self.ca.assert_that_pv_alarm_is(chan + ":SP:RBV", ChannelAccess.Alarms.NONE) for chan in POS_STRESS_STRAIN: for i in [0.123, 567]: _set_and_check(chan, i) @skip_if_recsim("Alarms not properly emulated in recsim") def test_GIVEN_a_tolerance_of_minus_one_WHEN_the_setpoint_is_set_THEN_the_setpoint_readback_has_alarms( self): def _set_and_check(chan, value): self.ca.set_pv_value(chan + ":SP", value) self.ca.set_pv_value(chan + ":TOLERANCE", -1) self.ca.assert_that_pv_alarm_is(chan + ":SP:RBV", ChannelAccess.Alarms.MINOR) for chan in POS_STRESS_STRAIN: for i in [0.234, 789]: _set_and_check(chan, i) @skip_if_recsim("In rec sim this test fails") def test_WHEN_ioc_gets_a_raw_position_reading_from_the_device_THEN_it_is_converted_correctly( self): for chan_scale in [0.1, 10.0]: self._lewis.backdoor_command( ["device", "set_channel_param", "1", "scale", str(chan_scale)]) self.ca.assert_that_pv_is("POS:SCALE", chan_scale) for raw_value in [0, 123]: self._lewis.backdoor_command([ "device", "set_channel_param", "1", "value", str(raw_value) ]) self.ca.assert_that_pv_is_number("POS:RAW", raw_value, tolerance=0.01) self.ca.assert_that_pv_is_number("POS", raw_value * chan_scale * 1000, tolerance=(0.01 * chan_scale * 1000)) @skip_if_recsim("In rec sim this test fails") def test_WHEN_ioc_gets_a_raw_stress_reading_from_the_device_THEN_it_is_converted_correctly( self): for chan_area in [0.1, 10.0]: self._lewis.backdoor_command( ["device", "set_channel_param", "2", "area", str(chan_area)]) self.ca.assert_that_pv_is("STRESS:AREA", chan_area) for chan_scale in [0.1, 10.0]: self._lewis.backdoor_command([ "device", "set_channel_param", "2", "scale", str(chan_scale) ]) self.ca.assert_that_pv_is("STRESS:SCALE", chan_scale) for raw_value in [0, 123]: self._lewis.backdoor_command([ "device", "set_channel_param", "2", "value", str(raw_value) ]) self.ca.assert_that_pv_is("STRESS:RAW", raw_value) self.ca.assert_that_pv_is( "STRESS", raw_value * chan_scale * (1.0 / chan_area)) @skip_if_recsim("In rec sim this test fails") def test_WHEN_strain_length_updates_on_device_THEN_pv_updates(self): for value in [1, 123]: self._lewis.backdoor_command( ["device", "set_channel_param", "3", "length", str(value)]) self.ca.assert_that_pv_is("STRAIN:LENGTH", value) @skip_if_recsim("In rec sim this test fails") def test_WHEN_ioc_gets_a_raw_strain_reading_from_the_device_THEN_it_is_converted_correctly( self): for chan_scale in [0.1, 10.0]: self._lewis.backdoor_command( ["device", "set_channel_param", "3", "scale", str(chan_scale)]) for chan_length in [0.1, 10.0]: self._lewis.backdoor_command([ "device", "set_channel_param", "3", "length", str(chan_length) ]) for raw_value in [0, 0.001]: self._lewis.backdoor_command([ "device", "set_channel_param", "3", "value", str(raw_value) ]) self.ca.assert_that_pv_is("STRAIN:SCALE", chan_scale) self.ca.assert_that_pv_is("STRAIN:LENGTH", chan_length) self.ca.assert_that_pv_is("STRAIN:RAW", raw_value) self.ca.assert_that_pv_is( "STRAIN", (raw_value * chan_scale * 100000 * (1 / chan_length))) def test_WHEN_the_area_setpoint_is_set_THEN_the_area_readback_updates( self): def _set_and_check(value): self.ca.set_pv_value("STRESS:AREA:SP", value) self.ca.assert_that_pv_is_number("STRESS:AREA", value, tolerance=0.01) self.ca.assert_that_pv_alarm_is("STRESS:AREA", ChannelAccess.Alarms.NONE) for val in [0.234, 789]: _set_and_check(val) def test_WHEN_the_area_setpoint_is_set_THEN_the_diameter_readback_updates( self): def _set_and_check(value): self.ca.set_pv_value("STRESS:AREA:SP", value) self.ca.assert_that_pv_is_number("STRESS:DIAMETER", (2 * math.sqrt(value / math.pi)), tolerance=0.01) self.ca.assert_that_pv_alarm_is("STRESS:DIAMETER", ChannelAccess.Alarms.NONE) for val in [0.234, 789]: _set_and_check(val) def test_WHEN_the_diameter_setpoint_is_set_THEN_the_diameter_readback_updates( self): def _set_and_check(value): self.ca.set_pv_value("STRESS:DIAMETER:SP", value) self.ca.assert_that_pv_is_number("STRESS:DIAMETER", value, tolerance=0.0005) self.ca.assert_that_pv_alarm_is("STRESS:DIAMETER", ChannelAccess.Alarms.NONE) for val in [0.234, 789]: _set_and_check(val) def test_WHEN_the_diameter_setpoint_is_set_THEN_the_area_readback_updates( self): def _set_and_check(value): self.ca.set_pv_value("STRESS:DIAMETER:SP", value) self.ca.assert_that_pv_is_number("STRESS:AREA", ((value / 2.0)**2 * math.pi), tolerance=0.0005) self.ca.assert_that_pv_alarm_is("STRESS:AREA", ChannelAccess.Alarms.NONE) for val in [0.234, 789]: _set_and_check(val) @skip_if_recsim("In rec sim this test fails") def test_WHEN_a_position_setpoint_is_set_THEN_it_is_converted_correctly( self): for scale in [2.34, 456.78]: self._lewis.backdoor_command( ["device", "set_channel_param", "1", "scale", str(scale)]) self.ca.assert_that_pv_is("POS:SCALE", scale) for val in [1.23, 123.45]: self.ca.set_pv_value("POS:SP", val) self.ca.assert_that_pv_is_number("POS:RAW:SP", val * (1.0 / 1000.0) * (1 / scale), tolerance=0.0000000001) self.ca.assert_that_pv_alarm_is("POS:RAW:SP", ChannelAccess.Alarms.NONE) @skip_if_recsim("In rec sim this test fails") def test_WHEN_a_stress_setpoint_is_set_THEN_it_is_converted_correctly( self): for area in [789, 543.21]: self._lewis.backdoor_command( ["device", "set_channel_param", "2", "area", str(area)]) self.ca.assert_that_pv_is("STRESS:AREA", area) for chan_scale in [2.34, 456.78]: self._lewis.backdoor_command([ "device", "set_channel_param", "2", "scale", str(chan_scale) ]) self.ca.assert_that_pv_is("STRESS:SCALE", chan_scale) for val in [1.23, 123.45]: self.ca.set_pv_value("STRESS:SP", val) self.ca.assert_that_pv_is_number("STRESS:RAW:SP", val * (1 / chan_scale) * area, tolerance=0.0000000001) self.ca.assert_that_pv_alarm_is("STRESS:RAW:SP", ChannelAccess.Alarms.NONE) @skip_if_recsim("In rec sim this test fails") def test_WHEN_a_strain_setpoint_is_set_THEN_it_is_converted_correctly( self): for length in [789, 543.21]: self._lewis.backdoor_command( ["device", "set_channel_param", "3", "length", str(length)]) self.ca.assert_that_pv_is("STRAIN:LENGTH", length) for chan_scale in [2.34, 456.78]: self._lewis.backdoor_command([ "device", "set_channel_param", "3", "scale", str(chan_scale) ]) self.ca.assert_that_pv_is("STRAIN:SCALE", chan_scale) for val in [1.23, 123.45]: self.ca.set_pv_value("STRAIN:SP", val) self.ca.assert_that_pv_is_number("STRAIN:RAW:SP", val * (1 / chan_scale) * length * (1.0 / 100000.0), tolerance=0.0000000001) self.ca.assert_that_pv_alarm_is("STRAIN:RAW:SP", ChannelAccess.Alarms.NONE) @skip_if_recsim("In rec sim this test fails") def test_WHEN_the_channel_type_updates_on_the_device_THEN_the_pv_updates( self): for chan_name, chan_num in [("POS", 1), ("STRESS", 2), ("STRAIN", 3)]: for value_1, value_2, return_value_1, return_value_2 in [ (0, 1, "Standard transducer", "Unrecognized"), (1, 10, "User transducer", "Ext. waveform generator") ]: self._lewis.backdoor_command([ "device", "set_channel_param", str(chan_num), "transducer_type", str(value_1) ]) self._lewis.backdoor_command([ "device", "set_channel_param", str(chan_num), "channel_type", str(value_2) ]) self.ca.assert_that_pv_is("" + chan_name + ":TYPE:STANDARD", return_value_1) self.ca.assert_that_pv_is("" + chan_name + ":TYPE", return_value_2) def test_WHEN_waveform_type_abs_set_on_axes_THEN_all_axes_are_set(self): def _set_and_check(set_value, return_value): self.ca.set_pv_value("AXES:RAMP:WFTYP:SP", set_value) for chan in POS_STRESS_STRAIN: self.ca.assert_that_pv_is("{0}:RAMP:WFTYP".format(chan), return_value) self.ca.assert_that_pv_alarm_is("{0}:RAMP:WFTYP".format(chan), ChannelAccess.Alarms.NONE) for set_value, return_value in enumerate(RAMP_WAVEFORM_TYPES): _set_and_check(set_value, return_value) @skip_if_recsim("In rec sim this test fails") def test_WHEN_channel_fails_check_THEN_channel_mbbi_record_is_invalid_and_has_tag_disabled( self): for index, (chan_name, type, index_as_name, channel_as_name) in enumerate( zip(POS_STRESS_STRAIN, (1, 1, 1), ("ZR", "ON", "TW"), ("Position", "Stress", "Strain"))): self._lewis.backdoor_command([ "device", "set_channel_param", str(index + 1), "channel_type", str(type) ]) self.ca.assert_that_pv_is("" + chan_name + ":TYPE:CHECK", "FAIL") self.ca.assert_that_pv_is("CHANNEL:SP.{}ST".format(index_as_name), "{0} - disabled".format(channel_as_name)) self.ca.set_pv_value("CHANNEL:SP", index) self.ca.assert_that_pv_alarm_is("CHANNEL:SP", ChannelAccess.Alarms.INVALID) @skip_if_recsim("In rec sim this test fails") def test_WHEN_channel_succeeds_check_THEN_channel_mbbi_record_is_invalid_and_has_tag_disabled( self): self.ca.set_pv_value("CHANNEL:SP", 3) for index, (chan_name, type, index_as_name, channel_as_name) in enumerate( zip(POS_STRESS_STRAIN, (3, 2, 4), ("ZR", "ON", "TW"), ("Position", "Stress", "Strain"))): self._lewis.backdoor_command([ "device", "set_channel_param", str(index + 1), "channel_type", str(type) ]) self.ca.assert_that_pv_is("" + chan_name + ":TYPE:CHECK", "PASS", timeout=30) self.ca.assert_that_pv_is("CHANNEL:SP.{}ST".format(index_as_name), channel_as_name) self.ca.assert_that_pv_is("CHANNEL:SP.{}SV".format(index_as_name), ChannelAccess.Alarms.NONE) self.ca.set_pv_value("CHANNEL:SP", index) self.ca.assert_that_pv_alarm_is("CHANNEL:SP", ChannelAccess.Alarms.NONE) @skip_if_recsim("In rec sim we can not disconnect the device from the IOC") def test_WHEN_the_rig_is_not_connected_THEN_the_status_has_alarm(self): self._lewis.backdoor_set_on_device("status", None) self.ca.assert_that_pv_alarm_is("STAT:DISP", ChannelAccess.Alarms.INVALID) # Waveform tests def check_running_state(self, status, running, continuing, timeout=None): self.ca.assert_that_pv_is(wave_prefixed("STATUS"), status, timeout) self.ca.assert_that_pv_is(wave_prefixed("RUNNING"), "Running" if running else "Not running", timeout) self.ca.assert_that_pv_is( wave_prefixed("CONTINUING"), "Continuing" if continuing else "Not continuing", timeout) @skip_if_recsim("No backdoor in LewisNone") def test_GIVEN_the_waveform_generator_is_stopped_WHEN_it_is_started_THEN_it_is_running( self): self.check_running_state(status="Stopped", running=False, continuing=False) self.ca.set_pv_value(wave_prefixed("START"), 1) self.check_running_state(status="Running", running=True, continuing=True) @skip_if_recsim("No backdoor in LewisNone") def test_GIVEN_the_waveform_generator_is_stopped_WHEN_it_is_aborted_THEN_it_is_stopped( self): self.check_running_state(status="Stopped", running=False, continuing=False) self.ca.set_pv_value(wave_prefixed("ABORT"), 1) self.check_running_state(status="Stopped", running=False, continuing=False) @skip_if_recsim("No backdoor in LewisNone") def test_GIVEN_the_waveform_generator_is_stopped_WHEN_it_is_stopped_THEN_it_is_stopped( self): self.check_running_state(status="Stopped", running=False, continuing=False) self.ca.set_pv_value(wave_prefixed("STOP"), 1) self.check_running_state(status="Stopped", running=False, continuing=False) @skip_if_recsim("No backdoor in LewisNone") def test_GIVEN_the_waveform_generator_is_running_WHEN_it_is_started_THEN_it_is_running( self): self._lewis.backdoor_command( ["device", "set_waveform_state", WAVEFORM_RUNNING]) self.check_running_state(status="Running", running=True, continuing=True) self.ca.set_pv_value(wave_prefixed("START"), 1) self.check_running_state(status="Running", running=True, continuing=True) @skip_if_recsim("No backdoor in LewisNone") def test_GIVEN_the_waveform_generator_is_running_WHEN_it_is_aborted_THEN_it_is_aborted( self): self._lewis.backdoor_command( ["device", "set_waveform_state", WAVEFORM_RUNNING]) self.check_running_state(status="Running", running=True, continuing=True) self.ca.set_pv_value(wave_prefixed("ABORT"), 1) self.check_running_state(status="Aborted", running=False, continuing=False) @skip_if_recsim("No backdoor in LewisNone") def test_GIVEN_the_waveform_generator_is_running_WHEN_it_is_stopped_THEN_it_is_finishing_and_then_stops_within_5_seconds( self): self._lewis.backdoor_command( ["device", "set_waveform_state", WAVEFORM_RUNNING]) self.check_running_state(status="Running", running=True, continuing=True) self.ca.set_pv_value(wave_prefixed("STOP"), 1) self.check_running_state(status="Finishing", running=True, continuing=False) self.check_running_state(status="Stopped", running=False, continuing=False, timeout=5) @skip_if_recsim("No backdoor in LewisNone") def test_GIVEN_the_waveform_generator_is_aborted_WHEN_it_is_started_THEN_it_is_running_and_keeps_running( self): self._lewis.backdoor_command( ["device", "set_waveform_state", WAVEFORM_ABORTED]) self.check_running_state(status="Aborted", running=False, continuing=False) self.ca.set_pv_value(wave_prefixed("START"), 1) self.check_running_state(status="Running", running=True, continuing=True) # We need to make sure it can keep running for a few scans. The IOC could feasibly stop the generator shortly # after it is started time.sleep(5) self.check_running_state(status="Running", running=True, continuing=True) @skip_if_recsim("No backdoor in LewisNone") def test_GIVEN_the_waveform_generator_is_aborted_WHEN_it_is_aborted_THEN_it_is_aborted( self): self._lewis.backdoor_command( ["device", "set_waveform_state", WAVEFORM_ABORTED]) self.check_running_state(status="Aborted", running=False, continuing=False) self.ca.set_pv_value(wave_prefixed("ABORT"), 1) self.check_running_state(status="Aborted", running=False, continuing=False) @skip_if_recsim("No backdoor in LewisNone") def test_GIVEN_the_waveform_generator_is_aborted_WHEN_it_is_stopped_THEN_it_is_aborted( self): self._lewis.backdoor_command( ["device", "set_waveform_state", WAVEFORM_ABORTED]) self.check_running_state(status="Aborted", running=False, continuing=False) self.ca.set_pv_value(wave_prefixed("STOP"), 1) self.check_running_state(status="Aborted", running=False, continuing=False) def test_WHEN_waveform_type_is_set_THEN_the_device_reports_it_has_changed( self): for index, wave_type in enumerate([ "Sine", "Triangle", "Square", "Haversine", "Havetriangle", "Haversquare", "Sensor", "Aux", "Sawtooth" ]): self.ca.assert_setting_setpoint_sets_readback( index, wave_prefixed("TYPE"), expected_value=wave_type) @skip_if_recsim("Recsim record does not handle multiple channels ") def test_GIVEN_multiple_channels_WHEN_waveform_frequency_is_set_THEN_the_device_is_updated_to_that_value( self): expected_values = [123.456, 789.012, 345.678] assert len(expected_values) == NUMBER_OF_CHANNELS # Do this as two separate loops so that we can verify that all 3 channel values are stored independently for device_channel in range(NUMBER_OF_CHANNELS): self.ca.set_pv_value("CHANNEL:SP.VAL", device_channel) self.ca.set_pv_value(wave_prefixed("FREQ:SP"), expected_values[device_channel]) for device_channel in range(NUMBER_OF_CHANNELS): self.ca.set_pv_value("CHANNEL:SP.VAL", device_channel) self.ca.assert_that_pv_is(wave_prefixed("FREQ"), expected_values[device_channel]) @unstable_test() @skip_if_recsim("Conversion factors initialized to 0") def test_GIVEN_multiple_channels_WHEN_waveform_amplitude_is_set_THEN_the_device_is_updated_to_that_value_with_channel_conversion_factor_applied( self): input_values = [123.4, 567.8, 91.2] conversion_factors = [ float(self.ca.get_pv_value("POS:SCALE")) * 1000, float(self.ca.get_pv_value("STRESS:SCALE")) / float(self.ca.get_pv_value("STRESS:AREA")), float(self.ca.get_pv_value("STRAIN:SCALE")) * 100000 * float(self.ca.get_pv_value("STRAIN:LENGTH")) ] for i in range(len(conversion_factors)): self.assertNotEqual(0, conversion_factors[i], "Factor {} was zero".format(i)) expected_values = [ input_values[i] / conversion_factors[i] for i in range(NUMBER_OF_CHANNELS) ] assert len(expected_values) == len(conversion_factors) == len( input_values) == NUMBER_OF_CHANNELS # Do this as two separate loops so that we can verify that all 3 channel values are stored independently for device_channel in range(NUMBER_OF_CHANNELS): self.ca.set_pv_value("CHANNEL:SP.VAL", device_channel) self.ca.set_pv_value(wave_prefixed("AMP:SP"), input_values[device_channel]) self.ca.assert_that_pv_is(wave_prefixed("AMP"), expected_values[device_channel]) self.ca.assert_that_pv_is(wave_prefixed("AMP:SP:RBV"), input_values[device_channel]) for device_channel in range(NUMBER_OF_CHANNELS): self.ca.set_pv_value("CHANNEL:SP.VAL", device_channel) self.ca.assert_that_pv_is(wave_prefixed("AMP"), expected_values[device_channel]) self.ca.assert_that_pv_is(wave_prefixed("AMP:SP:RBV"), input_values[device_channel]) @skip_if_recsim("RECSIM does not capture dynamic behaviour") def test_WHEN_the_quarter_counter_is_off_THEN_the_number_of_counts_is_and_remains_zero( self): self.ca.set_pv_value(quart_prefixed("OFF"), 1) self.ca.assert_that_pv_is("QUART", 0) self.ca.assert_that_pv_is("QUART", 0, timeout=5) @skip_if_recsim("Status more complicated than RECSIM can handle") def test_WHEN_the_quarter_counter_is_armed_THEN_the_status_is_armed(self): self.ca.set_pv_value(quart_prefixed("ARM"), 1) self.ca.assert_that_pv_is(quart_prefixed("STATUS"), "Armed") @skip_if_recsim("Counting part of dynamic device behaviour") def test_WHEN_the_waveform_generator_is_started_THEN_the_quarter_counter_starts_counting_and_keeps_increasing( self): self.ca.set_pv_value(wave_prefixed("START"), 1) self.ca.assert_that_pv_value_is_increasing("QUART", 5) @skip_if_recsim("Status more complicated than RECSIM can handle") def test_WHEN_the_quarter_counter_is_armed_THEN_the_number_of_quarts_never_exceeds_the_requested_maximum( self): cycles = 5 self.ca.set_pv_value(quart_prefixed("CYCLE:SP"), cycles) self.ca.assert_that_pv_is(quart_prefixed("SP"), cycles * 4) self.ca.set_pv_value(wave_prefixed("START"), 1) while self.ca.get_pv_value(quart_prefixed("STATUS")) == "Armed": self.assertLessEqual(float(self.ca.get_pv_value("QUART") / 4.0), cycles) self.ca.assert_that_pv_is(quart_prefixed("STATUS"), "Tripped") def test_GIVEN_the_waveform_generator_is_stopped_WHEN_instructed_to_hold_THEN_status_is_stopped( self): self.ca.assert_that_pv_is(wave_prefixed("STATUS"), "Stopped") self.ca.set_pv_value(wave_prefixed("HOLD"), 1) self.ca.assert_that_pv_is(wave_prefixed("STATUS"), "Stopped") @skip_if_recsim("No backdoor in LewisNone") def test_GIVEN_the_waveform_generator_is_running_WHEN_instructed_to_hold_THEN_status_is_holding( self): self._lewis.backdoor_command( ["device", "set_waveform_state", WAVEFORM_RUNNING]) self.ca.assert_that_pv_is(wave_prefixed("STATUS"), "Running") self.ca.set_pv_value(wave_prefixed("HOLD"), 1) self.ca.assert_that_pv_is(wave_prefixed("STATUS"), "Holding") @skip_if_recsim("No backdoor in LewisNone") def test_GIVEN_the_waveform_generator_is_holding_WHEN_instructed_to_hold_THEN_status_is_holding( self): self._lewis.backdoor_command( ["device", "set_waveform_state", WAVEFORM_HOLDING]) self.ca.assert_that_pv_is(wave_prefixed("STATUS"), "Holding") self.ca.set_pv_value(wave_prefixed("HOLD"), 1) self.ca.assert_that_pv_is(wave_prefixed("STATUS"), "Holding") @skip_if_recsim("No backdoor in LewisNone") def test_GIVEN_the_waveform_generator_is_finishing_WHEN_instructed_to_hold_THEN_status_is_finishing( self): self._lewis.backdoor_command( ["device", "set_waveform_state", WAVEFORM_FINISHING]) self.ca.assert_that_pv_is(wave_prefixed("STATUS"), "Finishing") self.ca.set_pv_value(wave_prefixed("HOLD"), 1) self.ca.assert_that_pv_is(wave_prefixed("STATUS"), "Finishing") def verify_channel_abs(self, expected_value): self.ca.assert_that_pv_is("POS:RAMP:WFTYP", expected_value) self.ca.assert_that_pv_is("STRAIN:RAMP:WFTYP", expected_value) self.ca.assert_that_pv_is("STRESS:RAMP:WFTYP", expected_value) def test_WHEN_the_waveform_generator_is_started_THEN_every_axis_is_set_to_ramp( self): self.ca.set_pv_value(wave_prefixed("START"), 1) self.verify_channel_abs(RAMP_WAVEFORM_TYPES[0]) def test_WHEN_the_waveform_generator_is_stopped_THEN_every_axis_is_set_to_absolute_ramp( self): self.ca.set_pv_value(wave_prefixed("STOP"), 1) self.verify_channel_abs(RAMP_WAVEFORM_TYPES[3]) @skip_if_recsim("Different statuses don't interact in RECSIM") def test_WHEN_the_waveform_generator_is_started_THEN_the_quarter_counter_is_armed( self): self.ca.set_pv_value(wave_prefixed("START"), 1) self.ca.assert_that_pv_is(quart_prefixed("STATUS"), "Armed") def test_WHEN_the_max_cyles_is_set_THEN_the_readback_matches_setpoint( self): value = 7 self.ca.assert_setting_setpoint_sets_readback( value=value, set_point_pv=quart_prefixed("CYCLE:SP"), readback_pv=quart_prefixed("CYCLE:SP:RBV"))
class KepcoTests(object): """ Tests for the KEPCO. """ def setUp(self): self._lewis, self._ioc = get_running_lewis_and_ioc("kepco", DEVICE_PREFIX) self.ca = ChannelAccess(default_timeout=30, device_prefix=DEVICE_PREFIX) self._lewis.backdoor_run_function_on_device("reset") self.ca.assert_that_pv_exists("VOLTAGE", timeout=30) reset_calibration_file(self.ca, "default_calib.dat") def _write_voltage(self, expected_voltage): self._lewis.backdoor_set_on_device("voltage", expected_voltage) self._ioc.set_simulated_value("SIM:VOLTAGE", expected_voltage) def _write_current(self, expected_current): self._lewis.backdoor_set_on_device("current", expected_current) self._ioc.set_simulated_value("SIM:CURRENT", expected_current) def _set_IDN(self, expected_idn_no_firmware, expected_firmware): self._lewis.backdoor_set_on_device("idn_no_firmware", expected_idn_no_firmware) self._lewis.backdoor_set_on_device("firmware", expected_firmware) expected_idn = "{}{}".format(expected_idn_no_firmware, str(expected_firmware))[:39] # EPICS limited to 40 chars self._ioc.set_simulated_value("SIM:IDN", expected_idn) self._ioc.set_simulated_value("SIM:FIRMWARE", str(expected_firmware)) # Both firmware and IDN are passive so must be updated self.ca.process_pv("FIRMWARE") self.ca.process_pv("IDN") return expected_idn def _set_output_mode(self, expected_output_mode): self._lewis.backdoor_set_on_device("output_mode", expected_output_mode) self._ioc.set_simulated_value("SIM:OUTPUTMODE", expected_output_mode) def _set_output_status(self, expected_output_status): self._lewis.backdoor_set_on_device("output_status", expected_output_status) def test_GIVEN_voltage_set_WHEN_read_THEN_voltage_is_as_expected(self): expected_voltage = 1.2 self._write_voltage(expected_voltage) self.ca.assert_that_pv_is("VOLTAGE", expected_voltage) def test_GIVEN_current_set_WHEN_read_THEN_current_is_as_expected(self): expected_current = 1.5 self._write_current(expected_current) self.ca.assert_that_pv_is("CURRENT", expected_current) def test_GIVEN_setpoint_voltage_set_WHEN_read_THEN_setpoint_voltage_is_as_expected(self): # Get current Voltage current_voltage = self.ca.get_pv_value("VOLTAGE") # Set new Voltage via SP self.ca.set_pv_value("VOLTAGE:SP", current_voltage + 5) # Check SP RBV matches new current self.ca.assert_that_pv_is("VOLTAGE:SP:RBV", current_voltage + 5) @parameterized.expand(parameterized_list([-5.1, 7.8])) def test_GIVEN_setpoint_current_set_WHEN_read_THEN_setpoint_current_is_as_expected(self, _, expected_current): self.ca.set_pv_value("CURRENT:SP", expected_current) # Check SP RBV matches new current self.ca.assert_that_pv_is("CURRENT:SP:RBV", expected_current) def test_GIVEN_output_mode_set_WHEN_read_THEN_output_mode_is_as_expected(self): expected_output_mode_flag = UnitFlags.CURRENT expected_output_mode_str = OutputMode.CURRENT self._set_output_mode(expected_output_mode_flag) # Check OUTPUT MODE matches new OUTPUT MODE self.ca.assert_that_pv_is("OUTPUTMODE", expected_output_mode_str) def test_GIVEN_output_status_set_WHEN_read_THEN_output_STATUS_is_as_expected(self): expected_output_status_flag = UnitFlags.ON expected_output_status_str = Status.ON self.ca.set_pv_value("OUTPUTSTATUS:SP", expected_output_status_flag) self.ca.assert_that_pv_is("OUTPUTSTATUS:SP:RBV", expected_output_status_str) @parameterized.expand(parameterized_list(IDN_LIST)) def test_GIVEN_idn_set_WHEN_read_THEN_idn_is_as_expected(self, _, idn_no_firmware, firmware): expected_idn = self._set_IDN(idn_no_firmware, firmware) self.ca.process_pv("IDN") self.ca.assert_that_pv_is("IDN", expected_idn) @skip_if_recsim("In rec sim you can not diconnect the device") def test_GIVEN_diconnected_WHEN_read_THEN_alarms_on_readbacks(self): self._lewis.backdoor_set_on_device("connected", False) self.ca.assert_that_pv_alarm_is("OUTPUTMODE", self.ca.Alarms.INVALID) self.ca.assert_that_pv_alarm_is("CURRENT", self.ca.Alarms.INVALID) self.ca.assert_that_pv_alarm_is("VOLTAGE", self.ca.Alarms.INVALID) def _test_ramp_to_target(self, start_current, target_current, ramp_rate, step_number, wait_between_changes): self._write_current(start_current) self.ca.set_pv_value("CURRENT:SP", start_current) self.ca.assert_that_pv_is("CURRENT:SP:RBV", start_current) self.ca.set_pv_value("RAMP:RATE:SP", ramp_rate) self.ca.set_pv_value("RAMP:STEPS:SP", step_number) self.ca.set_pv_value("RAMPON:SP", "ON") self.ca.set_pv_value("CURRENT:SP", target_current, sleep_after_set=0.0) if start_current < target_current: self.ca.assert_that_pv_value_is_increasing("CURRENT:SP:RBV", wait=wait_between_changes) else: self.ca.assert_that_pv_value_is_decreasing("CURRENT:SP:RBV", wait=wait_between_changes) self.ca.assert_that_pv_is("RAMPING", "YES") # Device stops ramping when it gets to target self.ca.assert_that_pv_is("CURRENT:SP:RBV", target_current, timeout=40) self._write_current(target_current) self.ca.assert_that_pv_is("RAMPING", "NO") self.ca.assert_that_pv_value_is_unchanged("CURRENT:SP:RBV", wait=wait_between_changes) self.ca.set_pv_value("RAMPON:SP", "OFF") def test_GIVEN_rampon_WHEN_target_set_THEN_current_ramps_to_target(self): self._test_ramp_to_target(1, 2, 2, 20, 7) def test_GIVEN_rampon_WHEN_target_set_with_different_step_rate_THEN_current_ramps_to_target_more_finely(self): self._test_ramp_to_target(4, 3, 2, 60, 2) @parameterized.expand(parameterized_list(IDN_LIST)) def test_GIVEN_idn_set_AND_firmware_set_THEN_firmware_pv_correct(self, _, idn_no_firmware, firmware): self._set_IDN(idn_no_firmware, firmware) self.ca.process_pv("FIRMWARE") self.ca.assert_that_pv_is("FIRMWARE", firmware) @parameterized.expand(parameterized_list([ ("default_calib.dat", 100, 100), ("field_double_amps.dat", 100, 50), ])) @skip_if_recsim("Calibration lookup does not work in recsim") def test_GIVEN_calibration_WHEN_field_set_THEN_current_as_expected(self, _, calibration_file, field, expected_current): with use_calibration_file(self.ca, calibration_file, "default_calib.dat"): self.ca.set_pv_value("FIELD:SP", field) self.ca.assert_that_pv_is("FIELD:SP:RBV", field) self.ca.assert_that_pv_is("CURRENT:SP", expected_current) self.ca.assert_that_pv_is("CURRENT:SP:RBV", expected_current) @parameterized.expand(parameterized_list([ ("default_calib.dat", 100, 100), ("field_double_amps.dat", 100, 200), ])) @skip_if_recsim("Calibration lookup does not work in recsim") def test_GIVEN_calibration_WHEN_current_set_THEN_field_as_expected(self, _, calibration_file, current, expected_field): with use_calibration_file(self.ca, calibration_file, "default_calib.dat"): self._write_current(current) self.ca.assert_that_pv_is("CURRENT", current) self.ca.assert_that_pv_is("FIELD", expected_field) @skip_if_recsim("Lewis not available in recsim") def test_WHEN_sending_setpoint_THEN_only_one_setpoint_sent(self): self._lewis.backdoor_set_and_assert_set("current_set_count", 0) self.ca.set_pv_value("CURRENT:SP", 100) self._lewis.assert_that_emulator_value_is("current_set_count", 1) # Wait a short time and make sure count is not being incremented again later. time.sleep(5) self._lewis.assert_that_emulator_value_is("current_set_count", 1)
class Dh2000Tests(unittest.TestCase): """ Tests for the Dh2000 IOC. """ def setUp(self): self._lewis, self._ioc = get_running_lewis_and_ioc( "dh2000", DEVICE_PREFIX) self.ca = ChannelAccess(device_prefix=DEVICE_PREFIX) self._lewis.backdoor_set_on_device("shutter_is_open", False) self._lewis.backdoor_set_on_device("interlock_is_triggered", False) self._lewis.backdoor_set_on_device("is_connected", True) self.ca.assert_that_pv_is("SHUTTER:A", "Closed") self.ca.assert_that_pv_is("INTERLOCK", "OK") @parameterized.expand([("shutter_open_interlock_off", True, False), ("shutter_closed_interlock_off", False, False), ("shutter_closed_interlock_on", False, True)]) def test_GIVEN_device_in_a_state_WHEN_status_requested_THEN_shutter_and_interlock_status_returned( self, _, shutter_is_open, interlock_is_triggered): # GIVEN self._lewis.backdoor_set_on_device("shutter_is_open", shutter_is_open) self._lewis.backdoor_set_on_device("interlock_is_triggered", interlock_is_triggered) # THEN self.ca.assert_that_pv_is( "INTERLOCK", "TRIGGERED" if interlock_is_triggered else "OK") self.ca.assert_that_pv_is("SHUTTER:A", "Open" if shutter_is_open else "Closed") def test_GIVEN_shutter_open_WHEN_interlock_triggered_THEN_shutter_closes( self): self._lewis.backdoor_set_on_device("shutter_is_open", True) self._lewis.backdoor_set_on_device("interlock_is_triggered", False) # GIVEN self.ca.assert_that_pv_is("SHUTTER:A", "Open") self.ca.assert_that_pv_is("INTERLOCK", "OK") # WHEN self._lewis.backdoor_set_on_device("interlock_is_triggered", True) # THEN self.ca.assert_that_pv_is("INTERLOCK", "TRIGGERED") self.ca.assert_that_pv_is("SHUTTER:A", "Closed") def test_GIVEN_shutter_open_WHEN_shutter_close_requested_THEN_shutter_closes( self): # GIVEN self._lewis.backdoor_set_on_device("shutter_is_open", True) self.ca.assert_that_pv_is("SHUTTER:A", "Open") # WHEN self.ca.set_pv_value("SHUTTER:A:SP", "Close") # THEN self.ca.assert_that_pv_is("SHUTTER:A", "Closed") def test_GIVEN_shutter_closed_and_interlock_not_triggered_WHEN_shutter_open_requested_THEN_shutter_opens( self): # GIVEN self.ca.assert_that_pv_is("INTERLOCK", "OK") self.ca.assert_that_pv_is("SHUTTER:A", "Closed") # WHEN self.ca.set_pv_value("SHUTTER:A:SP", "Open") # THEN self.ca.assert_that_pv_is("SHUTTER:A", "Open") def test_GIVEN_shutter_closed_and_interlock_triggered_WHEN_shutter_open_requested_THEN_shutter_does_not_open( self): # GIVEN self._lewis.backdoor_set_on_device("interlock_is_triggered", True) self.ca.assert_that_pv_is("INTERLOCK", "TRIGGERED") self.ca.assert_that_pv_is("SHUTTER:A", "Closed") # WHEN self.ca.set_pv_value("SHUTTER:A:SP", "Open") # THEN self.ca.assert_that_pv_is("SHUTTER:A", "Closed") def test_GIVEN_interlock_triggered_THEN_interlock_PV_has_major_alarm(self): # GIVEN self._lewis.backdoor_set_on_device("interlock_is_triggered", True) self.ca.assert_that_pv_alarm_is("INTERLOCK", self.ca.Alarms.MAJOR) def test_GIVEN_disconnected_device_THEN_interlock_and_shutter_status_show_INVALID( self): self._lewis.backdoor_set_on_device("is_connected", False) self.ca.assert_that_pv_alarm_is("SHUTTER:A", self.ca.Alarms.INVALID) self.ca.assert_that_pv_alarm_is("INTERLOCK", self.ca.Alarms.INVALID)
class Amint2lTests(unittest.TestCase): """ Tests for the AM Int2-L. """ def setUp(self): self._lewis, self._ioc = get_running_lewis_and_ioc( "amint2l", DEVICE_PREFIX) self.assertIsNotNone(self._lewis) self.assertIsNotNone(self._ioc) self.ca = ChannelAccess(device_prefix=DEVICE_PREFIX) self._lewis.backdoor_set_on_device('connected', True) self._lewis.backdoor_set_on_device("address", ADDRESS) def _set_pressure(self, expected_pressure): self._lewis.backdoor_set_on_device("pressure", expected_pressure) self._ioc.set_simulated_value("SIM:PRESSURE", expected_pressure) def test_GIVEN_pressure_set_WHEN_read_THEN_pressure_is_as_expected(self): expected_pressure = 1.23 self._set_pressure(expected_pressure) self.ca.assert_that_pv_is("PRESSURE", expected_pressure) self.ca.assert_that_pv_alarm_is("PRESSURE", self.ca.Alarms.NONE) self.ca.assert_that_pv_is("RANGE:ERROR", "No Error") def test_GIVEN_negative_pressure_set_WHEN_read_THEN_pressure_is_as_expected( self): expected_pressure = -123.34 self._set_pressure(expected_pressure) self.ca.assert_that_pv_is("PRESSURE", expected_pressure) def test_GIVEN_pressure_with_no_decimal_places_set_WHEN_read_THEN_pressure_is_as_expected( self): expected_pressure = 7 self._set_pressure(expected_pressure) self.ca.assert_that_pv_is("PRESSURE", expected_pressure) @skip_if_recsim("In rec sim this test fails") def test_GIVEN_pressure_over_range_set_WHEN_read_THEN_error(self): expected_pressure = "OR" self._set_pressure(expected_pressure) self.ca.assert_that_pv_alarm_is("PRESSURE", self.ca.Alarms.INVALID) self.ca.assert_that_pv_is("RANGE:ERROR", "Over Range") @skip_if_recsim("In rec sim this test fails") def test_GIVEN_pressure_under_range_set_WHEN_read_THEN_error(self): expected_pressure = "UR" self._set_pressure(expected_pressure) self.ca.assert_that_pv_alarm_is("PRESSURE", self.ca.Alarms.INVALID) self.ca.assert_that_pv_is("RANGE:ERROR", "Under Range") @skip_if_recsim("In rec sim this test fails") def test_GIVEN_device_disconnected_WHEN_read_THEN_pv_shows_disconnect( self): self._lewis.backdoor_set_on_device("pressure", None) # Setting none simulates no response from device which is like pulling the serial cable. Disconnecting the # emulator using the backdoor makes the record go udf not timeout which is what the actual device does. self.ca.assert_that_pv_alarm_is("PRESSURE", self.ca.Alarms.INVALID) @skip_if_recsim("Can not test disconnection in recsim") def test_GIVEN_device_not_connected_WHEN_get_pressure_THEN_alarm(self): self._lewis.backdoor_set_on_device('connected', False) self.ca.assert_that_pv_alarm_is('PRESSURE', ChannelAccess.Alarms.INVALID)
class IceFridgeTests(unittest.TestCase): """ Tests for the IceFrdge IOC. """ def setUp(self): self._lewis, self._ioc = get_running_lewis_and_ioc(IOCS[0]["emulator"], DEVICE_PREFIX) self.ca = ChannelAccess(device_prefix=DEVICE_PREFIX, default_timeout=25) if not IOCRegister.uses_rec_sim: self._lewis.backdoor_run_function_on_device("reset") self._lewis.backdoor_set_on_device("connected", True) def test_WHEN_device_is_started_THEN_it_is_not_disabled(self): self.ca.assert_that_pv_is("DISABLE", "COMMS ENABLED") @parameterized.expand(parameterized_list(VTI_TEMP_SUFFIXES)) @skip_if_recsim("Lewis backdoor not available in recsim") def test_WHEN_VTI_temp_set_backdoor_THEN_ioc_read_correctly(self, _, temp_num): self._lewis.backdoor_run_function_on_device("set_cryo_temp", (temp_num, 3.6)) self.ca.assert_that_pv_is_number("VTI:TEMP{}".format(temp_num), 3.6, 0.001) @parameterized.expand(parameterized_list(itertools.product(VTI_LOOPS, VTI_LOOP_TEST_INPUTS))) def test_WHEN_vti_loop_setpoint_THEN_readback_identical(self, _, loop_num, temp): self.ca.assert_setting_setpoint_sets_readback(temp, "VTI:LOOP{}:TSET".format(loop_num), "VTI:LOOP{}:TSET:SP".format(loop_num)) @parameterized.expand(parameterized_list(itertools.product(VTI_LOOPS, VTI_LOOP_TEST_INPUTS))) def test_WHEN_vti_loop_proportional_THEN_readback_identical(self, _, loop_num, temp): self.ca.assert_setting_setpoint_sets_readback(temp, "VTI:LOOP{}:P".format(loop_num), "VTI:LOOP{}:P:SP".format(loop_num)) @parameterized.expand(parameterized_list(itertools.product(VTI_LOOPS, VTI_LOOP_TEST_INPUTS))) def test_WHEN_vti_loop_integral_THEN_readback_identical(self, _, loop_num, temp): self.ca.assert_setting_setpoint_sets_readback(temp, "VTI:LOOP{}:I".format(loop_num), "VTI:LOOP{}:I:SP".format(loop_num)) @parameterized.expand(parameterized_list(itertools.product(VTI_LOOPS, VTI_LOOP_TEST_INPUTS))) def test_WHEN_vti_loop_derivative_THEN_readback_identical(self, _, loop_num, temp): self.ca.assert_setting_setpoint_sets_readback(temp, "VTI:LOOP{}:D".format(loop_num), "VTI:LOOP{}:D:SP".format(loop_num)) @parameterized.expand(parameterized_list(itertools.product(VTI_LOOPS, VTI_LOOP_TEST_INPUTS))) def test_WHEN_vti_loop_ramp_rate_THEN_readback_identical(self, _, loop_num, temp): self.ca.assert_setting_setpoint_sets_readback(temp, "VTI:LOOP{}:RAMPRATE".format(loop_num), "VTI:LOOP{}:RAMPRATE:SP".format(loop_num)) @skip_if_recsim("Lewis backdoor not available in recsim") def test_WHEN_Lakeshore_MC_Cernox_set_backdoor_THEN_ioc_read_correctly(self): self._lewis.backdoor_set_on_device("lakeshore_mc_cernox", 0.5) self.ca.assert_that_pv_is_number("LS:MC:CERNOX", 0.5, 0.001) @skip_if_recsim("Lewis backdoor not available in recsim") def test_WHEN_Lakeshore_MC_RuO_set_backdoor_THEN_ioc_read_correctly(self): self._lewis.backdoor_set_on_device("lakeshore_mc_ruo", 0.6) self.ca.assert_that_pv_is_number("LS:MC:RUO", 0.6, 0.001) @skip_if_recsim("Lewis backdoor not available in recsim") def test_WHEN_Lakeshore_still_temp_set_backdoor_THEN_ioc_read_correctly(self): self._lewis.backdoor_set_on_device("lakeshore_still_temp", 0.7) self.ca.assert_that_pv_is_number("LS:STILL:TEMP", 0.7, 0.001) def test_WHEN_Lakeshore_MC_setpoint_THEN_readback_identical(self): self.ca.assert_setting_setpoint_sets_readback(0.8, "LS:MC:TEMP", "LS:MC:TEMP:SP") @skip_if_recsim("Lewis assertion not working in recsim") def test_WHEN_Lakeshore_MC_setpoint_is_zero_THEN_scan_correct(self): self.ca.set_pv_value("LS:MC:TEMP:SP", 0) self._lewis.assert_that_emulator_value_is("lakeshore_scan", 1, 15) self._lewis.assert_that_emulator_value_is("lakeshore_cmode", 4, 15) @skip_if_recsim("Lewis assertion not working in recsim") def test_WHEN_Lakeshore_MC_setpoint_is_larger_than_zero_THEN_scan_correct(self): self.ca.set_pv_value("LS:MC:TEMP:SP", 4) self._lewis.assert_that_emulator_value_is("lakeshore_scan", 0, 15) self._lewis.assert_that_emulator_value_is("lakeshore_cmode", 1, 15) def test_WHEN_Lakeshore_MC_setpoint_negative_THEN_readback_zero(self): self.ca.set_pv_value("LS:MC:TEMP:SP", -1) self.ca.assert_that_pv_is("LS:MC:TEMP", 0) def test_WHEN_Lakeshore_MC_setpoint_over_limit_THEN_readback_at_limit(self): self.ca.set_pv_value("LS:MC:TEMP:SP", 301) self.ca.assert_that_pv_is("LS:MC:TEMP", 300) def test_WHEN_Lakeshore_MC_proportional_THEN_readback_identical(self): self.ca.assert_setting_setpoint_sets_readback(0.9, "LS:MC:P", "LS:MC:P:SP") @skip_if_recsim("pv updated when other pv processes, has no scan field") def test_WHEN_Lakeshore_MC_integral_THEN_readback_identical(self): self.ca.assert_setting_setpoint_sets_readback(11, "LS:MC:I", "LS:MC:I:SP") @skip_if_recsim("pv updated when other pv processes, has no scan field") def test_WHEN_Lakeshore_MC_derivative_THEN_readback_identical(self): self.ca.assert_setting_setpoint_sets_readback(12, "LS:MC:D", "LS:MC:D:SP") @parameterized.expand(parameterized_list(LS_MC_HTR_RANGE_VALUES)) def test_WHEN_Lakeshore_MC_heater_range_THEN_readback_identical(self, _, heater_range): self.ca.assert_setting_setpoint_sets_readback(heater_range, "LS:MC:HTR:RANGE", "LS:MC:HTR:RANGE:SP") @parameterized.expand(parameterized_list(LS_MC_HTR_RANGE_INVALID_VALUES)) @skip_if_recsim("Lewis backdoor not available in recsim") def test_WHEN_lakeshore_MC_heater_range_invalid_setpoint_THEN_pv_in_alarm(self, _, invalid_range): self.ca.assert_that_pv_alarm_is("LS:MC:HTR:RANGE", self.ca.Alarms.NONE, timeout=15) self._lewis.backdoor_set_on_device("lakeshore_mc_heater_range", invalid_range) self.ca.assert_that_pv_alarm_is("LS:MC:HTR:RANGE", self.ca.Alarms.INVALID, timeout=15) @skip_if_recsim("Lewis backdoor not available in recsim") def test_WHEN_Lakeshore_MC_heater_percentage_set_backdoor_THEN_ioc_read_correctly(self): self._lewis.backdoor_set_on_device("lakeshore_mc_heater_percentage", 50) self.ca.assert_that_pv_is_number("LS:MC:HTR:PERCENT", 50, 0.001) @skip_if_recsim("Lewis backdoor not available in recsim") def test_WHEN_Lakeshore_MC_still_output_set_backdoor_THEN_ioc_read_correctly(self): self._lewis.backdoor_set_on_device("lakeshore_still_output", 1.3) self.ca.assert_that_pv_is_number("LS:STILL", 1.3, 0.001) @parameterized.expand(parameterized_list(itertools.product(LS_VOLTAGE_CHANNELS, LS_VOLTAGE_RANGE_VALUES))) def test_WHEN_Lakeshore_voltage_range_THEN_readback_identical(self, _, voltage_channel, voltage_value): self.ca.assert_setting_setpoint_sets_readback(voltage_value, "LS:VLTG:RANGE:CH{}".format(voltage_channel), "LS:VLTG:RANGE:SP") @parameterized.expand(parameterized_list(itertools.product(LS_VOLTAGE_CHANNELS, LS_VOLTAGE_RANGE_INVALID_VALUES))) @skip_if_recsim("Lewis backdoor not available in recsim") def test_WHEN_Lakeshore_voltage_range_invalid_setpoint_THEN_pv_in_alarm(self, _, voltage_channel, invalid_range): self.ca.assert_that_pv_alarm_is("LS:VLTG:RANGE:CH{}".format(voltage_channel), self.ca.Alarms.NONE, timeout=15) self._lewis.backdoor_set_on_device("lakeshore_exc_voltage_range_ch{}".format(voltage_channel), invalid_range) self.ca.assert_that_pv_alarm_is("LS:VLTG:RANGE:CH{}".format(voltage_channel), self.ca.Alarms.INVALID, timeout=15) @parameterized.expand(parameterized_list(MIMIC_PRESSURE_SUFFIXES)) @skip_if_recsim("Lewis backdoor not available in recsim") def test_WHEN_pressure_set_backdoor_THEN_ioc_read_correctly(self, _, pressure_num): self._lewis.backdoor_run_function_on_device("set_pressure", (pressure_num, 1.4)) self.ca.assert_that_pv_is_number("PRESSURE{}".format(pressure_num), 1.4, 0.001) @parameterized.expand(parameterized_list(MIMIC_VALVE_NUMBERS)) @skip_if_recsim("pv updated when other pv processes, has no scan field") def test_WHEN_valve_status_open_THEN_readback_identical(self, _, valve_num): self.ca.assert_setting_setpoint_sets_readback("OPEN", "VALVE{}".format(valve_num), "VALVE{}:SP".format(valve_num)) @parameterized.expand(parameterized_list(MIMIC_VALVE_NUMBERS)) @skip_if_recsim("pv updated when other pv processes, has no scan field") def test_WHEN_valve_status_closed_THEN_readback_identical(self, _, valve_num): self.ca.assert_setting_setpoint_sets_readback("OPEN", "VALVE{}".format(valve_num), "VALVE{}:SP".format(valve_num)) self.ca.assert_setting_setpoint_sets_readback("CLOSED", "VALVE{}".format(valve_num), "VALVE{}:SP".format(valve_num)) @parameterized.expand(parameterized_list(MIMIC_PROPORTIONAL_VALVES_NUMBERS)) @skip_if_recsim("pv updated when other pv processes, has no scan field") def test_WHEN_proportional_valve_THEN_readback_identical(self, _, proportional_valve_num): self.ca.assert_setting_setpoint_sets_readback(1.5, "PROPORTIONAL_VALVE{}".format(proportional_valve_num), "PROPORTIONAL_VALVE{}:SP".format(proportional_valve_num)) @parameterized.expand(parameterized_list(itertools.product(MIMIC_PROPORTIONAL_VALVES_NUMBERS, [0.001, 2]))) @skip_if_recsim("pv updated when other pv processes, has no scan field") def test_WHEN_proportional_valve_not_0_THEN_calc_is_one(self, _, proportional_valve_num, test_value): self.ca.set_pv_value("PROPORTIONAL_VALVE{}:SP".format(proportional_valve_num), test_value) self.ca.assert_that_pv_is("PROPORTIONAL_VALVE{}:_CALC".format(proportional_valve_num), 1) @parameterized.expand(parameterized_list(MIMIC_PROPORTIONAL_VALVES_NUMBERS)) @skip_if_recsim("pv updated when other pv processes, has no scan field") def test_WHEN_proportional_valve_0_THEN_calc_is_zero(self, _, proportional_valve_num): self.ca.set_pv_value("PROPORTIONAL_VALVE{}:SP".format(proportional_valve_num), 1) self.ca.assert_that_pv_is("PROPORTIONAL_VALVE{}:_CALC".format(proportional_valve_num), 1) self.ca.set_pv_value("PROPORTIONAL_VALVE{}:SP".format(proportional_valve_num), 0) self.ca.assert_that_pv_is("PROPORTIONAL_VALVE{}:_CALC".format(proportional_valve_num), 0) @parameterized.expand(parameterized_list(MIMIC_PROPORTIONAL_VALVES_NUMBERS)) @skip_if_recsim("pv updated when other pv processes, has no scan field") def test_WHEN_proportional_valve_sp_negative_THEN_readback_zero(self, _, proportional_valve_num): self.ca.set_pv_value("PROPORTIONAL_VALVE{}:SP".format(proportional_valve_num), -1) self.ca.assert_that_pv_is("PROPORTIONAL_VALVE{}".format(proportional_valve_num), 0) @parameterized.expand(parameterized_list(MIMIC_PROPORTIONAL_VALVES_NUMBERS)) @skip_if_recsim("pv updated when other pv processes, has no scan field") def test_WHEN_proportional_valve_sp_over_limit_THEN_readback_at_limit(self, _, proportional_valve_num): self.ca.set_pv_value("PROPORTIONAL_VALVE{}:SP".format(proportional_valve_num), 101) self.ca.assert_that_pv_is("PROPORTIONAL_VALVE{}".format(proportional_valve_num), 100) @skip_if_recsim("pv updated when other pv processes, has no scan field") def test_WHEN_needle_valve_THEN_readback_identical(self): self.ca.assert_setting_setpoint_sets_readback(1.6, "NEEDLE_VALVE", "NEEDLE_VALVE:SP") @parameterized.expand(parameterized_list([0.001, 2])) @skip_if_recsim("pv updated when other pv processes, has no scan field") def test_WHEN_needle_valve_not_0_THEN_calc_is_one(self, _, test_value): self.ca.set_pv_value("NEEDLE_VALVE:SP", test_value) self.ca.assert_that_pv_is("NEEDLE_VALVE:_CALC", 1) @skip_if_recsim("pv updated when other pv processes, has no scan field") def test_WHEN_needle_valve_0_THEN_calc_is_zero(self): self.ca.set_pv_value("NEEDLE_VALVE:SP", 0) self.ca.assert_that_pv_is("NEEDLE_VALVE:_CALC", 0) @skip_if_recsim("pv updated when other pv processes, has no scan field") def test_WHEN_needle_valve_sp_negative_THEN_readback_zero(self): self.ca.set_pv_value("NEEDLE_VALVE:SP", -1) self.ca.assert_that_pv_is("NEEDLE_VALVE", 0) @skip_if_recsim("pv updated when other pv processes, has no scan field") def test_WHEN_needle_valve_sp_over_limit_THEN_readback_at_limit(self): self.ca.set_pv_value("NEEDLE_VALVE:SP", 101) self.ca.assert_that_pv_is("NEEDLE_VALVE", 100) @parameterized.expand(parameterized_list(MIMIC_SOLENOID_VALVES_NUMBERS)) @skip_if_recsim("pv updated when other pv processes, has no scan field") def test_WHEN_solenoid_valve_open_THEN_readback_identical(self, _, solenoid_valve_num): self.ca.assert_setting_setpoint_sets_readback("OPEN", "SOLENOID_VALVE{}".format(solenoid_valve_num), "SOLENOID_VALVE{}:SP".format(solenoid_valve_num)) @parameterized.expand(parameterized_list(MIMIC_SOLENOID_VALVES_NUMBERS)) @skip_if_recsim("pv updated when other pv processes, has no scan field") def test_WHEN_solenoid_valve_close_THEN_readback_identical(self, _, solenoid_valve_num): self.ca.assert_setting_setpoint_sets_readback("OPEN", "SOLENOID_VALVE{}".format(solenoid_valve_num), "SOLENOID_VALVE{}:SP".format(solenoid_valve_num)) self.ca.assert_setting_setpoint_sets_readback("CLOSED", "SOLENOID_VALVE{}".format(solenoid_valve_num), "SOLENOID_VALVE{}:SP".format(solenoid_valve_num)) @skip_if_recsim("lewis backdoor not available in recsim") def test_WHEN_1K_stage_temp_THEN_ioc_read_correctly(self): self._lewis.backdoor_set_on_device("temp_1K_stage", 1.7) self.ca.assert_that_pv_is_number("1K:TEMP", 1.7, 0.001) @skip_if_recsim("lewis backdoor not available in recsim") def test_WHEN_MC_temperature_THEN_ioc_read_correctly(self): self._lewis.backdoor_set_on_device("mixing_chamber_temp", 1.8) self.ca.assert_that_pv_is_number("MC:TEMP", 1.8, 0.001) @skip_if_recsim("lewis backdoor not available in recsim") def test_WHEN_MC_resistance_THEN_ioc_read_correctly(self): self._lewis.backdoor_set_on_device("mixing_chamber_resistance", 1.9) self.ca.assert_that_pv_is_number("MC:_RESISTANCE", 1.9, 0.001) @skip_if_recsim("lewis backdoor not available in recsim") def test_WHEN_MC_resistance_calc_THEN_calculation_correct(self): self._lewis.backdoor_set_on_device("mixing_chamber_resistance", 1918) self.ca.assert_that_pv_is_number("MC:RESISTANCE:CALC", 1.918, 0.001) def test_WHEN_mimic_mode_manual_THEN_buttons_disabled(self): self.ca.set_pv_value("MIMIC:MODE:SP", "MANUAL") self.ca.assert_that_pv_is("MIMIC:START:SP.DISP", '1') self.ca.assert_that_pv_is("MIMIC:SKIP:SP.DISP", '1') self.ca.assert_that_pv_is("MIMIC:STOP:SP.DISP", '1') def test_WHEN_mimic_mode_automatic_THEN_buttons_disabled(self): self.ca.set_pv_value("MIMIC:MODE:SP", "AUTOMATIC") self.ca.assert_that_pv_is("MIMIC:START:SP.DISP", '1') self.ca.assert_that_pv_is("MIMIC:SKIP:SP.DISP", '1') self.ca.assert_that_pv_is("MIMIC:STOP:SP.DISP", '1') def test_WHEN_mimic_mode_semi_automatic_THEN_buttons_enabled(self): self.ca.set_pv_value("MIMIC:MODE:SP", "SEMI AUTOMATIC") self.ca.assert_that_pv_is("MIMIC:START:SP.DISP", '0') self.ca.assert_that_pv_is("MIMIC:SKIP:SP.DISP", '0') self.ca.assert_that_pv_is("MIMIC:STOP:SP.DISP", '0') @skip_if_recsim("Lewis assertion not working in recsim") def test_WHEN_mimic_skip_THEN_skipped(self): self._lewis.assert_that_emulator_value_is("skipped", False, 15) self.ca.set_pv_value("MIMIC:MODE:SP", "SEMI AUTOMATIC") # does not matter what value the pv is set to, only that it processes self.ca.set_pv_value("MIMIC:SKIP:SP", "SKIP") self._lewis.assert_that_emulator_value_is("skipped", True, 15) @skip_if_recsim("Lewis assertion not working in recsim") def test_WHEN_mimic_stop_THEN_stopped(self): self._lewis.assert_that_emulator_value_is("stopped", False, 15) self.ca.set_pv_value("MIMIC:MODE:SP", "SEMI AUTOMATIC") # does not matter what value the pv is set to, only that it processes self.ca.set_pv_value("MIMIC:STOP:SP", "STOP") self._lewis.assert_that_emulator_value_is("stopped", True, 15) @skip_if_recsim("Lewis assertion not working in recsim") def test_WHEN_mimic_sequence_condense_THEN_condense(self): self._lewis.assert_that_emulator_value_is("condense", False, 15) self.ca.set_pv_value("MIMIC:MODE:SP", "SEMI AUTOMATIC") self.ca.set_pv_value("MIMIC:SEQUENCE:SP", "Condense") # does not matter what value the pv is set to, only that it processes self.ca.set_pv_value("MIMIC:START:SP", "START") self._lewis.assert_that_emulator_value_is("condense", True, 15) @skip_if_recsim("Lewis assertion not working in recsim") def test_WHEN_mimic_sequence_circulate_THEN_circulate(self): self._lewis.assert_that_emulator_value_is("circulate", False, 15) self.ca.set_pv_value("MIMIC:MODE:SP", "SEMI AUTOMATIC") self.ca.set_pv_value("MIMIC:SEQUENCE:SP", "Circulate") # does not matter what value the pv is set to, only that it processes self.ca.set_pv_value("MIMIC:START:SP", "START") self._lewis.assert_that_emulator_value_is("circulate", True, 15) @skip_if_recsim("Lewis assertion not working in recsim") def test_WHEN_mimic_sequence_condense_and_circulate_THEN_condense_and_circulate(self): self._lewis.assert_that_emulator_value_is("condense", False, 15) self._lewis.assert_that_emulator_value_is("circulate", False, 15) self.ca.set_pv_value("MIMIC:MODE:SP", "SEMI AUTOMATIC") self.ca.set_pv_value("MIMIC:SEQUENCE:SP", "Condense & Circulate") # does not matter what value the pv is set to, only that it processes self.ca.set_pv_value("MIMIC:START:SP", "START") self._lewis.assert_that_emulator_value_is("condense", True, 15) self._lewis.assert_that_emulator_value_is("circulate", True, 15) @skip_if_recsim("Lewis assertion not working in recsim") def test_WHEN_mimic_sequence_temp_control_THEN_readback_identical(self): self._lewis.assert_that_emulator_value_is("temp_control", 0, 15) self.ca.set_pv_value("MIMIC:MODE:SP", "SEMI AUTOMATIC") self.ca.set_pv_value("MIMIC:SEQUENCE:SP", "Temperature Control") self.ca.set_pv_value("MIMIC:SEQUENCE:TEMP:SP", 2.3) # does not matter what value the pv is set to, only that it processes self.ca.set_pv_value("MIMIC:START:SP", "START") self.ca.assert_that_pv_is("MIMIC:SEQUENCE:TEMP", 2.3) @skip_if_recsim("Lewis assertion not working in recsim") def test_WHEN_mimic_sequence_make_safe_THEN_make_safe(self): self._lewis.assert_that_emulator_value_is("make_safe", False, 15) self.ca.set_pv_value("MIMIC:MODE:SP", "SEMI AUTOMATIC") self.ca.set_pv_value("MIMIC:SEQUENCE:SP", "Make Safe") # does not matter what value the pv is set to, only that it processes self.ca.set_pv_value("MIMIC:START:SP", "START") self._lewis.assert_that_emulator_value_is("make_safe", True, 15) @skip_if_recsim("Lewis assertion not working in recsim") def test_WHEN_mimic_sequence_warm_up_THEN_warm_up(self): self._lewis.assert_that_emulator_value_is("warm_up", False, 15) self.ca.set_pv_value("MIMIC:MODE:SP", "SEMI AUTOMATIC") self.ca.set_pv_value("MIMIC:SEQUENCE:SP", "Warm Up") # does not matter what value the pv is set to, only that it processes self.ca.set_pv_value("MIMIC:START:SP", "START") self._lewis.assert_that_emulator_value_is("warm_up", True, 15) @skip_if_recsim("Lewis backdoor not working in recsim") def test_WHEN_mimic_info_THEN_ioc_read_correctly(self): self._lewis.backdoor_set_on_device("mimic_info", "RBMK reactors do not explode!") self.ca.assert_that_pv_is("MIMIC:INFO", "RBMK reactors do not explode!") @skip_if_recsim("Lewis backdoor not working in recsim") def test_WHEN_state_THEN_ioc_read_correctly(self): self._lewis.backdoor_set_on_device("state", "It\\'s disgraceful, really!") self.ca.assert_that_pv_is("STATE", "It's disgraceful, really!") def test_WHEN_nv_mode_setpoint_manual_THEN_readback_identical(self): self.ca.assert_setting_setpoint_sets_readback("MANUAL", "NVMODE", "NVMODE:SP") def test_WHEN_nv_mode_setpoint_auto_THEN_readback_identical(self): self.ca.assert_setting_setpoint_sets_readback("MANUAL", "NVMODE", "NVMODE:SP") self.ca.assert_setting_setpoint_sets_readback("AUTO", "NVMODE", "NVMODE:SP") def test_WHEN_1K_pump_off_THEN_readback_identical(self): self.ca.assert_setting_setpoint_sets_readback("ON", "1K:PUMP", "1K:PUMP:SP") self.ca.assert_setting_setpoint_sets_readback("OFF", "1K:PUMP", "1K:PUMP:SP") def test_WHEN_1K_pump_on_THEN_readback_identical(self): self.ca.assert_setting_setpoint_sets_readback("OFF", "1K:PUMP", "1K:PUMP:SP") self.ca.assert_setting_setpoint_sets_readback("ON", "1K:PUMP", "1K:PUMP:SP") @skip_if_recsim("pv updated when other pv processes, has no scan field") def test_WHEN_He3_pump_off_THEN_readback_identical(self): self.ca.assert_setting_setpoint_sets_readback("OFF", "HE3:PUMP", "HE3:PUMP:SP") @skip_if_recsim("pv updated when other pv processes, has no scan field") def test_WHEN_He3_pump_on_THEN_readback_identical(self): self.ca.assert_setting_setpoint_sets_readback("OFF", "HE3:PUMP", "HE3:PUMP:SP") self.ca.assert_setting_setpoint_sets_readback("ON", "HE3:PUMP", "HE3:PUMP:SP") @skip_if_recsim("pv updated when other pv processes, has no scan field") def test_WHEN_roots_pump_off_THEN_readback_identical(self): self.ca.assert_setting_setpoint_sets_readback("OFF", "ROOTS", "ROOTS:SP") @skip_if_recsim("pv updated when other pv processes, has no scan field") def test_WHEN_roots_pump_on_THEN_readback_identical(self): self.ca.assert_setting_setpoint_sets_readback("OFF", "ROOTS", "ROOTS:SP") self.ca.assert_setting_setpoint_sets_readback("ON", "ROOTS", "ROOTS:SP") @skip_if_recsim("testing lack of connection to device makes no sense in recsim") def test_WHEN_ioc_disconnected_THEN_all_pvs_in_alarm(self): for pv in TEST_ALARM_STATUS_PVS: self.ca.assert_that_pv_alarm_is(pv, self.ca.Alarms.NONE) self._lewis.backdoor_set_on_device("connected", False) for pv in TEST_ALARM_STATUS_PVS: self.ca.assert_that_pv_alarm_is(pv, self.ca.Alarms.INVALID)
class RknpsTests(DanfysikCommon, unittest.TestCase): """ Tests for the RIKEN Multidrop Danfysik Power Supplies. """ def setUp(self): self._lewis, self._ioc = get_running_lewis_and_ioc("rknps", PREFIX) self.ca = ChannelAccess(device_prefix=PREFIX, default_timeout=60) self._lewis.backdoor_set_on_device("connected", True) self.current_readback_factor = 1000 self.id_prefixes = [ID + ":" for ID in IDS] for id_prefix in self.id_prefixes: self.ca.assert_that_pv_exists("{}ADDRESS".format(id_prefix), timeout=30) def disconnect_device(self): """RIKEN PSU emulator disconnects slightly differently""" self._lewis.backdoor_set_on_device("connected", False) def set_voltage(self, voltage): self._lewis.backdoor_set_on_device("set_all_volt_values", voltage) def _activate_interlocks(self): """ Activate both interlocks in the emulator. """ self._lewis.backdoor_set_on_device("set_all_interlocks", True) def _deactivate_interlocks(self): """ Deactivate both interlocks in the emulator. """ self._lewis.backdoor_set_on_device("set_all_interlocks", False) @skip_if_recsim("Interlock statuses depend on emulator") def test_WHEN_interlocks_are_active_THEN_ilk_is_Interlocked(self): self._activate_interlocks() for IDN in IDS: self.ca.assert_that_pv_is("{0}:ILK".format(IDN), HAS_TRIPPED[True]) @skip_if_recsim("Interlock statuses depend on emulator") def test_WHEN_interlocks_are_inactive_THEN_ilk_is_not_Interlocked(self): self._deactivate_interlocks() for IDN in IDS: self.ca.assert_that_pv_is("{0}:ILK".format(IDN), HAS_TRIPPED[False]) @skip_if_recsim( "In rec sim this test fails as the changes are not propagated to all appropriate PVs" ) def test_GIVEN_a_positive_value_and_emulator_in_use_WHEN_current_is_set_THEN_values_are_as_expected( self): expected_value = 480 for IDN in IDS: self.ca.set_pv_value("{0}:CURR:SP".format(IDN), expected_value) self.ca.assert_that_pv_is("{0}:CURR".format(IDN), expected_value) self.ca.assert_that_pv_is("{0}:RA".format(IDN), expected_value) self.ca.assert_that_pv_is("{0}:POL".format(IDN), "+") @skip_if_recsim( "In rec sim this test fails as the changes are not propagated to all appropriate PVs" ) def test_GIVEN_a_negative_value_and_emulator_in_use_WHEN_current_is_set_THEN_values_are_as_expected( self): expected_value = -123 for IDN in IDS: self.ca.set_pv_value("{0}:CURR:SP".format(IDN), expected_value) self.ca.assert_that_pv_is("{0}:CURR".format(IDN), expected_value) self.ca.assert_that_pv_is("{0}:RA".format(IDN), abs(expected_value)) self.ca.assert_that_pv_is("{0}:POL".format(IDN), "-") @skip_if_devsim("In dev sim this test fails as the emulator " "handles the difference in values between write and read") def test_GIVEN_a_negative_value_and_emulator_not_in_use_WHEN_current_is_set_THEN_values_are_as_expected( self): set_value = -123 return_value = set_value * 1000 for IDN in IDS: self.ca.set_pv_value("{0}:CURR:SP".format(IDN), set_value) self.ca.assert_that_pv_is("{0}:CURR".format(IDN), return_value) self.ca.assert_that_pv_is("{0}:RA".format(IDN), return_value) @skip_if_recsim("Power updates through protocol redirection") def test_GIVEN_rb3_status_changes_THEN_rb3_banner_pv_updates_correctly( self): if "RB3" not in IDS: self.fail("Didn't find RB3 for test.") for powered_on in (True, False): self.ca.set_pv_value("RB3:POWER:SP", powered_on) self.ca.assert_that_pv_is( "RB3:BANNER", "on; beam to ports 1,2" if powered_on else "off; ports 1,2 safe") @skip_if_recsim("Power updates through protocol redirection") def test_GIVEN_rb4_status_changes_THEN_rb4_banner_pv_updates_correctly( self): if "RB4" not in IDS: self.fail("Didn't find RB4 for test.") for powered_on in (True, False): self.ca.set_pv_value("RB4:POWER:SP", powered_on) self.ca.assert_that_pv_is( "RB4:BANNER", "on; beam to ports 3,4" if powered_on else "off; ports 3,4 safe") @parameterized.expand(INTERLOCKS) @skip_if_recsim("Test requires emulator to change interlock state") def test_GIVEN_interlock_status_WHEN_read_all_status_THEN_status_is_as_expected( self, interlock): for boolean_value, expected_value in HAS_TRIPPED.items(): for IDN, ADDR in zip(IDS, PSU_ADDRESSES): # GIVEN self._lewis.backdoor_run_function_on_device( "set_{0}".format(interlock), (boolean_value, ADDR)) # THEN self.ca.assert_that_pv_is("{0}:ILK:{1}".format(IDN, interlock), expected_value) self.ca.assert_that_pv_alarm_is( "{0}:ILK:{1}".format(IDN, interlock), self.ca.Alarms.NONE) @parameterized.expand(INTERLOCKS) @skip_if_recsim("Test requires emulator") def test_GIVEN_individual_interlock_read_WHEN_device_not_connected_THEN_interlock_PV_in_alarm( self, interlock): # WHEN self._lewis.backdoor_set_on_device("connected", False) # THEN for IDN, ADDR in zip(IDS, PSU_ADDRESSES): self.ca.assert_that_pv_alarm_is( "{0}:ILK:{1}".format(IDN, interlock), self.ca.Alarms.INVALID) @parameterized.expand( parameterized_list([ ("FAULT STATE", 0, 0), ("BEND 1", 1, 0), ("BEND 2", 0, 1), ("SEPTUM", 1, 1), ])) @skip_if_devsim("DAQ does not exist in devsim") def test_GIVEN_mock_DAQ_inputs_THEN_RB2_mode_is_correct( self, _, state, val1, val2): self.ca.set_pv_value("DAQ:R04:DATA:SIM", val1) self.ca.set_pv_value("DAQ:R05:DATA:SIM", val2) self.ca.assert_that_pv_is("RB2:MODE", state) @parameterized.expand( parameterized_list([ ("FAULT (LOW)", 0, 0), ("PORT 3 (RQ18-20)", 1, 0), ("PORT 4 (RQ21-23)", 0, 1), ("FAULT (HIGH)", 1, 1), ])) @skip_if_devsim("DAQ does not exist in devsim") def test_GIVEN_mock_DAQ_inputs_THEN_PORT3_4_mode_is_correct( self, _, state, val1, val2): self.ca.set_pv_value("DAQ:R02:DATA:SIM", val1) self.ca.set_pv_value("DAQ:R03:DATA:SIM", val2) self.ca.assert_that_pv_is("PORT3_4:MODE", state) @skip_if_devsim("DAQ does not exist in devsim") def test_GIVEN_fault_condition_THEN_RB2_alarms_correct(self): self.ca.set_pv_value("DAQ:R04:DATA:SIM", 0) self.ca.set_pv_value("DAQ:R05:DATA:SIM", 0) self.ca.assert_that_pv_alarm_is("RB2:MODE", ChannelAccess.Alarms.MAJOR) @skip_if_devsim("DAQ does not exist in devsim") def test_GIVEN_high_fault_condition_THEN_PORT3_4_alarms_correct(self): self.ca.set_pv_value("DAQ:R02:DATA:SIM", 1) self.ca.set_pv_value("DAQ:R03:DATA:SIM", 1) self.ca.assert_that_pv_alarm_is("PORT3_4:MODE", ChannelAccess.Alarms.MAJOR) @skip_if_devsim("DAQ does not exist in devsim") def test_GIVEN_low_fault_condition_THEN_PORT3_4_alarms_correct(self): self.ca.set_pv_value("DAQ:R02:DATA:SIM", 0) self.ca.set_pv_value("DAQ:R03:DATA:SIM", 0) self.ca.assert_that_pv_alarm_is("PORT3_4:MODE", ChannelAccess.Alarms.MAJOR)
class VoltageTests(unittest.TestCase): voltage_values = [0, 10.1111111, 10e1, 20e-2, 200] voltage_values_which_give_alarms = [ -50, MIN_SEPARATOR_VOLT, MAX_SEPARATOR_VOLT, 250 ] def setUp(self): self.ca = ChannelAccess(20, device_prefix=DEVICE_PREFIX) shared_setup(self.ca) def test_that_GIVEN_sim_val_0_and_data_0_WHEN_voltage_set_point_changed_THEN_data_changed( self): # GIVEN self.ca.set_pv_value("DAQ:VOLT:SIM", 0) self.ca.assert_that_pv_is("DAQ:VOLT:SP", 0) # WHEN self.ca.set_pv_value("VOLT:SP", 20.) # THEN self.ca.assert_that_pv_is("DAQ:VOLT:SP", 20. * DAQ_VOLT_WRITE_SCALE_FACTOR) @parameterized.expand(parameterized_list(voltage_values)) def test_that_WHEN_set_THEN_the_voltage_changes(self, _, value): # WHEN self.ca.set_pv_value("VOLT:SP", value) # THEN self.ca.assert_that_pv_is_number("VOLT", value, MARGIN_OF_ERROR) @parameterized.expand(parameterized_list(voltage_values_which_give_alarms)) def test_that_WHEN_voltage_out_of_range_THEN_alarm_raised(self, _, value): # WHEN self.ca.set_pv_value("VOLT:SP", value) # THEN self.ca.assert_that_pv_alarm_is("VOLT", ChannelAccess.Alarms.MAJOR) def test_that_GIVEN_voltage_in_range_WHEN_setpoint_is_above_range_THEN_setpoint_is_set_to_max_value( self): # GIVEN self.ca.set_pv_value("VOLT:SP", 30) self.ca.assert_that_pv_is("VOLT", 30) # WHEN self.ca.set_pv_value("VOLT:SP", 215.) # THEN self.ca.assert_that_pv_is("VOLT:SP", MAX_SEPARATOR_VOLT) self.ca.assert_that_pv_is("VOLT", MAX_SEPARATOR_VOLT) def test_that_GIVEN_voltage_in_range_WHEN_setpoint_is_below_range_THEN_setpoint_is_set_to_min_value( self): # GIVEN self.ca.set_pv_value("VOLT:SP", 30) self.ca.assert_that_pv_is("VOLT", 30) # WHEN self.ca.set_pv_value("VOLT:SP", -50) # THEN self.ca.assert_that_pv_is("VOLT", MIN_SEPARATOR_VOLT) self.ca.assert_that_pv_is("VOLT:SP", MIN_SEPARATOR_VOLT) def test_GIVEN_data_to_be_filtered_WHEN_filtering_applied_THEN_returned_data_has_correct_shape( self): # stride_length = 1 # GIVEN self.ca.set_pv_value("DAQ:VOLT:WV:SIM", DAQ_DATA) # THEN returned_data_shape = self.ca.get_pv_value("FILTERED:VOLT.NORD") # THEN self.assertEqual(returned_data_shape, len(DAQ_DATA) - STRIDE_LENGTH) def test_GIVEN_unfiltered_data_WHEN_filtering_applied_THEN_corrected_data_is_returned( self): # GIVEN # Writing directly to DAQ:VOLT, need to remove the scaling factor self.ca.set_pv_value("DAQ:VOLT:WV:SIM", DAQ_DATA * DAQ_VOLT_WRITE_SCALE_FACTOR) # THEN returned_data_shape = int(self.ca.get_pv_value("FILTERED:VOLT.NORD")) returned_data = self.ca.get_pv_value( "FILTERED:VOLT")[:returned_data_shape] filtered_data = apply_average_filter(DAQ_DATA) self.assertEqual(len(returned_data), len(filtered_data)) for filtered_value, reference_value in zip(returned_data, filtered_data): self.assertAlmostEqual(filtered_value, reference_value, places=3)
class Sans2dVacuumSystemTests(unittest.TestCase): """ Tests for the SANS2D vacuum system, based on a FINS PLC. """ def setUp(self): self._ioc = IOCRegister.get_running("FINS_01") self.ca = ChannelAccess(device_prefix=ioc_prefix) def test_WHEN_ioc_is_run_THEN_heartbeat_record_exists(self): self.ca.assert_setting_setpoint_sets_readback(1, "HEARTBEAT", "SIM:HEARTBEAT") @parameterized.expand([(1, "IN", ChannelAccess.Alarms.NONE), (2, "OUT", ChannelAccess.Alarms.NONE), (3, "UNKNOWN", ChannelAccess.Alarms.MAJOR), (4, "ERROR", ChannelAccess.Alarms.MAJOR), (5, "ERROR(IN)", ChannelAccess.Alarms.MAJOR), (6, "ERROR(OUT)", ChannelAccess.Alarms.MAJOR)]) def test_WHEN_data_changes_THEN_monitor_status_correct( self, raw_value, enum_string, alarm): self.ca.set_pv_value("SIM:ADDR:1001", raw_value) self.ca.assert_that_pv_is("MONITOR3:STATUS", enum_string) self.ca.assert_that_pv_alarm_is("MONITOR3:STATUS", alarm) @parameterized.expand([(7, "CLOSED", ChannelAccess.Alarms.NONE), (8, "OPEN", ChannelAccess.Alarms.NONE), (16, "ERROR", ChannelAccess.Alarms.MAJOR), (24, "ERROR(OPEN)", ChannelAccess.Alarms.MAJOR)]) def test_WHEN_data_changes_THEN_shutter_status_correct( self, raw_value, enum_string, alarm): self.ca.set_pv_value("SIM:ADDR:1001", raw_value) self.ca.assert_that_pv_is("SHUTTER:STATUS", enum_string) self.ca.assert_that_pv_alarm_is("SHUTTER:STATUS", alarm) @parameterized.expand([(127, "CLOSED", ChannelAccess.Alarms.NONE), (128, "OPEN", ChannelAccess.Alarms.NONE), (256, "ERROR", ChannelAccess.Alarms.MAJOR), (384, "ERROR(OPEN)", ChannelAccess.Alarms.MAJOR)]) def test_WHEN_data_changes_THEN_v8_status_correct(self, raw_value, enum_string, alarm): self.ca.set_pv_value("SIM:ADDR:1001", raw_value) self.ca.assert_that_pv_is("V8:STATUS", enum_string) self.ca.assert_that_pv_alarm_is("V8:STATUS", alarm) def test_WHEN_common_alarm_low_THEN_common_alarm_bad_high_and_alarm(self): self.ca.set_pv_value("SIM:ADDR:1001", 0) self.ca.assert_that_pv_is("COMMON_ALARM:BAD", 1) self.ca.assert_that_pv_alarm_is("COMMON_ALARM:BAD", ChannelAccess.Alarms.MAJOR) def test_WHEN_common_alarm_high_THEN_common_alarm_bad_low_and_no_alarm( self): self.ca.set_pv_value("SIM:ADDR:1001", 32768) self.ca.assert_that_pv_is("COMMON_ALARM:BAD", 0) self.ca.assert_that_pv_alarm_is("COMMON_ALARM:BAD", ChannelAccess.Alarms.NONE) @parameterized.expand([(1, "DEFLATED", ChannelAccess.Alarms.NONE), (2, "INFLATING", ChannelAccess.Alarms.NONE), (4, "INFLATED", ChannelAccess.Alarms.NONE), (8, "DEFLATING", ChannelAccess.Alarms.NONE)]) def test_WHEN_data_changes_THEN_seal_status_correct( self, raw_value, enum_string, alarm): self.ca.set_pv_value("SIM:ADDR:1004", raw_value) self.ca.assert_that_pv_is("SEAL:STATUS", enum_string) self.ca.assert_that_pv_alarm_is("SEAL:STATUS", alarm) @parameterized.expand([(0, 0), (4000, 10000), (2000, 5000)]) def test_WHEN_seal_supply_pressure_changes_THEN_correctly_converted( self, raw_value, expected_converted_val): self.ca.set_pv_value("SIM:SEAL:SUPPLY:PRESS:RAW", raw_value) self.ca.assert_that_pv_is("SEAL:SUPPLY:PRESS", expected_converted_val) def _set_sp_and_assert(self, set_pv, state, expected_state=None, int_state=None): if int_state is None: int_state = state if expected_state is None: expected_state = state self.ca.set_pv_value("{}:STATUS:SP".format(set_pv), int_state) self.ca.assert_that_pv_monitor_gets_values( "{}:{}:SP".format(set_pv, expected_state), [expected_state, "..."]) def test_WHEN_opening_and_closing_shutter_THEN_propogates(self): self._set_sp_and_assert("SHUTTER", "OPEN") self._set_sp_and_assert("SHUTTER", "CLOSE") self._set_sp_and_assert("SHUTTER", "OPEN") def test_WHEN_opening_and_closing_shutter_with_numbers_THEN_propogates( self): self._set_sp_and_assert("SHUTTER", "OPEN", 1) self._set_sp_and_assert("SHUTTER", "CLOSE", 0) self._set_sp_and_assert("SHUTTER", "OPEN", 1) def test_WHEN_insert_and_extract_monitor_THEN_propogates(self): self._set_sp_and_assert("MONITOR3", "IN", "INSERT") self._set_sp_and_assert("MONITOR3", "OUT", "EXTRACT") self._set_sp_and_assert("MONITOR3", "IN", "INSERT") def test_WHEN_insert_and_extract_monitor_with_numbers_THEN_propogates( self): self._set_sp_and_assert("MONITOR3", "IN", "INSERT", 1) self._set_sp_and_assert("MONITOR3", "OUT", "EXTRACT", 0) self._set_sp_and_assert("MONITOR3", "IN", "INSERT", 1) def test_WHEN_start_and_stop_guide_THEN_propogates(self): self._set_sp_and_assert("GUIDE", "START") self._set_sp_and_assert("GUIDE", "STOP") self._set_sp_and_assert("GUIDE", "START") def test_WHEN_start_and_stop_guide_with_numbers_THEN_propogates(self): self._set_sp_and_assert("GUIDE", "START", 1) self._set_sp_and_assert("GUIDE", "STOP", 0) self._set_sp_and_assert("GUIDE", "START", 1) def test_WHEN_begin_run_in_auto_shutter_mode_THEN_shutter_opened(self): self.ca.set_pv_value("SHUTTER:STATUS:SP", "CLOSE", wait=True) self.ca.set_pv_value("SHUTTER:AUTO", 1, wait=True) self.ca.process_pv("SHUTTER:OPEN_IF_AUTO") self.ca.assert_that_pv_is("SHUTTER:STATUS:SP", "OPEN") def test_WHEN_begin_run_in_manual_shutter_mode_THEN_shutter_opened(self): self.ca.set_pv_value("SHUTTER:STATUS:SP", "CLOSE", wait=True) self.ca.set_pv_value("SHUTTER:AUTO", 0, wait=True) self.ca.process_pv("SHUTTER:OPEN_IF_AUTO") self.ca.assert_that_pv_is_not("SHUTTER:STATUS:SP", "OPEN", timeout=5) def test_WHEN_end_run_in_auto_shutter_mode_THEN_shutter_opened(self): self.ca.set_pv_value("SHUTTER:STATUS:SP", "OPEN", wait=True) self.ca.set_pv_value("SHUTTER:AUTO", 1, wait=True) self.ca.process_pv("SHUTTER:CLOSE_IF_AUTO") self.ca.assert_that_pv_is("SHUTTER:STATUS:SP", "CLOSE") def test_WHEN_end_run_in_manual_shutter_mode_THEN_shutter_opened(self): self.ca.set_pv_value("SHUTTER:STATUS:SP", "OPEN", wait=True) self.ca.set_pv_value("SHUTTER:AUTO", 0, wait=True) self.ca.process_pv("SHUTTER:CLOSE_IF_AUTO") self.ca.assert_that_pv_is_not("SHUTTER:STATUS:SP", "CLOSE", timeout=5)
class StabilityTests(unittest.TestCase): STOP_DATA_THREAD = threading.Event() def setUp(self): self.ca = ChannelAccess(20, device_prefix=DEVICE_PREFIX) self.STOP_DATA_THREAD.set() shared_setup(self.ca) @staticmethod def evaluate_current_instability(current_values): """ Evaluates the input current values against the stability criterion. Args: current_values: Array of input currents Returns: current_instability: Boolean array of len(current_values). True where element is unstable, else False """ current_instability = [ curr_measured >= (CURR_STEADY + CURR_LIMIT) for curr_measured in current_values ] return current_instability @staticmethod def evaluate_voltage_instability(voltage_values): """ Evaluates the input voltages against the stability criterion. Args: voltage_values: Array of input voltages Returns: voltage_instability: Boolean array of len(voltage_values). True where element is unstable, else False """ voltage_instability = [(VOLT_LOWERLIM >= volt_measured) or (volt_measured >= VOLT_UPPERLIM) for volt_measured in voltage_values] return voltage_instability def get_out_of_range_samples(self, current_values, voltage_values): """ Calculates the number of points which lie out of stability limits for a current and voltage dataset. Args: current_values: Array of input current values voltage_values: Array of input voltage values Returns: out_of_range_count: Integer, the number of samples in the dataset which are out of range """ current_instability = self.evaluate_current_instability(current_values) voltage_instability = self.evaluate_voltage_instability(voltage_values) overall_instability = [ curr or volt for curr, volt in zip(current_instability, voltage_instability) ] out_of_range_count = sum(overall_instability) return out_of_range_count def write_simulated_current(self, curr_data): """ Scales a current waveform to the DAQ voltage range and writes to current simulation PV Inputs: curr_data: list of floats containing the current data in engineering units """ scaled_data = [x / DAQ_CURR_READ_SCALE_FACTOR for x in curr_data] self.ca.set_pv_value("DAQ:CURR:WV:SIM", scaled_data, wait=True, sleep_after_set=0.0) def write_simulated_voltage(self, volt_data): """ Scales a voltage waveform to the DAQ voltage range and writes to voltage simulation PV Inputs: volt_data: list of floats containing the voltage data in engineering units """ scaled_data = [x * DAQ_VOLT_WRITE_SCALE_FACTOR for x in volt_data] self.ca.set_pv_value("DAQ:VOLT:WV:SIM", scaled_data, wait=True, sleep_after_set=0.0) @parameterized.expand([ ("steady_current_steady_voltage", [CURR_STEADY] * SAMPLE_LEN, [VOLT_STEADY] * SAMPLE_LEN), ("steady_current_unsteady_voltage", [CURR_STEADY] * SAMPLE_LEN, VOLTAGE_DATA), ("unsteady_current_steady_voltage", CURRENT_DATA, [VOLT_STEADY] * SAMPLE_LEN), ("unsteady_current_and_voltage", CURRENT_DATA, VOLTAGE_DATA) ]) def test_GIVEN_current_and_voltage_data_WHEN_limits_are_tested_THEN_number_of_samples_out_of_range_returned( self, _, curr_data, volt_data): self.write_simulated_current(curr_data) self.write_simulated_voltage(volt_data) averaged_volt_data = apply_average_filter(volt_data, stride=STRIDE_LENGTH) expected_out_of_range_samples = self.get_out_of_range_samples( curr_data, averaged_volt_data) self.ca.assert_that_pv_is_number("_STABILITYCHECK", expected_out_of_range_samples, tolerance=0.05 * expected_out_of_range_samples) def test_GIVEN_noisy_voltage_data_WHEN_moving_average_is_applied_THEN_averaged_data_has_fewer_out_of_range_points( self): curr_data = [CURR_STEADY] * SAMPLE_LEN self.write_simulated_current(curr_data) self.write_simulated_voltage(DAQ_DATA) out_of_range_samples_before_average = self.get_out_of_range_samples( curr_data, DAQ_DATA) out_of_range_samples_after_average = self.ca.get_pv_value( "_STABILITYCHECK") self.assertLess(out_of_range_samples_after_average, out_of_range_samples_before_average) @parameterized.expand([("unsteady_current", simulate_current_data(), [VOLT_STEADY] * SAMPLE_LEN), ("unsteady_voltage", [CURR_STEADY] * SAMPLE_LEN, simulate_voltage_data())]) def test_GIVEN_multiple_samples_in_one_second_WHEN_buffer_read_THEN_buffer_reads_all_out_of_range_samples( self, _, curr_data, volt_data): # Setting this to 3 as channel access takes ~0.25s. May need reducing for slow machines. writes_per_second = 3 # Change buffer length is 1 so UNSTABLETIME PV only reports 1 second of data length_of_buffer = 1 self.ca.set_pv_value("WINDOWSIZE", length_of_buffer) self.ca.set_pv_value("RESETWINDOW", 1) averaged_volt_data = apply_average_filter(volt_data, stride=STRIDE_LENGTH) expected_out_of_range_samples = self.get_out_of_range_samples( curr_data, averaged_volt_data) * writes_per_second self.STOP_DATA_THREAD.clear() scaled_curr_data = [x / DAQ_CURR_READ_SCALE_FACTOR for x in curr_data] scaled_volt_data = [x * DAQ_VOLT_WRITE_SCALE_FACTOR for x in volt_data] data_supply_thread = threading.Thread( target=stream_data, args=(self.ca, writes_per_second, scaled_curr_data, scaled_volt_data, self.STOP_DATA_THREAD)) # GIVEN data_supply_thread.start() self.assertGreater(writes_per_second, 1) # THEN self.ca.assert_that_pv_is_number("UNSTABLETIME", expected_out_of_range_samples * SAMPLETIME, timeout=60.0, tolerance=0.1 * SAMPLETIME) self.STOP_DATA_THREAD.set() def test_GIVEN_buffer_with_data_WHEN_resetwindow_PV_processed_THEN_buffer_is_cleared( self): number_of_writes = 50 averaged_volt_data = apply_average_filter(DAQ_DATA, stride=STRIDE_LENGTH) expected_out_of_range_samples = self.get_out_of_range_samples( CURRENT_DATA, averaged_volt_data) expected_out_of_range_samples *= number_of_writes * SAMPLETIME # GIVEN for i in range(number_of_writes): self.write_simulated_current(CURRENT_DATA) self.write_simulated_voltage(DAQ_DATA) self.ca.assert_that_pv_is_number("UNSTABLETIME", expected_out_of_range_samples) # WHEN self.ca.set_pv_value("RESETWINDOW", 1) # THEN self.ca.assert_that_pv_is_number("UNSTABLETIME", 0) def test_GIVEN_full_buffer_WHEN_more_data_added_to_buffer_THEN_oldest_values_overwritten( self): length_of_buffer = 10 testvalue = 50.0 self.assertNotEqual(testvalue, 1.0) self.ca.set_pv_value("WINDOWSIZE", length_of_buffer) self.ca.set_pv_value("RESETWINDOW", 1) self.ca.set_pv_value("_COUNTERTIMING.SCAN", "Passive") # GIVEN for i in range(length_of_buffer): self.ca.set_pv_value("_ADDCOUNTS", 1.0 / SAMPLETIME, wait=True, sleep_after_set=0.0) self.ca.set_pv_value("_COUNTERTIMING.PROC", 1, wait=True, sleep_after_set=0.0) self.ca.assert_that_pv_is_number("UNSTABLETIME", length_of_buffer) # WHEN self.ca.set_pv_value("_ADDCOUNTS", testvalue / SAMPLETIME, wait=True, sleep_after_set=0.0) self.ca.set_pv_value("_COUNTERTIMING.PROC", 1, wait=True, sleep_after_set=0.0) # THEN self.ca.assert_that_pv_is_number("UNSTABLETIME", (length_of_buffer - 1.) + testvalue) def test_GIVEN_input_data_over_several_seconds_WHEN_stability_PV_read_THEN_all_unstable_time_counted( self): # This number needs to be large enough to write over several seconds. Writing over multiple seconds is asserted. number_of_writes = 50 time1 = perf_counter() expected_out_of_range_samples = self.get_out_of_range_samples( CURRENT_DATA, VOLTAGE_DATA) * number_of_writes * SAMPLETIME # GIVEN for i in range(number_of_writes): self.write_simulated_current(CURRENT_DATA) self.write_simulated_voltage(VOLTAGE_DATA) processtime = perf_counter() - time1 self.assertGreater(processtime, 1.) # THEN self.ca.assert_that_pv_is_number("UNSTABLETIME", expected_out_of_range_samples, tolerance=0.025 * expected_out_of_range_samples) def test_GIVEN_power_supply_switched_on_WHEN_power_supply_out_of_stability_threshold_THEN_stability_PV_equals_zero_and_goes_into_alarm( self): # GIVEN self.ca.set_pv_value("VOLT:SP", 10) self.ca.assert_that_pv_is("POWER:STAT", "ON") # WHEN stability_threshold = self.ca.get_pv_value("THRESHOLD") testvalue = stability_threshold * 100 self.ca.set_pv_value("_ADDCOUNTS", testvalue / SAMPLETIME, wait=True, sleep_after_set=0.0) self.ca.assert_that_pv_is("STABILITY", "Unstable") # THEN self.ca.assert_that_pv_alarm_is("STABILITY", ChannelAccess.Alarms.MAJOR) def test_GIVEN_power_supply_switched_off_WHEN_power_supply_out_of_stability_threshold_THEN_no_alarms_raised( self): # GIVEN self.ca.set_pv_value("VOLT:SP", 0) self.ca.assert_that_pv_is("POWER:STAT", "OFF") # WHEN stability_threshold = self.ca.get_pv_value("THRESHOLD") testvalue = stability_threshold * 100 self.ca.set_pv_value("_ADDCOUNTS", testvalue / SAMPLETIME, wait=True, sleep_after_set=0.0) self.ca.assert_that_pv_is("STABILITY", "Unstable") # THEN self.ca.assert_that_pv_alarm_is("STABILITY", ChannelAccess.Alarms.NONE) def test_GIVEN_separator_stable_THEN_separator_stability_text_label_is_stable( self): # GIVEN # asserted in setup that separator is stable # THEN self.ca.assert_that_pv_is("STABILITY", "Stable")
class Lakeshore372Tests(unittest.TestCase): """ Tests for the lakeshore 372 IOC. """ def setUp(self): self._lewis, self._ioc = get_running_lewis_and_ioc( _EMULATOR_NAME, DEVICE_PREFIX) self.ca = ChannelAccess(device_prefix=DEVICE_PREFIX, default_timeout=15) def test_WHEN_device_is_started_THEN_it_is_not_disabled(self): self.ca.assert_that_pv_is("DISABLE", "COMMS ENABLED") def _assert_readback_alarm_states(self, alarm): for readback_pv in [ "TEMP", "TEMP:SP:RBV", "P", "I", "D", "HEATER:POWER", "RESISTANCE", "HEATER:RANGE" ]: self.ca.assert_that_pv_alarm_is(readback_pv, alarm) @contextlib.contextmanager def _simulate_disconnected_device(self): self._lewis.backdoor_set_on_device("connected", False) try: yield finally: self._lewis.backdoor_set_on_device("connected", True) @parameterized.expand(parameterized_list(TEST_TEMPERATURES)) def test_WHEN_temp_setpoint_is_set_THEN_actual_temperature_updates( self, _, temperature): self.ca.assert_setting_setpoint_sets_readback(temperature, set_point_pv="TEMP:SP", readback_pv="TEMP") @parameterized.expand(parameterized_list(TEST_TEMPERATURES)) def test_WHEN_temp_setpoint_is_set_THEN_setpoint_readback_updates( self, _, temperature): self.ca.assert_setting_setpoint_sets_readback( temperature, set_point_pv="TEMP:SP", readback_pv="TEMP:SP:RBV") @parameterized.expand(parameterized_list(HEATER_RANGES)) def test_WHEN_heater_range_is_set_THEN_heater_range_readback_updates( self, _, rng): self.ca.assert_setting_setpoint_sets_readback( rng, set_point_pv="HEATER:RANGE:SP", readback_pv="HEATER:RANGE") @parameterized.expand(parameterized_list(TEST_HEATER_POWER_PERCENTAGES)) @skip_if_recsim("Uses lewis backdoor") def test_WHEN_heater_power_is_set_via_backdoor_THEN_heater_power_pv_updates( self, _, pwr): self._lewis.backdoor_set_on_device("heater_power", pwr) self.ca.assert_that_pv_is_number("HEATER:POWER", pwr, tolerance=0.001) @parameterized.expand(parameterized_list(TEST_SENSOR_RESISTANCES)) @skip_if_recsim("Uses lewis backdoor") def test_WHEN_sensor_resistance_is_set_via_backdoor_THEN_resistance_pv_updates( self, _, res): self._lewis.backdoor_set_on_device("sensor_resistance", res) self.ca.assert_that_pv_is_number("RESISTANCE", res, tolerance=0.000001) @parameterized.expand(parameterized_list(TEST_PID_PARAMS)) def test_WHEN_pid_parameters_are_set_THEN_readbacks_update( self, _, p, i, d): # Simulate a script by writing PIDs all in one go without waiting for update first. self.ca.set_pv_value("P:SP", p) self.ca.set_pv_value("I:SP", i) self.ca.set_pv_value("D:SP", d) self.ca.assert_that_pv_is("P", p) self.ca.assert_that_pv_is("I", i) self.ca.assert_that_pv_is("D", d) @skip_if_recsim("Recsim does not support simulated disconnection") def test_WHEN_device_does_not_respond_THEN_pvs_go_into_invalid_alarm(self): self._assert_readback_alarm_states(self.ca.Alarms.NONE) with self._simulate_disconnected_device(): self._assert_readback_alarm_states(self.ca.Alarms.INVALID) # Assert alarms clear on reconnection self._assert_readback_alarm_states(self.ca.Alarms.NONE) @skip_if_recsim("Complex logic not testable in recsim") def test_WHEN_temperature_setpoint_is_sent_THEN_control_mode_changed_to_5( self): # 5 is the control mode for closed loop PID control, which should always be sent along with a temperature set. self._lewis.backdoor_set_on_device("control_mode", 0) self._lewis.assert_that_emulator_value_is("control_mode", 0, cast=int) self.ca.set_pv_value("TEMP:SP", 0) self._lewis.assert_that_emulator_value_is("control_mode", 5, cast=int)
class Sans2dVacCollisionAvoidanceTests(unittest.TestCase): """ Tests for the sans2d vacuum tank motor extensions. """ def setUp(self): self.ca = ChannelAccess(device_prefix="MOT", default_timeout=30) with ManagerMode(ChannelAccess()): self._disable_collision_avoidance() for axis in BAFFLES_AND_DETECTORS_Z_AXES: current_position = self.ca.get_pv_value("{}".format(axis)) new_position = self._get_axis_default_position( "{}".format(axis)) self.ca.set_pv_value("{}:MTR.VMAX".format(axis), TEST_SPEED, sleep_after_set=0) self.ca.set_pv_value("{}:MTR.VELO".format(axis), TEST_SPEED, sleep_after_set=0) self.ca.set_pv_value("{}:MTR.ACCL".format(axis), TEST_ACCELERATION, sleep_after_set=0) if current_position != new_position: self.ca.set_pv_value("{}:SP".format(axis), new_position, sleep_after_set=0) timeout = self._get_timeout_for_moving_to_position( axis, new_position) self.ca.assert_that_pv_is("{}".format(axis), new_position, timeout=timeout) # re-enable collision avoidance self._enable_collision_avoidance() def _disable_collision_avoidance(self): self._set_collision_avoidance_state(1, "DISABLED") def _enable_collision_avoidance(self): self._set_collision_avoidance_state(0, "ENABLED") def _set_collision_avoidance_state(self, write_value, read_value): # Do nothing if manager mode is already in correct state if ChannelAccess().get_pv_value(ManagerMode.MANAGER_MODE_PV) != "Yes": cm = ManagerMode(ChannelAccess()) else: cm = nullcontext() with cm: err = None for _ in range(20): try: self.ca.set_pv_value("SANS2DVAC:COLLISION_AVOIDANCE", write_value, sleep_after_set=0) break except WriteAccessException as e: err = e sleep(1) else: raise err self.ca.assert_that_pv_is("SANS2DVAC:COLLISION_AVOIDANCE", read_value) @contextlib.contextmanager def _assert_last_stop_time_updated(self, timeout=5): initial_stop_time = self.ca.get_pv_value("SANS2DVAC:_LAST_STOP_TIME") try: yield except Exception as e: raise e else: self.ca.assert_that_pv_is_not("SANS2DVAC:_LAST_STOP_TIME", initial_stop_time, timeout=timeout) @parameterized.expand(parameterized_list(AXIS_PAIRS)) def test_GIVEN_motor_interval_above_minor_warning_threshold_THEN_interval_is_correct_and_not_in_alarm( self, _, axis_pair): # disable collision avoidance so it does not interfere with checking the intervals and their alarm status self._disable_collision_avoidance() rear_axis_position = self.ca.get_pv_value(axis_pair.rear_axis) front_axis_position = rear_axis_position - 50 - MINOR_ALARM_INTERVAL_THRESHOLD expected_interval = rear_axis_position - front_axis_position self.ca.set_pv_value(axis_pair.front_axis_sp, front_axis_position, sleep_after_set=0) timeout = self._get_timeout_for_moving_to_position( axis_pair.front_axis, front_axis_position) self.ca.assert_that_pv_is_number("SANS2DVAC:{}:INTERVAL".format( axis_pair.name), expected_interval, timeout=timeout, tolerance=0.1) self.ca.assert_that_pv_alarm_is( "SANS2DVAC:{}:INTERVAL".format(axis_pair.name), self.ca.Alarms.NONE) @parameterized.expand(parameterized_list(AXIS_PAIRS)) def test_GIVEN_setpoint_interval_above_minor_warning_threshold_THEN_interval_is_correct_and_not_in_alarm( self, _, axis_pair): # disable collision avoidance so it does not interfere with checking the intervals and their alarm status self._disable_collision_avoidance() rear_axis_position = 1000 front_axis_position = rear_axis_position - 50 - MINOR_ALARM_INTERVAL_THRESHOLD expected_interval = rear_axis_position - front_axis_position self.ca.set_pv_value(axis_pair.front_axis_sp, front_axis_position, sleep_after_set=0) self.ca.set_pv_value(axis_pair.rear_axis_sp, rear_axis_position, sleep_after_set=0) self._assert_axis_position_reached(axis_pair.front_axis, front_axis_position) self._assert_axis_position_reached(axis_pair.rear_axis, rear_axis_position) self.ca.assert_that_pv_is_number("SANS2DVAC:{}:INTERVAL".format( axis_pair.name), expected_interval, timeout=5, tolerance=0.1) self.ca.assert_that_pv_alarm_is( "SANS2DVAC:{}:INTERVAL".format(axis_pair.name), self.ca.Alarms.NONE) @parameterized.expand(parameterized_list(AXIS_PAIRS)) def test_GIVEN_motor_interval_under_minor_warning_threshold_THEN_interval_is_correct_and_in_minor_alarm( self, _, axis_pair): # disable collision avoidance so it does not interfere with checking the intervals and their alarm status self._disable_collision_avoidance() rear_position = self.ca.get_pv_value(axis_pair.rear_axis) front_new_position = rear_position - MINOR_ALARM_INTERVAL_THRESHOLD + 1 expected_interval = rear_position - front_new_position self.ca.set_pv_value(axis_pair.front_axis_sp, front_new_position, sleep_after_set=0) timeout = self._get_timeout_for_moving_to_position( axis_pair.front_axis, front_new_position) self.ca.assert_that_pv_is_number("SANS2DVAC:{}:INTERVAL".format( axis_pair.name), expected_interval, timeout=timeout, tolerance=0.1) self.ca.assert_that_pv_alarm_is( "SANS2DVAC:{}:INTERVAL".format(axis_pair.name), self.ca.Alarms.MINOR) @parameterized.expand(parameterized_list(AXIS_PAIRS)) def test_GIVEN_setpoint_interval_under_minor_warning_threshold_THEN_interval_is_correct_and_in_minor_alarm( self, _, axis_pair): # disable collision avoidance so it does not interfere with checking the intervals and their alarm status self._disable_collision_avoidance() rear_axis_position = 1000 front_axis_position = rear_axis_position - MINOR_ALARM_INTERVAL_THRESHOLD + 1 expected_interval = rear_axis_position - front_axis_position self.ca.set_pv_value(axis_pair.front_axis_sp, front_axis_position, sleep_after_set=0) self.ca.set_pv_value(axis_pair.rear_axis_sp, rear_axis_position, sleep_after_set=0) self._assert_axis_position_reached(axis_pair.front_axis, front_axis_position) self._assert_axis_position_reached(axis_pair.rear_axis, rear_axis_position) self.ca.assert_that_pv_is_number("SANS2DVAC:{}:INTERVAL".format( axis_pair.name), expected_interval, timeout=5, tolerance=0.1) self.ca.assert_that_pv_alarm_is( "SANS2DVAC:{}:INTERVAL".format(axis_pair.name), self.ca.Alarms.MINOR) @parameterized.expand(parameterized_list(AXIS_PAIRS)) def test_GIVEN_motor_interval_under_major_warning_threshold_THEN_interval_is_correct_and_in_major_alarm( self, _, axis_pair): # disable collision avoidance so it does not interfere with checking the intervals and their alarm status self._disable_collision_avoidance() rear_axis_position = self.ca.get_pv_value(axis_pair.rear_axis) front_axis_position = rear_axis_position - MAJOR_ALARM_INTERVAL_THRESHOLD + 1 expected_interval = rear_axis_position - front_axis_position self.ca.set_pv_value(axis_pair.front_axis_sp, front_axis_position, sleep_after_set=0) timeout = self._get_timeout_for_moving_to_position( axis_pair.front_axis, front_axis_position) self.ca.assert_that_pv_is_number("SANS2DVAC:{}:INTERVAL".format( axis_pair.name), expected_interval, timeout=timeout, tolerance=0.1) self.ca.assert_that_pv_alarm_is( "SANS2DVAC:{}:INTERVAL".format(axis_pair.name), self.ca.Alarms.MAJOR) @parameterized.expand(parameterized_list(AXIS_PAIRS)) def test_GIVEN_setpoint_interval_under_major_warning_threshold_THEN_interval_is_correct_and_in_major_alarm( self, _, axis_pair): # disable collision avoidance so it does not interfere with checking the intervals and their alarm status self._disable_collision_avoidance() rear_axis_position = 1000 front_axis_position = rear_axis_position - MAJOR_ALARM_INTERVAL_THRESHOLD + 1 expected_interval = rear_axis_position - front_axis_position self.ca.set_pv_value(axis_pair.front_axis_sp, front_axis_position, sleep_after_set=0) self.ca.set_pv_value(axis_pair.rear_axis_sp, rear_axis_position, sleep_after_set=0) self._assert_axis_position_reached(axis_pair.front_axis, front_axis_position) self._assert_axis_position_reached(axis_pair.rear_axis, rear_axis_position) self.ca.assert_that_pv_is_number("SANS2DVAC:{}:INTERVAL".format( axis_pair.name), expected_interval, timeout=5, tolerance=0.1) self.ca.assert_that_pv_alarm_is( "SANS2DVAC:{}:INTERVAL".format(axis_pair.name), self.ca.Alarms.MAJOR) @parameterized.expand(parameterized_list(AXIS_PAIRS)) def test_GIVEN_front_axis_moves_towards_rear_axis_WHEN_setpoint_interval_greater_than_threshold_THEN_motor_not_stopped( self, _, axis_pair): front_axis_new_position = (self.ca.get_pv_value(axis_pair.rear_axis) - axis_pair.minimum_interval) - 50 self.ca.set_pv_value(axis_pair.front_axis_sp, front_axis_new_position, sleep_after_set=0) self._assert_axis_position_reached(axis_pair.front_axis, front_axis_new_position) @parameterized.expand(parameterized_list(AXIS_PAIRS)) def test_GIVEN_front_axis_moves_towards_rear_axis_WHEN_setpoint_interval_smaller_than_threshold_THEN_motor_stops( self, _, axis_pair): with self._assert_last_stop_time_updated(): front_axis_new_position = (self.ca.get_pv_value( axis_pair.rear_axis) - axis_pair.minimum_interval) + 50 self.ca.set_pv_value(axis_pair.front_axis_sp, front_axis_new_position, sleep_after_set=0) self.ca.assert_that_pv_is("{}:MTR.MOVN".format( axis_pair.front_axis), 1, timeout=1) self.ca.assert_that_pv_is("{}:MTR.TDIR".format( axis_pair.front_axis), 1, timeout=1) timeout = self._get_timeout_for_moving_to_position( axis_pair.front_axis, front_axis_new_position) assert_axis_not_moving(axis_pair.front_axis, timeout=timeout) self.ca.assert_that_pv_is_not(axis_pair.front_axis, front_axis_new_position, timeout=timeout) @parameterized.expand(parameterized_list(AXIS_PAIRS)) def test_GIVEN_front_axis_within_threhsold_distance_to_rear_axis_WHEN_set_to_move_away_THEN_motor_not_stopped( self, _, axis_pair): front_axis_new_position = (self.ca.get_pv_value(axis_pair.rear_axis) - axis_pair.minimum_interval) + 50 self.ca.set_pv_value(axis_pair.front_axis_sp, front_axis_new_position, sleep_after_set=0) self.ca.assert_that_pv_is("{}:MTR.MOVN".format(axis_pair.front_axis), 1, timeout=1) self.ca.assert_that_pv_is("{}:MTR.TDIR".format(axis_pair.front_axis), 1, timeout=1) timeout = self._get_timeout_for_moving_to_position( axis_pair.front_axis, front_axis_new_position) assert_axis_not_moving(axis_pair.front_axis, timeout=timeout) self.ca.assert_that_pv_is_not(axis_pair.front_axis, front_axis_new_position, timeout=timeout) front_axis_new_position = self.ca.get_pv_value( axis_pair.front_axis) - 200 self.ca.set_pv_value(axis_pair.front_axis_sp, front_axis_new_position, sleep_after_set=0) assert_axis_moving(axis_pair.front_axis, timeout=1) self.ca.assert_that_pv_is("{}:MTR.TDIR".format(axis_pair.front_axis), 0, timeout=1) self._assert_axis_position_reached(axis_pair.front_axis, front_axis_new_position) @parameterized.expand(parameterized_list(AXIS_PAIRS)) def test_GIVEN_rear_axis_moves_towards_front_axis_WHEN_setpoint_interval_greater_than_threshold_THEN_motor_not_stopped( self, _, axis_pair): rear_axis_position = (self.ca.get_pv_value(axis_pair.front_axis) + axis_pair.minimum_interval) + 50 self.ca.set_pv_value(axis_pair.rear_axis_sp, rear_axis_position, sleep_after_set=0) self._assert_axis_position_reached(axis_pair.rear_axis, rear_axis_position) @parameterized.expand(parameterized_list(AXIS_PAIRS)) def test_GIVEN_rear_axis_moves_towards_front_axis_WHEN_setpoint_interval_smaller_than_threshold_THEN_motor_stops( self, _, axis_pair): with self._assert_last_stop_time_updated(): rear_axis_new_position = (self.ca.get_pv_value( axis_pair.front_axis) + axis_pair.minimum_interval) - 50 self.ca.set_pv_value(axis_pair.rear_axis_sp, rear_axis_new_position, sleep_after_set=0) self.ca.assert_that_pv_is("{}:MTR.MOVN".format( axis_pair.rear_axis), 1, timeout=1) self.ca.assert_that_pv_is("{}:MTR.TDIR".format( axis_pair.rear_axis), 0, timeout=1) timeout = self._get_timeout_for_moving_to_position( axis_pair.rear_axis, rear_axis_new_position) assert_axis_not_moving(axis_pair.rear_axis, timeout=timeout) self.ca.assert_that_pv_is_not(axis_pair.rear_axis, rear_axis_new_position, timeout=timeout) @parameterized.expand(parameterized_list(AXIS_PAIRS)) def test_GIVEN_rear_axis_within_threhsold_distance_to_front_axis_WHEN_set_to_move_away_THEN_motor_not_stopped( self, _, axis_pair): rear_axis_new_position = (self.ca.get_pv_value(axis_pair.front_axis) + axis_pair.minimum_interval) - 50 self.ca.set_pv_value(axis_pair.rear_axis_sp, rear_axis_new_position, sleep_after_set=0) self.ca.assert_that_pv_is("{}:MTR.MOVN".format(axis_pair.rear_axis), 1, timeout=1) self.ca.assert_that_pv_is("{}:MTR.TDIR".format(axis_pair.rear_axis), 0, timeout=1) timeout = self._get_timeout_for_moving_to_position( axis_pair.rear_axis, rear_axis_new_position) assert_axis_not_moving(axis_pair.rear_axis, timeout=timeout) self.ca.assert_that_pv_is_not(axis_pair.rear_axis, rear_axis_new_position, timeout=timeout) rear_axis_new_position = self.ca.get_pv_value( axis_pair.rear_axis) + 200 self.ca.set_pv_value(axis_pair.rear_axis_sp, rear_axis_new_position, sleep_after_set=0) assert_axis_moving(axis_pair.rear_axis, timeout=1) self.ca.assert_that_pv_is("{}:MTR.TDIR".format(axis_pair.rear_axis), 1, timeout=1) self._assert_axis_position_reached(axis_pair.rear_axis, rear_axis_new_position) def _invalid_alarm_on_motor(self, motor, invalid): """ Puts a motor into invalid alarm. The simulated motor record doesn't respect SIMS so instead use a readback link pointing at an invalid PV and tell the motor to use the readback link. This causes an invalid alarm as desired. """ if invalid: self.ca.set_pv_value("{}:MTR.RDBL".format(motor), "fake_input_link_doesnt_connect", sleep_after_set=0) self.ca.set_pv_value("{}:MTR.URIP".format(motor), "Yes" if invalid else "No", sleep_after_set=0) self.ca.assert_that_pv_alarm_is( motor, self.ca.Alarms.INVALID if invalid else self.ca.Alarms.NONE) @parameterized.expand(parameterized_list(AXIS_PAIRS)) def test_GIVEN_axis_has_comms_error_THEN_axes_are_stopped( self, _, axis_pair): with self._assert_last_stop_time_updated(timeout=10): self._invalid_alarm_on_motor(axis_pair.front_axis, True) self.ca.assert_that_pv_alarm_is( "SANS2DVAC:{}:INTERVAL".format(axis_pair.name), self.ca.Alarms.INVALID) self._invalid_alarm_on_motor(axis_pair.front_axis, False) self.ca.assert_that_pv_alarm_is( "SANS2DVAC:{}:INTERVAL".format(axis_pair.name), self.ca.Alarms.NONE) with self._assert_last_stop_time_updated(timeout=10): self._invalid_alarm_on_motor(axis_pair.rear_axis, True) self.ca.assert_that_pv_alarm_is( "SANS2DVAC:{}:INTERVAL".format(axis_pair.name), self.ca.Alarms.INVALID) self._invalid_alarm_on_motor(axis_pair.rear_axis, False) self.ca.assert_that_pv_alarm_is( "SANS2DVAC:{}:INTERVAL".format(axis_pair.name), self.ca.Alarms.NONE) def _get_axis_default_position(self, axis): if axis == "FRONTDETZ": new_position = 1000 elif axis == "FRONTBAFFLEZ": new_position = 3000 elif axis == "REARBAFFLEZ": new_position = 5000 elif axis == "REARDETZ": new_position = 7000 else: raise ValueError("invalid axis!") return new_position def _get_timeout_for_moving_to_position(self, moving_axis, new_position): distance_to_travel = abs(new_position - self.ca.get_pv_value(moving_axis)) time_to_accelerate_and_decelerate = 2 * TEST_ACCELERATION # between 0 and full speed, the average speed is half the full speed, same for when decelerating. # Therefore, the distance traveled when accelerating and decelerating is # 2 * (full_speed/2 * acceleration_time), so full_speed / acceleration_time time_at_full_speed = (distance_to_travel - TEST_SPEED * TEST_ACCELERATION) / TEST_SPEED total_time = ceil(time_to_accelerate_and_decelerate + time_at_full_speed) return total_time + 10 # +10 as a small tolerance to avoid instability def _assert_axis_position_reached(self, axis, position): timeout = self._get_timeout_for_moving_to_position(axis, position) self.ca.assert_that_pv_is_number(axis, position, tolerance=0.1, timeout=timeout)
class KeylkgTests(unittest.TestCase): """ Tests for the Keylkg IOC. """ def setUp(self): self._lewis, self._ioc = get_running_lewis_and_ioc( EMULATOR_NAME, DEVICE_PREFIX) self.ca = ChannelAccess(device_prefix=DEVICE_PREFIX) self._lewis.backdoor_run_function_on_device("reset") def test_GIVEN_running_ioc_WHEN_change_to_communication_mode_THEN_mode_changed( self): expected_value = "SET-UP" self.ca.set_pv_value("MODE:SP", expected_value) self.ca.assert_that_pv_is("MODE", expected_value) def test_GIVEN_running_ioc_WHEN_change_to_normal_mode_THEN_mode_changed( self): expected_value = "MEASURE" self.ca.set_pv_value("MODE:SP", expected_value) self.ca.assert_that_pv_is("MODE", expected_value) @parameterized.expand([('low limit', -99.9999), ('test_value_1', -2.3122), ('test_value_2', 12.3423), ('high limit', 99.9999)]) def test_GIVEN_running_ioc_WHEN_set_output1_offset_THEN_output1_offset_updated( self, _, mock_offset): expected_value = mock_offset self.ca.set_pv_value("OFFSET:OUTPUT:1:SP", expected_value) self.ca.assert_that_pv_is_number("OFFSET:OUTPUT:1", expected_value, tolerance=0.001) @parameterized.expand([('exceeds low limit', -100.0000), ('exceeds high limit', 100.000)]) def test_GIVEN_running_ioc_WHEN_set_output1_offset_outside_of_limits_THEN_output1_offset_within_limits( self, _, mock_offset): expected_value = mock_offset self.ca.set_pv_value("OFFSET:OUTPUT:1:SP", expected_value) self.ca.assert_that_pv_is_within_range("OFFSET:OUTPUT:1", -99.9999, 99.9999) @parameterized.expand([('low limit', -99.9999), ('test_value_1', -2.3122), ('test_value_2', 12.3423), ('high limit', 99.9999)]) def test_GIVEN_running_ioc_WHEN_set_output2_offset_THEN_output1_offset_updated( self, _, mock_offset): expected_value = mock_offset self.ca.set_pv_value("OFFSET:OUTPUT:2:SP", expected_value) self.ca.assert_that_pv_is_number("OFFSET:OUTPUT:2", expected_value, tolerance=0.001) @parameterized.expand([('exceeds low limit', -100.0000), ('exceeds high limit', 100.000)]) def test_GIVEN_running_ioc_WHEN_set_output2_offset_outside_of_limits_THEN_output2_offset_within_limits( self, _, mock_offset): expected_value = mock_offset self.ca.set_pv_value("OFFSET:OUTPUT:1:SP", expected_value) self.ca.assert_that_pv_is_within_range("OFFSET:OUTPUT:2", -99.9999, 99.9999) def test_GIVEN_running_ioc_WHEN_change_to_head1_measurement_mode_THEN_mode_changed( self): expected_value = "MULTI-REFLECTIVE" self.ca.set_pv_value("MEASUREMODE:HEAD:A:SP", expected_value) self.ca.assert_that_pv_is("MEASUREMODE:HEAD:A", expected_value) def test_GIVEN_running_ioc_WHEN_change_to_head2_measurement_mode_THEN_mode_changed( self): expected_value = "TRANSPARENT OBJ 1" self.ca.set_pv_value("MEASUREMODE:HEAD:B:SP", expected_value) self.ca.assert_that_pv_is("MEASUREMODE:HEAD:B", expected_value) @skip_if_recsim('Cannot use lewis backdoor in RECSIM') def test_GIVEN_running_ioc_WHEN_in_measure_mode_THEN_output1_takes_data( self): expected_value = 0.1234 self._lewis.backdoor_set_on_device("detector_1_raw_value", expected_value) self.ca.set_pv_value("MODE:SP", "MEASURE") self.ca.assert_that_pv_is("VALUE:OUTPUT:1", expected_value) @skip_if_recsim('No emulation of data capture in RECSIM') def test_GIVEN_running_ioc_WHEN_in_measure_mode_THEN_output2_takes_data( self): expected_value = 0.1234 self._lewis.backdoor_set_on_device("detector_2_raw_value", expected_value) self.ca.set_pv_value("MODE:SP", "MEASURE") self.ca.assert_that_pv_is("VALUE:OUTPUT:2", expected_value) @skip_if_recsim("Unable to use lewis backdoor in RECSIM") def test_GIVEN_device_not_connected_WHEN_get_error_THEN_alarm(self): self._lewis.backdoor_set_on_device('connected', False) self.ca.assert_that_pv_alarm_is('MODE:SP', ChannelAccess.Alarms.INVALID) @skip_if_recsim("Unable to use lewis backdoor in RECSIM") def test_GIVEN_device_not_connected_WHEN_get_error_THEN_alarm(self): expected_value = "Command error" self._lewis.backdoor_set_on_device('input_correct', False) self.ca.assert_that_pv_is_not("ERROR", expected_value) @skip_if_recsim("Unable to use lewis backdoor in RECSIM") def test_GIVEN_running_ioc_WHEN_in_measure_mode_and_reset_output1_THEN_output1_reset( self): expected_value = 0.0000 test_value = 0.1234 self._lewis.backdoor_set_on_device("detector_1_raw_value", test_value) self.ca.set_pv_value("MODE:SP", "MEASURE") self.ca.assert_that_pv_is("VALUE:OUTPUT:1", test_value) self.ca.set_pv_value("RESET:OUTPUT:1:SP", "RESET") self.ca.assert_that_pv_is("VALUE:OUTPUT:1", expected_value) @skip_if_recsim("Unable to use lewis backdoor in RECSIM") def test_GIVEN_running_ioc_WHEN_in_measure_mode_and_reset_output2_THEN_output2_reset( self): expected_value = 0.0000 test_value = 0.1234 self._lewis.backdoor_set_on_device("detector_2_raw_value", test_value) self.ca.set_pv_value("MODE:SP", "MEASURE") self.ca.assert_that_pv_is("VALUE:OUTPUT:2", test_value) self.ca.set_pv_value("RESET:OUTPUT:2:SP", "RESET") self.ca.assert_that_pv_is("VALUE:OUTPUT:2", expected_value) @skip_if_recsim('Cannot use lewis backdoor in RECSIM') def test_GIVEN_running_ioc_WHEN_in_setup_mode_THEN_output1_switches_to_measurement_mode_and_takes_data( self): expected_value = 0.1234 self.ca.set_pv_value("MODE:SP", "SET-UP") self._lewis.backdoor_set_on_device("detector_1_raw_value", expected_value) self.ca.assert_that_pv_is("VALUE:OUTPUT:1", expected_value)
class Lksh218Tests(unittest.TestCase): """ Tests for the Lksh218 IOC. """ def setUp(self): self._lewis, self._ioc = get_running_lewis_and_ioc( "Lksh218", DEVICE_PREFIX) self.ca = ChannelAccess(device_prefix=DEVICE_PREFIX) self._lewis.backdoor_set_on_device("connected", True) def tearDown(self): self._lewis.backdoor_set_on_device("connected", True) def _set_temperature(self, number, temperature): pv = "SIM:TEMP{}".format(number) self._lewis.backdoor_run_function_on_device("set_temp", [number, temperature]) self._ioc.set_simulated_value(pv, temperature) def _set_sensor(self, number, value): pv = "SIM:SENSOR{}".format(number) self._lewis.backdoor_run_function_on_device("set_sensor", [number, value]) self._ioc.set_simulated_value(pv, value) def test_WHEN_ioc_started_THEN_ioc_is_not_disabled(self): self.ca.assert_that_pv_is("DISABLE", "COMMS ENABLED") def test_that_GIVEN_temp_float_WHEN_temp_pvs_are_read_THEN_temp_is_as_expected( self): expected_value = 10.586 for index in range(1, 9): pv = "TEMP{}".format(index) self._set_temperature(index, expected_value) self.ca.assert_that_pv_is(pv, expected_value) def test_that_GIVEN_sensor_float_WHEN_sensor_pvs_are_read_THEN_sensor_is_as_expected( self): expected_value = 11.386 for index in range(1, 9): pv = "SENSOR{}".format(index) self._set_sensor(index, expected_value) self.ca.assert_that_pv_is(pv, expected_value) def test_that_WHEN_reading_all_temps_pv_THEN_all_temp_pv_are_as_expected( self): expected_string = "10.4869" self._lewis.backdoor_set_on_device("temp_all", expected_string) self._ioc.set_simulated_value("SIM:TEMPALL", expected_string) self.ca.process_pv("TEMPALL") self.ca.assert_that_pv_is("TEMPALL", expected_string) def test_that_WHEN_reading_sensor_all_pv_THEN_sensor_all_pv_returns_as_expected( self): expected_string = "12.129" self._lewis.backdoor_set_on_device("sensor_all", expected_string) self._ioc.set_simulated_value("SIM:SENSORALL", expected_string) self.ca.process_pv("SENSORALL") self.ca.assert_that_pv_is("SENSORALL", expected_string) @skip_if_recsim("Recsim is unable to simulate a disconnected device.") def test_that_WHEN_the_emulator_is_disconnected_THEN_an_alarm_is_raised_on_TEMP_and_SENSOR( self): self._lewis.backdoor_set_on_device("connected", False) for i in range(1, 9): self.ca.assert_that_pv_alarm_is("TEMP{}".format(i), ChannelAccess.Alarms.INVALID) self.ca.assert_that_pv_alarm_is("SENSOR{}".format(i), ChannelAccess.Alarms.INVALID) @skip_if_recsim("Recsim is unable to simulate a disconnected device.") def test_that_WHEN_the_emulator_is_disconnected_THEN_an_alarm_is_raised_on_SENSORALL( self): self._lewis.backdoor_set_on_device("connected", False) self.ca.process_pv("SENSORALL") self.ca.assert_that_pv_alarm_is("SENSORALL", ChannelAccess.Alarms.INVALID) @unstable_test() @skip_if_recsim("Recsim is unable to simulate a disconnected device.") def test_that_WHEN_the_emulator_is_disconnected_THEN_an_alarm_is_raised_on_TEMPALL( self): self._lewis.backdoor_set_on_device("connected", False) self.ca.process_pv("TEMPALL") self.ca.assert_that_pv_alarm_is("TEMPALL", ChannelAccess.Alarms.INVALID)
class ZeroFieldTests(unittest.TestCase): """ Tests for the muon zero field controller IOC. """ def _set_simulated_measured_fields(self, fields, overload=False, wait_for_update=True): """ Args: fields (dict[AnyStr, float]): A dictionary with the same keys as FIELD_AXES and values corresponding to the required fields overload (bool): whether to simulate the magnetometer being overloaded wait_for_update (bool): whether to wait for the statemachine to pick up the new readings """ for axis in FIELD_AXES: self.magnetometer_ca.set_pv_value("SIM:DAQ:{}".format(axis), fields[axis], sleep_after_set=0) # Just overwrite the calculation to return a constant as we are not interested in testing the # overload logic in the magnetometer in these tests (that logic is tested separately). self.magnetometer_ca.set_pv_value("OVERLOAD:_CALC.CALC", "1" if overload else "0", sleep_after_set=0) if wait_for_update: for axis in FIELD_AXES: self.zfcntrl_ca.assert_that_pv_is("FIELD:{}".format(axis), fields[axis]) self.zfcntrl_ca.assert_that_pv_is("FIELD:{}:MEAS".format(axis), fields[axis]) def _set_user_setpoints(self, fields): """ Args: fields (dict[AnyStr, float]): A dictionary with the same keys as FIELD_AXES and values corresponding to the required fields """ for axis in FIELD_AXES: self.zfcntrl_ca.set_pv_value("FIELD:{}:SP".format(axis), fields[axis], sleep_after_set=0) def _set_simulated_power_supply_currents(self, currents, wait_for_update=True): """ Args: currents (dict[AnyStr, float]): A dictionary with the same keys as FIELD_AXES and values corresponding to the required currents wait_for_update (bool): whether to wait for the readback and setpoint readbacks to update """ for axis in FIELD_AXES: self.zfcntrl_ca.set_pv_value("OUTPUT:{}:CURR:SP".format(axis), currents[axis], sleep_after_set=0) if wait_for_update: for axis in FIELD_AXES: self.zfcntrl_ca.assert_that_pv_is( "OUTPUT:{}:CURR".format(axis), currents[axis]) self.zfcntrl_ca.assert_that_pv_is( "OUTPUT:{}:CURR:SP:RBV".format(axis), currents[axis]) def _set_simulated_power_supply_voltages(self, voltages, wait_for_update=True): """ Args: voltages (dict[AnyStr, float]): A dictionary with the same keys as FIELD_AXES and values corresponding to the required voltages wait_for_update (bool): whether to wait for the readback and setpoint readbacks to update """ for axis in FIELD_AXES: self.zfcntrl_ca.set_pv_value("OUTPUT:{}:VOLT:SP".format(axis), voltages[axis], sleep_after_set=0) if wait_for_update: for axis in FIELD_AXES: self.zfcntrl_ca.assert_that_pv_is( "OUTPUT:{}:VOLT".format(axis), voltages[axis]) self.zfcntrl_ca.assert_that_pv_is( "OUTPUT:{}:VOLT:SP:RBV".format(axis), voltages[axis]) def _assert_at_setpoint(self, status): """ Args: status (string): value of AT_SETPOINT PV (either Yes, No or N/A) """ self.zfcntrl_ca.assert_that_pv_is("AT_SETPOINT", status) def _assert_status(self, status): """ Args: status (Tuple[str, str]): the controller status and error to assert. """ name, expected_alarm = status # Special case - this alarm should be suppressed in manual mode. This is because, in manual mode, the # scientists will intentionally apply large fields (which overload the magnetometer), but they do not want # alarms for this case as it is a "normal" mode of operation. if name == Statuses.MAGNETOMETER_OVERLOAD[ 0] and self.zfcntrl_ca.get_pv_value( "AUTOFEEDBACK") == "Manual": expected_alarm = self.zfcntrl_ca.Alarms.NONE self.zfcntrl_ca.assert_that_pv_is("STATUS", name) self.zfcntrl_ca.assert_that_pv_alarm_is("STATUS", expected_alarm) def _set_autofeedback(self, autofeedback): self.zfcntrl_ca.set_pv_value( "AUTOFEEDBACK", "Auto-feedback" if autofeedback else "Manual") def _set_scaling_factors(self, px, py, pz, fiddle): """ Args: px (float): Amps per mG for the X axis. py (float): Amps per mG for the Y axis. pz (float): Amps per mG for the Z axis. fiddle (float): The feedback (sometimes called "fiddle") factor. """ self.zfcntrl_ca.set_pv_value("P:X", px, sleep_after_set=0) self.zfcntrl_ca.set_pv_value("P:Y", py, sleep_after_set=0) self.zfcntrl_ca.set_pv_value("P:Z", pz, sleep_after_set=0) self.zfcntrl_ca.set_pv_value("P:FEEDBACK", fiddle, sleep_after_set=0) def _set_output_limits(self, lower_limits, upper_limits): """ Args: lower_limits (dict[AnyStr, float]): A dictionary with the same keys as FIELD_AXES and values corresponding to the required output lower limits upper_limits (dict[AnyStr, float]): A dictionary with the same keys as FIELD_AXES and values corresponding to the required output upper limits """ for axis in FIELD_AXES: self.zfcntrl_ca.set_pv_value("OUTPUT:{}:CURR:SP.DRVL".format(axis), lower_limits[axis], sleep_after_set=0) self.zfcntrl_ca.set_pv_value("OUTPUT:{}:CURR:SP.LOLO".format(axis), lower_limits[axis], sleep_after_set=0) self.zfcntrl_ca.set_pv_value("OUTPUT:{}:CURR:SP.DRVH".format(axis), upper_limits[axis], sleep_after_set=0) self.zfcntrl_ca.set_pv_value("OUTPUT:{}:CURR:SP.HIHI".format(axis), upper_limits[axis], sleep_after_set=0) self.zfcntrl_ca.set_pv_value("OUTPUT:{}:CURR.LOLO".format(axis), lower_limits[axis], sleep_after_set=0) self.zfcntrl_ca.set_pv_value("OUTPUT:{}:CURR.HIHI".format(axis), upper_limits[axis], sleep_after_set=0) self.zfcntrl_ca.set_pv_value( "OUTPUT:{}:CURR:SP:RBV.LOLO".format(axis), lower_limits[axis], sleep_after_set=0) self.zfcntrl_ca.set_pv_value( "OUTPUT:{}:CURR:SP:RBV.HIHI".format(axis), upper_limits[axis], sleep_after_set=0) @contextlib.contextmanager def _simulate_disconnected_magnetometer(self): """ While this context manager is active, the magnetometer IOC will fail to take any new readings or process any PVs """ self.magnetometer_ca.set_pv_value("DISABLE", 1, sleep_after_set=0) try: yield finally: self.magnetometer_ca.set_pv_value("DISABLE", 0, sleep_after_set=0) @contextlib.contextmanager def _simulate_invalid_magnetometer_readings(self): """ While this context manager is active, any new readings from the magnetometer will be marked as INVALID """ for axis in FIELD_AXES: self.magnetometer_ca.set_pv_value( "DAQ:{}:_RAW.SIMS".format(axis), self.magnetometer_ca.Alarms.INVALID, sleep_after_set=0) # Wait for RAW PVs to process for axis in FIELD_AXES: self.magnetometer_ca.assert_that_pv_alarm_is( "DAQ:{}:_RAW.SEVR".format(axis), self.magnetometer_ca.Alarms.INVALID) try: yield finally: for axis in FIELD_AXES: self.magnetometer_ca.set_pv_value( "DAQ:{}:_RAW.SIMS".format(axis), self.magnetometer_ca.Alarms.NONE, sleep_after_set=0) # Wait for RAW PVs to process for axis in FIELD_AXES: self.magnetometer_ca.assert_that_pv_alarm_is( "DAQ:{}:_RAW.SEVR".format(axis), self.magnetometer_ca.Alarms.NONE) @contextlib.contextmanager def _simulate_invalid_power_supply(self): """ While this context manager is active, the readback values from all power supplies will be marked as INVALID (this simulates the device not being plugged in, for example) """ pvs_to_make_invalid = ("CURRENT", "_CURRENT:SP:RBV", "OUTPUTMODE", "OUTPUTSTATUS", "VOLTAGE", "VOLTAGE:SP:RBV") for ca, pv in itertools.product( (self.x_psu_ca, self.y_psu_ca, self.z_psu_ca), pvs_to_make_invalid): # 3 is the Enum value for an invalid alarm ca.set_pv_value("{}.SIMS".format(pv), 3, sleep_after_set=0) # Use a separate loop to avoid needing to wait for a 1-second scan 6 times. for ca, pv in itertools.product( (self.x_psu_ca, self.y_psu_ca, self.z_psu_ca), pvs_to_make_invalid): ca.assert_that_pv_alarm_is(pv, ca.Alarms.INVALID) try: yield finally: for ca, pv in itertools.product( (self.x_psu_ca, self.y_psu_ca, self.z_psu_ca), pvs_to_make_invalid): ca.set_pv_value("{}.SIMS".format(pv), 0, sleep_after_set=0) # Use a separate loop to avoid needing to wait for a 1-second scan 6 times. for ca, pv in itertools.product( (self.x_psu_ca, self.y_psu_ca, self.z_psu_ca), pvs_to_make_invalid): ca.assert_that_pv_alarm_is(pv, ca.Alarms.NONE) @contextlib.contextmanager def _simulate_failing_power_supply_writes(self): """ While this context manager is active, any writes to the power supply PVs will be ignored. This simulates the device being in local mode, for example. Note that this does not mark readbacks as invalid (for that, use _simulate_invalid_power_supply instead). """ pvs = [ "CURRENT:SP.DISP", "VOLTAGE:SP.DISP", "OUTPUTMODE:SP.DISP", "OUTPUTSTATUS:SP.DISP" ] for ca, pv in itertools.product( (self.x_psu_ca, self.y_psu_ca, self.z_psu_ca), pvs): ca.set_pv_value(pv, 1, sleep_after_set=0) try: yield finally: for ca, pv in itertools.product( (self.x_psu_ca, self.y_psu_ca, self.z_psu_ca), pvs): ca.set_pv_value(pv, 0, sleep_after_set=0) @contextlib.contextmanager def _simulate_measured_fields_changing_with_outputs( self, psu_amps_at_measured_zero): """ Calculates and sets somewhat realistic simulated measured fields based on the current values of power supplies. Args: psu_amps_at_measured_zero: Dictionary containing the Amps of the power supplies when the measured field corresponds to zero. i.e. if the system is told to go to zero field, these are the power supply readings it will require to get there. """ # Always start at zero current self._set_simulated_power_supply_currents({"X": 0, "Y": 0, "Z": 0}) thread = multiprocessing.Process(target=_update_fields_continuously, args=(psu_amps_at_measured_zero, )) thread.start() try: yield finally: thread.terminate() def _wait_for_all_iocs_up(self): """ Waits for the "primary" pv(s) from each ioc to be available """ for ca in (self.x_psu_ca, self.y_psu_ca, self.z_psu_ca): ca.assert_that_pv_exists("CURRENT") ca.assert_that_pv_exists("CURRENT:SP") ca.assert_that_pv_exists("CURRENT:SP:RBV") for axis in FIELD_AXES: self.zfcntrl_ca.assert_that_pv_exists("FIELD:{}".format(axis)) self.magnetometer_ca.assert_that_pv_exists( "CORRECTEDFIELD:{}".format(axis)) def setUp(self): _, self._ioc = get_running_lewis_and_ioc(None, ZF_DEVICE_PREFIX) timeout = 20 self.zfcntrl_ca = ChannelAccess(device_prefix=ZF_DEVICE_PREFIX, default_timeout=timeout) self.magnetometer_ca = ChannelAccess( device_prefix=MAGNETOMETER_DEVICE_PREFIX, default_timeout=timeout) self.x_psu_ca = ChannelAccess(default_timeout=timeout, device_prefix=X_KEPCO_DEVICE_PREFIX) self.y_psu_ca = ChannelAccess(default_timeout=timeout, device_prefix=Y_KEPCO_DEVICE_PREFIX) self.z_psu_ca = ChannelAccess(default_timeout=timeout, device_prefix=Z_KEPCO_DEVICE_PREFIX) self._wait_for_all_iocs_up() self.zfcntrl_ca.set_pv_value("TOLERANCE", STABILITY_TOLERANCE, sleep_after_set=0) self.zfcntrl_ca.set_pv_value("STATEMACHINE:LOOP_DELAY", LOOP_DELAY_MS, sleep_after_set=0) self._set_autofeedback(False) # Set the magnetometer calibration to the 3x3 identity matrix for x, y in itertools.product(range(1, 3 + 1), range(1, 3 + 1)): self.magnetometer_ca.set_pv_value("SENSORMATRIX:{}{}".format(x, y), 1 if x == y else 0, sleep_after_set=0) self._set_simulated_measured_fields(ZERO_FIELD, overload=False) self._set_user_setpoints(ZERO_FIELD) self._set_simulated_power_supply_currents(ZERO_FIELD, wait_for_update=True) self._set_scaling_factors(1, 1, 1, 1) self._set_output_limits( lower_limits={ "X": DEFAULT_LOW_OUTPUT_LIMIT, "Y": DEFAULT_LOW_OUTPUT_LIMIT, "Z": DEFAULT_LOW_OUTPUT_LIMIT }, upper_limits={ "X": DEFAULT_HIGH_OUTPUT_LIMIT, "Y": DEFAULT_HIGH_OUTPUT_LIMIT, "Z": DEFAULT_HIGH_OUTPUT_LIMIT }, ) self._assert_at_setpoint(AtSetpointStatuses.NA) self._assert_status(Statuses.NO_ERROR) def test_WHEN_ioc_is_started_THEN_it_is_not_disabled(self): self.zfcntrl_ca.assert_that_pv_is("DISABLE", "COMMS ENABLED") @parameterized.expand(parameterized_list(FIELD_AXES)) def test_WHEN_manual_mode_and_any_readback_value_is_not_equal_to_setpoint_THEN_at_setpoint_field_is_na( self, _, axis_to_vary): fields = {"X": 10, "Y": 20, "Z": 30} self._set_simulated_measured_fields(fields, overload=False) self._set_user_setpoints(fields) # Set one of the parameters to a completely different value self.zfcntrl_ca.set_pv_value("FIELD:{}:SP".format(axis_to_vary), 100, sleep_after_set=0) self._assert_at_setpoint(AtSetpointStatuses.NA) self._assert_status(Statuses.NO_ERROR) def test_GIVEN_manual_mode_and_magnetometer_not_overloaded_WHEN_readback_values_are_equal_to_setpoints_THEN_at_setpoint_field_is_na( self): fields = {"X": 55, "Y": 66, "Z": 77} self._set_simulated_measured_fields(fields, overload=False) self._set_user_setpoints(fields) self._assert_at_setpoint(AtSetpointStatuses.NA) self._assert_status(Statuses.NO_ERROR) def test_GIVEN_manual_mode_and_within_tolerance_WHEN_magnetometer_is_overloaded_THEN_status_overloaded_and_setpoint_field_is_na( self): fields = {"X": 55, "Y": 66, "Z": 77} self._set_simulated_measured_fields(fields, overload=True) self._set_user_setpoints(fields) self._assert_at_setpoint(AtSetpointStatuses.NA) self._assert_status(Statuses.MAGNETOMETER_OVERLOAD) def test_GIVEN_manual_mode_and_just_outside_tolerance_WHEN_magnetometer_is_overloaded_THEN_status_overloaded_and_setpoint_field_is_na( self): fields = {"X": 55, "Y": 66, "Z": 77} self._set_simulated_measured_fields(fields, overload=True) self._set_user_setpoints({ k: v + 1.01 * STABILITY_TOLERANCE for k, v in six.iteritems(fields) }) self._assert_at_setpoint(AtSetpointStatuses.NA) self._assert_status(Statuses.MAGNETOMETER_OVERLOAD) def test_GIVEN_manual_mode_and_just_within_tolerance_WHEN_magnetometer_is_overloaded_THEN_status_overloaded_and_setpoint_field_is_na( self): fields = {"X": 55, "Y": 66, "Z": 77} self._set_simulated_measured_fields(fields, overload=True) self._set_user_setpoints({ k: v + 0.99 * STABILITY_TOLERANCE for k, v in six.iteritems(fields) }) self._assert_at_setpoint(AtSetpointStatuses.NA) self._assert_status(Statuses.MAGNETOMETER_OVERLOAD) def test_WHEN_magnetometer_ioc_does_not_respond_THEN_status_is_magnetometer_read_error( self): fields = {"X": 1, "Y": 2, "Z": 3} self._set_simulated_measured_fields(fields, overload=False) self._set_user_setpoints(fields) with self._simulate_disconnected_magnetometer(): self._assert_status(Statuses.MAGNETOMETER_READ_ERROR) for axis in FIELD_AXES: self.zfcntrl_ca.assert_that_pv_alarm_is( "FIELD:{}".format(axis), self.zfcntrl_ca.Alarms.INVALID) self.zfcntrl_ca.assert_that_pv_alarm_is( "FIELD:{}:MEAS".format(axis), self.zfcntrl_ca.Alarms.INVALID) # Now simulate recovery and assert error gets cleared correctly self._assert_status(Statuses.NO_ERROR) for axis in FIELD_AXES: self.zfcntrl_ca.assert_that_pv_alarm_is( "FIELD:{}".format(axis), self.zfcntrl_ca.Alarms.NONE) self.zfcntrl_ca.assert_that_pv_alarm_is( "FIELD:{}:MEAS".format(axis), self.zfcntrl_ca.Alarms.NONE) def test_WHEN_magnetometer_ioc_readings_are_invalid_THEN_status_is_magnetometer_invalid( self): fields = {"X": 1, "Y": 2, "Z": 3} self._set_simulated_measured_fields(fields, overload=False) self._set_user_setpoints(fields) with self._simulate_invalid_magnetometer_readings(): self._assert_status(Statuses.MAGNETOMETER_DATA_INVALID) for axis in FIELD_AXES: self.zfcntrl_ca.assert_that_pv_alarm_is( "FIELD:{}".format(axis), self.zfcntrl_ca.Alarms.INVALID) self.zfcntrl_ca.assert_that_pv_alarm_is( "FIELD:{}:MEAS".format(axis), self.zfcntrl_ca.Alarms.INVALID) # Now simulate recovery and assert error gets cleared correctly self._assert_status(Statuses.NO_ERROR) for axis in FIELD_AXES: self.zfcntrl_ca.assert_that_pv_alarm_is( "FIELD:{}".format(axis), self.zfcntrl_ca.Alarms.NONE) self.zfcntrl_ca.assert_that_pv_alarm_is( "FIELD:{}:MEAS".format(axis), self.zfcntrl_ca.Alarms.NONE) def test_WHEN_power_supplies_are_invalid_THEN_status_is_power_supplies_invalid( self): fields = {"X": 1, "Y": 2, "Z": 3} self._set_simulated_measured_fields(fields, overload=False) self._set_user_setpoints(fields) self._set_autofeedback(True) with self._simulate_invalid_power_supply(): self._assert_at_setpoint( AtSetpointStatuses.TRUE ) # Invalid power supplies do not mark the field as "not at setpoint" self._assert_status(Statuses.PSU_INVALID) # Now simulate recovery and assert error gets cleared correctly self._assert_at_setpoint(AtSetpointStatuses.TRUE) self._assert_status(Statuses.NO_ERROR) def test_WHEN_power_supplies_writes_fail_THEN_status_is_power_supply_writes_failed( self): fields = {"X": 1, "Y": 2, "Z": 3} self._set_simulated_measured_fields(fields, overload=False) # For this test we need changing fields so that we can detect that the writes failed self._set_user_setpoints({ k: v + 10 * STABILITY_TOLERANCE for k, v in six.iteritems(fields) }) # ... and we also need large limits so that we see that the writes failed as opposed to a limits error self._set_output_limits(lower_limits={k: -999999 for k in FIELD_AXES}, upper_limits={k: 999999 for k in FIELD_AXES}) self._set_autofeedback(True) with self._simulate_failing_power_supply_writes(): self._assert_status(Statuses.PSU_WRITE_FAILED) # Now simulate recovery and assert error gets cleared correctly self._assert_status(Statuses.NO_ERROR) def test_GIVEN_measured_field_and_setpoints_are_identical_THEN_setpoints_remain_unchanged( self): fields = {"X": 5, "Y": 10, "Z": -5} outputs = {"X": -1, "Y": -2, "Z": -3} self._set_simulated_measured_fields(fields, overload=False) self._set_user_setpoints(fields) self._set_simulated_power_supply_currents(outputs, wait_for_update=True) self._set_autofeedback(True) for axis in FIELD_AXES: self.zfcntrl_ca.assert_that_pv_is_number( "OUTPUT:{}:CURR".format(axis), outputs[axis], tolerance=0.0001) self.zfcntrl_ca.assert_that_pv_value_is_unchanged( "OUTPUT:{}:CURR".format(axis), wait=5) @parameterized.expand( parameterized_list([ # If measured field is smaller than the setpoint, we want to adjust the output upwards to compensate (operator.sub, operator.gt, 1), # If measured field is larger than the setpoint, we want to adjust the output downwards to compensate (operator.add, operator.lt, 1), # If measured field is smaller than the setpoint, and A/mg is negative, we want to adjust the output downwards # to compensate (operator.sub, operator.lt, -1), # If measured field is larger than the setpoint, and A/mg is negative, we want to adjust the output upwards # to compensate (operator.add, operator.gt, -1), # If measured field is smaller than the setpoint, and A/mg is zero, then power supply output should remain # unchanged (operator.sub, operator.eq, 0), # If measured field is larger than the setpoint, and A/mg is zero, then power supply output should remain # unchanged (operator.add, operator.eq, 0), ])) def test_GIVEN_autofeedback_WHEN_measured_field_different_from_setpoints_THEN_power_supply_outputs_move_in_correct_direction( self, _, measured_field_modifier, output_comparator, scaling_factor): fields = {"X": 5, "Y": 0, "Z": -5} adjustment_amount = 10 * STABILITY_TOLERANCE # To ensure that it is not considered stable to start with measured_fields = { k: measured_field_modifier(v, adjustment_amount) for k, v in six.iteritems(fields) } self._set_scaling_factors(scaling_factor, scaling_factor, scaling_factor, fiddle=1) self._set_simulated_measured_fields(measured_fields, overload=False) self._set_user_setpoints(fields) self._set_simulated_power_supply_currents({ "X": 0, "Y": 0, "Z": 0 }, wait_for_update=True) self._set_output_limits(lower_limits={k: -999999 for k in FIELD_AXES}, upper_limits={k: 999999 for k in FIELD_AXES}) self._assert_status(Statuses.NO_ERROR) self._set_autofeedback(True) self._assert_at_setpoint(AtSetpointStatuses.FALSE) for axis in FIELD_AXES: self.zfcntrl_ca.assert_that_pv_value_over_time_satisfies_comparator( "OUTPUT:{}:CURR".format(axis), wait=5, comparator=output_comparator) # In this happy-path case, we shouldn't be hitting any long timeouts, so loop times should remain fairly quick self.zfcntrl_ca.assert_that_pv_is_within_range( "STATEMACHINE:LOOP_TIME", min_value=0, max_value=2 * LOOP_DELAY_MS) def test_GIVEN_output_limits_too_small_for_required_field_THEN_status_error_and_alarm( self): self._set_output_limits( lower_limits={ "X": -0.1, "Y": -0.1, "Z": -0.1 }, upper_limits={ "X": 0.1, "Y": 0.1, "Z": 0.1 }, ) # The measured field is smaller than the setpoint, i.e. the output needs to go up to the limits self._set_simulated_measured_fields({"X": -1, "Y": -1, "Z": -1}) self._set_user_setpoints(ZERO_FIELD) self._set_simulated_power_supply_currents(ZERO_FIELD) self._set_autofeedback(True) self._assert_status(Statuses.PSU_ON_LIMITS) for axis in FIELD_AXES: # Value should be on one of the limits self.zfcntrl_ca.assert_that_pv_is_one_of( "OUTPUT:{}:CURR:SP".format(axis), [-0.1, 0.1]) # ...and in alarm self.zfcntrl_ca.assert_that_pv_alarm_is( "OUTPUT:{}:CURR:SP".format(axis), self.zfcntrl_ca.Alarms.MAJOR) def test_GIVEN_limits_wrong_way_around_THEN_appropriate_error_raised(self): # Set upper limits < lower limits self._set_output_limits( lower_limits={ "X": 0.1, "Y": 0.1, "Z": 0.1 }, upper_limits={ "X": -0.1, "Y": -0.1, "Z": -0.1 }, ) self._set_autofeedback(True) self._assert_status(Statuses.INVALID_PSU_LIMITS) @parameterized.expand( parameterized_list([ { "X": 45.678, "Y": 0.123, "Z": 12.345 }, { "X": 0, "Y": 0, "Z": 0 }, { "X": -45.678, "Y": -0.123, "Z": -12.345 }, ])) def test_GIVEN_measured_values_updating_realistically_WHEN_in_auto_mode_THEN_converges_to_correct_answer( self, _, psu_amps_at_zero_field): self._set_output_limits(lower_limits={k: -100 for k in FIELD_AXES}, upper_limits={k: 100 for k in FIELD_AXES}) self._set_user_setpoints({"X": 0, "Y": 0, "Z": 0}) self._set_simulated_power_supply_currents({"X": 0, "Y": 0, "Z": 0}) # Set fiddle small to get a relatively slow response, which should theoretically be stable self._set_scaling_factors(0.001, 0.001, 0.001, fiddle=0.05) with self._simulate_measured_fields_changing_with_outputs( psu_amps_at_measured_zero=psu_amps_at_zero_field): self._set_autofeedback(True) for axis in FIELD_AXES: self.zfcntrl_ca.assert_that_pv_is_number( "OUTPUT:{}:CURR:SP:RBV".format(axis), psu_amps_at_zero_field[axis], tolerance=STABILITY_TOLERANCE * 0.001, timeout=60) self.zfcntrl_ca.assert_that_pv_is_number( "FIELD:{}".format(axis), 0.0, tolerance=STABILITY_TOLERANCE) self._assert_at_setpoint(AtSetpointStatuses.TRUE) self.zfcntrl_ca.assert_that_pv_value_is_unchanged("AT_SETPOINT", wait=20) self._assert_status(Statuses.NO_ERROR) @parameterized.expand(parameterized_list(FIELD_AXES)) def test_GIVEN_output_is_off_WHEN_autofeedback_switched_on_THEN_psu_is_switched_back_on( self, _, axis): self.zfcntrl_ca.assert_setting_setpoint_sets_readback( "Off", "OUTPUT:{}:STATUS".format(axis)) self._set_autofeedback(True) self.zfcntrl_ca.assert_that_pv_is("OUTPUT:{}:STATUS".format(axis), "On") @parameterized.expand(parameterized_list(FIELD_AXES)) def test_GIVEN_output_mode_is_voltage_WHEN_autofeedback_switched_on_THEN_psu_is_switched_to_current_mode( self, _, axis): self.zfcntrl_ca.assert_setting_setpoint_sets_readback( "Voltage", "OUTPUT:{}:MODE".format(axis), expected_alarm=self.zfcntrl_ca.Alarms.MAJOR) self._set_autofeedback(True) self.zfcntrl_ca.assert_that_pv_is("OUTPUT:{}:MODE".format(axis), "Current") @parameterized.expand(parameterized_list(FIELD_AXES)) def test_GIVEN_output_is_off_and_cannot_write_to_psu_WHEN_autofeedback_switched_on_THEN_get_psu_write_error( self, _, axis): self.zfcntrl_ca.assert_setting_setpoint_sets_readback( "Off", "OUTPUT:{}:STATUS".format(axis)) with self._simulate_failing_power_supply_writes(): self._set_autofeedback(True) self._assert_status(Statuses.PSU_WRITE_FAILED) # Check it can recover when writes work again self._assert_status(Statuses.NO_ERROR) self.zfcntrl_ca.assert_that_pv_is("OUTPUT:{}:STATUS".format(axis), "On") @parameterized.expand(parameterized_list(FIELD_AXES)) def test_GIVEN_output_mode_is_voltage_and_cannot_write_to_psu_WHEN_autofeedback_switched_on_THEN_get_psu_write_error( self, _, axis): self.zfcntrl_ca.assert_setting_setpoint_sets_readback( "Voltage", "OUTPUT:{}:MODE".format(axis), expected_alarm=self.zfcntrl_ca.Alarms.MAJOR) with self._simulate_failing_power_supply_writes(): self._set_autofeedback(True) self._assert_status(Statuses.PSU_WRITE_FAILED) # Check it can recover when writes work again self._assert_status(Statuses.NO_ERROR) self.zfcntrl_ca.assert_that_pv_is("OUTPUT:{}:MODE".format(axis), "Current") @parameterized.expand( parameterized_list([ (True, True), (False, True), (True, False), (False, False), ])) def test_GIVEN_magnetometer_overloaded_THEN_error_suppressed_if_in_manual_mode( self, _, autofeedback, overloaded): self._set_autofeedback(autofeedback) self._set_simulated_measured_fields(ZERO_FIELD, overload=overloaded, wait_for_update=True) self._assert_status(Statuses.MAGNETOMETER_OVERLOAD if overloaded else Statuses.NO_ERROR) self.zfcntrl_ca.assert_that_pv_alarm_is( "STATUS", self.zfcntrl_ca.Alarms.MAJOR if overloaded and autofeedback else self.zfcntrl_ca.Alarms.NONE) def test_GIVEN_power_supply_voltage_limit_is_set_incorrectly_WHEN_going_into_auto_mode_THEN_correct_limits_applied( self): self._set_simulated_power_supply_voltages({"X": 0, "Y": 0, "Z": 0}) self._set_autofeedback(True) self.zfcntrl_ca.assert_that_pv_is("OUTPUT:X:VOLT:SP:RBV", X_KEPCO_VOLTAGE_LIMIT) self.zfcntrl_ca.assert_that_pv_is("OUTPUT:Y:VOLT:SP:RBV", Y_KEPCO_VOLTAGE_LIMIT) self.zfcntrl_ca.assert_that_pv_is("OUTPUT:Z:VOLT:SP:RBV", Z_KEPCO_VOLTAGE_LIMIT)
class RikenChangeover(object): """ Tests for a riken changeover. This class is inherited by the riken port changeover tests and also the RB2 mode change tests as they are very similar (just the PSUs that they look at / control are different) """ @abstractmethod def get_input_pv(self): return "" @abstractmethod def get_acknowledgement_pv(self): return "" @abstractmethod def get_power_supplies(self): return [] @abstractmethod def get_coord_prefix(self): return "" @abstractmethod def get_prefix(self): return "" def _set_input_pv(self, ok_to_run_psus): self.ca.set_pv_value("{}:SIM".format(self.get_input_pv()), 1 if ok_to_run_psus else 0) self.ca.assert_that_pv_is("{}:SIM".format(self.get_input_pv()), 1 if ok_to_run_psus else 0) self.ca.assert_that_pv_alarm_is("{}".format(self.get_input_pv()), self.ca.Alarms.NONE) self.ca.assert_that_pv_is("{}".format(self.get_input_pv()), 1 if ok_to_run_psus else 0) def _set_power_supply_state(self, supply, on): self.ca.set_pv_value("{}:POWER:SP".format(supply), 1 if on else 0) self.ca.assert_that_pv_is("{}:POWER".format(supply), "On" if on else "Off") def _assert_power_supply_disabled(self, supply, disabled): self.ca.assert_that_pv_is_number("{}:POWER:SP.DISP".format(supply), 1 if disabled else 0) def _set_all_power_supply_states(self, on): for supply in self.get_power_supplies(): self._set_power_supply_state(supply, on) def _assert_all_power_supplies_disabled(self, disabled): for supply in self.get_power_supplies(): self._assert_power_supply_disabled(supply, disabled) def _assert_necessary_pvs_exist(self): self.ca.assert_that_pv_exists("{}:PSUS:DISABLE".format(self.get_coord_prefix())) self.ca.assert_that_pv_exists(self.get_input_pv()) self.ca.assert_that_pv_exists(self.get_acknowledgement_pv()) for id in self.get_power_supplies(): self.ca.assert_that_pv_exists("{}:POWER".format(id)) def setUp(self): self.ca = ChannelAccess(device_prefix=self.get_prefix(), default_timeout=10) self._assert_necessary_pvs_exist() self._set_input_pv(False) # Set it to false so that the sequencer see the change, # probably to do with it being in RECSIM self._set_input_pv(True) self._assert_all_power_supplies_disabled(False) self._set_all_power_supply_states(False) def test_GIVEN_value_on_input_ioc_changes_THEN_coord_psus_disable_pv_updates_with_the_same_value(self): def _set_and_check(ok_to_run_psus): self._set_input_pv(ok_to_run_psus) self.ca.assert_that_pv_is("{}:PSUS:DISABLE".format(self.get_coord_prefix()), "ENABLED" if ok_to_run_psus else "DISABLED") for ok_to_run_psus in [True, False, True]: # Check both transitions _set_and_check(ok_to_run_psus) def test_GIVEN_all_power_supplies_off_WHEN_value_on_input_ioc_changes_THEN_power_supplies_have_their_disp_field_set(self): def _set_and_check_disabled_status(ok_to_run_psus): self._set_input_pv(ok_to_run_psus) self._assert_all_power_supplies_disabled(not ok_to_run_psus) for ok_to_run_psus in [True, False, True]: # Check both transitions _set_and_check_disabled_status(ok_to_run_psus) def test_WHEN_any_power_supply_is_on_THEN_power_all_pv_is_high(self): self._set_all_power_supply_states(False) self.ca.assert_that_pv_is_number("{}:PSUS:POWER".format(self.get_coord_prefix()), 0) for psu in self.get_power_supplies(): self._set_power_supply_state(psu, True) self.ca.assert_that_pv_is_number("{}:PSUS:POWER".format(self.get_coord_prefix()), 1) self._set_power_supply_state(psu, False) self.ca.assert_that_pv_is_number("{}:PSUS:POWER".format(self.get_coord_prefix()), 0) def test_GIVEN_power_supplies_on_WHEN_value_on_input_ioc_changes_THEN_power_supplies_are_not_disabled_until_they_are_switched_off(self): self._set_all_power_supply_states(True) self._set_input_pv(False) self._assert_all_power_supplies_disabled(False) self._set_all_power_supply_states(False) self._assert_all_power_supplies_disabled(True) def test_GIVEN_plc_cancels_changeover_before_psus_are_all_switched_off_WHEN_psus_become_switched_off_THEN_they_do_not_get_disabled(self): self._set_all_power_supply_states(True) self._set_input_pv(False) self._assert_all_power_supplies_disabled(False) # Power supplies not disabled because still powered on self._set_input_pv(True) # PLC now cancels request to do a changeover self._set_all_power_supply_states(False) self._assert_all_power_supplies_disabled(False) def test_GIVEN_a_power_supply_is_in_alarm_THEN_the_power_any_pv_is_also_in_alarm(self): for supply in self.get_power_supplies(): with self.ca.put_simulated_record_into_alarm("{}:POWER".format(supply), self.ca.Alarms.INVALID): self.ca.assert_that_pv_alarm_is("{}:PSUS:POWER".format(self.get_coord_prefix()), self.ca.Alarms.INVALID) self.ca.assert_that_pv_alarm_is("{}:PSUS:POWER".format(self.get_coord_prefix()), self.ca.Alarms.NONE) def test_GIVEN_all_power_supply_are_in_alarm_THEN_the_power_any_pv_is_also_in_alarm(self): with ExitStack() as stack: for supply in self.get_power_supplies(): stack.enter_context( self.ca.put_simulated_record_into_alarm("{}:POWER".format(supply), self.ca.Alarms.INVALID) ) self.ca.assert_that_pv_alarm_is("{}:PSUS:POWER".format(self.get_coord_prefix()), self.ca.Alarms.INVALID) self.ca.assert_that_pv_alarm_is("{}:PSUS:POWER".format(self.get_coord_prefix()), self.ca.Alarms.NONE) def test_GIVEN_a_power_supply_is_in_alarm_THEN_the_power_any_pv_reports_that_psus_are_active(self): for supply in self.get_power_supplies(): with self.ca.put_simulated_record_into_alarm("{}:POWER".format(supply), self.ca.Alarms.INVALID): self.ca.assert_that_pv_is_number("{}:PSUS:POWER".format(self.get_coord_prefix()), 1) self.ca.assert_that_pv_is_number("{}:PSUS:POWER".format(self.get_coord_prefix()), 0) def test_GIVEN_all_power_supply_are_in_alarm_THEN_the_power_any_pv_reports_that_psus_are_active(self): with ExitStack() as stack: for supply in self.get_power_supplies(): stack.enter_context( self.ca.put_simulated_record_into_alarm("{}:POWER".format(supply), self.ca.Alarms.INVALID) ) self.ca.assert_that_pv_is_number("{}:PSUS:POWER".format(self.get_coord_prefix()), 1) self.ca.assert_that_pv_is_number("{}:PSUS:POWER".format(self.get_coord_prefix()), 0) def test_GIVEN_changeover_initiated_WHEN_power_supplies_off_THEN_acknowledgement_pv_true(self): self._set_all_power_supply_states(False) self._set_input_pv(False) self.ca.assert_that_pv_is_not_number(self.get_acknowledgement_pv(), 0) self._set_input_pv(True) # Some time later the PLC sends signal to say it has finished the changeover sequence self.ca.assert_that_pv_is_number(self.get_acknowledgement_pv(), 0) def test_GIVEN_changeover_sequence_completes_THEN_power_supplies_are_reenabled_after_sequence(self): self._set_all_power_supply_states(True) self._set_input_pv(False) self._assert_all_power_supplies_disabled(False) # Power supplies not disabled because still powered on self._set_all_power_supply_states(False) # Power supplies now switched off so changeover can continue self._assert_all_power_supplies_disabled(True) # All power supplies are now disabled self.ca.assert_that_pv_is_not_number(self.get_acknowledgement_pv(), 0) self._set_input_pv(True) # Some time later, changeover is finished self._assert_all_power_supplies_disabled(False) # Power supplies should now be reenabled self.ca.assert_that_pv_is(self.get_acknowledgement_pv(), 0) # And "ok to run changeover" line should be cleared
class Lakeshore340Tests(unittest.TestCase): """ Tests for the lakeshore 340 IOC. """ def setUp(self): self._lewis, self._ioc = get_running_lewis_and_ioc( _EMULATOR_NAME, DEVICE_PREFIX) self.ca = ChannelAccess(device_prefix=DEVICE_PREFIX, default_timeout=15) def test_WHEN_device_is_started_THEN_it_is_not_disabled(self): self.ca.assert_that_pv_is("DISABLE", "COMMS ENABLED") @parameterized.expand( parameterized_list(itertools.product(SENSORS, TEST_TEMPERATURES))) @skip_if_recsim("Lewis backdoor not available in recsim") def test_WHEN_temperature_set_via_backdoor_THEN_it_can_be_read_back( self, _, sensor, value): self._lewis.backdoor_set_on_device("temp_{}".format(sensor.lower()), value) self.ca.assert_that_pv_is_number("{}:TEMP".format(sensor.upper()), value) @parameterized.expand( parameterized_list(itertools.product(SENSORS, TEST_TEMPERATURES))) @skip_if_recsim("Lewis backdoor not available in recsim") def test_WHEN_measurement_set_via_backdoor_THEN_it_can_be_read_back( self, _, sensor, value): self._lewis.backdoor_set_on_device( "measurement_{}".format(sensor.lower()), value) self.ca.assert_that_pv_is_number("{}:RDG".format(sensor.upper()), value) @parameterized.expand(parameterized_list(TEST_TEMPERATURES)) def test_WHEN_tset_is_changed_THEN_readback_updates(self, _, val): self.ca.assert_setting_setpoint_sets_readback( val, readback_pv="A:TEMP:SP:RBV", set_point_pv="A:TEMP:SP") @parameterized.expand( parameterized_list(itertools.product(PID_SETTINGS, PID_TEST_VALUES))) def test_WHEN_pid_settings_changed_THEN_can_be_read_back( self, _, setting, value): if setting == "D": value = int( value) # Derivative is only allowed to take integer values. self.ca.assert_setting_setpoint_sets_readback(value, setting) @parameterized.expand(parameterized_list(PID_MODES)) def test_WHEN_pid_settings_changed_THEN_can_be_read_back(self, _, mode): self.ca.assert_setting_setpoint_sets_readback(mode, "PIDMODE") @parameterized.expand(parameterized_list(LOOP_STATES)) def test_WHEN_loop_turned_on_or_off_THEN_can_be_read_back( self, _, loopstate): self.ca.assert_setting_setpoint_sets_readback(loopstate, "LOOP") @parameterized.expand(parameterized_list(TEST_TEMPERATURES)) def test_WHEN_max_temperature_set_THEN_can_be_read_back(self, _, temp): self.ca.assert_setting_setpoint_sets_readback(temp, "TEMP:MAX") @parameterized.expand(parameterized_list(HEATER_PERCENTAGES)) @skip_if_recsim("Lewis backdoor not available in recsim") def test_WHEN_heater_power_set_via_backdoor_THEN_can_be_read_back( self, _, output): self._lewis.backdoor_set_on_device("heater_output", output) self.ca.assert_that_pv_is_number("OUTPUT", output) @parameterized.expand(parameterized_list(RANGES)) def test_WHEN_heater_range_set_THEN_can_be_read_back(self, _, range): self.ca.assert_setting_setpoint_sets_readback(range, "RANGE") @parameterized.expand(parameterized_list(EXCITATIONS)) def test_WHEN_excitation_a_set_THEN_can_be_read_back(self, _, excitation): self.ca.assert_setting_setpoint_sets_readback(excitation, "EXCITATIONA") @parameterized.expand(parameterized_list(EXCITATIONS)) @skip_if_recsim def test_WHEN_excitation_set_by_backdoor_THEN_can_be_read_back( self, _, excitation): self._lewis.backdoor_set_on_device("excitationa", EXCITATIONS.index(excitation)) self.ca.assert_that_pv_is("EXCITATIONA", excitation) def test_WHEN_use_valid_file_THEN_threshold_file_is_none(self): self.ca.assert_that_pv_is_path(THRESHOLD_FILE_PV, THRESHOLD_FILES_DIR + THRESHOLDS_FILE) self.ca.assert_that_pv_is(THRESHOLDS_ERROR_PV, "No Error") self.ca.assert_that_pv_is_path("THRESHOLDS:USE", "YES") def test_WHEN_do_not_use_file_THEN_threshold_file_is_not_set(self): with self._ioc.start_with_macros( {"USE_EXCITATION_THRESHOLD_FILE": "NO"}, pv_to_wait_for=THRESHOLD_FILE_PV): self.ca.assert_that_pv_is_path("THRESHOLDS:USE", "NO") self.ca.assert_that_pv_is(THRESHOLDS_ERROR_PV, "No Error") def test_WHEN_initialise_with_incorrect_macro_THEN_pv_is_in_alarm(self): filename = "DoesNotExist.txt" with self._ioc.start_with_macros( { "EXCITATION_THRESHOLD_FILE": filename, "USE_EXCITATION_THRESHOLD_FILE": "YES" }, pv_to_wait_for=THRESHOLD_FILE_PV): self.ca.assert_that_pv_is(THRESHOLDS_ERROR_PV, "File Not Found") self.ca.assert_that_pv_is_path("THRESHOLDS:USE", "YES") def test_WHEN_initialise_with_invalid_file_THEN_pv_is_in_alarm(self): filename = "InvalidLines.txt" with self._ioc.start_with_macros( { "EXCITATION_THRESHOLD_FILE": filename, "USE_EXCITATION_THRESHOLD_FILE": "YES" }, pv_to_wait_for=THRESHOLD_FILE_PV): self.ca.assert_that_pv_is(THRESHOLDS_ERROR_PV, "Invalid Lines In File") self.ca.assert_that_pv_is_path("THRESHOLDS:USE", "YES") def reset_thresholds_values(self, thresholds_excitations, thresholds_temp, excitationa, error, delay_change, temp): self.ca.assert_setting_setpoint_sets_readback(excitationa, EXCITATIONA_PV) self.ca.set_pv_value(THRESHOLD_TEMP_PV, thresholds_temp) self.ca.set_pv_value(THRESHOLD_EXCITATIONS_PV, thresholds_excitations) self.ca.set_pv_value(THRESHOLDS_DELAY_CHANGE_PV, delay_change) self.ca.set_pv_value(THRESHOLDS_ERROR_PV, error) self._lewis.backdoor_set_on_device("temp_a", temp) def assert_threshold_values(self, thresholds_excitations, thresholds_temp, excitationa, error, error_severity, delay_change): self.ca.assert_that_pv_is(THRESHOLD_EXCITATIONS_PV, thresholds_excitations) self.ca.assert_that_pv_is(THRESHOLD_TEMP_PV, thresholds_temp) self.ca.assert_that_pv_is(THRESHOLDS_DELAY_CHANGE_PV, delay_change) self.ca.assert_that_pv_is(THRESHOLDS_ERROR_PV, error) self.ca.assert_that_pv_alarm_is(THRESHOLDS_ERROR_PV, error_severity) self.ca.assert_that_pv_is(EXCITATIONA_PV, excitationa) @parameterized.expand(parameterized_list(TEMP_SP_EXCITATIONS)) @skip_if_recsim def test_WHEN_set_temp_sp_THEN_thresholds_recalculated( self, _, temp_sp_excitations_map): new_temp_sp = temp_sp_excitations_map["TEMP:SP"] expected_thresholds_temp = temp_sp_excitations_map["THRESHOLDS:TEMP"] expected_thresholds_excitation = temp_sp_excitations_map[ "THRESHOLDS:EXCITATION"] # Reset pv values to test self.reset_thresholds_values("Off", 0, "Off", "No Error", "NO", new_temp_sp - 10) # Set setpoint self.ca.assert_setting_setpoint_sets_readback( new_temp_sp, readback_pv="A:TEMP:SP:RBV", set_point_pv="A:TEMP:SP") # Confirm change is delayed but threshold temp is set self.assert_threshold_values(expected_thresholds_excitation, expected_thresholds_temp, "Off", "No Error", "NO_ALARM", "YES") # Make temperature equal setpoint self._lewis.backdoor_set_on_device("temp_a", new_temp_sp) # Confirm Excitations is set correctly self.assert_threshold_values(expected_thresholds_excitation, expected_thresholds_temp, expected_thresholds_excitation, "No Error", "NO_ALARM", "NO") @parameterized.expand( parameterized_list([("None.txt", "NO_ALARM", "No Error"), ("DoesNotExist.txt", "MINOR", "File Not Found"), ("InvalidLines.txt", "MINOR", "Invalid Lines In File")])) @skip_if_recsim def test_GIVEN_not_using_excitations_OR_invalid_file_WHEN_set_temp_sp_THEN_thresholds_not_recalculated( self, _, filename, expected_error_severity, expected_error): with self._ioc.start_with_macros( {"EXCITATION_THRESHOLD_FILE": filename}, pv_to_wait_for=THRESHOLD_FILE_PV): for temp_sp, temp, excitation in [(5.2, 3.1, "30 nA"), (16.4, 18.2, "100 nA"), (20.9, 0, "Off"), (400.2, 20.3, "1 mV")]: # Reset pv values to test self.reset_thresholds_values(excitation, temp, excitation, "No Error", "NO", temp_sp - 10) # Set temp self.ca.assert_setting_setpoint_sets_readback( temp_sp, readback_pv="A:TEMP:SP:RBV", set_point_pv="A:TEMP:SP") self._lewis.backdoor_set_on_device("temp_a", temp_sp) # Assert nothing has changed self.assert_threshold_values(excitation, temp, excitation, expected_error, expected_error_severity, "NO")
class Jsco4180Tests(unittest.TestCase): """ Tests for the Jsco4180 IOC. """ def setUp(self): self._lewis, self._ioc = get_running_lewis_and_ioc(DEVICE_NAME, DEVICE_PREFIX) self.ca = ChannelAccess(device_prefix=DEVICE_PREFIX, default_timeout=30) for pv in required_pvs: self.ca.assert_that_pv_exists(pv, timeout=30) self._lewis.backdoor_run_function_on_device("reset") @skip_if_recsim("Unable to use lewis backdoor in RECSIM") def test_GIVEN_wrong_component_on_device_WHEN_running_THEN_retry_run_and_updates_component(self): expected_value_A = 30 expected_value_B = 15 expected_value_C = 55 self.ca.set_pv_value("COMP:A:SP", expected_value_A) self.ca.set_pv_value("COMP:B:SP", expected_value_B) self.ca.set_pv_value("COMP:C:SP", expected_value_C) self.ca.set_pv_value("START:SP", 1) sleep(10) # Setting an incorrect component on the device will result in the state machine attempting # to rerun the pump and reset components. self._lewis.backdoor_set_on_device("component_A", 25) self._lewis.backdoor_set_on_device("component_B", 10) self._lewis.backdoor_set_on_device("component_C", 14) self.ca.assert_that_pv_is("COMP:A", expected_value_A, timeout=30) self.ca.assert_that_pv_is("COMP:B", expected_value_B, timeout=30) self.ca.assert_that_pv_is("COMP:C", expected_value_C, timeout=30) # there was a previous problem where if setpoint and readback differed a sleep and resend was started, # but the old state machine did not look to see if a new sp was issued while it was asleep and so then # resent the old out of date SP @unstable_test(max_retries=2, wait_between_runs=60) @skip_if_recsim("Unable to use lewis backdoor in RECSIM") def test_GIVEN_wrong_component_on_device_WHEN_send_new_sp_THEN_state_machine_aborts_resend(self): value = 50 self.ca.set_pv_value("COMP:A:SP", value) self.ca.set_pv_value("COMP:B:SP", value) self.ca.set_pv_value("START:SP", 1) self.ca.assert_that_pv_is("STATUS", "Pumping", timeout=5) self.ca.assert_that_pv_is("COMP:A", value, timeout=30) self.ca.assert_that_pv_is("COMP:B", value, timeout=30) # Setting an incorrect component on the device will result in the state machine attempting # to rerun the pump and reset components after a delay initial_delay = self.ca.get_pv_value("ERROR:DELAY") # delay before state machine reset delay = 30 # Increase delay to avoid race conditions self.ca.set_pv_value("ERROR:DELAY", delay) try: with self.ca.assert_pv_not_processed("RESET:SP"): self._lewis.backdoor_set_on_device("component_A", value - 5) self.ca.assert_that_pv_is("COMP:A", value - 5, timeout=5) sleep(delay / 2.0) # however if we change setpoint, the loop should start again self._lewis.backdoor_set_on_device("component_A", value - 5) self.ca.set_pv_value("COMP:A:SP", value - 10) self.ca.set_pv_value("COMP:B:SP", value + 10) # reset should not have happened yet self.ca.assert_that_pv_is("COMP:A", value - 5, timeout=delay / 2.0) self.ca.assert_that_pv_value_is_unchanged("COMP:A", wait=delay / 2.0) # Reset should now happen within a further timeout/2 seconds (but give it longer to avoid races) with self.ca.assert_pv_processed("RESET:SP"): self.ca.assert_that_pv_is("COMP:A", value - 10, timeout=delay * 2) finally: # Put error delay back to it's initial value self.ca.set_pv_value("ERROR:DELAY", initial_delay) @skip_if_recsim("Unable to use lewis backdoor in RECSIM") def test_GIVEN_wrong_component_on_device_WHEN_running_continuous_THEN_retry_run_and_updates_component_in_correct_mode( self): value = 50 expected_value = "Pumping" self.ca.set_pv_value("COMP:A:SP", value) self.ca.set_pv_value("COMP:B:SP", value) self.ca.set_pv_value("START:SP", 1) # Give the device some time running in a good state sleep(10) # Sabotage! - Setting an incorrect component on the device will result in the state machine attempting # to rerun the pump and reset components. self._lewis.backdoor_set_on_device("component_A", 33) self.ca.assert_that_pv_is("STATUS", expected_value, timeout=30) @skip_if_recsim("Unable to use lewis backdoor in RECSIM") def test_GIVEN_wrong_component_on_device_WHEN_running_timed_THEN_retry_run_and_updates_component_in_correct_mode( self): value = 50 expected_value = "Pumping" self.ca.set_pv_value("COMP:A:SP", value) self.ca.set_pv_value("COMP:B:SP", value) self.ca.set_pv_value("TIME:RUN:SP", 100) self.ca.set_pv_value("PUMP_FOR_TIME:SP", 1) # Give the device some time running in a good state sleep(10) # Sabotage! - Setting an incorrect component on the device will result in the state machine attempting # to rerun the pump and reset components. self._lewis.backdoor_set_on_device("component_A", 33) self.ca.assert_that_pv_is("STATUS", expected_value, timeout=30) @skip_if_recsim("Flowrate device logic not supported in RECSIM") def test_GIVEN_an_ioc_WHEN_set_flowrate_THEN_flowrate_setpoint_is_correct(self): error_delay = float(self.ca.get_pv_value("ERROR:DELAY")) sleep(2 * error_delay) # To make sure we're not in the middle of the error-checking state machine expected_value = 1.000 self.ca.set_pv_value("FLOWRATE:SP", expected_value) self.ca.assert_that_pv_is("FLOWRATE:SP:RBV", expected_value) self.ca.set_pv_value("TIME:RUN:SP", 100) self.ca.set_pv_value("START:SP", "Start") self.ca.assert_that_pv_is("FLOWRATE", expected_value) @skip_if_recsim("LeWIS backdoor not supported in RECSIM") def test_GIVEN_an_ioc_WHEN_set_flowrate_and_pump_volume_THEN_ioc_uses_rbv_for_calculation_of_remaining_time(self): expected_sp_value = 1.000 expected_rbv_value = 2.000 pump_for_volume = 2 expected_time_value = (pump_for_volume / expected_rbv_value) * 60 error_delay = float(self.ca.get_pv_value("ERROR:DELAY")) sleep(2 * error_delay) # To make sure we're not in the middle of the error-checking state machine # 1. set invalid flowrate setpoint (FLOWRATE:SP) self.ca.set_pv_value("FLOWRATE:SP", expected_sp_value) self.ca.assert_that_pv_is("FLOWRATE:SP:RBV", expected_sp_value) # 2. set valid hardware flowrate (FLOWRATE:SP:RBV) via backdoor command self._lewis.backdoor_set_on_device("flowrate_rbv", expected_rbv_value) self.ca.assert_that_pv_is("FLOWRATE:SP:RBV", expected_rbv_value) # 3. set volume setpoint and start pump self.ca.set_pv_value("TIME:VOL:SP", pump_for_volume) self.ca.set_pv_value("START:SP", "Start") # 4. check calculated time is based on flowrate setpoint readback (:SP:RBV rather than :SP) self.ca.assert_that_pv_is("TIME:VOL:CALCRUN", expected_time_value) @skip_if_recsim("LeWIS backdoor not supported in RECSIM") def test_GIVEN_an_ioc_WHEN_set_flowrate_and_pump_time_THEN_ioc_uses_rbv_for_calculation_of_remaining_volume(self): expected_sp_value = 1.000 expected_rbv_value = 2.000 pump_for_time = 120 expected_volume_value = (pump_for_time * expected_rbv_value) / 60 error_delay = float(self.ca.get_pv_value("ERROR:DELAY")) sleep(2 * error_delay) # To make sure we're not in the middle of the error-checking state machine # 1. set invalid flowrate setpoint (FLOWRATE:SP) self.ca.set_pv_value("FLOWRATE:SP", expected_sp_value) self.ca.assert_that_pv_is("FLOWRATE:SP:RBV", expected_sp_value) # 2. set valid hardware flowrate (FLOWRATE:SP:RBV) via backdoor command self._lewis.backdoor_set_on_device("flowrate_rbv", expected_rbv_value) self.ca.assert_that_pv_is("FLOWRATE:SP:RBV", expected_rbv_value) # 3. set time setpoint and start pump self.ca.set_pv_value("TIME:RUN:SP", pump_for_time) self.ca.set_pv_value("START:SP", "Start") # 4. check calculated volume is based on flowrate setpoint readback (:SP:RBV rather than :SP) self.ca.assert_that_pv_is("TIME:RUN:CALCVOL", expected_volume_value) # test to check that the IOC updates the flowrate RBV quickly enough # for the remaining volume calculation to be valid. simulates operation of a script. def test_GIVEN_an_ioc_WHEN_set_flowrate_and_immediately_set_pump_to_start_THEN_ioc_updates_rbv_for_calculation_of_remaining_volume( self): expected_sp_value = 2.000 script_sp_value = 3.000 pump_for_time = 120 # 1. initialize flowrate self.ca.set_pv_value("FLOWRATE:SP", expected_sp_value) self.ca.assert_that_pv_is("FLOWRATE:SP:RBV", expected_sp_value, timeout=5) # 2. set new flowrate and immediately set pump to run, to simulate script self.ca.set_pv_value("FLOWRATE:SP", script_sp_value) self.ca.set_pv_value("TIME:RUN:SP", pump_for_time) self.ca.set_pv_value("START:SP", "Start") # 3. calculate remaining volume expected_volume_value = (pump_for_time * self.ca.get_pv_value("FLOWRATE:SP:RBV")) / 60 # 4. check ioc calculation is as expected self.ca.assert_that_pv_is("TIME:RUN:CALCVOL", expected_volume_value) @skip_if_recsim("Lewis device logic not supported in RECSIM") def test_GIVEN_an_ioc_WHEN_set_maximum_pressure_limit_THEN_maximum_pressure_limit_is_correct(self): expected_value = 200 self.ca.assert_setting_setpoint_sets_readback(expected_value, "PRESSURE:MAX") @skip_if_recsim("Lewis device logic not supported in RECSIM") def test_GIVEN_an_ioc_WHEN_set_minimum_pressure_limit_THEN_minimum_pressure_limit_is_correct(self): expected_value = 100 self.ca.set_pv_value("PRESSURE:MIN:SP", expected_value) self.ca.assert_setting_setpoint_sets_readback(expected_value, "PRESSURE:MIN") @skip_if_recsim("Lewis device logic not supported in RECSIM") def test_GIVEN_an_ioc_WHEN_continuous_pump_set_THEN_pump_on(self): self.ca.set_pv_value("START:SP", 1) self.ca.assert_that_pv_is("STATUS", "Pumping") @skip_if_recsim("Lewis device logic not supported in RECSIM") def test_GIVEN_an_ioc_WHEN_timed_pump_set_THEN_timed_pump_on(self): # Set a run time for a timed run self.ca.set_pv_value("TIME:RUN:SP", 10000) self.ca.set_pv_value("PUMP_FOR_TIME:SP", 1) self.ca.assert_that_pv_is("STATUS", "Pumping") @skip_if_recsim("Unable to use lewis backdoor in RECSIM") def test_GIVEN_an_ioc_WHEN_get_current_pressure_THEN_current_pressure_returned(self): expected_value = 300 self._lewis.backdoor_set_on_device("pressure", expected_value) self.ca.assert_that_pv_is("PRESSURE", expected_value) @parameterized.expand([ ("component_{}".format(suffix), suffix) for suffix in ["A", "B", "C", "D"] ]) @skip_if_recsim("Reliant on setUP lewis backdoor call") def test_GIVEN_an_ioc_WHEN_get_component_THEN_correct_component_returned(self, component, suffix): expected_value = 10.0 self._lewis.backdoor_set_on_device(component, expected_value) self.ca.assert_that_pv_is("COMP:{}".format(suffix), expected_value) @parameterized.expand([ ("COMP:{}".format(suffix), suffix) for suffix in ["A", "B", "C"] ]) @skip_if_recsim("Reliant on setUP lewis backdoor call") def test_GIVEN_an_ioc_WHEN_set_component_THEN_correct_component_set(self, component, suffix): expected_value = 100.0 self.ca.set_pv_value("COMP:{}:SP".format(suffix), expected_value) if component == "COMP:A": self.ca.set_pv_value("COMP:B:SP", 0) self.ca.set_pv_value("COMP:C:SP", 0) elif component == "COMP:B": self.ca.set_pv_value("COMP:A:SP", 0) self.ca.set_pv_value("COMP:C:SP", 0) elif component == "COMP:C": self.ca.set_pv_value("COMP:A:SP", 0) self.ca.set_pv_value("COMP:B:SP", 0) self.ca.set_pv_value("PUMP_FOR_TIME:SP", "Start") self.ca.assert_that_pv_is(component, expected_value) def test_GIVEN_ioc_initial_state_WHEN_get_error_THEN_error_returned(self): expected_value = "No error" self.ca.assert_that_pv_is("ERROR", expected_value) @skip_if_recsim("Unable to use lewis backdoor in RECSIM") def test_GIVEN_ioc_in_hardware_error_state_WHEN_get_error_THEN_hardware_error_returned(self): expected_value = "Hardware error" self._lewis.backdoor_set_on_device("error", ERROR_STATE_HARDWARE_FAULT) self.ca.assert_that_pv_is("ERROR", expected_value) @skip_if_recsim("Unable to use lewis backdoor in RECSIM") def test_GIVEN_ioc_in_error_state_WHEN_reset_error_THEN_error_reset(self): expected_value = "No error" self._lewis.backdoor_set_on_device("error", ERROR_STATE_NO_ERROR) self.ca.set_pv_value("ERROR:SP", "Reset") self.ca.assert_that_pv_is("ERROR", expected_value) @skip_if_recsim("Unable to use lewis backdoor in RECSIM") def test_GIVEN_ioc_in_error_state_WHEN_reset_error_THEN_error_reset(self): expected_value = "No error" self._lewis.backdoor_set_on_device("error", ERROR_STATE_HARDWARE_FAULT) self.ca.assert_that_pv_is("ERROR", expected_value) @skip_if_recsim("Unable to use lewis backdoor in RECSIM") def test_GIVEN_device_not_connected_WHEN_get_error_THEN_alarm(self): self._lewis.backdoor_set_on_device('connected', False) self.ca.assert_that_pv_alarm_is('ERROR:SP', ChannelAccess.Alarms.INVALID) @skip_if_recsim("Reliant on setUP lewis backdoor call") def test_GIVEN_timed_pump_WHEN_get_program_runtime_THEN_program_runtime_increments(self): self.ca.set_pv_value("TIME:RUN:SP", 10000) self.ca.set_pv_value("PUMP_FOR_TIME:SP", 1) self.ca.assert_that_pv_value_is_increasing("TIME", wait=2) @skip_if_recsim("Lewis device logic not supported in RECSIM") def test_GIVEN_timed_pump_WHEN_set_constant_pump_THEN_state_updated_to_constant_pump(self): # Set a run time for a timed run self.ca.set_pv_value("TIME:RUN:SP", 10000) self.ca.process_pv("PUMP_FOR_TIME:SP") expected_value = "Pumping" self.ca.assert_that_pv_is("STATUS", expected_value) self.ca.process_pv("START:SP") expected_value = "Pumping" self.ca.assert_that_pv_is("STATUS", expected_value) @skip_if_recsim("Lewis device logic not supported in RECSIM") def test_GIVEN_constant_pump_WHEN_set_timed_pump_THEN_state_updated_to_timed_pump(self): expected_value = "Pumping" self.ca.process_pv("START:SP") self.ca.assert_that_pv_is("STATUS", expected_value) # Set a run time for a timed run self.ca.set_pv_value("TIME:RUN:SP", 10000) self.ca.process_pv("PUMP_FOR_TIME:SP") self.ca.assert_that_pv_is("STATUS", expected_value) @skip_if_recsim("Lewis device logic not supported in RECSIM") def test_GIVEN_input_incorrect_WHEN_set_flowrate_THEN_trouble_message_returned(self): self._lewis.backdoor_set_on_device("input_correct", False) self.ca.set_pv_value("FLOWRATE:SP", 0.010) self.ca.assert_that_pv_is("ERROR:STR", "[Error:stack underflow]") @skip_if_recsim("Lewis device logic not supported in RECSIM") def test_GIVEN_command_seq_that_would_crash_pump_WHEN_command_seq_called_THEN_pump_crashes(self): self.ca.set_pv_value("_TEST_CRASH.PROC", 1) self.ca.assert_that_pv_alarm_is("COMP:A", ChannelAccess.Alarms.INVALID, timeout=30) @skip_if_recsim("Lewis device logic not supported in RECSIM") def test_GIVEN_pump_running_WHEN_set_file_number_command_called_THEN_program_is_busy_error(self): expected_value = "[Program is Busy]" self.ca.set_pv_value("START:SP", 1) self.ca.set_pv_value("FILE:SP", 0) self.ca.assert_that_pv_is("ERROR:STR", expected_value) @parameterized.expand([("low_set_time", 100, 1, 1), ("high_set_time", 1000, 10, 1), ("non_standard_set_time", 456, 5, 1)]) @unstable_test(max_retries=5) @skip_if_recsim("Lewis device logic not supported in RECSIM") def test_GIVEN_pump_for_volume_WHEN_pumping_THEN_device_is_pumping_set_volume(self, _, time, volume, flowrate): # Set a target pump time a target pump volume. When we start a pump set volume run, then the remaining # time should be related to the target volume, and not the target time (that would be used for a pump for time). set_time = time set_volume = volume set_flowrate = flowrate expected_time = set_volume * set_flowrate * 60 # flow rate units = mL/min, so convert to seconds self.ca.set_pv_value("TIME:RUN:SP", set_time) self.ca.set_pv_value("TIME:VOL:SP", set_volume) self.ca.set_pv_value("FLOWRATE:SP", set_flowrate) self.ca.process_pv("PUMP_SET_VOLUME:SP") self.ca.assert_that_pv_is_within_range("TIME:REMAINING", min_value=expected_time - 20, max_value=expected_time + 20)