class OerconeTests(unittest.TestCase):
    """
    Tests for the Oercone 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)

    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(TEST_PRESSURE_VALUES))
    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_pressure_is_set_via_backdoor_THEN_readback_updates(self, _, pressure):
        self._lewis.backdoor_set_on_device("pressure", pressure)
        self.ca.assert_that_pv_is_number("PRESSURE", pressure)

    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_measurement_units_is_set_via_backdoor_THEN_readback_updates(self):
        for units in Units:
            self._lewis.backdoor_run_function_on_device("backdoor_set_units", [units.value])
            expected_units = units.name
            self.ca.assert_that_pv_is("PRESSURE.EGU", expected_units)

    @parameterized.expand(parameterized_list(Units))
    def test_WHEN_units_setpoint_set_THEN_read_back_is_correct(self, _, units):
        self.ca.assert_setting_setpoint_sets_readback(units.name, "UNITS")
Beispiel #2
0
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 DriftTests(unittest.TestCase):
    # Tuple format (reading, temperature, expected_drift)
    drift_test_data = (
        ('+1386.05,+4000,101', 47.424, 0.),
        ('+1387.25,+4360,101', 47.243, -0.000666667),
        ('+1388.51,+4720,101', 47.053, -0.00135333),
        ('+1389.79,+5080,101', 46.860, -0.00202627),
        ('+1391.07,+5440,101', 46.667, -0.00268574),
        ('+1392.35,+5800,101', 46.474, -0.00333203),
        ('+1393.71,+6160,101', 46.269, -0.00399872),
        ('+1395.01,+6520,101', 46.072, -0.00461874),
        ('+1396.38,+6880,101', 45.866, -0.0052597),
        ('+1397.70,+7240,101', 45.667, -0.00585451),
    )

    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", "")
        if not IOCRegister.uses_rec_sim:
            self._lewis.backdoor_set_on_device("simulate_readings", False)

    @skip_if_recsim("Cannot use lewis backdoor in recsim")
    def test_GIVEN_empty_buffer_WHEN_values_added_THEN_temp_AND_drift_correct(
            self, test_data=drift_test_data):
        _reset_drift_channels(self)
        readings = [
            r[0] for r in test_data
        ]  # extract reading strings from test data to insert to buffer
        self.ca.set_pv_value("BUFF:SIZE:SP", 1000)
        # GIVEN in setup
        # WHEN
        for i in range(0, len(test_data)):
            _insert_reading(self, [readings[i]])
            # THEN
            self.ca.assert_that_pv_is_number("CHNL:101:DRIFT",
                                             test_data[i][2],
                                             tolerance=DRIFT_TOLERANCE)
            self.ca.assert_that_pv_is_number("CHNL:101:TEMP",
                                             test_data[i][1],
                                             tolerance=TEMP_TOLERANCE)
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 ReflTests(unittest.TestCase):
    """
    Tests for reflectometry server
    """
    def setUp(self):
        self._ioc = IOCRegister.get_running("refl")
        self.ca = ChannelAccess(default_timeout=30,
                                device_prefix=DEVICE_PREFIX)
        self.ca_galil = ChannelAccess(default_timeout=30, device_prefix="MOT")
        self.ca_cs = ChannelAccess(default_timeout=30, device_prefix="CS")
        self.ca_no_prefix = ChannelAccess()
        self.ca_cs.set_pv_value("MOT:STOP:ALL", 1)
        self.ca_cs.assert_that_pv_is("MOT:MOVING", 0, timeout=60)
        self.ca.set_pv_value("BL:MODE:SP", "NR")
        self.ca.set_pv_value("PARAM:S1:SP", 0)
        self.ca.set_pv_value("PARAM:S3:SP", 0)
        self.ca.set_pv_value("PARAM:SMANGLE:SP", 0)
        self.ca.set_pv_value("PARAM:SMOFFSET:SP", 0)
        self.ca.set_pv_value("PARAM:SMINBEAM:SP", "OUT")
        self.ca.set_pv_value("PARAM:THETA:SP", 0)
        self.ca.set_pv_value("PARAM:DET_POS:SP", 0)
        self.ca.set_pv_value("PARAM:DET_ANG:SP", 0)
        self.ca.set_pv_value("PARAM:DET_LONG:SP", 0)
        self.ca.set_pv_value("PARAM:S3INBEAM:SP", "IN")
        self.ca.set_pv_value("PARAM:CHOICE:SP", "MTR0205")
        self.ca_galil.set_pv_value("MTR0207", 0)
        self.ca.set_pv_value("PARAM:NOTINMODE:SP", 0)
        self.ca.set_pv_value("BL:MODE:SP", "NR")
        self.ca.set_pv_value("BL:MOVE", 1)
        self.ca_galil.assert_that_pv_is("MTR0105", 0.0)
        self.ca_cs.assert_that_pv_is("MOT:MOVING", 0, timeout=60)

    def set_up_velocity_tests(self, velocity):
        self.ca_galil.set_pv_value("MTR0102.VELO", velocity)
        self.ca_galil.set_pv_value("MTR0104.VELO", velocity)
        self.ca_galil.set_pv_value(
            "MTR0105.VELO",
            FAST_VELOCITY)  # Remove angle as a speed limiting factor

    def _check_param_pvs(self, param_name, expected_value):
        self.ca.assert_that_pv_is_number("PARAM:%s" % param_name,
                                         expected_value, 0.01)
        self.ca.assert_that_pv_is_number("PARAM:%s:SP" % param_name,
                                         expected_value, 0.01)
        self.ca.assert_that_pv_is_number("PARAM:%s:SP:RBV" % param_name,
                                         expected_value, 0.01)

    @contextmanager
    def _assert_pv_monitors(self, param_name, expected_value):
        with self.ca.assert_that_pv_monitor_is_number("PARAM:%s" % param_name, expected_value, 0.01), \
             self.ca.assert_that_pv_monitor_is_number("PARAM:%s:SP" % param_name, expected_value, 0.01), \
             self.ca.assert_that_pv_monitor_is_number("PARAM:%s:SP:RBV" % param_name, expected_value, 0.01):
            yield

    def test_GIVEN_loaded_WHEN_read_status_THEN_status_ok(self):
        self.ca.assert_that_pv_is("STAT", "OKAY")

    def test_GIVEN_slit_with_beam_along_z_axis_WHEN_set_value_THEN_read_back_MTR_and_setpoints_moves_to_given_value(
            self):
        expected_value = 3.0

        self.ca.set_pv_value("PARAM:S1:SP_NO_ACTION", expected_value)
        self.ca.assert_that_pv_is("PARAM:S1:SP_NO_ACTION", expected_value)
        self.ca.set_pv_value("BL:MOVE", 1)

        self.ca.assert_that_pv_is("PARAM:S1:SP:RBV", expected_value)
        self.ca_galil.assert_that_pv_is("MTR0101", expected_value)
        self.ca_galil.assert_that_pv_is("MTR0101.RBV", expected_value)
        self.ca.assert_that_pv_is("PARAM:S1", expected_value)

    def test_GIVEN_slit_with_beam_along_z_axis_WHEN_set_value_THEN_monitors_updated(
            self):
        expected_value = 3.0

        self.ca.set_pv_value("PARAM:S1:SP_NO_ACTION", expected_value)
        self.ca.set_pv_value("BL:MOVE", 1)
        self.ca.assert_that_pv_monitor_is("PARAM:S1", expected_value)

    def test_GIVEN_theta_with_detector_and_slits3_WHEN_set_theta_THEN_values_are_all_correct_rbvs_updated_via_monitors_and_are_available_via_gets(
            self):
        theta_angle = 2
        self.ca.set_pv_value("PARAM:THETA:SP", theta_angle)

        expected_s3_value = SPACING * tan(radians(theta_angle * 2.0))

        with self._assert_pv_monitors("S1", 0.0), \
             self._assert_pv_monitors("S3", 0.0), \
             self._assert_pv_monitors("THETA", theta_angle), \
             self._assert_pv_monitors("DET_POS", 0.0), \
             self._assert_pv_monitors("DET_ANG", 0.0):

            self.ca.set_pv_value("BL:MOVE", 1)

        # s1 not moved
        self._check_param_pvs("S1", 0.0)
        self.ca_galil.assert_that_pv_is_number("MTR0101", 0.0, 0.01)

        # s3 moved in line
        self._check_param_pvs("S3", 0.0)
        self.ca_galil.assert_that_pv_is_number("MTR0102", expected_s3_value,
                                               0.01)

        # theta set
        self._check_param_pvs("THETA", theta_angle)

        # detector moved in line
        self._check_param_pvs("DET_POS", 0.0)
        expected_det_value = 2 * SPACING * tan(radians(theta_angle * 2.0))
        self.ca_galil.assert_that_pv_is_number("MTR0104", expected_det_value,
                                               0.01)

        # detector angle faces beam
        self._check_param_pvs("DET_ANG", 0.0)
        expected_det_angle = 2.0 * theta_angle
        self.ca_galil.assert_that_pv_is_number("MTR0105", expected_det_angle,
                                               0.01)

    def test_GIVEN_s3_in_beam_WHEN_disable_THEN_monitor_updates_and_motor_moves_to_out_of_beam_position(
            self):
        expected_value = "OUT"

        with self.ca.assert_that_pv_monitor_is("PARAM:S3INBEAM",
                                               expected_value):
            self.ca.set_pv_value("PARAM:S3INBEAM:SP_NO_ACTION", expected_value)
            self.ca.set_pv_value("BL:MOVE", 1)

        self.ca_galil.assert_that_pv_is("MTR0102", OUT_POSITION_HIGH)

    def test_GIVEN_mode_is_NR_WHEN_change_mode_THEN_monitor_updates_to_new_mode(
            self):
        expected_value = "POLARISED"

        with self.ca.assert_that_pv_monitor_is("BL:MODE", expected_value), \
             self.ca.assert_that_pv_monitor_is("BL:MODE.VAL", expected_value):
            self.ca.set_pv_value("BL:MODE:SP", expected_value)

    @unstable_test()
    def test_GIVEN_new_parameter_setpoint_WHEN_triggering_move_THEN_SP_is_only_set_on_motor_when_difference_above_motor_resolution(
            self):
        target_mres = 0.001
        pos_above_res = 0.01
        pos_below_res = pos_above_res + 0.0001
        self.ca_galil.set_pv_value("MTR0101.MRES", target_mres)

        with self.ca_galil.assert_that_pv_monitor_is_number("MTR0101.VAL", pos_above_res), \
             self.ca_galil.assert_that_pv_monitor_is_number("MTR0101.RBV", pos_above_res):

            self.ca.set_pv_value("PARAM:S1:SP", pos_above_res)

        with self.ca_galil.assert_that_pv_monitor_is_number("MTR0101.VAL", pos_above_res), \
             self.ca_galil.assert_that_pv_monitor_is_number("MTR0101.RBV", pos_above_res):

            self.ca.set_pv_value("PARAM:S1:SP", pos_below_res)

    def test_GIVEN_motor_velocity_altered_by_move_WHEN_move_completed_THEN_velocity_reverted_to_original_value(
            self):
        expected = INITIAL_VELOCITY
        self.set_up_velocity_tests(expected)

        self.ca.set_pv_value("PARAM:THETA:SP", 5)

        self.ca_galil.assert_that_pv_is("MTR0102.DMOV", 1, timeout=10)
        self.ca_galil.assert_that_pv_is("MTR0102.VELO", expected)
        self.ca_galil.assert_that_pv_is("MTR0104.DMOV", 1, timeout=10)
        self.ca_galil.assert_that_pv_is("MTR0104.VELO", expected)

    def test_GIVEN_motor_velocity_altered_by_move_WHEN_moving_THEN_velocity_altered(
            self):
        # Given a known initial velocity, confirm that on a move the velocity has changed for the axes
        self.set_up_velocity_tests(INITIAL_VELOCITY)

        self.ca.set_pv_value("PARAM:THETA:SP", 15)

        self.ca_galil.assert_that_pv_is("MTR0102.DMOV", 0, timeout=10)
        self.ca_galil.assert_that_pv_is_not("MTR0102.VELO", INITIAL_VELOCITY)
        self.ca_galil.assert_that_pv_is("MTR0104.DMOV", 0, timeout=10)
        self.ca_galil.assert_that_pv_is_not("MTR0104.VELO", INITIAL_VELOCITY)

    def test_GIVEN_motor_velocity_altered_by_move_WHEN_move_interrupted_THEN_velocity_reverted_to_original_value(
            self):
        expected = INITIAL_VELOCITY
        final_position = SPACING
        self.set_up_velocity_tests(expected)

        # move and wait for completion
        self.ca.set_pv_value("PARAM:THETA:SP", 22.5)
        self.ca_galil.set_pv_value("MTR0102.STOP", 1)
        self.ca_galil.set_pv_value("MTR0104.STOP", 1)
        self.ca_galil.set_pv_value("MTR0105.STOP", 1)

        self.ca_galil.assert_that_pv_is("MTR0102.DMOV", 1, timeout=2)
        self.ca_galil.assert_that_pv_is_not_number("MTR0102.RBV",
                                                   final_position,
                                                   tolerance=0.1)
        self.ca_galil.assert_that_pv_is("MTR0102.VELO", expected)
        self.ca_galil.assert_that_pv_is("MTR0104.DMOV", 1, timeout=2)
        self.ca_galil.assert_that_pv_is_not_number("MTR0104.RBV",
                                                   2 * final_position,
                                                   tolerance=0.1)
        self.ca_galil.assert_that_pv_is("MTR0104.VELO", expected)

    def test_GIVEN_move_was_issued_while_different_move_already_in_progress_WHEN_move_completed_THEN_velocity_reverted_to_value_before_first_move(
            self):
        expected = INITIAL_VELOCITY
        self.set_up_velocity_tests(expected)
        self.ca_galil.set_pv_value("MTR0102", -4)

        self.ca_galil.assert_that_pv_is("MTR0102.DMOV", 0, timeout=1)
        self.ca.set_pv_value("PARAM:THETA:SP", 22.5)

        self.ca_galil.assert_that_pv_is("MTR0102.DMOV", 1, timeout=30)
        self.ca_galil.assert_that_pv_is("MTR0102.VELO", expected)

    def test_GIVEN_move_in_progress_WHEN_modifying_motor_velocity_THEN_velocity_reverted_to_value_before_modified_velocity(
            self):
        # The by-design behaviour (but maybe not expected by the user) is that if a velocity is sent during a move
        # then we ignore this and restore the cached value we had for the currently issued move.
        initial = INITIAL_VELOCITY
        altered = INITIAL_VELOCITY + 5
        expected = INITIAL_VELOCITY
        self.set_up_velocity_tests(initial)

        self.ca.set_pv_value("PARAM:THETA:SP", 22.5)
        self.ca_galil.assert_that_pv_is("MTR0102.DMOV", 0, timeout=1)

        self.ca_galil.set_pv_value("MTR0102.VELO", altered)

        self.ca_galil.assert_that_pv_is("MTR0102.DMOV", 1, timeout=30)
        self.ca_galil.assert_that_pv_is("MTR0102.VELO", expected)

    def test_GIVEN_mode_is_NR_WHEN_change_mode_THEN_monitor_updates_to_new_mode_and_PVs_inmode_are_labeled_as_such(
            self):

        expected_mode_value = "TESTING"
        PARAM_PREFIX = "PARAM:"
        IN_MODE_SUFFIX = ":IN_MODE"
        expected_in_mode_value = "YES"
        expected_out_of_mode_value = "NO"

        with self.ca.assert_that_pv_monitor_is("BL:MODE", expected_mode_value), \
             self.ca.assert_that_pv_monitor_is("BL:MODE.VAL", expected_mode_value):
            self.ca.set_pv_value("BL:MODE:SP", expected_mode_value)

        test_in_mode_param_names = ["S1", "S3", "THETA", "DET_POS", "S3INBEAM"]
        test_out_of_mode_params = ["DET_ANG", "THETA_AUTO"]

        for param in test_in_mode_param_names:
            self.ca.assert_that_pv_monitor_is(
                "{}{}{}".format(PARAM_PREFIX, param, IN_MODE_SUFFIX),
                expected_in_mode_value)

        for param in test_out_of_mode_params:
            self.ca.assert_that_pv_monitor_is(
                "{}{}{}".format(PARAM_PREFIX, param, IN_MODE_SUFFIX),
                expected_out_of_mode_value)

    def test_GIVEN_jaws_set_to_value_WHEN_change_sp_at_low_level_THEN_jaws_sp_rbv_does_not_change(
            self):

        expected_gap_in_refl = 0.2
        expected_change_to_gap = 1.0

        self.ca.set_pv_value("PARAM:S1HG:SP", expected_gap_in_refl)
        self.ca.assert_that_pv_is_number("PARAM:S1HG",
                                         expected_gap_in_refl,
                                         timeout=15,
                                         tolerance=MOTOR_TOLERANCE)

        self.ca_galil.set_pv_value("JAWS1:HGAP:SP", expected_change_to_gap)
        self.ca_galil.assert_that_pv_is_number("JAWS1:HGAP",
                                               expected_change_to_gap,
                                               timeout=15,
                                               tolerance=MOTOR_TOLERANCE)

        self.ca.assert_that_pv_is("PARAM:S1HG", expected_change_to_gap)
        self.ca.assert_that_pv_is("PARAM:S1HG:SP:RBV", expected_gap_in_refl)

    @parameterized.expand([("slits", "S1", 30.00),
                           ("multi_component", "THETA", 20.00),
                           ("angle", "DET_ANG", -80.0),
                           ("displacement", "DET_POS", 20.0),
                           ("binary", "S3INBEAM", 0)])
    def test_GIVEN_new_parameter_sp_WHEN_parameter_rbv_changing_THEN_parameter_changing_pv_correct(
            self, _, param, value):
        expected_value = "YES"
        value = value

        self.ca.set_pv_value("PARAM:{}:SP".format(param), value)
        self.ca.assert_that_pv_is("PARAM:{}:CHANGING".format(param),
                                  expected_value)

    @parameterized.expand([("slits", "S1", 500.00),
                           ("multi_component", "THETA", -500.00),
                           ("angle", "DET_ANG", -800.0),
                           ("displacement", "DET_POS", 500.0),
                           ("binary", "S3INBEAM", 0)])
    def test_GIVEN_new_parameter_sp_WHEN_parameter_rbv_not_changing_THEN_parameter_changing_pv_correct(
            self, _, param, value):
        expected_value = "NO"
        value = value

        self.ca.set_pv_value("PARAM:{}:SP".format(param), value)
        self.ca_cs.set_pv_value("MOT:STOP:ALL", 1)

        self.ca.assert_that_pv_is("PARAM:{}:CHANGING".format(param),
                                  expected_value)

    @parameterized.expand([("slits", "S1", 500.00),
                           ("multi_component", "THETA", 500.00),
                           ("angle", "DET_ANG", -800.0),
                           ("displacement", "DET_POS", 500.0),
                           ("binary", "S3INBEAM", "OUT")])
    def test_GIVEN_new_parameter_sp_WHEN_parameter_rbv_outside_of_sp_target_tolerance_THEN_parameter_at_rbv_pv_correct(
            self, _, param, value):
        expected_value = "NO"
        value = value

        self.ca.set_pv_value("PARAM:{}:SP".format(param), value)
        self.ca_cs.set_pv_value("MOT:STOP:ALL", 1)

        self.ca.assert_that_pv_is("PARAM:{}:RBV:AT_SP".format(param),
                                  expected_value)

    @parameterized.expand([("slits", "S1", 0.00),
                           ("multi_component", "THETA", 0.00),
                           ("angle", "DET_ANG", 0.0),
                           ("displacement", "DET_POS", 0.0),
                           ("binary", "S3INBEAM", 1)])
    def test_GIVEN_new_parameter_sp_WHEN_parameter_rbv_within_sp_target_tolerance_THEN_parameter_at_rbv_pv_correct(
            self, _, param, value):
        expected_value = "YES"
        value = value

        self.ca.set_pv_value("PARAM:{}:SP".format(param), value)
        self.ca_cs.set_pv_value("MOT:STOP:ALL", 1)

        self.ca.assert_that_pv_is("PARAM:{}:RBV:AT_SP".format(param),
                                  expected_value)

    def test_GIVEN_a_low_level_beamline_change_WHEN_values_changed_THEN_high_level_parameters_updated(
            self):
        self.ca_galil.set_pv_value("MTR0102", -400)

        self.ca.assert_that_pv_value_is_changing("PARAM:S3", wait=2)
        self.ca.assert_that_pv_is("PARAM:S3:RBV:AT_SP", "NO")

    def test_GIVEN_engineering_correction_WHEN_move_THEN_move_includes_engineering_correction(
            self):
        theta = 2
        self.ca.set_pv_value("PARAM:THETA:SP", theta)
        self.ca.set_pv_value("PARAM:S5:SP", 0)

        self.ca.assert_that_pv_is(
            "COR:MOT:MTR0206:DESC",
            "Interpolated from file s4_correction.dat on MOT:MTR0206 for s5")
        self.ca.assert_that_pv_is("COR:MOT:MTR0206", theta /
                                  10.0)  # s4 correction is a 1/10 of theta

        # soon after movement starts and before movement stops the velocity should be the same
        distance_from_sample_to_s4 = (3.5 - 2.0) * 2
        expected_position = distance_from_sample_to_s4 * tan(radians(
            theta * 2)) + theta / 10.0
        self.ca_galil.assert_that_pv_is_number("MTR0206.RBV",
                                               expected_position,
                                               tolerance=MOTOR_TOLERANCE,
                                               timeout=30)
        self.ca.assert_that_pv_is_number("PARAM:S5",
                                         0,
                                         tolerance=MOTOR_TOLERANCE,
                                         timeout=10)

    def test_GIVEN_param_not_in_mode_and_sp_changed_WHEN_performing_beamline_move_THEN_sp_is_applied(
            self):
        expected = 1.0
        self.ca.set_pv_value("PARAM:NOTINMODE:SP_NO_ACTION", expected)

        self.ca.set_pv_value("BL:MOVE", 1, wait=True)

        self.ca_galil.assert_that_pv_is("MTR0205.DMOV", 1, timeout=10)
        self.ca.assert_that_pv_is_number("PARAM:NOTINMODE:SP:RBV", expected)
        self.ca.assert_that_pv_is_number("PARAM:NOTINMODE", expected)

    def test_GIVEN_param_not_in_mode_and_sp_changed_WHEN_performing_individual_move_THEN_sp_is_applied(
            self):
        expected = 1.0
        self.ca.set_pv_value("PARAM:NOTINMODE:SP_NO_ACTION", expected)

        self.ca.set_pv_value("PARAM:NOTINMODE:ACTION", 1, wait=True)

        self.ca_galil.assert_that_pv_is("MTR0205.DMOV", 1, timeout=10)
        self.ca.assert_that_pv_is_number("PARAM:NOTINMODE:SP:RBV", expected)
        self.ca.assert_that_pv_is_number("PARAM:NOTINMODE", expected)

    def test_GIVEN_param_not_in_mode_and_sp_changed_WHEN_performing_individual_move_on_other_param_THEN_no_value_applied(
            self):
        param_sp = 0.0
        motor_pos = 1.0
        self.ca.set_pv_value("PARAM:NOTINMODE:SP", param_sp)
        self.ca_galil.set_pv_value("MTR0205", motor_pos, wait=True)
        self.ca_galil.assert_that_pv_is("MTR0205.DMOV", 1, timeout=10)
        self.ca.assert_that_pv_is_number("PARAM:NOTINMODE", motor_pos)

        self.ca.set_pv_value("PARAM:THETA:SP", 0.2, wait=True)
        self.ca_galil.assert_that_pv_is("MTR0205.DMOV", 1, timeout=10)
        self.ca.assert_that_pv_is_number("PARAM:NOTINMODE:SP", param_sp)
        self.ca.assert_that_pv_is_number("PARAM:NOTINMODE:SP:RBV", param_sp)
        self.ca_galil.assert_that_pv_is_number("MTR0205", motor_pos)

    def test_GIVEN_param_not_in_mode_and_sp_unchanged_WHEN_performing_beamline_move_THEN_no_value_applied(
            self):
        param_sp = 0.0
        motor_pos = 1.0
        self.ca_galil.set_pv_value("MTR0205", motor_pos, wait=True)
        self.ca_galil.assert_that_pv_is("MTR0205.DMOV", 1, timeout=10)
        self.ca.assert_that_pv_is_number("PARAM:NOTINMODE", motor_pos)

        self.ca.set_pv_value("BL:MOVE", 1, wait=True)

        self.ca_galil.assert_that_pv_is("MTR0205.DMOV", 1, timeout=10)
        self.ca.assert_that_pv_is_number("PARAM:NOTINMODE:SP", param_sp)
        self.ca.assert_that_pv_is_number("PARAM:NOTINMODE:SP:RBV", param_sp)
        self.ca_galil.assert_that_pv_is_number("MTR0205", motor_pos)

    def test_GIVEN_param_not_in_mode_and_sp_unchanged_WHEN_performing_individual_move_THEN_sp_is_applied(
            self):
        param_sp = 0.0
        motor_pos = 1.0
        self.ca_galil.set_pv_value("MTR0205", motor_pos, wait=True)
        self.ca_galil.assert_that_pv_is("MTR0205.DMOV", 1, timeout=10)
        self.ca.assert_that_pv_is_number("PARAM:NOTINMODE", motor_pos)

        self.ca.set_pv_value("PARAM:NOTINMODE:ACTION", 1, wait=True)

        self.ca_galil.assert_that_pv_is("MTR0205.DMOV", 1, timeout=10)
        self.ca.assert_that_pv_is_number("PARAM:NOTINMODE:SP", param_sp)
        self.ca.assert_that_pv_is_number("PARAM:NOTINMODE:SP:RBV", param_sp)
        self.ca.assert_that_pv_is_number("PARAM:NOTINMODE", param_sp)

    def test_GIVEN_param_not_in_mode_and_sp_unchanged_WHEN_performing_individual_move_on_other_param_THEN_no_value_applied(
            self):
        param_sp = 0.0
        motor_pos = 1.0
        self.ca_galil.set_pv_value("MTR0205", motor_pos, wait=True)
        self.ca_galil.assert_that_pv_is("MTR0205.DMOV", 1, timeout=10)
        self.ca.assert_that_pv_is_number("PARAM:NOTINMODE", motor_pos)

        self.ca.set_pv_value("PARAM:THETA:SP", 0.2, wait=True)

        self.ca_galil.assert_that_pv_is("MTR0205.DMOV", 1, timeout=10)
        self.ca.assert_that_pv_is_number("PARAM:NOTINMODE:SP", param_sp)
        self.ca.assert_that_pv_is_number("PARAM:NOTINMODE:SP:RBV", param_sp)
        self.ca_galil.assert_that_pv_is_number("MTR0205", motor_pos)

    def test_GIVEN_non_synchronised_axis_WHEN_move_which_should_change_velocity_THEN_velocity_not_changed(
            self):
        self.ca_galil.set_pv_value("MTR0103.VELO", MEDIUM_VELOCITY)

        self.ca.set_pv_value("PARAM:THETA:SP", 22.5)

        # soon after movement starts and before movement stops the velocity should be the same
        self.ca_galil.assert_that_pv_is("MTR0103.DMOV", 0, timeout=10)
        self.ca_galil.assert_that_pv_is("MTR0103.VELO",
                                        MEDIUM_VELOCITY,
                                        timeout=0.5)
        self.ca_galil.assert_that_pv_is("MTR0103.DMOV", 0, timeout=10)

        # when the movement finishes it should still be the same
        self.ca_galil.assert_that_pv_is("MTR0103.DMOV", 1, timeout=10)
        self.ca_galil.assert_that_pv_is("MTR0103.VELO", MEDIUM_VELOCITY)

    def test_GIVEN_motor_axis_is_angle_WHEN_motor_alarm_status_is_updated_THEN_alarms_propagate_to_correct_parameters_on_component(
            self):
        expected_severity_code = "MINOR"
        expected_status_code = "HIGH"
        no_alarm_code = "NO_ALARM"

        # Setting High Limit = Low limit produces alarm on 0105 (detector angle)
        self.ca_galil.set_pv_value("MTR0105.HLM", SOFT_LIMIT_LO)

        # detector angle should be in alarm
        self.ca.assert_that_pv_is("PARAM:DET_ANG.STAT", expected_status_code)
        self.ca.assert_that_pv_is("PARAM:DET_ANG.SEVR", expected_severity_code)
        # detector offset is independent and should not be in alarm
        self.ca.assert_that_pv_is("PARAM:DET_POS.STAT", no_alarm_code)
        self.ca.assert_that_pv_is("PARAM:DET_POS.SEVR", no_alarm_code)

        # Setting High Limit back clears alarm
        self.ca_galil.set_pv_value("MTR0105.HLM", SOFT_LIMIT_HI)

        self.ca.assert_that_pv_is("PARAM:DET_ANG.STAT", no_alarm_code)
        self.ca.assert_that_pv_is("PARAM:DET_ANG.SEVR", no_alarm_code)

    def test_GIVEN_motor_axis_is_displacement_WHEN_motor_alarm_status_is_updated_THEN_alarms_propagate_to_correct_parameters_on_component(
            self):
        expected_severity_code = "MINOR"
        expected_status_code = "HIGH"
        no_alarm_code = "NO_ALARM"

        # Setting High Limit = Low limit produces alarm on 0104 (detector height)
        self.ca_galil.set_pv_value("MTR0104.HLM", SOFT_LIMIT_LO)

        # detector offset should be in alarm
        self.ca.assert_that_pv_is("PARAM:DET_POS.STAT", expected_status_code)
        self.ca.assert_that_pv_is("PARAM:DET_POS.SEVR", expected_severity_code)
        # theta is derived from detector offset and should be in alarm
        self.ca.assert_that_pv_is("PARAM:THETA.STAT", expected_status_code)
        self.ca.assert_that_pv_is("PARAM:THETA.SEVR", expected_severity_code)
        # detector angle is independent and should not be in alarm
        self.ca.assert_that_pv_is("PARAM:DET_ANG.STAT", no_alarm_code)
        self.ca.assert_that_pv_is("PARAM:DET_ANG.SEVR", no_alarm_code)

        # Setting High Limit back clears alarm
        self.ca_galil.set_pv_value("MTR0104.HLM", SOFT_LIMIT_HI)

        self.ca.assert_that_pv_is("PARAM:DET_POS.STAT", no_alarm_code)
        self.ca.assert_that_pv_is("PARAM:DET_POS.SEVR", no_alarm_code)
        self.ca.assert_that_pv_is("PARAM:THETA.STAT", no_alarm_code)
        self.ca.assert_that_pv_is("PARAM:THETA.SEVR", no_alarm_code)

    @parameterized.expand([("Variable", "DET_POS", "MTR0104"),
                           ("Frozen", "DET_POS", "MTR0104"),
                           ("Frozen", "DET_ANG", "MTR0105")])
    def test_GIVEN_motors_not_at_zero_WHEN_define_motor_position_to_THEN_motor_position_is_changed_without_move(
            self, initial_foff, param_name, motor_name):
        offset = 10
        new_position = 2
        self.ca.set_pv_value("PARAM:{}:SP".format(param_name), offset)
        self.ca_galil.set_pv_value("MTR0104.FOFF", initial_foff)
        self.ca_galil.set_pv_value("MTR0104.OFF", 0)
        self.ca.assert_that_pv_is_number("PARAM:{}".format(param_name),
                                         offset,
                                         tolerance=MOTOR_TOLERANCE,
                                         timeout=30)
        self.ca_galil.assert_that_pv_is("MTR0104.DMOV", 1, timeout=30)

        with ManagerMode(self.ca_no_prefix):
            self.ca.set_pv_value(
                "PARAM:{}:DEFINE_POSITION_AS".format(param_name), new_position)

        # soon after change there should be no movement, ie a move is triggered but the motor itself does not move so it
        # is very quick
        self.ca_galil.assert_that_pv_is("{}.DMOV".format(motor_name),
                                        1,
                                        timeout=1)
        self.ca_galil.assert_that_pv_is("{}.RBV".format(motor_name),
                                        new_position)
        self.ca_galil.assert_that_pv_is("{}.VAL".format(motor_name),
                                        new_position)
        self.ca_galil.assert_that_pv_is("{}.SET".format(motor_name), "Use")
        self.ca_galil.assert_that_pv_is("{}.FOFF".format(motor_name),
                                        initial_foff)
        self.ca_galil.assert_that_pv_is_number("{}.OFF".format(motor_name),
                                               0.0,
                                               tolerance=MOTOR_TOLERANCE)

        self.ca.assert_that_pv_is("PARAM:{}".format(param_name), new_position)
        self.ca.assert_that_pv_is("PARAM:{}:SP".format(param_name),
                                  new_position)
        self.ca.assert_that_pv_is("PARAM:{}:SP_NO_ACTION".format(param_name),
                                  new_position)
        self.ca.assert_that_pv_is("PARAM:{}:CHANGED".format(param_name), "NO")
        self.ca.assert_that_pv_is("PARAM:THETA", 0)
        self.ca.assert_that_pv_is("PARAM:THETA:SP", 0)
        self.ca.assert_that_pv_is("PARAM:THETA:SP:RBV", 0)

    def test_GIVEN_jaws_not_at_zero_WHEN_define_motor_position_for_jaw_gaps_THEN_jaws_position_are_changed_without_move(
            self):
        param_name = "S1VG"
        jaw_motors = ["MTR0201", "MTR0202"]
        initial_gap = 1.0
        initial_centre = 2.0
        new_gap = 4.0
        expected_pos = {
            "MTR0201": new_gap / 2.0 - initial_centre,
            "MTR0202": new_gap / 2.0 + initial_centre
        }
        self.ca.assert_setting_setpoint_sets_readback(initial_gap,
                                                      "PARAM:S1VG",
                                                      expected_alarm=None,
                                                      timeout=30)
        self.ca.assert_setting_setpoint_sets_readback(initial_centre,
                                                      "PARAM:S1VC",
                                                      expected_alarm=None,
                                                      timeout=30)
        for motor_name in jaw_motors:
            self.ca_galil.set_pv_value("{}.FOFF".format(motor_name), "Frozen")
            self.ca_galil.set_pv_value("{}.OFF".format(motor_name), 0)
        for motor_name in jaw_motors:
            self.ca_galil.assert_that_pv_is("{}.DMOV".format(motor_name),
                                            1,
                                            timeout=30)

        with ManagerMode(self.ca_no_prefix):
            self.ca.set_pv_value(
                "PARAM:{}:DEFINE_POSITION_AS".format(param_name), new_gap)

        # soon after change there should be no movement, ie a move is triggered but the motor itself does not move so it
        # is very quick
        for motor_name in jaw_motors:
            self.ca_galil.assert_that_pv_is("{}.DMOV".format(motor_name),
                                            1,
                                            timeout=1)

        for motor_name in jaw_motors:
            # jaws are open to half the gap
            self.ca_galil.assert_that_pv_is("{}.RBV".format(motor_name),
                                            expected_pos[motor_name])
            self.ca_galil.assert_that_pv_is("{}.VAL".format(motor_name),
                                            expected_pos[motor_name])
            self.ca_galil.assert_that_pv_is("{}.SET".format(motor_name), "Use")
            self.ca_galil.assert_that_pv_is("{}.FOFF".format(motor_name),
                                            "Frozen")
            self.ca_galil.assert_that_pv_is_number("{}.OFF".format(motor_name),
                                                   0.0,
                                                   tolerance=MOTOR_TOLERANCE)

        self.ca.assert_that_pv_is("PARAM:{}".format(param_name), new_gap)
        self.ca.assert_that_pv_is("PARAM:{}:SP".format(param_name), new_gap)
        self.ca.assert_that_pv_is("PARAM:{}:SP_NO_ACTION".format(param_name),
                                  new_gap)
        self.ca.assert_that_pv_is("PARAM:{}:CHANGED".format(param_name), "NO")

    def test_GIVEN_jaws_not_at_zero_WHEN_define_motor_position_for_jaw_centres_THEN_jaws_position_are_changed_without_move(
            self):
        param_name = "S1HC"
        jaw_motors = ["MTR0203", "MTR0204"]
        initial_gap = 1.0
        initial_centre = 2.0
        new_centre = 4.0
        expected_pos = {
            "MTR0203": initial_gap / 2.0 + new_centre,
            "MTR0204": initial_gap / 2.0 - new_centre
        }
        self.ca.assert_setting_setpoint_sets_readback(initial_gap,
                                                      "PARAM:S1HG",
                                                      expected_alarm=None,
                                                      timeout=30)
        self.ca.assert_setting_setpoint_sets_readback(initial_centre,
                                                      "PARAM:S1HC",
                                                      expected_alarm=None,
                                                      timeout=30)
        for motor_name in jaw_motors:
            self.ca_galil.set_pv_value("{}.FOFF".format(motor_name), "Frozen")
            self.ca_galil.set_pv_value("{}.OFF".format(motor_name), 0)
        for motor_name in jaw_motors:
            self.ca_galil.assert_that_pv_is("{}.DMOV".format(motor_name),
                                            1,
                                            timeout=30)

        with ManagerMode(self.ca_no_prefix):
            self.ca.set_pv_value(
                "PARAM:{}:DEFINE_POSITION_AS".format(param_name), new_centre)

        # soon after change there should be no movement, ie a move is triggered but the motor itself does not move so it
        # is very quick
        for motor_name in jaw_motors:
            self.ca_galil.assert_that_pv_is("{}.DMOV".format(motor_name),
                                            1,
                                            timeout=1)

        for motor_name in jaw_motors:
            # jaws are open to half the gap
            self.ca_galil.assert_that_pv_is("{}.RBV".format(motor_name),
                                            expected_pos[motor_name])
            self.ca_galil.assert_that_pv_is("{}.VAL".format(motor_name),
                                            expected_pos[motor_name])
            self.ca_galil.assert_that_pv_is("{}.SET".format(motor_name), "Use")
            self.ca_galil.assert_that_pv_is("{}.FOFF".format(motor_name),
                                            "Frozen")
            self.ca_galil.assert_that_pv_is_number("{}.OFF".format(motor_name),
                                                   0.0,
                                                   tolerance=MOTOR_TOLERANCE)

        self.ca.assert_that_pv_is("PARAM:{}".format(param_name), new_centre)
        self.ca.assert_that_pv_is("PARAM:{}:SP".format(param_name), new_centre)
        self.ca.assert_that_pv_is("PARAM:{}:SP_NO_ACTION".format(param_name),
                                  new_centre)
        self.ca.assert_that_pv_is("PARAM:{}:CHANGED".format(param_name), "NO")

    def test_GIVEN_theta_THEN_define_position_as_does_not_exist(self):
        param_name = "THETA"
        self.ca.assert_that_pv_exists("PARAM:{}".format(param_name))
        self.ca.assert_that_pv_does_not_exist(
            "PARAM:{}:DEFINE_POSITION_AS".format(param_name))

    def test_GIVEN_motors_at_zero_WHEN_define_motor_position_to_and_back_multiple_times_THEN_motor_position_is_changed_without_move(
            self):
        param_name = "DET_POS"
        motor_name = "MTR0104"
        initial_foff = "Frozen"
        self.ca.set_pv_value("PARAM:{}:SP".format(param_name), 0)
        self.ca_galil.set_pv_value("MTR0104.FOFF", initial_foff)
        self.ca_galil.set_pv_value("MTR0104.OFF", 0)
        self.ca.assert_that_pv_is_number("PARAM:{}".format(param_name),
                                         0,
                                         tolerance=MOTOR_TOLERANCE,
                                         timeout=30)
        self.ca_galil.assert_that_pv_is("MTR0104.DMOV", 1, timeout=30)

        for i in range(20):
            new_position = i - 5
            with ManagerMode(self.ca_no_prefix):
                self.ca.set_pv_value(
                    "PARAM:{}:DEFINE_POSITION_AS".format(param_name),
                    new_position)

            # soon after change there should be no movement, ie a move is triggered but the motor itself does not move so it
            # is very quick
            self.ca_galil.assert_that_pv_is("{}.DMOV".format(motor_name),
                                            1,
                                            timeout=1)
            self.ca_galil.assert_that_pv_is("{}.RBV".format(motor_name),
                                            new_position)
            self.ca_galil.assert_that_pv_is("{}.VAL".format(motor_name),
                                            new_position)
            self.ca_galil.assert_that_pv_is("{}.SET".format(motor_name), "Use")
            self.ca_galil.assert_that_pv_is("{}.FOFF".format(motor_name),
                                            initial_foff)
            self.ca_galil.assert_that_pv_is_number("{}.OFF".format(motor_name),
                                                   0.0,
                                                   tolerance=MOTOR_TOLERANCE)

            self.ca.assert_that_pv_is("PARAM:{}".format(param_name),
                                      new_position)
            self.ca.assert_that_pv_is("PARAM:{}:SP".format(param_name),
                                      new_position)
            self.ca.assert_that_pv_is(
                "PARAM:{}:SP_NO_ACTION".format(param_name), new_position)
            self.ca.assert_that_pv_is("PARAM:{}:CHANGED".format(param_name),
                                      "NO")

    def test_GIVEN_parameter_not_in_manager_mode_WHEN_define_position_THEN_position_is_not_defined(
            self):
        new_position = 10

        param_pv = "PARAM:{}:DEFINE_POSITION_AS".format("DET_POS")
        self.assertRaises(IOError, self.ca.set_pv_value, param_pv,
                          new_position)

        self.ca.assert_that_pv_is_not(param_pv, new_position)

    def test_GIVEN_value_parameter_WHEN_read_THEN_value_returned(self):

        param_pv = "CONST:TEN"

        self.ca.assert_that_pv_is(param_pv, 10)
        self.ca.assert_that_pv_is("{}.DESC".format(param_pv), "The value 10")

    def test_GIVEN_bool_parameter_WHEN_read_THEN_value_returned(self):

        param_pv = "CONST:YES"

        self.ca.assert_that_pv_is(param_pv, "YES")

    def test_GIVEN_string_constant_parameter_WHEN_read_THEN_value_returned(
            self):

        param_pv = "CONST:STRING"

        self.ca.assert_that_pv_is(param_pv, "Test String")

    def test_GIVEN_PNR_mode_with_SM_angle_WHEN_move_in_disable_mode_and_into_PNR_THEN_beamline_is_updated_on_mode_change_and_value_of_pd_offsets_correct(
            self):

        self.ca.set_pv_value("BL:MODE:SP", "POLARISED")
        self.ca.set_pv_value("PARAM:SMANGLE:SP_NO_ACTION", 0.2)
        self.ca.set_pv_value("BL:MOVE", 1)
        self.ca.assert_that_pv_is_number("PARAM:SMANGLE", 0.2, tolerance=1e-2)

        self.ca.set_pv_value("BL:MODE:SP", "DISABLED")
        self.ca.set_pv_value("PARAM:SMANGLE:SP", 0)
        self.ca.assert_that_pv_is_number("PARAM:SMANGLE", 0.0, tolerance=1e-2)

        self.ca.assert_that_pv_is_number("PARAM:S3", 0.0, tolerance=1e-2)
        self.ca.assert_that_pv_is_number("PARAM:DET_POS", 0.0, tolerance=1e-2)

        self.ca.set_pv_value("BL:MODE:SP", "POLARISED")

        # In polarised mode the sm angle will now make everything appear to be in the wrong place.
        # This test will also check that on changing modes the beamline is updated
        self.ca.assert_that_pv_is_not_number("PARAM:S3", 0.0, tolerance=1e-2)
        self.ca.assert_that_pv_is_not_number("PARAM:DET_POS",
                                             0.0,
                                             tolerance=1e-2)

    @parameterized.expand([(0, OUT_POSITION_HIGH), (22.5, OUT_POSITION_LOW)])
    def test_GIVEN_component_with_multiple_parked_positions_WHEN_moving_out_of_beam_THEN_driver_moves_to_correct_out_of_beam_position_based_on_beam_interception(
            self, theta_sp, expected_out_of_beam_position):
        self.ca.assert_that_pv_is("PARAM:S3INBEAM", "IN")

        self.ca.set_pv_value("PARAM:THETA:SP_NO_ACTION", theta_sp, wait=True)
        self.ca.set_pv_value("PARAM:S3INBEAM:SP_NO_ACTION", "OUT", wait=True)
        self.ca.set_pv_value("BL:MOVE", 1, wait=True)

        self.ca_galil.assert_that_pv_is_number("MTR0102.VAL",
                                               expected_out_of_beam_position,
                                               timeout=5)

    def test_GIVEN_component_with_multiple_out_of_beam_positions_is_out_of_beam_WHEN_beam_intercept_moves_above_threshold_THEN_driver_moves_to_correct_out_of_beam_position(
            self):
        self.ca.assert_that_pv_is("PARAM:S3INBEAM", "IN")
        self.ca.set_pv_value("PARAM:S3INBEAM:SP", "OUT", wait=True)
        self.ca.assert_that_pv_is("PARAM:S3INBEAM:CHANGING", "NO", timeout=30)
        self.ca_galil.assert_that_pv_is_number("MTR0102.RBV",
                                               OUT_POSITION_HIGH,
                                               timeout=20)

        self.ca.set_pv_value("PARAM:THETA:SP", 22.5, wait=True)

        self.ca_galil.assert_that_pv_is_number("MTR0102.VAL",
                                               OUT_POSITION_LOW,
                                               timeout=5)

    @parameterized.expand([(0, OUT_POSITION_HIGH, "OUT"),
                           (0, OUT_POSITION_LOW, "IN"),
                           (22.5, OUT_POSITION_HIGH, "IN"),
                           (22.5, OUT_POSITION_LOW, "OUT")])
    def test_GIVEN_component_with_multiple_parked_positions_WHEN_moving_axis_to_sp_THEN_inbeam_param_reports_correct_rbv(
            self, theta_sp, axis_sp, expected_inbeam_status):
        self.ca.assert_that_pv_is("PARAM:S3INBEAM", "IN")

        self.ca.set_pv_value("PARAM:THETA:SP", theta_sp, wait=True)
        self.ca.assert_that_pv_is("PARAM:THETA:CHANGING", "NO", timeout=30)

        self.ca_galil.set_pv_value("MTR0102.VAL", axis_sp, wait=True)

        self.ca.assert_that_pv_is("PARAM:S3INBEAM:CHANGING", "NO", timeout=30)
        self.ca.assert_that_pv_is("PARAM:S3INBEAM", expected_inbeam_status)

    def test_GIVEN_driver_with_param_value_dependent_axis_WHEN_set_value_THEN_correct_axis_drives_to_position_and_read_back_is_correct(
            self):
        expected_offset1 = 0.3
        expected_offset2 = 0.2
        for expected_offset, choice, mot0205, mot0207 in [
            (expected_offset1, "MTR0207", 0, expected_offset1),
            (expected_offset2, "MTR0205", expected_offset2, expected_offset1)
        ]:
            self.ca.assert_setting_setpoint_sets_readback(
                choice, "PARAM:CHOICE")
            self.ca.set_pv_value("PARAM:NOTINMODE:SP", expected_offset)

            self.ca.assert_that_pv_is("PARAM:NOTINMODE",
                                      expected_offset,
                                      timeout=20)
            self.ca_galil.assert_that_pv_is_number("MTR0205.RBV",
                                                   mot0205,
                                                   timeout=20)
            self.ca_galil.assert_that_pv_is_number("MTR0207.RBV",
                                                   mot0207,
                                                   timeout=20)

    def test_GIVEN_theta_WHEN_detector_long_axis_changes_THEN_detector_tracks(
            self):
        theta_angle = 2
        long_axis_addition = 1
        self.ca.set_pv_value("PARAM:THETA:SP", theta_angle)
        self.ca.set_pv_value("BL:MOVE", 1)

        # theta set
        self._check_param_pvs("THETA", theta_angle)

        self.ca.set_pv_value("PARAM:DET_LONG:SP", long_axis_addition)
        self.ca.set_pv_value("BL:MOVE", 1)

        expected_det_value = (2 * SPACING + long_axis_addition) * tan(
            radians(theta_angle * 2.0))

        self._check_param_pvs("DET_LONG", long_axis_addition)
        self._check_param_pvs("DET_POS", 0.0)
        self.ca_galil.assert_that_pv_is_number("MTR0104", expected_det_value,
                                               0.01)
class Itc503Tests(unittest.TestCase):
    """
    Tests for the Itc503 IOC.
    """
    def setUp(self):
        self._lewis, self._ioc = get_running_lewis_and_ioc(
            "itc503", DEVICE_PREFIX)
        self.ca = ChannelAccess(device_prefix=DEVICE_PREFIX,
                                default_timeout=20)
        self.ca.assert_that_pv_exists("DISABLE")
        self._make_device_scan_faster()

    def _make_device_scan_faster(self):
        """
        Purely so that the tests run faster, the real IOC scans excruciatingly slowly.
        """
        # Skip setting the PVs if the scan rate is already fast
        self.ca.assert_that_pv_exists("FAN1")
        self.ca.assert_that_pv_exists("FAN2")
        if self.ca.get_pv_value("FAN1.SCAN") != ".1 second":
            for i in range(1, 8 + 1):
                # Ensure all DLY links are 0 in both FAN records
                self.ca.set_pv_value("FAN1.DLY{}".format(i), 0)
                self.ca.set_pv_value("FAN2.DLY{}".format(i), 0)

            # Set the scan rate to .1 second (setting string does not work, have to use numeric value)
            self.ca.set_pv_value("FAN1.SCAN", 9)

    def test_WHEN_ioc_is_started_THEN_it_is_not_disabled(self):
        self.ca.assert_that_pv_is("DISABLE", "COMMS ENABLED")

    @parameterized.expand(
        (pv, val)
        for pv, val in itertools.product(["P", "I", "D"], TEST_VALUES))
    def test_WHEN_setting_pid_settings_THEN_can_be_read_back(self, pv, val):
        self.ca.set_pv_value("{}:SP".format(pv), val)
        self.ca.assert_that_pv_is_number(
            pv, val, tolerance=0.1)  # Only comes back to 1dp

    @parameterized.expand(val for val in parameterized_list(TEST_VALUES))
    def test_WHEN_setting_flows_THEN_can_be_read_back(self, _, val):
        self.ca.set_pv_value("GASFLOW:SP", val)
        self.ca.assert_that_pv_is_number(
            "GASFLOW", val, tolerance=0.1)  # Only comes back to 1dp

    @parameterized.expand(mode for mode in parameterized_list(MODES))
    def test_WHEN_setting_gas_flow_control_mode_THEN_can_be_read_back(
            self, _, mode):
        self.ca.assert_setting_setpoint_sets_readback(mode, "MODE:GAS")

    @parameterized.expand(mode for mode in parameterized_list(MODES))
    def test_WHEN_setting_heater_flow_control_mode_THEN_can_be_read_back(
            self, _, mode):
        self.ca.assert_setting_setpoint_sets_readback(mode, "MODE:HTR")

    @parameterized.expand(val for val in parameterized_list(TEST_VALUES))
    def test_WHEN_temperature_is_set_THEN_temperature_and_setpoint_readbacks_update_to_new_value(
            self, _, val):
        self.ca.set_pv_value("TEMP:SP", val)
        self.ca.assert_that_pv_is_number("TEMP:SP:RBV", val, tolerance=0.1)
        self.ca.assert_that_pv_is_number("TEMP:1", val, tolerance=0.1)
        self.ca.assert_that_pv_is_number("TEMP:2", val, tolerance=0.1)
        self.ca.assert_that_pv_is_number("TEMP:3", val, tolerance=0.1)

    @parameterized.expand(chan for chan in parameterized_list(CHANNELS))
    @skip_if_recsim(
        "Comes back via record redirection which recsim can't handle easily")
    def test_WHEN_control_channel_is_set_THEN_control_channel_can_be_read_back(
            self, _, chan):
        self.ca.assert_setting_setpoint_sets_readback(chan, "CTRLCHANNEL")

    @parameterized.expand(mode for mode in CTRL_MODE_ALARMS)
    @skip_if_recsim(
        "Comes back via record redirection which recsim can't handle easily")
    def test_WHEN_setting_control_mode_THEN_can_be_read_back(self, mode):
        self.ca.assert_setting_setpoint_sets_readback(
            mode, "CTRL", expected_alarm=CTRL_MODE_ALARMS[mode])

    @skip_if_recsim("Backdoor does not exist in recsim")
    def test_WHEN_sweeping_mode_is_set_via_backdoor_THEN_it_updates_in_the_ioc(
            self):
        self._lewis.backdoor_set_on_device("sweeping", False)
        self.ca.assert_that_pv_is("SWEEPING", "Not Sweeping")

        self._lewis.backdoor_set_on_device("sweeping", True)
        self.ca.assert_that_pv_is("SWEEPING", "Sweeping")

    @parameterized.expand(state for state in ("ON", "OFF"))
    @skip_if_recsim(
        "Comes back via record redirection which recsim can't handle easily")
    def test_WHEN_setting_autopid_THEN_readback_reflects_setting_just_sent(
            self, state):
        self.ca.assert_setting_setpoint_sets_readback(state, "AUTOPID")

    @parameterized.expand(val for val in parameterized_list(TEST_VALUES))
    @skip_if_recsim("Backdoor does not exist in recsim")
    def test_WHEN_heater_voltage_is_set_THEN_heater_voltage_updates(
            self, _, val):
        self.ca.set_pv_value("HEATERP:SP", val)
        self.ca.assert_that_pv_is_number("HEATERP", val, tolerance=0.1)

        # Emulator responds with heater p == heater v. Test that heater p is also reading.
        self.ca.assert_that_pv_is_number("HEATERV", val, tolerance=0.1)

    @parameterized.expand(
        control_command
        for control_command in parameterized_list(ALL_CONTROL_COMMANDS))
    @skip_if_recsim(
        "Comes back via record redirection which recsim can't handle easily")
    def test_WHEN_control_command_sent_THEN_remote_unlocked_set(
            self, _, control_pv, set_value):
        self.ca.set_pv_value("CTRL", "Locked")
        self.ca.set_pv_value("{}:SP".format(control_pv), set_value)
        self.ca.assert_that_pv_is("CTRL", "Local and remote")
        self.ca.set_pv_value("CTRL", "Locked")

    @skip_if_recsim(
        "Comes back via record redirection which recsim can't handle easily")
    def test_WHEN_sweeping_reported_by_hardware_THEN_correct_sweep_state_reported(
            self):
        """
        The hardware can report the control channel with and without a leading zero (depending on the hardware).
        Ensure we catch all cases.
        """
        for report_sweep_state_with_leading_zero in [True, False]:
            for sweeping in [True, False]:
                self._lewis.backdoor_set_on_device(
                    "report_sweep_state_with_leading_zero",
                    report_sweep_state_with_leading_zero)
                self._lewis.backdoor_set_on_device("sweeping", sweeping)
                self.ca.assert_that_pv_is(
                    "SWEEPING", "Sweeping" if sweeping else "Not Sweeping")
class CybamanTests(unittest.TestCase):
    """
    Tests for the cybaman IOC.
    """

    AXES = ["A", "B", "C"]
    test_positions = [-200, -1.23, 0, 180.0]

    def setUp(self):
        self._lewis, self._ioc = get_running_lewis_and_ioc(
            EMULATOR_DEVICE, DEVICE_PREFIX)
        self.ca = ChannelAccess(default_timeout=20,
                                device_prefix=DEVICE_PREFIX)
        self.ca.assert_that_pv_exists("INITIALIZE", timeout=30)

        self._lewis.backdoor_set_on_device('connected', True)

        # Check that all the relevant PVs are up.
        for axis in self.AXES:
            self.ca.assert_that_pv_exists(axis)
            self.ca.assert_that_pv_exists("{}:SP".format(axis))

        # Initialize the device, do this in setup to avoid doing it in every test
        self.ca.set_pv_value("INITIALIZE", 1)
        self.ca.assert_that_pv_is("INITIALIZED", "TRUE")

    def test_WHEN_ioc_is_started_THEN_ioc_is_not_disabled(self):
        self.ca.assert_that_pv_is("DISABLE", "COMMS ENABLED")

    @skip_if_recsim("Uses lewis backdoor command")
    def test_WHEN_position_setpoints_are_set_via_backdoor_THEN_positions_move_towards_setpoints(
            self):
        for axis in self.AXES:
            for pos in self.test_positions:
                self._lewis.backdoor_set_on_device(
                    "{}_setpoint".format(axis.lower()), pos)
                self.ca.assert_that_pv_is_number("{}".format(axis),
                                                 pos,
                                                 tolerance=0.01)

    @skip_if_recsim("Uses lewis backdoor command")
    def test_GIVEN_home_position_is_set_WHEN_home_pv_is_set_THEN_position_moves_towards_home(
            self):
        for axis in self.AXES:
            for pos in self.test_positions:
                self._lewis.backdoor_set_on_device(
                    "home_position_axis_{}".format(axis.lower()), pos)
                self.ca.set_pv_value("{}:HOME".format(axis), 1)
                self.ca.assert_that_pv_is_number("{}".format(axis),
                                                 pos,
                                                 tolerance=0.01)

    @skip_if_recsim("Uses lewis backdoor command")
    def test_GIVEN_a_device_in_some_other_state_WHEN_reset_command_is_sent_THEN_device_is_reset_to_original_state(
            self):

        modifier = 12.34

        # Reset cybaman
        self.ca.set_pv_value("RESET", 1)
        self.ca.assert_that_pv_is("INITIALIZED", "FALSE")
        self.ca.set_pv_value("INITIALIZE", 1)
        self.ca.assert_that_pv_is("INITIALIZED", "TRUE")
        self.ca.assert_that_pv_value_is_unchanged("INITIALIZED", 10)

        original = {}
        for axis in self.AXES:
            original[axis] = float(
                self.ca.get_pv_value("{}".format(axis.upper())))

            # Set both value and setpoint to avoid the device moving back towards the setpoint
            self._lewis.backdoor_set_on_device(
                "{}_setpoint".format(axis.lower()), original[axis] + modifier)
            self._lewis.backdoor_set_on_device("{}".format(axis.lower()),
                                               original[axis] + modifier)

            self.ca.assert_that_pv_is_number("{}".format(axis.upper()),
                                             original[axis] + modifier,
                                             tolerance=0.001)

        # Reset cybaman
        self.ca.set_pv_value("RESET", 1)

        # Check that a, b and c values are now at original values
        for axis in self.AXES:
            self.ca.assert_that_pv_is_number("{}".format(axis.upper()),
                                             original[axis],
                                             tolerance=0.001)

    def test_GIVEN_a_device_in_initialized_state_WHEN_setpoints_are_sent_THEN_device_goes_to_setpoint(
            self):
        for axis in self.AXES:
            for pos in self.test_positions:
                self.ca.set_pv_value("{}:SP".format(axis.upper()), pos)
                self.ca.assert_that_pv_is_number("{}".format(axis.upper()),
                                                 pos)

    @skip_if_recsim("Uses lewis backdoor command")
    def test_GIVEN_a_device_with_a_setpoint_less_than_minus_150_WHEN_homed_THEN_setpoint_is_set_to_minus_150_before_home(
            self):
        for axis in self.AXES:
            # Ensure home position is known
            self._lewis.backdoor_set_on_device(
                "home_position_axis_{}".format(axis.lower()), 100)

            # Ensure setpoint and readback are less than -150
            self.ca.set_pv_value("{}:SP".format(axis.upper()), -155)
            self.ca.assert_that_pv_is_number("{}".format(axis.upper()),
                                             -155,
                                             tolerance=0.01)

            # Tell axis to home
            self.ca.set_pv_value("{}:HOME".format(axis.upper()), 1)

            # Ensure that setpoint is updated to -150 before home
            self.ca.assert_that_pv_is_number("{}:SP".format(axis.upper()),
                                             -150,
                                             tolerance=0.01)

            # Let device actually reach home position
            self.ca.assert_that_pv_is_number("{}".format(axis.upper()), 100)

    @skip_if_recsim("Uses lewis backdoor command")
    def test_GIVEN_a_device_with_a_setpoint_more_than_minus_150_WHEN_homed_THEN_setpoint_is_not_set_before_home(
            self):
        for axis in self.AXES:
            # Ensure home position is known
            self._lewis.backdoor_set_on_device(
                "home_position_axis_{}".format(axis.lower()), 100)

            # Ensure setpoint and readback are more than -150
            self.ca.set_pv_value("{}:SP".format(axis.upper()), -145)
            self.ca.assert_that_pv_is_number("{}".format(axis.upper()),
                                             -145,
                                             tolerance=0.01)

            # Tell axis to home
            self.ca.set_pv_value("{}:HOME".format(axis.upper()), 1)

            # Ensure that setpoint has not been updated
            self.ca.assert_that_pv_is_number("{}:SP".format(axis.upper()),
                                             -145,
                                             tolerance=0.01)

            # Let device actually reach home position
            self.ca.assert_that_pv_is_number("{}".format(axis.upper()), 100)

    def test_GIVEN_a_device_at_a_specific_position_WHEN_setpoint_is_updated_THEN_tm_val_is_calculated_correctly(
            self):

        test_cases = (
            # No change in setpoint, TM val should be 4000
            {
                "old_pos": (-1, -2, -3),
                "axis_to_change": "A",
                "new_setpoint": -1,
                "expected_tm_val": 4000
            },
            # Test case provided from flowchart specification
            {
                "old_pos": (0, 0, 0),
                "axis_to_change": "A",
                "new_setpoint": 30,
                "expected_tm_val": 6000
            },
            # Test case provided from flowchart specification
            {
                "old_pos": (11, -5, 102),
                "axis_to_change": "C",
                "new_setpoint": 50,
                "expected_tm_val": 10000
            },
            # Very small change, TM val should be 4000
            {
                "old_pos": (10, 20, 30),
                "axis_to_change": "B",
                "new_setpoint": 21,
                "expected_tm_val": 4000
            },
        )

        for case in test_cases:
            # Ensure original position is what it's meant to be
            for axis, setpoint in zip(self.AXES, case["old_pos"]):
                self.ca.set_pv_value("{}:SP".format(axis.upper()), setpoint)
                self.ca.assert_that_pv_is_number("{}".format(axis.upper()),
                                                 setpoint,
                                                 tolerance=0.01)

            # Change the relevant axis to a new setpoint
            self.ca.set_pv_value(
                "{}:SP".format(case["axis_to_change"].upper()),
                case["new_setpoint"])

            # Assert that the TM val calculation record contains the correct value
            # Tolerance is 1001 because rounding errors would get multiplied by 1000
            self.ca.assert_that_pv_is_number("{}:_CALC_TM_AND_SET".format(
                case["axis_to_change"].upper()),
                                             case["expected_tm_val"],
                                             tolerance=1001)
class GemJawsTests(unittest.TestCase):
    """
    Tests for the gem beamscraper jaws
    """
    def setUp(self):
        self._ioc = IOCRegister.get_running("gem_jaws")
        self.ca = ChannelAccess(default_timeout=30)

        [self.ca.assert_that_pv_exists(mot) for mot in all_motors]

    def _test_readback(self, underlying_motor, calibrated_axis, to_read_func, x):
        self.ca.set_pv_value(underlying_motor, x, wait=True)
        self.ca.assert_that_pv_is_number(underlying_motor + ".DMOV", 1)  # Wait for axis to finish moving
        self.ca.assert_that_pv_is_number(calibrated_axis + ".RBV", to_read_func(x), TOLERANCE)

    def _test_set_point(self, underlying_motor, calibrated_axis, to_write_func, x):
        self.ca.set_pv_value(calibrated_axis, x, wait=True)
        self.ca.assert_that_pv_is_number(underlying_motor + ".DMOV", 1)  # Wait for axis to finish moving
        self.ca.assert_that_pv_is_number(underlying_motor + ".VAL", to_write_func(x), TOLERANCE)

    def test_WHEN_underlying_quadratic_motor_set_to_a_position_THEN_calibrated_axis_as_expected(self):
        motors = {MOTOR_E: UNDERLYING_MTR_EAST, MOTOR_W: UNDERLYING_MTR_WEST}
        for mot, underlying in motors.items():
            for position in TEST_POSITIONS:
                self._test_readback(underlying, mot, calc_expected_quad_read, position)

    def test_WHEN_calibrated_quadratic_motor_set_to_a_position_THEN_underlying_motor_as_expected(self):
        motors = {MOTOR_E: UNDERLYING_MTR_EAST, MOTOR_W: UNDERLYING_MTR_WEST}
        for mot, underlying in motors.items():
            for position in TEST_POSITIONS:
                self._test_set_point(underlying, mot, calc_expected_quad_write, position)

    def test_WHEN_underlying_linear_motor_set_to_a_position_THEN_calibrated_axis_as_expected(self):
        motors = {MOTOR_N: UNDERLYING_MTR_NORTH, MOTOR_S: UNDERLYING_MTR_SOUTH}
        for mot, underlying in motors.items():
            for position in TEST_POSITIONS:
                self._test_readback(underlying, mot, calc_expected_linear_read, position)

    def test_WHEN_calibrated_linear_motor_set_to_a_position_THEN_underlying_motor_as_expected(self):
        motors = {MOTOR_N: UNDERLYING_MTR_NORTH, MOTOR_S: UNDERLYING_MTR_SOUTH}
        for mot, underlying in motors.items():
            for position in TEST_POSITIONS:
                self._test_set_point(underlying, mot, calc_expected_linear_write, position)

    def test_GIVEN_quad_calibrated_motor_limits_set_THEN_underlying_motor_limits_set(self):
        for lim in [".LLM", ".HLM"]:
            underlying_limit = self.ca.get_pv_value(UNDERLYING_MTR_WEST + lim)

            self.ca.assert_that_pv_is_number(MOTOR_W + lim, calc_expected_quad_read(underlying_limit),
                                             TOLERANCE)

    def test_GIVEN_linear_calibrated_motor_limits_set_THEN_underlying_motor_limits_set(self):
        for lim in [".LLM", ".HLM"]:
            underlying_limit = self.ca.get_pv_value(UNDERLYING_MTR_NORTH + lim)

            self.ca.assert_that_pv_is_number(MOTOR_N + lim, calc_expected_linear_read(underlying_limit),
                                             TOLERANCE)
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 GemorcTests(unittest.TestCase):
    """
    Tests for the Gemorc IOC.
    """
    def reset_emulator(self):
        self._lewis.backdoor_set_on_device("reset", True)
        sleep(
            1
        )  # Wait for reset to finish so we don't jump the gun. No external indicator from emulator

    def reset_ioc(self):
        self.ca.set_pv_value("RESET", 1)
        # INIT:ONCE is a property held exclusively in the IOC
        calc_pv = "INIT:ONCE:CALC.CALC"
        original_calc = self.ca.get_pv_value(calc_pv)
        self.ca.set_pv_value(calc_pv, "0")
        self.ca.assert_that_pv_is("INIT:ONCE", "No")
        self.ca.set_pv_value(calc_pv, original_calc)

    def setUp(self):
        self._lewis, self._ioc = get_running_lewis_and_ioc(
            "gemorc", DEVICE_PREFIX)
        self.ca = ChannelAccess(device_prefix=DEVICE_PREFIX,
                                default_timeout=DEFAULT_TIMEOUT)
        self.ca.assert_that_pv_exists("ID", timeout=30)
        self.reset_ioc()
        if not IOCRegister.uses_rec_sim:
            self.reset_emulator()
            self.check_init_state(False, False, True, False)
            self.ca.assert_that_pv_is_number("CYCLES", 0)

    def check_init_state(self, initialising, initialised,
                         initialisation_required, oscillating):
        def bi_to_bool(val):
            return val == "Yes"

        # Do all states at once.
        match = False
        total_time = 0.0
        max_wait = DEFAULT_TIMEOUT
        interval = 1.0

        actual_initialising = None
        actual_initialised = None
        actual_initialisation_required = None
        actual_oscillating = None

        while not match and total_time < max_wait:
            actual_initialising = bi_to_bool(
                self.ca.get_pv_value("INIT:PROGRESS"))
            actual_initialised = bi_to_bool(self.ca.get_pv_value("INIT:DONE"))
            actual_initialisation_required = bi_to_bool(
                self.ca.get_pv_value("INIT:REQUIRED"))
            actual_oscillating = bi_to_bool(self.ca.get_pv_value("STAT:OSC"))

            match = all([
                initialising == actual_initialising,
                initialised == actual_initialised,
                initialisation_required == actual_initialisation_required,
                oscillating == actual_oscillating
            ])

            total_time += interval
            sleep(interval)

        try:
            self.assertTrue(match)
        except AssertionError:
            message_format = "State did not match the required state (initialising, initialised, initialisation " \
                             "required, oscillating)\nExpected: ({}, {}, {}, {})\nActual: ({}, {}, {}, {})"
            self.fail(
                message_format.format(initialising, initialised,
                                      initialisation_required, oscillating,
                                      actual_initialising, actual_initialised,
                                      actual_initialisation_required,
                                      actual_oscillating))

    def initialise(self):
        self.ca.set_pv_value("INIT", 1)
        self.ca.assert_that_pv_is("INIT:DONE", "Yes", timeout=10)

    def start_oscillating(self):
        self.initialise()
        self.ca.set_pv_value("START", 1)

    def wait_for_re_initialisation_required(self, interval=10):
        self.ca.set_pv_value("INIT:OPT", interval)
        self.start_oscillating()
        while self.ca.get_pv_value("CYCLES") < interval:
            sleep(1)

    @staticmethod
    def backlash(speed, acceleration):
        return int(0.5 * speed**2 / float(acceleration))

    @staticmethod
    def utility(width, backlash):
        return width / float(width + backlash) * 100.0

    @staticmethod
    def period(width, backlash, speed):
        return 2.0 * (width + backlash) / float(speed)

    @staticmethod
    def frequency(width, backlash, speed):
        return 1.0 / GemorcTests.period(width, backlash, speed)

    def set_and_confirm_state(self,
                              width=None,
                              speed=None,
                              acceleration=None,
                              offset=None):
        pv_value_pairs = [("WIDTH", width), ("SPEED", speed),
                          ("ACC", acceleration), ("OFFSET", offset)]
        filtered_pv_values = [(pv, value) for pv, value in pv_value_pairs
                              if value is not None]
        for pv, value in filtered_pv_values:
            self.ca.set_pv_value("{}:SP".format(pv), value)
        # Do all sets then all confirms to reduce wait time
        for pv, value in filtered_pv_values:
            self.ca.assert_that_pv_is_number(pv, value)

    def test_WHEN_width_setpoint_set_THEN_local_readback_matches(self):
        self.ca.assert_setting_setpoint_sets_readback(DEFAULT_WIDTH + 1,
                                                      "WIDTH:SP:RBV",
                                                      "WIDTH:SP")

    def test_WHEN_width_setpoint_set_THEN_remote_readback_matches(self):
        self.ca.assert_setting_setpoint_sets_readback(DEFAULT_WIDTH + 1,
                                                      "WIDTH")

    def test_WHEN_speed_setpoint_set_THEN_local_readback_matches(self):
        self.ca.assert_setting_setpoint_sets_readback(DEFAULT_SPEED + 1,
                                                      "SPEED:SP:RBV",
                                                      "SPEED:SP")

    def test_WHEN_speed_setpoint_set_THEN_remote_readback_matches(self):
        self.ca.assert_setting_setpoint_sets_readback(DEFAULT_SPEED + 1,
                                                      "SPEED")

    def test_WHEN_acceleration_setpoint_set_THEN_local_readback_matches(self):
        self.ca.assert_setting_setpoint_sets_readback(DEFAULT_ACCELERATION + 1,
                                                      "ACC:SP:RBV", "ACC:SP")

    def test_WHEN_acceleration_setpoint_set_THEN_remote_readback_matches(self):
        self.ca.assert_setting_setpoint_sets_readback(DEFAULT_ACCELERATION + 1,
                                                      "ACC")

    def test_WHEN_offset_setpoint_set_THEN_local_readback_matches(self):
        self.ca.assert_setting_setpoint_sets_readback(DEFAULT_OFFSET + 1,
                                                      "OFFSET:SP:RBV",
                                                      "OFFSET:SP")

    def test_WHEN_offset_setpoint_set_THEN_remote_readback_matches(self):
        self.ca.assert_setting_setpoint_sets_readback(DEFAULT_OFFSET + 1,
                                                      "OFFSET")

    def test_WHEN_offset_setpoint_set_to_negative_value_THEN_remote_readback_matches(
            self):
        self.ca.assert_setting_setpoint_sets_readback(-DEFAULT_OFFSET,
                                                      "OFFSET")

    def test_WHEN_device_first_started_THEN_initialisation_required(self):
        self.check_init_state(initialising=False,
                              initialised=False,
                              initialisation_required=True,
                              oscillating=False)

    @skip_if_recsim("Device reset requires Lewis backdoor")
    def test_GIVEN_starting_state_WHEN_initialisation_requested_THEN_initialising_becomes_true(
            self):
        self.ca.set_pv_value("INIT", 1)
        self.check_init_state(initialising=True,
                              initialised=False,
                              initialisation_required=False,
                              oscillating=False)

    @skip_if_recsim("Device reset requires Lewis backdoor")
    def test_GIVEN_starting_state_WHEN_initialisation_requested_THEN_becomes_initialised_when_no_longer_in_progress(
            self):
        self.ca.set_pv_value("INIT", 1)

        total_wait = 0
        max_wait = DEFAULT_TIMEOUT
        interval = 1
        initialisation_complete = self.ca.get_pv_value("INIT:DONE")
        while self.ca.get_pv_value(
                "INIT:PROGRESS") == "Yes" and total_wait < max_wait:
            # Always check value from before we confirmed initialisation was in progress to avoid race conditions
            self.assertNotEqual(initialisation_complete, 1)
            sleep(interval)
            total_wait += interval
            initialisation_complete = self.ca.get_pv_value("INIT:DONE")
        self.check_init_state(initialising=False,
                              initialised=True,
                              initialisation_required=False,
                              oscillating=False)

    @skip_if_recsim("Device reset requires Lewis backdoor")
    def test_GIVEN_initialised_WHEN_oscillation_requested_THEN_reports_oscillating(
            self):
        self.start_oscillating()
        self.ca.assert_that_pv_is("STAT:OSC", "Yes")

    @skip_if_recsim("Device reset requires Lewis backdoor")
    def test_GIVEN_initialised_WHEN_oscillation_requested_THEN_complete_cycles_increases(
            self):
        self.start_oscillating()
        self.ca.assert_that_pv_value_is_increasing("CYCLES", DEFAULT_TIMEOUT)

    @skip_if_recsim("Device reset requires Lewis backdoor")
    def test_GIVEN_oscillating_WHEN_oscillation_stopped_THEN_reports_not_oscillating(
            self):
        self.start_oscillating()
        self.ca.set_pv_value("STOP", 1)
        self.ca.assert_that_pv_is("STAT:OSC", "No")

    @skip_if_recsim("Device reset requires Lewis backdoor")
    def test_GIVEN_initialised_WHEN_oscillation_requested_THEN_complete_cycles_does_not_change(
            self):
        self.start_oscillating()
        self.ca.set_pv_value("STOP", 1)
        self.ca.assert_that_pv_value_is_unchanged("CYCLES", DEFAULT_TIMEOUT)

    @skip_if_recsim("Device reset requires Lewis backdoor")
    def test_GIVEN_oscillating_WHEN_initialisation_requested_THEN_initialises(
            self):
        self.start_oscillating()
        self.ca.set_pv_value("INIT", 1)
        self.check_init_state(initialising=True,
                              initialised=False,
                              initialisation_required=False,
                              oscillating=False)

    @skip_if_recsim("Device reset requires Lewis backdoor")
    def test_GIVEN_oscillating_and_initialisation_requested_WHEN_initialisation_complete_THEN_resumes_oscillation(
            self):
        self.start_oscillating()
        self.initialise()
        self.check_init_state(initialising=False,
                              initialised=True,
                              initialisation_required=False,
                              oscillating=True)

    def test_WHEN_settings_reset_requested_THEN_settings_return_to_default_values(
            self):
        settings = (
            ("WIDTH", DEFAULT_WIDTH),
            ("ACC", DEFAULT_ACCELERATION),
            ("SPEED", DEFAULT_SPEED),
            ("OFFSET", DEFAULT_OFFSET),
            ("INIT:AUTO", DEFAULT_AUTO_INITIALISE),
            ("INIT:OPT", DEFAULT_OPT_INITIALISE),
        )
        for pv, default in settings:
            self.ca.set_pv_value("{}:SP".format(pv),
                                 default + 1)  # I prefer the two lines here
            self.ca.assert_that_pv_is_not_number(pv, default)

        self.ca.set_pv_value("RESET", 1)

        for pv, default in settings:
            self.ca.assert_that_pv_is_number(pv, default)

    @skip_if_recsim("ID is emulator specific")
    def test_WHEN_device_is_running_THEN_it_gets_PnP_identity_from_emulator(
            self):
        self.ca.assert_that_pv_is(
            "ID",
            "0002 0001 ISIS Gem Oscillating Rotary Collimator (IBEX EMULATOR)",
            timeout=20)  # On a very slow scan

    def test_GIVEN_standard_test_cases_WHEN_backlash_calculated_locally_THEN_result_is_in_range_supported_by_device(
            self):
        for _, speed, acceleration in SETTINGS_TEST_CASES:
            self.assertTrue(0 <= self.backlash(speed, acceleration) <= 999)

    @skip_if_recsim("Depends on emulator value")
    def test_WHEN_emulator_running_THEN_backlash_has_value_derived_from_speed_and_acceleration(
            self):
        for width, speed, acceleration in SETTINGS_TEST_CASES:
            self.set_and_confirm_state(speed=speed, acceleration=acceleration)
            self.ca.assert_that_pv_is_number(
                "BACKLASH", self.backlash(speed, acceleration))

    def test_GIVEN_non_zero_speed_WHEN_width_and_speed_set_THEN_utility_time_corresponds_to_formula_in_test(
            self):
        for width, speed, acceleration in SETTINGS_TEST_CASES:
            self.set_and_confirm_state(width, speed, acceleration)
            backlash = self.ca.get_pv_value("BACKLASH")
            self.ca.assert_that_pv_is_number("UTILITY",
                                             self.utility(width, backlash),
                                             tolerance=DEFAULT_TOLERANCE)

    def test_WHEN_emulator_running_THEN_period_has_value_as_derived_from_speed_width_and_backlash(
            self):
        for width, speed, acceleration in SETTINGS_TEST_CASES:
            self.set_and_confirm_state(width, speed, acceleration)
            backlash = self.ca.get_pv_value("BACKLASH")
            self.ca.assert_that_pv_is_number("PERIOD",
                                             self.period(
                                                 width, backlash, speed),
                                             tolerance=DEFAULT_TOLERANCE)

    def test_WHEN_emulator_running_THEN_frequency_has_value_as_derived_from_speed_width_and_backlash(
            self):
        for width, speed, acceleration in SETTINGS_TEST_CASES:
            self.set_and_confirm_state(width, speed, acceleration)
            backlash = self.ca.get_pv_value("BACKLASH")
            self.ca.assert_that_pv_is_number("FREQ",
                                             self.frequency(
                                                 width, backlash, speed),
                                             tolerance=DEFAULT_TOLERANCE)

    @skip_if_recsim("This behaviour not implemented in recsim")
    def test_GIVEN_non_zero_offset_WHEN_re_zeroed_to_datum_THEN_offset_is_zero(
            self):
        self.ca.assert_setting_setpoint_sets_readback(DEFAULT_OFFSET + 1,
                                                      "OFFSET", "OFFSET:SP")
        self.ca.assert_that_pv_is_not_number("OFFSET", 0)
        self.ca.set_pv_value("ZERO", 1)
        self.ca.assert_that_pv_is_number("OFFSET", 0)

    def test_WHEN_auto_initialisation_interval_set_THEN_readback_matches_set_value(
            self):
        self.ca.assert_setting_setpoint_sets_readback(
            DEFAULT_AUTO_INITIALISE + 1, "INIT:AUTO")

    def test_WHEN_opt_initialisation_interval_set_THEN_readback_matches_set_value(
            self):
        self.ca.assert_setting_setpoint_sets_readback(
            DEFAULT_OPT_INITIALISE + 1, "INIT:OPT")

    @skip_if_recsim("Cycle counting not performed in Recsim")
    def test_GIVEN_oscillating_WHEN_number_of_cycles_exceeds_optional_init_interval_THEN_initialisation_required(
            self):
        self.wait_for_re_initialisation_required()
        self.check_init_state(False, True, True, True)

    @skip_if_recsim("Cycle counting not performed in Recsim")
    def test_GIVEN_initialisation_required_after_oscillating_WHEN_reinitialised_THEN_re_initialisation_not_required(
            self):
        self.wait_for_re_initialisation_required()
        self.ca.set_pv_value("INIT:OPT", DEFAULT_OPT_INITIALISE)
        self.initialise()
        self.check_init_state(False, True, False, True)

    @skip_if_recsim("Initialisation logic not performed in Recsim")
    def test_WHEN_device_initialised_THEN_initialised_once(self):
        self.initialise()
        self.ca.assert_that_pv_is("INIT:ONCE", "Yes")

    @skip_if_recsim("Initialisation logic not performed in Recsim")
    def test_WHEN_oscillating_THEN_initialised_once(self):
        self.start_oscillating()
        self.ca.assert_that_pv_is("INIT:ONCE", "Yes")

    @skip_if_recsim("Initialisation logic not performed in Recsim")
    def test_WHEN_oscillating_and_initialisation_required_THEN_initialised_once(
            self):
        self.wait_for_re_initialisation_required()
        self.ca.assert_that_pv_is("INIT:ONCE", "Yes")

    @skip_if_recsim("Initialisation logic not performed in Recsim")
    def test_WHEN_reinitialising_THEN_initialised_once(self):
        self.wait_for_re_initialisation_required()
        self.ca.set_pv_value("INIT", 1)
        self.ca.assert_that_pv_is("INIT:ONCE", "Yes")

    @skip_if_recsim("Initialisation logic not performed in Recsim")
    def test_WHEN_reinitialised_THEN_initialised_once(self):
        self.wait_for_re_initialisation_required()
        self.initialise()
        self.ca.assert_that_pv_is("INIT:ONCE", "Yes")

    @skip_if_recsim("Initialisation logic not performed in Recsim")
    def test_GIVEN_oscillating_WHEN_stopped_and_immediately_initialised_THEN_number_of_cycles_goes_to_zero(
            self):
        self.start_oscillating()
        self.ca.set_pv_value("STOP", 1)
        self.ca.set_pv_value("INIT", 1)
        self.ca.assert_that_pv_is_number("CYCLES", 0)

    @skip_if_recsim("Initialisation logic not performed in Recsim")
    def test_WHEN_oscillating_THEN_auto_reinitialisation_triggers_after_counter_reaches_auto_trigger_value(
            self):
        initialisation_interval = 100
        initial_status_string = "Sequence not run since IOC startup"
        self.ca.set_pv_value("INIT:AUTO", initialisation_interval)
        self.start_oscillating()
        while self.ca.get_pv_value("CYCLES") < initialisation_interval:
            self.ca.assert_that_pv_is("INIT:PROGRESS", "No")
            self.ca.assert_that_pv_is("INIT:STAT", initial_status_string)
            sleep(1)
        self.ca.assert_that_pv_is_not("INIT:STAT", initial_status_string)
        self.ca.assert_that_pv_is(
            "STAT:OSC", "No",
            timeout=10)  # Initialisation seq has a 5s wait at the start
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")
Beispiel #12
0
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")
Beispiel #13
0
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 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 Keithley2400Tests(unittest.TestCase):
    """
    Tests for the keithley 2400.
    """
    def setUp(self):
        self._lewis, self._ioc = get_running_lewis_and_ioc(
            "keithley_2400", DEVICE_PREFIX)

        self._lewis.backdoor_set_on_device("random_output", False)

        self.ca = ChannelAccess(device_prefix=DEVICE_PREFIX)

    def calculate_resistance_range(self, value):
        """
        The resistance ranges of the device are 2.1*10^x, where x is between 0 and 8
        """

        for r in [2.1 * 10**i for i in range(1, 8)]:
            if value < r:
                return r / 10

    @skip_if_recsim("Recsim does not work with lewis backdoor tests")
    def test_WHEN_current_value_is_set_THEN_readback_returns_value_just_set(
            self):
        self.ca.set_pv_value("OUTPUT:MODE:SP", "On")
        for test_val in TEST_OUTPUTS:
            self._lewis.backdoor_set_on_device("current", test_val)
            self.ca.assert_that_pv_is_number("CURR",
                                             test_val,
                                             tolerance=0.05 * abs(test_val))

    @skip_if_recsim("Recsim does not work with lewis backdoor tests")
    def test_WHEN_voltage_value_is_set_THEN_readback_returns_value_just_set(
            self):
        self.ca.set_pv_value("OUTPUT:MODE:SP", "On")
        for test_val in TEST_OUTPUTS:
            self._lewis.backdoor_set_on_device("voltage", test_val)
            self.ca.assert_that_pv_is_number("VOLT",
                                             test_val,
                                             tolerance=0.05 * abs(test_val))

    @skip_if_recsim("Recsim does not work with lewis backdoor tests")
    def test_WHEN_voltage_and_current_are_set_THEN_readback_returns_valid_resistance(
            self):
        self.ca.set_pv_value("OUTPUT:MODE:SP", "On")
        for volts, amps in itertools.product([4.5, 6.7], [6.7, 4.5]):
            self._lewis.backdoor_set_on_device("current", amps)
            self._lewis.backdoor_set_on_device("voltage", volts)

            resistance = volts / amps

            self.ca.assert_that_pv_is_number("RES",
                                             resistance,
                                             tolerance=0.05 * resistance)

    def test_WHEN_ioc_is_started_THEN_ioc_is_not_disabled(self):
        self.ca.assert_that_pv_is("DISABLE", "COMMS ENABLED")

    def test_WHEN_output_mode_is_set_THEN_readback_updates_with_the_value_just_set(
            self):
        for mode in ["On", "Off"]:
            self.ca.assert_setting_setpoint_sets_readback(mode, "OUTPUT:MODE")

    @skip_if_recsim("Recsim does not work with lewis backdoor tests")
    def test_WHEN_output_mode_is_unset_THEN_current_readback_does_not_update(
            self):
        # Write to the RAW pv, test that the CURR pv does not update with the new value when output is off
        self.ca.set_pv_value("CURR:RAW", 1.0)
        self.ca.set_pv_value("OUTPUT:MODE:SP", "Off")

        self._lewis.backdoor_set_on_device("current", 5.0)

        self.ca.assert_that_pv_value_is_unchanged("CURR", 1.0)

    @skip_if_recsim("Recsim does not work with lewis backdoor tests")
    def test_WHEN_output_mode_is_unset_THEN_voltage_readback_does_not_update(
            self):
        # Write to the RAW value, test that the VOLT pv does not update with the new value when output is off
        self.ca.set_pv_value("VOLT:RAW", 1.0)
        self.ca.set_pv_value("OUTPUT:MODE:SP", "Off")

        self._lewis.backdoor_set_on_device("voltage", 5.0)

        self.ca.assert_that_pv_value_is_unchanged("VOLT", 1.0)

    def test_WHEN_resistance_mode_is_set_THEN_readback_updates_with_the_value_just_set(
            self):
        for mode in ["Manual", "Auto"]:
            self.ca.assert_setting_setpoint_sets_readback(mode, "RES:MODE")

    def test_WHEN_remote_sensing_mode_is_set_THEN_readback_updates_with_the_value_just_set(
            self):
        for mode in ["On", "Off"]:
            self.ca.assert_setting_setpoint_sets_readback(mode, "SENS:MODE")

    def test_WHEN_automatic_range_is_set_THEN_readback_updates_with_the_value_just_set(
            self):
        for test_val in ["Manual", "Auto"]:
            self.ca.assert_setting_setpoint_sets_readback(
                test_val, "RES:RANGE:AUTO")

    @skip_if_recsim("Banded record behaviour too complex for recsim")
    def test_WHEN_range_is_set_THEN_readback_updates_with_appropriate_range_for_value_just_set(
            self):
        for test_val in TEST_OUTPUTS:
            ideal_range = self.calculate_resistance_range(test_val)
            self.ca.set_pv_value("RES:RANGE:SP", test_val)
            self.ca.assert_that_pv_is_number("RES:RANGE",
                                             ideal_range,
                                             tolerance=0.05 * ideal_range)

    def test_WHEN_source_mode_is_set_THEN_readback_updates_with_the_value_just_set(
            self):
        for test_val in ["Current", "Voltage"]:
            self.ca.assert_setting_setpoint_sets_readback(
                test_val, "SOURCE:MODE")

    def test_WHEN_current_compliance_is_set_THEN_readback_updates_with_the_value_just_set(
            self):
        for test_val in TEST_OUTPUTS:
            self.ca.assert_setting_setpoint_sets_readback(
                test_val, "CURR:COMPLIANCE")

    def test_WHEN_voltage_compliance_is_set_THEN_readback_updates_with_the_value_just_set(
            self):
        for test_val in TEST_OUTPUTS:
            self.ca.assert_setting_setpoint_sets_readback(
                test_val, "VOLT:COMPLIANCE")

    def test_WHEN_source_voltage_is_set_THEN_readback_updates_with_the_value_just_set(
            self):
        for test_val in TEST_OUTPUTS:
            self.ca.assert_setting_setpoint_sets_readback(
                test_val, "VOLT:SOURCE")

    def test_WHEN_source_current_is_set_THEN_readback_updates_with_the_value_just_set(
            self):
        for test_val in TEST_OUTPUTS:
            self.ca.assert_setting_setpoint_sets_readback(
                test_val, "CURR:SOURCE")

    def test_WHEN_source_current_autoranging_is_set_THEN_readback_updates_with_the_value_just_set(
            self):
        for test_val in ["Off", "On"]:
            self.ca.assert_setting_setpoint_sets_readback(
                test_val, "CURR:SOURCE:AUTORANGE")

    def test_WHEN_source_voltage_autoranging_is_set_THEN_readback_updates_with_the_value_just_set(
            self):
        for test_val in ["On", "Off"]:
            self.ca.assert_setting_setpoint_sets_readback(
                test_val, "VOLT:SOURCE:AUTORANGE")

    def test_WHEN_measurement_current_autoranging_is_set_THEN_readback_updates_with_the_value_just_set(
            self):
        for test_val in ["On", "Off"]:
            self.ca.assert_setting_setpoint_sets_readback(
                test_val, "CURR:MEAS:AUTORANGE")

    def test_WHEN_measurement_voltage_autoranging_is_set_THEN_readback_updates_with_the_value_just_set(
            self):
        for test_val in ["On", "Off"]:
            self.ca.assert_setting_setpoint_sets_readback(
                test_val, "VOLT:MEAS:AUTORANGE")

    @skip_if_recsim("Banded record behaviour too complex for recsim")
    def test_WHEN_source_current_range_is_set_THEN_readback_updates_with_the_appropriate_range_for_value_just_set(
            self):
        """
        Current ranges on the KHLY2400 are simply powers of ten, the whole range of which is covered here
        """
        for test_val in RANGE_MAGNITUDES:
            self.ca.set_pv_value("CURR:SOURCE:RANGE:SP", test_val)
            self.ca.assert_that_pv_is_number("CURR:SOURCE:RANGE",
                                             test_val,
                                             tolerance=0.05 * test_val)

    @skip_if_recsim("Banded record behaviour too complex for recsim")
    def test_WHEN_source_voltage_range_is_set_THEN_readback_updates_with_the_appropriate_range_for_value_just_set(
            self):
        """
        Voltage ranges on the KHLY2400 are 2*multiples of ten, the whole range is covered here.

        """
        for magnitude in RANGE_MAGNITUDES:
            test_val = 2. * magnitude
            self.ca.set_pv_value("VOLT:SOURCE:RANGE:SP", test_val)
            self.ca.assert_that_pv_is_number("VOLT:SOURCE:RANGE",
                                             test_val,
                                             tolerance=0.05 * test_val)

    @skip_if_recsim("Banded record behaviour too complex for recsim")
    def test_WHEN_volts_measurement_range_is_set_THEN_readback_updates_with_appropriate_range_for_value_just_set(
            self):
        """
        Voltage ranges on the KHLY2400 are 2*multiples of ten, the whole range is covered here.

        """
        for magnitude in RANGE_MAGNITUDES:
            test_val = 2. * magnitude
            self.ca.set_pv_value("VOLT:MEAS:RANGE:SP", test_val)
            self.ca.assert_that_pv_is_number("VOLT:MEAS:RANGE",
                                             test_val,
                                             tolerance=0.05 * test_val)

    @skip_if_recsim("Banded record behaviour too complex for recsim")
    def test_WHEN_current_measurement_range_is_set_THEN_readback_updates_with_appropriate_range_for_value_just_set(
            self):
        """
        Current ranges on the KHLY2400 are simply powers of ten, the whole range of which is covered here
        """
        for test_val in RANGE_MAGNITUDES:
            self.ca.set_pv_value("CURR:MEAS:RANGE:SP", test_val)
            self.ca.assert_that_pv_is_number("CURR:MEAS:RANGE",
                                             test_val,
                                             tolerance=0.05 * test_val)
Beispiel #16
0
class IegTests(unittest.TestCase):
    """
    Tests for the IEG IOC.
    """

    operation_modes = [
        (1, "Pump, Purge & Fill"),
        (2, "Pump"),
        (3, "Gas Flow"),
        (4, "Gas - Single Shot"),
    ]

    error_codes = [(0, "No error"), (2, "Samp vol: leak detected"),
                   (3, "Samp vol: no vacuum"), (4, "Buff vol: did not fill"),
                   (5, "Samp vol: fill iterations"),
                   (6, "Samp vol: pump timeout"),
                   (7, "Samp vol: fill timeout"), (8, "Buff or samp vol leak"),
                   (9, "Shot didn't raise press."), (10, "Manual stop"),
                   (11, "Vacuum tank interlock"),
                   (12, "Samp vol: leak detected"),
                   (13, "Sensor broken/disconnect")]

    test_device_ids = [0, 123, 255]
    test_pressures = [0, 10, 1024]

    @staticmethod
    def _get_actual_from_raw(value):
        return value * CALIBRATION_A + CALIBRATION_B

    @staticmethod
    def _get_raw_from_actual(value):
        return int(round((value - CALIBRATION_B) / CALIBRATION_A))

    def setUp(self):
        self._lewis, self._ioc = get_running_lewis_and_ioc(
            "ieg", DEVICE_PREFIX)

        self.ca = ChannelAccess(device_prefix=DEVICE_PREFIX,
                                default_timeout=20)
        self.ca.assert_that_pv_exists("DISABLE", timeout=30)

    def test_WHEN_ioc_is_started_THEN_ioc_is_not_disabled(self):
        self.ca.assert_that_pv_is("DISABLE", "COMMS ENABLED")

    def test_WHEN_mode_setpoint_is_set_THEN_readback_updates(self):
        for set_val, return_val in self.operation_modes:
            self.ca.assert_setting_setpoint_sets_readback(
                set_val,
                set_point_pv="MODE:SP",
                readback_pv="MODE",
                expected_value=return_val)

    @skip_if_recsim("Not implemented in recsim")
    def test_GIVEN_device_not_in_dormant_state_WHEN_kill_command_is_sent_THEN_device_goes_to_dormant_state(
            self):
        set_val, return_val = self.operation_modes[0]
        self.ca.assert_setting_setpoint_sets_readback(
            set_val,
            set_point_pv="MODE:SP",
            readback_pv="MODE",
            expected_value=return_val)

        self.ca.set_pv_value("ABORT", 1)
        self.ca.assert_that_pv_is("MODE", "Dormant")

    @skip_if_recsim("Uses lewis backdoor command")
    def test_WHEN_device_id_is_changed_on_device_THEN_device_id_pv_updates(
            self):
        for val in self.test_device_ids:
            self._lewis.backdoor_set_on_device("unique_id", val)
            self.ca.assert_that_pv_is("ID", val, timeout=30)
            self.ca.assert_that_pv_alarm_is("ID", self.ca.Alarms.NONE)

    @skip_if_recsim("Uses lewis backdoor command")
    def test_WHEN_valve_states_are_changed_on_device_THEN_valve_state_pv_updates(
            self):
        for gas_valve_open in [True, False]:
            self._lewis.backdoor_set_on_device("gas_valve_open",
                                               gas_valve_open)
            for buffer_valve_open in [True, False]:
                self._lewis.backdoor_set_on_device("buffer_valve_open",
                                                   buffer_valve_open)
                for pump_valve_open in [True, False]:
                    self._lewis.backdoor_set_on_device("pump_valve_open",
                                                       pump_valve_open)
                    self.ca.assert_that_pv_is_number(
                        "VALVESTATE.B0", 1 if pump_valve_open else 0)
                    self.ca.assert_that_pv_is_number(
                        "VALVESTATE.B1", 1 if buffer_valve_open else 0)
                    self.ca.assert_that_pv_is_number(
                        "VALVESTATE.B2", 1 if gas_valve_open else 0)
                    self.ca.assert_that_pv_alarm_is("VALVESTATE",
                                                    self.ca.Alarms.NONE)

    @skip_if_recsim("Uses lewis backdoor command")
    def test_WHEN_valve_states_are_changed_on_device_THEN_valve_state_pv_updates(
            self):
        for error_num, error in self.error_codes:
            self._lewis.backdoor_set_on_device("error", error_num)
            self.ca.assert_that_pv_is("ERROR", error)
            self.ca.assert_that_pv_alarm_is(
                "ERROR",
                self.ca.Alarms.MAJOR if error_num else self.ca.Alarms.NONE)

    @skip_if_recsim("Uses lewis backdoor command")
    def test_WHEN_pressure_is_changed_on_device_THEN_raw_pressure_pv_updates(
            self):
        for pressure in self.test_pressures:
            self._lewis.backdoor_set_on_device("sample_pressure", pressure)
            self.ca.assert_that_pv_is("PRESSURE:RAW", pressure)
            self.ca.assert_that_pv_alarm_is("PRESSURE:RAW",
                                            self.ca.Alarms.NONE)

    @skip_if_recsim("Uses lewis backdoor command")
    def test_WHEN_pressure_and_upper_limit_are_changed_on_device_THEN_pressure_high_pv_updates(
            self):
        for pressure in self.test_pressures:
            self._lewis.backdoor_set_on_device("sample_pressure", pressure)
            for upper_limit in [pressure - 1, pressure + 1]:
                self._lewis.backdoor_set_on_device(
                    "sample_pressure_high_limit", upper_limit)
                self.ca.assert_that_pv_is(
                    "PRESSURE:HIGH",
                    "Out of range" if pressure > upper_limit else "In range")
                self.ca.assert_that_pv_alarm_is(
                    "PRESSURE:HIGH", self.ca.Alarms.MINOR
                    if pressure > upper_limit else self.ca.Alarms.NONE)

    @skip_if_recsim("Uses lewis backdoor command")
    def test_WHEN_pressure_and_lower_limit_are_changed_on_device_THEN_pressure_low_pv_updates(
            self):
        for pressure in self.test_pressures:
            self._lewis.backdoor_set_on_device("sample_pressure", pressure)
            for lower_limit in [pressure - 1, pressure + 1]:
                self._lewis.backdoor_set_on_device("sample_pressure_low_limit",
                                                   lower_limit)
                self.ca.assert_that_pv_is(
                    "PRESSURE:LOW",
                    "Out of range" if pressure < lower_limit else "In range")
                self.ca.assert_that_pv_alarm_is(
                    "PRESSURE:LOW", self.ca.Alarms.MINOR
                    if pressure < lower_limit else self.ca.Alarms.NONE)

    @skip_if_recsim("Uses lewis backdoor command")
    def test_WHEN_buffer_pressure_high_is_changed_on_device_THEN_buffer_pressure_high_pv_updates(
            self):
        for value in [True, False]:
            self._lewis.backdoor_set_on_device("buffer_pressure_high", value)

            # Intentionally this way round - the manual
            # says that 0 means 'above high threshold' and 1 is 'below high threshold'
            self.ca.assert_that_pv_is("PRESSURE:BUFFER:HIGH",
                                      "Out of range" if value else "In range")
            self.ca.assert_that_pv_alarm_is(
                "PRESSURE:BUFFER:HIGH",
                self.ca.Alarms.MINOR if value else self.ca.Alarms.NONE)

    @skip_if_recsim("Uses lewis backdoor command")
    def test_WHEN_pressure_is_over_350_THEN_displayed_as_greater_than_350_mBar(
            self):
        self._lewis.backdoor_set_on_device("sample_pressure",
                                           self._get_raw_from_actual(400))
        self.ca.assert_that_pv_is("PRESSURE:GUI.OSV", ">350 mbar")
        self.ca.assert_that_pv_alarm_is("PRESSURE:GUI", self.ca.Alarms.MAJOR)

    @skip_if_recsim("Uses lewis backdoor command")
    def test_WHEN_pressure_is_less_than_0_THEN_displayed_as_less_than_0_mBar(
            self):
        self._lewis.backdoor_set_on_device("sample_pressure",
                                           self._get_raw_from_actual(-10))
        self.ca.assert_that_pv_is("PRESSURE:GUI.OSV", "<0 mbar")
        self.ca.assert_that_pv_alarm_is("PRESSURE:GUI", self.ca.Alarms.MAJOR)

    @skip_if_recsim("Uses lewis backdoor command")
    def test_WHEN_pressure_is_set_THEN_it_is_converted_correctly_using_the_calibration(
            self):
        for raw_pressure in self.test_pressures:
            actual_pressure = self._get_actual_from_raw(raw_pressure)

            self._lewis.backdoor_set_on_device("sample_pressure", raw_pressure)
            # PRESSURE is restricted to use [0-350]. PRESSURE:CALC is unrestricted
            self.ca.assert_that_pv_is_number("PRESSURE:CALC",
                                             actual_pressure,
                                             tolerance=0.05)
            self.ca.assert_that_pv_alarm_is(
                "PRESSURE", self.ca.Alarms.NONE
                if .0 < actual_pressure < 350 else self.ca.Alarms.MAJOR)
Beispiel #17
0
class Ilm200Tests(unittest.TestCase):
    """
    Tests for the Ilm200 IOC.
    """
    DEFAULT_SCAN_RATE = 1
    SLOW = "Slow"
    FAST = "Fast"
    LEVEL_TOLERANCE = 0.1

    FULL = 100.0
    LOW = 10.0
    FILL = 5.0

    RATE = "RATE"
    LEVEL = "LEVEL"
    TYPE = "TYPE"
    CURRENT = "CURR"

    @staticmethod
    def channel_range():
        number_of_channels = 3
        starting_index = 1
        return range(starting_index, starting_index + number_of_channels)

    def helium_channels(self):
        for i in self.channel_range():
            if self.ca.get_pv_value(self.ch_pv(i, self.TYPE)) != "Nitrogen":
                yield i

    @staticmethod
    def ch_pv(channel, pv):
        return "CH{}:{}".format(channel, pv)

    def setUp(self):
        self._lewis, self._ioc = get_running_lewis_and_ioc("ilm200", DEVICE_PREFIX)
        self.ca = ChannelAccess(device_prefix=DEVICE_PREFIX, default_wait_time=0.0)
        self.ca.assert_that_pv_exists("VERSION", timeout=30)
        self._lewis.backdoor_set_on_device("cycle", False)

        self._lewis.backdoor_run_function_on_device("set_cryo_type", (1, Ilm200ChannelTypes.NITROGEN))
        self._lewis.backdoor_run_function_on_device("set_cryo_type", (2, Ilm200ChannelTypes.HELIUM))
        self._lewis.backdoor_run_function_on_device("set_cryo_type", (3, Ilm200ChannelTypes.HELIUM_CONT))

    def set_level_via_backdoor(self, channel, level):
        self._lewis.backdoor_command(["device", "set_level", str(channel), str(level)])

    def set_helium_current_via_backdoor(self, channel, is_on):
        self._lewis.backdoor_command(["device", "set_helium_current", str(channel), str(is_on)])

    def check_state(self, channel, level, is_filling, is_low):
        self.ca.assert_that_pv_is_number(self.ch_pv(channel, self.LEVEL), level, self.LEVEL_TOLERANCE)
        self.ca.assert_that_pv_is(self.ch_pv(channel, "FILLING"), "Filling" if is_filling else "Not filling")
        self.ca.assert_that_pv_is(self.ch_pv(channel, "LOW"), "Low" if is_low else "Not low")

    def test_GIVEN_ilm200_THEN_has_version(self):
        self.ca.assert_that_pv_is_not("VERSION", "")
        self.ca.assert_that_pv_alarm_is("VERSION", self.ca.Alarms.NONE)

    def test_GIVEN_ilm200_THEN_each_channel_has_type(self):
        for i in self.channel_range():
            self.ca.assert_that_pv_is_not(self.ch_pv(i, self.TYPE), "Not in use")
            self.ca.assert_that_pv_alarm_is(self.ch_pv(i, self.TYPE), self.ca.Alarms.NONE)

    def set_and_check_level(self):
        for i in self.channel_range():
            level = ALARM_THRESHOLDS[i] + 10
            self.set_level_via_backdoor(i, level)
            self.ca.assert_that_pv_is_number(self.ch_pv(i, self.LEVEL), level, tolerance=0.1)
            self.ca.assert_that_pv_alarm_is(self.ch_pv(i, self.LEVEL), self.ca.Alarms.NONE)

    @skip_if_recsim("no backdoor in recsim")
    def test_GIVEN_ilm_200_THEN_can_read_level(self):
        self.set_and_check_level()

    @skip_if_recsim("no backdoor in recsim")
    def test_GIVEN_ilm_200_non_isobus_THEN_can_read_level(self):
        with self._ioc.start_with_macros({"USE_ISOBUS": "NO"}, pv_to_wait_for="VERSION"):
            self.set_and_check_level()

    @skip_if_recsim("Cannot do back door of dynamic behaviour in recsim")
    def test_GIVEN_ilm_200_WHEN_level_set_on_device_THEN_reported_level_matches_set_level(self):
        for i in self.channel_range():
            expected_level = i*12.3
            self.set_level_via_backdoor(i, expected_level)
            self.ca.assert_that_pv_is_number(self.ch_pv(i, self.LEVEL), expected_level, self.LEVEL_TOLERANCE)

    @skip_if_recsim("No dynamic behaviour recsim")
    def test_GIVEN_ilm_200_WHEN_is_cycling_THEN_channel_levels_change_over_time(self):
        self._lewis.backdoor_set_on_device("cycle", True)
        for i in self.channel_range():
            def not_equal(a, b):
                tolerance = self.LEVEL_TOLERANCE
                return abs(a-b)/(a+b+tolerance) > tolerance
            self.ca.assert_that_pv_value_over_time_satisfies_comparator(self.ch_pv(i, self.LEVEL), 2 * Ilm200Tests.DEFAULT_SCAN_RATE, not_equal)

    def test_GIVEN_ilm200_channel_WHEN_rate_change_requested_THEN_rate_changed(self):
        for i in self.channel_range():
            initial_rate = self.ca.get_pv_value(self.ch_pv(i, self.RATE))
            alternate_rate = self.SLOW if initial_rate == self.FAST else self.SLOW

            self.ca.assert_setting_setpoint_sets_readback(alternate_rate, self.ch_pv(i, self.RATE))
            self.ca.assert_setting_setpoint_sets_readback(initial_rate, self.ch_pv(i, self.RATE))

    def test_GIVEN_ilm200_channel_WHEN_rate_set_to_current_value_THEN_rate_unchanged(self):
        for i in self.channel_range():
            self.ca.assert_setting_setpoint_sets_readback(self.ca.get_pv_value(self.ch_pv(i, self.RATE)),
                                                          self.ch_pv(i, self.RATE))

    @skip_if_recsim("Cannot do back door of dynamic behaviour in recsim")
    def test_GIVEN_ilm200_WHEN_channel_full_THEN_not_filling_and_not_low(self):
        for i in self.channel_range():
            level = self.FULL
            self.set_level_via_backdoor(i, level)
            self.check_state(i, level, False, False)

    @skip_if_recsim("Cannot do back door of dynamic behaviour in recsim")
    def test_GIVEN_ilm200_WHEN_channel_low_but_auto_fill_not_triggered_THEN_not_filling_and_low(self):
        for i in self.channel_range():
            level = self.LOW - (self.LOW - self.FILL)/2  # Somewhere between fill and low
            self.set_level_via_backdoor(i, level)
            self.check_state(i, level, False, True)

    @skip_if_recsim("Cannot do back door of dynamic behaviour in recsim")
    def test_GIVEN_ilm200_WHEN_channel_low_but_and_auto_fill_triggered_THEN_filling_and_low(self):
        for i in self.channel_range():
            level = self.FILL/2
            self.set_level_via_backdoor(i, level)
            self.check_state(i, level, True, True)

    @skip_if_recsim("Cannot do back door of dynamic behaviour in recsim")
    def test_GIVEN_ilm200_WHEN_channel_low_THEN_alarm(self):
        for i in self.channel_range():
            level = self.FILL/2
            self.set_level_via_backdoor(i, level)
            self.ca.assert_that_pv_alarm_is(self.ch_pv(i, "LOW"), self.ca.Alarms.MINOR)

    @skip_if_recsim("Cannot do back door in recsim")
    def test_GIVEN_helium_channel_WHEN_helium_current_set_on_THEN_ioc_reports_current(self):
        for i in self.helium_channels():
            self.set_helium_current_via_backdoor(i, True)
            self.ca.assert_that_pv_is(self.ch_pv(i, self.CURRENT), "On")

    @skip_if_recsim("Cannot do back door in recsim")
    def test_GIVEN_helium_channel_WHEN_helium_current_set_off_THEN_ioc_reports_no_current(self):
        for i in self.helium_channels():
            self.set_helium_current_via_backdoor(i, False)
            self.ca.assert_that_pv_is(self.ch_pv(i, self.CURRENT), "Off")

    @skip_if_recsim("cannot do back door in recsim")
    def test_GIVEN_not_in_use_channel_THEN_being_in_neither_fast_nor_slow_mode_does_not_cause_alarm(self):
        self._lewis.backdoor_run_function_on_device("set_cryo_type", (1, Ilm200ChannelTypes.NOT_IN_USE))

        # Assert in neither fast nor slow mode
        self.ca.assert_that_pv_is(self.ch_pv(1, "STAT:RAW.B1"), "0")
        self.ca.assert_that_pv_is(self.ch_pv(1, "STAT:RAW.B2"), "0")

        # Assert that this does not cause an alarm
        self.ca.assert_that_pv_alarm_is(self.ch_pv(1, "RATE:ASSERT"), self.ca.Alarms.NONE)

    @skip_if_recsim("no backdoor in recsim")
    def test_GIVEN_level_reading_is_below_threshold_THEN_goes_into_alarm(self):
        for channel in self.channel_range():
            self.set_level_via_backdoor(channel, ALARM_THRESHOLDS[channel] + 0.1)
            self.ca.assert_that_pv_alarm_is(self.ch_pv(channel, "LEVEL"), self.ca.Alarms.NONE)

            self.set_level_via_backdoor(channel, ALARM_THRESHOLDS[channel] - 0.1)
            self.ca.assert_that_pv_alarm_is(self.ch_pv(channel, "LEVEL"), self.ca.Alarms.MAJOR)
Beispiel #18
0
class Moxa12XXBase(object):
    """
    Tests for a moxa ioLogik e1240. (8x DC Voltage/Current measurements)
    """
    @abstractmethod
    def get_device_prefix(self):
        pass

    @abstractmethod
    def get_PV_name(self):
        pass

    @abstractmethod
    def get_number_of_channels(self):
        pass

    @abstractmethod
    def get_setter_function_name(self):
        pass

    @abstractmethod
    def get_starting_reg_addr(self):
        pass

    @abstractmethod
    def get_test_values(self):
        pass

    @abstractmethod
    def get_raw_ir_setter(self):
        pass

    @abstractmethod
    def get_raw_ir_pv(self):
        pass

    @abstractmethod
    def get_alarm_limits(self):
        pass

    @abstractmethod
    def get_registers_per_channel(self):
        pass

    @abstractmethod
    def get_channel_format(self):
        pass

    @abstractmethod
    def get_scaling_factor(self):
        pass

    def setUp(self):

        self.NUMBER_OF_CHANNELS = self.get_number_of_channels()

        self.CHANNELS = range(self.NUMBER_OF_CHANNELS)

        self.SCALING_FACTOR = self.get_scaling_factor()

        self.low_alarm_limit, self.high_alarm_limit = self.get_alarm_limits()

        # Alarm limits are in scaled units, these are normalised to 'device units' for clarity in the tests.
        self.low_alarm_limit /= self.SCALING_FACTOR
        self.high_alarm_limit /= self.SCALING_FACTOR

        self._lewis, self._ioc = get_running_lewis_and_ioc(
            "moxa12xx", self.get_device_prefix())

        self.ca = ChannelAccess(device_prefix=self.get_device_prefix())

        # Sends a backdoor command to the device to reset all input registers (IRs) to 0
        reset_value = 0
        self._lewis.backdoor_run_function_on_device(
            "set_ir",
            (self.get_starting_reg_addr(), [reset_value] *
             self.get_registers_per_channel() * self.NUMBER_OF_CHANNELS))

    def test_WHEN_an_AI_input_is_changed_THEN_that_channel_readback_updates(
            self):
        for channel, test_value in product(self.CHANNELS,
                                           self.get_test_values()):
            register_offset = channel * self.get_registers_per_channel()

            self._lewis.backdoor_run_function_on_device(
                self.get_setter_function_name(),
                (self.get_starting_reg_addr() + register_offset, test_value))

            expected_value = self.SCALING_FACTOR * test_value

            self.ca.assert_that_pv_is_number(
                "CH{:01d}:{PV}".format(channel, PV=self.get_PV_name()),
                expected_value,
                tolerance=0.1 * abs(expected_value))

    def test_WHEN_device_voltage_is_within_user_limits_THEN_PV_shows_no_alarm(
            self):
        for channel in self.CHANNELS:
            register_offset = channel * self.get_registers_per_channel()

            value_to_set = 0.5 * (self.low_alarm_limit + self.high_alarm_limit)

            expected_value = self.SCALING_FACTOR * value_to_set

            self._lewis.backdoor_run_function_on_device(
                self.get_setter_function_name(),
                (self.get_starting_reg_addr() + register_offset, value_to_set))

            self.ca.assert_that_pv_is_number(
                "CH{:01d}:{PV}".format(channel, PV=self.get_PV_name()),
                expected_value,
                tolerance=0.1 * abs(expected_value))

            self.ca.assert_that_pv_alarm_is(
                "CH{:01d}:{PV}".format(channel, PV=self.get_PV_name()),
                self.ca.Alarms.NONE)

    def test_WHEN_device_voltage_is_outside_user_limits_THEN_PV_shows_major_alarm(
            self):
        test_values = [self.low_alarm_limit - 1.0, self.high_alarm_limit + 1.0]

        for channel, value_to_set in product(self.CHANNELS, test_values):
            register_offset = channel * self.get_registers_per_channel()

            expected_value = self.SCALING_FACTOR * value_to_set

            self._lewis.backdoor_run_function_on_device(
                self.get_setter_function_name(),
                (self.get_starting_reg_addr() + register_offset, value_to_set))

            self.ca.assert_that_pv_is_number(
                "CH{:01d}:{PV}".format(channel, PV=self.get_PV_name()),
                expected_value,
                tolerance=0.1 * abs(expected_value))

            self.ca.assert_that_pv_alarm_is(
                "CH{:01d}:{PV}".format(channel, PV=self.get_PV_name()),
                self.ca.Alarms.MAJOR)

    def test_WHEN_a_channel_is_aliased_THEN_a_PV_with_that_alias_exists(self):
        for channel in self.CHANNELS:
            self.ca.assert_that_pv_exists(
                self.get_channel_format().format(channel))
Beispiel #19
0
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 LoqApertureTests(unittest.TestCase):
    """
    Tests for the LOQ Aperture
    """
    def setUp(self):
        self._ioc = IOCRegister.get_running("GALIL_01")
        self.ca = ChannelAccess(default_timeout=30)
        self.ca.assert_that_pv_exists(MOTOR, timeout=60)
        self.ca.assert_that_pv_exists(CLOSESTSHUTTER)
        self.ca.assert_that_pv_exists(CLOSEAPERTURE)

    # Closest positions defined in ticket 3623
    @parameterized.expand([
        ("Aperture_large", 0, 1),
        ("Stop_01", 1, 1),
        ("Aperture_medium", 2, 3),
        ("Stop_02", 3, 3),
        ("Aperture_small", 4, 3),
    ])
    def test_GIVEN_motor_on_an_aperture_position_WHEN_motor_set_to_closest_beamstop_THEN_motor_moves_to_closest_beamstop(
            self, start_position, start_index, closest_stop):
        # GIVEN
        self.ca.set_pv_value(POSITION_SP, start_index)
        self.ca.assert_that_pv_is_number(POSITION_INDEX,
                                         start_index,
                                         tolerance=TOLERANCE)
        self.ca.assert_that_pv_is_number(MOTOR,
                                         MOTION_SETPOINT[start_position],
                                         tolerance=TOLERANCE)

        # WHEN
        self.ca.process_pv(CLOSEAPERTURE)

        # THEN
        self.ca.assert_that_pv_is_number(CLOSESTSHUTTER, closest_stop)
        self.ca.assert_that_pv_is_number(POSITION_INDEX,
                                         closest_stop,
                                         timeout=5)
        self.ca.assert_that_pv_is_number(
            MOTOR,
            list(MOTION_SETPOINT.values())[closest_stop],
            tolerance=TOLERANCE)

    # Closest positions defined in ticket 3623
    @parameterized.expand([
        ("Aperture_large", 0, 1),
        ("Stop_01", 1, 1),
        ("Aperture_medium", 2, 3),
        ("Stop_02", 3, 3),
        ("Aperture_small", 4, 3),
    ])
    def test_GIVEN_motor_off_setpoint_WHEN_motor_set_to_closest_beamstop_THEN_motor_moves_to_closest_beamstop(
            self, _, start_index, closest_stop):
        # GIVEN
        # Move 25 per cent forwards and backwards off centre of setpoint
        for fraction_moved_off_setpoint in [0.25, -0.25]:
            initial_position = list(MOTION_SETPOINT.values())[start_index] + (
                fraction_moved_off_setpoint * SETPOINT_GAP)
            self.ca.set_pv_value(MOTOR, initial_position)
            self.ca.assert_that_pv_is_number(MOTOR,
                                             initial_position,
                                             tolerance=TOLERANCE)

            # This assertion ensures that this calc record has updated with the closest beam stop position
            self.ca.assert_that_pv_is_number(CLOSESTSHUTTER, closest_stop)

            # WHEN
            self.ca.process_pv(CLOSEAPERTURE)

            # THEN
            self.ca.assert_that_pv_is_number(CLOSESTSHUTTER, closest_stop)
            self.ca.assert_that_pv_is_number(POSITION_INDEX,
                                             closest_stop,
                                             timeout=5)
            self.ca.assert_that_pv_is_number(
                MOTOR,
                list(MOTION_SETPOINT.values())[closest_stop],
                tolerance=TOLERANCE)
class OscillatingCollimatorTests(unittest.TestCase):
    """
    Tests for the LET Oscillating collimator.

    The CA.Client.Exceptions these tests generate are expected because of a workaround we had to make in the DB
    file to prevent a hang in the case of using asynFloat64 for the SP types. Issue described in ticket #2736
    """
    def setUp(self):
        self._ioc = IOCRegister.get_running("GALIL_01")
        ca_mot = ChannelAccess()
        ca_mot.assert_that_pv_exists("MOT:MTR0103", timeout=30)
        ca_mot.assert_setting_setpoint_sets_readback(DEFAULT_MOTOR_RESOLUTION,
                                                     set_point_pv="MOT:MTR0103.MRES", readback_pv="MOT:MTR0103.MRES", )
        self.ca = ChannelAccess(device_prefix=PREFIX)
        self.ca.assert_that_pv_exists("VEL:SP", timeout=30)

    def _custom_name_func(testcase_func, param_num, param):
        return "{}_ang_{}_freq_{}_rad_{}".format(
            testcase_func.__name__,
            *param.args[0]
            )

    @parameterized.expand(
        # [(angle, frequency, radius), (expected distance, expected velocity)
        # Values confirmed via LabView VI
        [[(2.0, 0.5, 10.0), (281, 283)],
         [(1.0, 0.5, 10.0), (140, 140)],
         [(0.5, 0.5, 10.0), (70, 70)],
         [(2.0, 0.1, 10.0), (279, 56)],
         [(1.0, 0.1, 10.0), (140, 28)],
         [(0.5, 0.1, 10.0), (70, 14)],

         [(2.0, 0.5, 50.0), (1442, 1487)],
         [(1.0, 0.5, 50.0), (709, 719)],
         [(0.5, 0.5, 50.0), (352, 354)],

         [(2.0, 0.1, 50.0), (1398, 280)],
         [(1.0, 0.1, 50.0), (699, 140)],
         [(0.5, 0.1, 50.0), (349, 70)]], testcase_func_name=_custom_name_func
    )
    def test_GIVEN_angle_frequency_and_radius_WHEN_set_THEN_distance_and_velocity_match_LabView_generated_values(self, settings, expected_values):

        # Arrange
        tolerance = 0.5

        # Act
        # in normal operations the radius is not dynamic so set it first so it is considered in future calcs
        self.ca.set_pv_value(RADIUS, settings[2])
        self.ca.set_pv_value(ANGLE, settings[0])
        self.ca.set_pv_value(FREQUENCY, settings[1])

        # Assert
        self.ca.assert_that_pv_is_number("DIST:SP", expected_values[0], tolerance)
        self.ca.assert_that_pv_is_number("VEL:SP", expected_values[1], tolerance)

    def test_WHEN_angle_set_negative_THEN_angle_is_zero(self):
        self.ca.set_pv_value(ANGLE, -1.0)
        self.ca.assert_that_pv_is_number(ANGLE, 0.0)

    def test_WHEN_angle_set_greater_than_two_THEN_angle_is_two(self):
        self.ca.set_pv_value(ANGLE, 5.0)
        self.ca.assert_that_pv_is_number(ANGLE, 2.0)

    def test_WHEN_frequency_set_negative_THEN_angle_is_zero(self):
        self.ca.set_pv_value(FREQUENCY, -1.0)
        self.ca.assert_that_pv_is_number(FREQUENCY, 0.0)

    def test_WHEN_angle_set_greater_than_half_THEN_angle_is_half(self):
        self.ca.set_pv_value(FREQUENCY, 1.0)
        self.ca.assert_that_pv_is_number(FREQUENCY, 0.5)

    def test_WHEN_frq_set_greater_than_two_THEN_angle_is_two(self):
        self.ca.set_pv_value(ANGLE, 5.0)
        self.ca.assert_that_pv_is_number(ANGLE, 2.0)

    def test_WHEN_input_values_cause_discriminant_to_be_negative_THEN_discriminant_pv_is_one(self):

        # Act
        # in normal operations the radius is not dynamic so set it first so it is considered in future calcs
        self.ca.set_pv_value(RADIUS, 1000.0)
        self.ca.set_pv_value(ANGLE, 2.0)
        self.ca.set_pv_value(FREQUENCY, 0.5)

        # Assert
        self.ca.assert_that_pv_is_number(DISCRIMINANT, 1.0)

    def test_WHEN_input_values_cause_discriminant_to_be_positive_THEN_discriminant_pv_is_zero(self):

        # Act
        # in normal operations the radius is not dynamic so set it first so it is considered in future calcs
        self.ca.set_pv_value(RADIUS, 1.0)
        self.ca.set_pv_value(ANGLE, 2.0)
        self.ca.set_pv_value(FREQUENCY, 0.5)

        # Assert
        self.ca.assert_that_pv_is_number(DISCRIMINANT, 0.0)

    def test_WHEN_collimator_running_THEN_thread_is_not_on_reserved_thread(self):
        # Threads 0 and 1 are reserved for homing under IBEX
        self.ca.assert_that_pv_is_not("THREAD", "0")
        self.ca.assert_that_pv_is_not("THREAD", "1")
Beispiel #22
0
class IndfurnTests(unittest.TestCase):
    """
    Tests for the Indfurn 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=30)

    def test_that_disable_pv_exists(self):
        self.ca.assert_that_pv_is("DISABLE", "COMMS ENABLED")

    @skip_if_recsim("Recsim does not emulate version command")
    def test_that_version_pv_exists(self):
        self.ca.assert_that_pv_is("VERSION", "EMULATED FURNACE")

    @parameterized.expand(parameterized_list(TEST_TEMPERATURES))
    def test_GIVEN_a_setpoint_WHEN_ask_for_the_setpoint_readback_THEN_get_the_value_just_set(
            self, _, temp, alarm):
        self.ca.assert_setting_setpoint_sets_readback(
            temp,
            set_point_pv="TEMP:SP",
            readback_pv="TEMP:SP:RBV",
            expected_alarm=alarm)

    @parameterized.expand(parameterized_list(TEST_TEMPERATURES))
    def test_GIVEN_a_setpoint_WHEN_ask_for_the_current_temperature_THEN_get_the_value_just_set(
            self, _, temp, alarm):
        self.ca.assert_setting_setpoint_sets_readback(temp,
                                                      set_point_pv="TEMP:SP",
                                                      readback_pv="TEMP",
                                                      expected_alarm=alarm)

    @parameterized.expand(parameterized_list(TEST_TEMPERATURES))
    def test_GIVEN_a_setpoint_WHEN_ask_for_the_sample_temperature_THEN_get_the_value_just_set(
            self, _, temp, alarm):
        self.ca.assert_setting_setpoint_sets_readback(
            temp,
            set_point_pv="TEMP:SP",
            readback_pv="SAMPLE:TEMP",
            expected_alarm=alarm)

    @parameterized.expand(parameterized_list(TEST_DIAGNOSTIC_TEMPERATURES))
    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_GIVEN_pipe_temperature_set_via_backdoor_when_read_pipe_temperature_THEN_get_value_just_set(
            self, _, temp, alarm):
        self._lewis.backdoor_set_on_device("pipe_temperature", temp)
        self.ca.assert_that_pv_is_number("PIPE:TEMP", temp, tolerance=0.1)
        self.ca.assert_that_pv_alarm_is("PIPE:TEMP", alarm)

    @parameterized.expand(parameterized_list(TEST_DIAGNOSTIC_TEMPERATURES))
    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_GIVEN_capacitor_temperature_set_via_backdoor_when_read_capacitor_temperature_THEN_get_value_just_set(
            self, _, temp, alarm):
        self._lewis.backdoor_set_on_device("capacitor_bank_temperature", temp)
        self.ca.assert_that_pv_is_number("CAPACITOR:TEMP", temp, tolerance=0.1)
        self.ca.assert_that_pv_alarm_is("CAPACITOR:TEMP", alarm)

    @parameterized.expand(parameterized_list(TEST_DIAGNOSTIC_TEMPERATURES))
    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_GIVEN_fet_temperature_set_via_backdoor_when_read_fet_temperature_THEN_get_value_just_set(
            self, _, temp, alarm):
        self._lewis.backdoor_set_on_device("fet_temperature", temp)
        self.ca.assert_that_pv_is_number("FET:TEMP", temp, tolerance=0.1)
        self.ca.assert_that_pv_alarm_is("FET:TEMP", alarm)

    @parameterized.expand(parameterized_list(TEST_PID_VALUES))
    def test_GIVEN_p_parameter_changed_WHEN_read_p_THEN_value_can_be_read_back(
            self, _, val):
        self.ca.assert_setting_setpoint_sets_readback(val, "P")

    @parameterized.expand(parameterized_list(TEST_PID_VALUES))
    def test_GIVEN_i_parameter_changed_WHEN_read_i_THEN_value_can_be_read_back(
            self, _, val):
        self.ca.assert_setting_setpoint_sets_readback(val, "I")

    @parameterized.expand(parameterized_list(TEST_PID_VALUES))
    def test_GIVEN_d_parameter_changed_WHEN_read_d_THEN_value_can_be_read_back(
            self, _, val):
        self.ca.assert_setting_setpoint_sets_readback(val, "D")

    @parameterized.expand(parameterized_list(TEST_SAMPLE_TIMES))
    def test_GIVEN_sample_time_changed_WHEN_read_sample_time_THEN_value_can_be_read_back(
            self, _, sample_time):
        self.ca.assert_setting_setpoint_sets_readback(sample_time,
                                                      "SAMPLETIME")

    def test_GIVEN_pid_direction_is_set_THEN_it_can_be_read_back(self):
        for mode in ["Heating", "Cooling",
                     "Heating"]:  # Check both transitions
            self.ca.assert_setting_setpoint_sets_readback(
                mode, "PID:DIRECTION")

    def test_GIVEN_pid_run_status_is_set_THEN_it_can_be_read_back(self):
        for mode in ["Stopped", "Running",
                     "Stopped"]:  # Check both transitions
            self.ca.assert_setting_setpoint_sets_readback(mode, "PID:RUNNING")

    @parameterized.expand(parameterized_list(TEST_PID_LIMITS))
    def test_GIVEN_pid_lower_limit_is_set_THEN_it_can_be_read_back(
            self, _, pid_limit):
        self.ca.assert_setting_setpoint_sets_readback(pid_limit,
                                                      "PID:LIMIT:LOWER")

    @parameterized.expand(parameterized_list(TEST_PID_LIMITS))
    def test_GIVEN_pid_upper_limit_is_set_THEN_it_can_be_read_back(
            self, _, pid_limit):
        self.ca.assert_setting_setpoint_sets_readback(pid_limit,
                                                      "PID:LIMIT:UPPER")

    @parameterized.expand(parameterized_list(TEST_PSU_VOLTAGES))
    def test_GIVEN_psu_voltage_is_set_THEN_it_can_be_read_back(
            self, _, psu_volt):
        self.ca.assert_setting_setpoint_sets_readback(psu_volt, "PSU:VOLT")

    @parameterized.expand(parameterized_list(TEST_PSU_CURRENTS))
    def test_GIVEN_psu_current_is_set_THEN_it_can_be_read_back(
            self, _, psu_curr):
        self.ca.assert_setting_setpoint_sets_readback(psu_curr, "PSU:CURR")

    @parameterized.expand(parameterized_list(TEST_OUTPUTS))
    def test_GIVEN_output_is_set_THEN_it_can_be_read_back(self, _, output):
        self.ca.assert_setting_setpoint_sets_readback(output, "OUTPUT")

    def test_GIVEN_pid_mode_is_set_THEN_it_can_be_read_back(self):
        for mode in ["Automatic", "Manual",
                     "Automatic"]:  # Check both transitions
            self.ca.assert_setting_setpoint_sets_readback(mode, "PID:MODE")

    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_GIVEN_power_supply_mode_is_set_to_either_local_or_remote_THEN_it_sets_successfully_in_emulator(
            self):
        for remote in [False, True, False]:  # Check both transitions
            self.ca.assert_setting_setpoint_sets_readback(
                "Remote" if remote else "Local",
                "PSU:CONTROLMODE",
                expected_alarm=self.ca.Alarms.NONE
                if remote else self.ca.Alarms.MAJOR)

    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_GIVEN_power_supply_output_is_set_to_either_on_or_off_THEN_it_sets_successfully_in_emulator(
            self):
        for output in [False, True, False]:  # Check both transitions
            self.ca.assert_setting_setpoint_sets_readback(
                "On" if output else "Off", "PSU:POWER")

    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_GIVEN_sample_area_led_is_set_to_either_on_or_off_THEN_it_sets_successfully_in_emulator(
            self):
        for led_on in [False, True, False]:  # Check both transitions
            self.ca.assert_setting_setpoint_sets_readback(
                "On" if led_on else "Off", "LED")

    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_GIVEN_power_supply_hf_is_set_to_either_on_or_off_THEN_it_sets_successfully_in_emulator(
            self):
        for hf_on in [False, True, False]:  # Check both transitions
            self.ca.assert_setting_setpoint_sets_readback(
                "On" if hf_on else "Off", "PSU:HF")

    @skip_if_recsim("Can't use lewis backdoor in recsim")
    def test_GIVEN_psu_goes_over_temperature_THEN_alarm_comes_on_AND_can_reset_via_pv(
            self):
        self._lewis.backdoor_set_on_device("psu_overtemp", True)
        self.ca.assert_that_pv_is("ALARM:PSUTEMP", "ALARM")
        self.ca.assert_that_pv_alarm_is("ALARM:PSUTEMP", self.ca.Alarms.MAJOR)
        self.ca.set_pv_value("ALARM:CLEAR", 1)
        self.ca.assert_that_pv_is("ALARM:PSUTEMP", "OK")
        self.ca.assert_that_pv_alarm_is("ALARM:PSUTEMP", self.ca.Alarms.NONE)

    @skip_if_recsim("Can't use lewis backdoor in recsim")
    def test_GIVEN_psu_goes_over_voltage_THEN_alarm_comes_on_AND_can_reset_via_pv(
            self):
        self._lewis.backdoor_set_on_device("psu_overvolt", True)
        self.ca.assert_that_pv_is("ALARM:PSUVOLT", "ALARM")
        self.ca.assert_that_pv_alarm_is("ALARM:PSUVOLT", self.ca.Alarms.MAJOR)
        self.ca.set_pv_value("ALARM:CLEAR", 1)
        self.ca.assert_that_pv_is("ALARM:PSUVOLT", "OK")
        self.ca.assert_that_pv_alarm_is("ALARM:PSUVOLT", self.ca.Alarms.NONE)

    @skip_if_recsim("Can't use lewis backdoor in recsim")
    def test_GIVEN_cooling_water_flow_turns_off_THEN_this_is_visible_from_ioc_and_causes_alarm(
            self):

        self._lewis.backdoor_set_on_device("cooling_water_flow", 0)
        self.ca.assert_that_pv_is("COOLINGWATER:FLOW", 0)
        self.ca.assert_that_pv_is("COOLINGWATER:STATUS", "ALARM")
        self.ca.assert_that_pv_alarm_is("COOLINGWATER:STATUS",
                                        self.ca.Alarms.MAJOR)

        self._lewis.backdoor_set_on_device("cooling_water_flow", 500)
        self.ca.assert_that_pv_is("COOLINGWATER:FLOW", 500)
        self.ca.assert_that_pv_is("COOLINGWATER:STATUS", "OK")
        self.ca.assert_that_pv_alarm_is("COOLINGWATER:STATUS",
                                        self.ca.Alarms.NONE)

    @skip_if_recsim("Recsim can't handle arbitrary commands")
    def test_GIVEN_an_arbitrary_command_THEN_get_a_response(self):
        self.ca.set_pv_value("ARBITRARY:SP", "?ver")
        self.ca.assert_that_pv_is(
            "ARBITRARY", "<EMULATED FURNACE\r\n<EMULATED FURNACE\r\n")

    @parameterized.expand(parameterized_list(SAMPLE_HOLDER_MATERIALS))
    def test_GIVEN_sample_holder_material_is_set_THEN_sample_holder_material_can_be_read_back(
            self, _, material):
        self.ca.assert_setting_setpoint_sets_readback(material, "SAMPLEHOLDER")

    @skip_if_recsim("Can't use lewis backdoor in recsim")
    def test_GIVEN_thermocouple_1_fault_on_device_THEN_read_successfully(self):
        self._lewis.backdoor_set_on_device("thermocouple_1_fault", 0)
        for fault_pv in FAULTS:
            self.ca.assert_that_pv_is("TC1FAULTS:{}".format(fault_pv), "OK")

        for fault_pv, fault_number in FAULTS.items():
            self._lewis.backdoor_set_on_device("thermocouple_1_fault",
                                               fault_number)
            self.ca.assert_that_pv_is("TC1FAULTS:{}".format(fault_pv), "FAULT")

    @skip_if_recsim("Can't use lewis backdoor in recsim")
    def test_GIVEN_thermocouple_2_fault_on_device_THEN_read_successfully(self):
        self._lewis.backdoor_set_on_device("thermocouple_2_fault", 0)
        for fault_pv in FAULTS:
            self.ca.assert_that_pv_is("TC2FAULTS:{}".format(fault_pv), "OK")

        for fault_pv, fault_number in FAULTS.items():
            self._lewis.backdoor_set_on_device("thermocouple_2_fault",
                                               fault_number)
            self.ca.assert_that_pv_is("TC2FAULTS:{}".format(fault_pv), "FAULT")
Beispiel #23
0
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)
Beispiel #24
0
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)
Beispiel #25
0
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 SimpleTests(unittest.TestCase):
    """
    Tests for the stability checking logic
    """

    def setUp(self):
        self._ioc = IOCRegister.get_running(DEVICE_PREFIX)
        self.assertIsNotNone(self._ioc)

        self.ca = ChannelAccess(device_prefix=DEVICE_PREFIX, default_timeout=3)

        self.ca.assert_that_pv_exists("VAL")
        self.ca.assert_that_pv_exists("VAL:SP")

        self.ca.assert_that_pv_is_number("STAB:IS_STABLE.E", TOLERANCE)

        # Need to do this to ensure buffer is properly up before starting any tests
        self.ca.assert_that_pv_exists("STAB:_VAL_BUFF")
        while int(self.ca.get_pv_value("STAB:_VAL_BUFF.NUSE")) < NUMBER_OF_SAMPLES:
            self.ca.process_pv("VAL")

        self.ca.set_pv_value("VAL.SIMS", 0)

    def test_GIVEN_pv_not_changing_and_WHEN_pv_exactly_equal_to_sp_THEN_stable(self):
        test_value = 100
        self.ca.set_pv_value("VAL:SP", test_value)
        for _ in range(NUMBER_OF_SAMPLES):
            self.ca.set_pv_value("VAL", test_value)

        self.ca.assert_that_pv_is("STAB:HAS_RECENT_ALARM", False)
        self.ca.assert_that_pv_is("STAB:IS_STABLE", True)

    @parameterized.expand(parameterized_list([operator.add, operator.sub]))
    def test_GIVEN_pv_not_changing_and_WHEN_pv_outside_tolerance_of_sp_THEN_stable(self, _, op):
        test_value = 200
        self.ca.set_pv_value("VAL:SP", test_value)
        for _ in range(NUMBER_OF_SAMPLES):
            self.ca.set_pv_value("VAL", op(test_value, 1.1 * TOLERANCE))

        self.ca.assert_that_pv_is("STAB:HAS_RECENT_ALARM", False)
        self.ca.assert_that_pv_is("STAB:IS_STABLE", False)

    @parameterized.expand(parameterized_list([operator.add, operator.sub]))
    def test_GIVEN_pv_not_changing_and_WHEN_pv_inside_tolerance_of_sp_THEN_stable(self, _, op):
        test_value = 300
        self.ca.set_pv_value("VAL:SP", test_value)
        for _ in range(NUMBER_OF_SAMPLES):
            self.ca.set_pv_value("VAL", op(test_value, 0.9 * TOLERANCE))

        self.ca.assert_that_pv_is("STAB:HAS_RECENT_ALARM", False)
        self.ca.assert_that_pv_is("STAB:IS_STABLE", True)

    def test_GIVEN_one_out_of_range_value_at_end_of_buffer_THEN_unstable(self):
        stable_value = 400
        self.ca.set_pv_value("VAL:SP", stable_value)
        for _ in range(NUMBER_OF_SAMPLES - 1):
            self.ca.set_pv_value("VAL", stable_value)
        self.ca.set_pv_value("VAL", stable_value + 1.1 * TOLERANCE)

        self.ca.assert_that_pv_is("STAB:HAS_RECENT_ALARM", False)
        self.ca.assert_that_pv_is("STAB:IS_STABLE", False)

    def test_GIVEN_one_out_of_range_value_at_beginning_of_buffer_THEN_unstable(self):
        stable_value = 500
        self.ca.set_pv_value("VAL:SP", stable_value)
        self.ca.set_pv_value("VAL", stable_value + 1.1 * TOLERANCE)

        for _ in range(NUMBER_OF_SAMPLES - 1):
            self.ca.set_pv_value("VAL", stable_value)

        self.ca.assert_that_pv_is("STAB:HAS_RECENT_ALARM", False)
        self.ca.assert_that_pv_is("STAB:IS_STABLE", False)

    def test_GIVEN_one_alarmed_value_at_end_of_buffer_THEN_unstable(self):
        stable_value = 400
        self.ca.set_pv_value("VAL:SP", stable_value)
        for _ in range(NUMBER_OF_SAMPLES - 1):
            self.ca.set_pv_value("VAL", stable_value)

        self.ca.set_pv_value("VAL.SIMS", 3)

        self.ca.assert_that_pv_is("STAB:HAS_RECENT_ALARM", False)
        self.ca.assert_that_pv_is("STAB:IS_STABLE", False)

    def test_GIVEN_one_alarmed_value_at_beginning_of_buffer_THEN_unstable(self):
        stable_value = 500
        self.ca.set_pv_value("VAL:SP", stable_value)
        self.ca.set_pv_value("VAL", stable_value)
        self.ca.set_pv_value("VAL.SIMS", 3)
        self.ca.set_pv_value("VAL.SIMS", 0)

        for _ in range(NUMBER_OF_SAMPLES - 2):  # -2 because setting SEVR back to zero will cause a record to process.
            self.ca.set_pv_value("VAL", stable_value)

        self.ca.assert_that_pv_is("STAB:HAS_RECENT_ALARM", False)
        self.ca.assert_that_pv_is("STAB:IS_STABLE", False)

        # Adding one more valid reading at the end of the buffer should cause the invalid value at the beginning
        # to be forgotten, meaning it should then be considered stable
        self.ca.set_pv_value("VAL", stable_value)
        self.ca.assert_that_pv_is("STAB:IS_STABLE", True)
class CryoSMSTests(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=10)

        if IOCRegister.uses_rec_sim:
            self.ca.assert_that_pv_exists("DISABLE", timeout=30)
        else:
            self.ca.assert_that_pv_is("INIT", "Startup complete", timeout=60)
            self._lewis.backdoor_set_on_device("mid_target", 0)
            self._lewis.backdoor_set_on_device("output", 0)
            self.ca.set_pv_value("MID:SP", 0)
            self.ca.set_pv_value("START:SP", 1)
            self.ca.set_pv_value("PAUSE:SP", 1)
            self._lewis.backdoor_set_on_device("output", 0)
            self.ca.set_pv_value("ABORT:SP", 1)
            self.ca.set_pv_value("PAUSE:SP", 0)
            self.ca.assert_that_pv_is("RAMP:STAT", "HOLDING ON TARGET")
            self.ca.assert_that_pv_is("OUTPUT:RAW", 0)

    @skip_if_recsim("Cannot properly simulate device startup in recsim")
    def test_GIVEN_certain_macros_WHEN_IOC_loads_THEN_correct_values_initialised(
            self):
        expectedValues = {
            "OUTPUT:SP": 0,
            "OUTPUT": 0,
            "OUTPUT:COIL": 0,
            "OUTPUT:PERSIST": 0,
            "OUTPUT:VOLT": 0,
            "RAMP:RATE": 1.12,
            "READY": 1,
            "RAMP:RAMPING": 0,
            "TARGET:TIME": 0,
            "STAT": "",
            "HEATER:STAT": "OFF",
            "START:SP.DISP": "0",
            "PAUSE:SP.DISP": "0",
            "ABORT:SP.DISP": "0",
            "OUTPUT:SP.DISP": "0",
            "MAGNET:MODE.DISP": "1",
            "RAMP:LEADS.DISP": "1",
        }
        failedPVs = []
        for PV in expectedValues:
            try:
                self.ca.assert_that_pv_is(PV, expectedValues[PV], timeout=5)
            except Exception as e:
                failedPVs.append(e.message)
        if failedPVs:
            self.fail("The following PVs generated errors:\n{}".format(
                "\n".join(failedPVs)))

    def test_GIVEN_outputmode_sp_correct_WHEN_outputmode_sp_written_to_THEN_outputmode_changes(
            self):
        # For all other tests, alongside normal operation, communication should be in amps
        self.ca.assert_setting_setpoint_sets_readback("TESLA",
                                                      "OUTPUTMODE",
                                                      "OUTPUTMODE:SP",
                                                      timeout=10)
        self.ca.assert_setting_setpoint_sets_readback("AMPS",
                                                      "OUTPUTMODE",
                                                      "OUTPUTMODE:SP",
                                                      timeout=10)

    @parameterized.expand(parameterized_list(TEST_RAMPS))
    @skip_if_recsim("C++ driver can not correctly initialise in recsim")
    def test_GIVEN_psu_at_field_strength_A_WHEN_told_to_ramp_to_B_THEN_correct_rates_used(
            self, _, ramp_data):
        startPoint, endPoint = ramp_data[0]
        ramp_rates = ramp_data[1]
        # When setting output, convert from Gauss to Amps by dividing by 10000 and T_TO_A, also ensure sign handled
        # correctly
        sign = 1 if startPoint >= 0 else -1
        self._lewis.backdoor_run_function_on_device("switch_direction", [sign])
        self._lewis.backdoor_set_on_device("output",
                                           abs(startPoint) / (0.037 * 10000))
        self.ca.set_pv_value("MID:SP", endPoint)
        self.ca.set_pv_value("START:SP", 1)
        for rate in ramp_rates:
            self.ca.assert_that_pv_is("RAMP:RATE", rate, timeout=20)
        self.ca.assert_that_pv_is("RAMP:STAT", "HOLDING ON TARGET", timeout=25)
        self.ca.assert_that_pv_is_within_range("OUTPUT", endPoint - 0.01,
                                               endPoint + 0.01)

    @skip_if_recsim("C++ driver can not correctly initialise in recsim")
    def test_GIVEN_IOC_not_ramping_WHEN_ramp_started_THEN_simulated_ramp_performed(
            self):
        self.ca.set_pv_value("MID:SP", 10000)
        self.ca.set_pv_value("START:SP", 1)
        self.ca.assert_that_pv_is("RAMP:STAT",
                                  "RAMPING",
                                  msg="Ramping failed to start")
        self.ca.assert_that_pv_is("RAMP:STAT", "HOLDING ON TARGET", timeout=10)

    @skip_if_recsim("C++ driver can not correctly initialise in recsim")
    def test_GIVEN_IOC_ramping_WHEN_paused_and_unpaused_THEN_ramp_is_paused_resumed_and_completes(
            self):
        # GIVEN ramping
        self.ca.set_pv_value("MID:SP", 10000)
        self.ca.set_pv_value("START:SP", 1)
        self.ca.assert_that_pv_is("RAMP:STAT", "RAMPING")
        # Pauses when pause set to true
        self.ca.set_pv_value("PAUSE:SP", 1)
        self.ca.assert_that_pv_is("RAMP:STAT",
                                  "HOLDING ON PAUSE",
                                  msg="Ramping failed to pause")
        self.ca.assert_that_pv_is_not(
            "RAMP:STAT",
            "HOLDING ON TARGET",
            timeout=5,
            msg="Ramp completed even though it should have paused")
        # Resumes when pause set to false, completes ramp
        self.ca.set_pv_value("PAUSE:SP", 0)
        self.ca.assert_that_pv_is("RAMP:STAT",
                                  "RAMPING",
                                  msg="Ramping failed to resume")
        self.ca.assert_that_pv_is("RAMP:STAT",
                                  "HOLDING ON TARGET",
                                  timeout=10,
                                  msg="Ramping failed to complete")

    @skip_if_recsim("C++ driver can not correctly initialise in recsim")
    def test_GIVEN_IOC_ramping_WHEN_aborted_THEN_ramp_aborted(self):
        # Given Ramping
        self.ca.set_pv_value("MID:SP", 10000)
        self.ca.set_pv_value("START:SP", 1)
        self.ca.assert_that_pv_is("RAMP:STAT", "RAMPING")
        # Aborts when abort set to true, then hits ready again
        self.ca.set_pv_value("ABORT:SP", 1)
        self.ca.assert_that_pv_is("RAMP:STAT", "HOLDING ON TARGET", timeout=10)

    @skip_if_recsim("C++ driver can not correctly initialise in recsim")
    def test_GIVEN_IOC_paused_WHEN_aborted_THEN_ramp_aborted(self):
        # GIVEN paused
        self.ca.set_pv_value("MID:SP", 10000)
        self.ca.set_pv_value("START:SP", 1)
        self.ca.set_pv_value("PAUSE:SP", 1)
        rampTarget = self.ca.get_pv_value("MID")
        self.ca.assert_that_pv_is("RAMP:STAT",
                                  "HOLDING ON PAUSE",
                                  msg="Ramping failed to pause")
        # Aborts when abort set to true, then hits ready again
        self.ca.set_pv_value("ABORT:SP", 1)
        self.ca.assert_that_pv_is("RAMP:STAT", "HOLDING ON TARGET", timeout=10)
        self.ca.assert_that_pv_is_not("MID", rampTarget)

    @skip_if_recsim(
        "Test is to tell whether data from emulator is correctly received")
    def test_GIVEN_output_nonzero_WHEN_units_changed_THEN_output_raw_adjusts(
            self):
        # Check that it is currently working correctly in Amps
        self._lewis.backdoor_set_on_device("is_paused", True)
        self._lewis.backdoor_set_on_device("output",
                                           1 / 0.037)  # 1T (0.037 = T_TO_A)
        self.ca.assert_that_pv_is_number("OUTPUT:RAW", 1 / 0.037, 0.001)
        self.ca.assert_that_pv_is_number("OUTPUT", 10000,
                                         1)  # OUTPUT should remain in Gauss
        # Set outputmode to tesla
        self.ca.set_pv_value("OUTPUTMODE:SP", "TESLA")
        self.ca.assert_that_pv_is_number("OUTPUT:RAW", 1, 0.001)
        self.ca.assert_that_pv_is_number("OUTPUT", 10000, 1)
        # Confirm functionality returns to normal when going back to Amps
        self.ca.set_pv_value("OUTPUTMODE:SP", "AMPS")
        self.ca.assert_that_pv_is_number("OUTPUT:RAW", 1 / 0.037, 0.001)
        self.ca.assert_that_pv_is_number("OUTPUT", 10000, 1)
Beispiel #28
0
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)
Beispiel #29
0
class JawsManagerBase(object):
    """
    Base classes for all jaws manager tests.
    """
    def setUp(self):
        self._ioc = IOCRegister.get_running("GALIL_01")
        self.ca = ChannelAccess()
        self.ca.assert_that_pv_exists("MOT:MTR0101", timeout=30)
        for jaw in range(1, self.get_num_of_jaws() + 1):
            self.ca.assert_that_pv_exists(UNDERLYING_GAP_SP.format(jaw, "V"),
                                          timeout=30)
            self.ca.assert_that_pv_exists(UNDERLYING_GAP_SP.format(jaw, "H"),
                                          timeout=30)
        self.ca.assert_that_pv_exists(self.get_sample_pv() +
                                      ":{}GAP:SP".format("V"),
                                      timeout=30)

    def get_sample_pv(self):
        return "JAWMAN:SAMPLE"

    @abc.abstractmethod
    def get_num_of_jaws(self):
        pass

    def _test_WHEN_centre_is_changed_THEN_centres_of_all_jaws_follow_and_gaps_unchanged(
            self, direction):
        expected_gaps = [
            self.ca.get_pv_value(UNDERLYING_GAP_SP.format(jaw, direction))
            for jaw in range(1,
                             self.get_num_of_jaws() + 1)
        ]

        self.ca.set_pv_value(
            self.get_sample_pv() + ":{}CENT:SP".format(direction), 10)
        for jaw in range(1, self.get_num_of_jaws() + 1):
            self.ca.assert_that_pv_is_number(
                UNDERLYING_CENT_SP.format(jaw, direction), 10, 0.1)
            self.ca.assert_that_pv_is_number(
                UNDERLYING_GAP_SP.format(jaw, direction),
                expected_gaps[jaw - 1], 0.1)

    def _test_WHEN_sizes_at_moderator_and_sample_changed_THEN_centres_of_all_jaws_unchanged(
            self, direction):
        # Set up jaws initially to have "custom" centre.
        centre = 12.34

        for jaw in range(1, self.get_num_of_jaws() + 1):
            # Set to centre * jaw so that each jaw is given a different centre
            self.ca.set_pv_value(UNDERLYING_CENT_SP.format(jaw, direction),
                                 centre * jaw)

        # Now change size at sample + moderator
        self.ca.set_pv_value(
            "{}:{}GAP:SP".format(self.get_sample_pv(), direction), 11.111)
        self.ca.set_pv_value(MOD_GAP.format(direction), 22.222)

        # Assert that centres are unchanged
        for jaw in range(1, self.get_num_of_jaws() + 1):
            self.ca.assert_that_pv_is_number(
                UNDERLYING_CENT_SP.format(jaw, direction), centre * jaw, 0.001)

    def _test_WHEN_sample_gap_set_THEN_other_jaws_as_expected(
            self, direction, sample_gap, expected):
        self.ca.set_pv_value(
            self.get_sample_pv() + ":{}GAP:SP".format(direction), sample_gap)
        for i, exp in enumerate(expected):
            self.ca.assert_that_pv_is_number(UNDERLYING_GAP_SP.format(
                i + 1, direction),
                                             exp,
                                             0.1,
                                             timeout=1)
Beispiel #30
0
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)