예제 #1
0
class Mk3chopperTests(unittest.TestCase):

    # Remote access modes
    LOCAL = "LOCAL"
    REMOTE = "REMOTE"
    COMP_MODE_PV = "COMP:MODE"

    def setUp(self):
        self._ioc = IOCRegister.get_running(DEVICE_PREFIX)
        # Comp mode is on a slow 10s read. Needs a longer timeout than default
        self.ca = ChannelAccess(device_prefix=DEVICE_PREFIX, default_timeout=30)
        self.ca.assert_that_pv_exists(self.COMP_MODE_PV)

    def test_WHEN_ioc_is_in_remote_mode_THEN_it_has_no_alarm(self):
        # In RECSIM, switch to remote explicitly. DEVSIM can only have remote mode so no switch needed
        if IOCRegister.uses_rec_sim:
            # Bug in CA channel. Reports invalid alarm severity if you set enum directly
            self.ca.set_pv_value("SIM:{}.VAL".format(self.COMP_MODE_PV), 1)
        self.ca.assert_that_pv_is(self.COMP_MODE_PV, self.REMOTE)
        self.ca.assert_that_pv_alarm_is(self.COMP_MODE_PV, ChannelAccess.Alarms.NONE)

    @skip_if_devsim("Can't switch to local mode in DEVSIM")
    def test_WHEN_ioc_is_in_local_mode_THEN_it_has_a_major_alarm(self):
        # Bug in CA channel. Reports invalid alarm severity if you set enum directly
        self.ca.set_pv_value("SIM:{}.VAL".format(self.COMP_MODE_PV), 0)
        self.ca.assert_that_pv_is(self.COMP_MODE_PV, self.LOCAL)
        self.ca.assert_that_pv_alarm_is(self.COMP_MODE_PV, ChannelAccess.Alarms.MAJOR)
class InhibitrTests(unittest.TestCase):
    """
    Tests for the Inhibitr IOC.
    """
    def setUp(self):
        self._ioc = IOCRegister.get_running(IOC_PREFIX)
        self.ca = ChannelAccess(default_timeout=20)
        self.values = ["SIMPLE:VALUE1:SP", "SIMPLE:VALUE2:SP"]

        for pv in self.values:
            self.ca.assert_that_pv_exists(pv)

        self.reset_values_to_zero()

    def reset_values_to_zero(self):
        for val in self.values:
            self.ca.set_pv_value(val, 0)
            self.ca.assert_that_pv_is(val, 0)
        for val in self.values:
            self.ca.assert_that_pv_is(val + ".DISP", "0")

    def test_GIVEN_both_inputs_are_zero_WHEN_setting_either_input_THEN_this_is_allowed(
            self):
        for val in self.values:
            self.ca.assert_that_pv_is("{}.DISP".format(val), "0")

    @unstable_test()
    def test_GIVEN_one_input_is_one_WHEN_setting_other_value_to_one_THEN_this_is_not_allowed(
            self):
        self.ca.set_pv_value("SIMPLE:VALUE1:SP", 1)
        self.ca.assert_that_pv_is("SIMPLE:VALUE1:SP", 1)
        # When value1 is set to non-zero, the disallowed value of value2 should be 1
        # i.e 'Not allowed to set this value to non-zero'
        self.ca.assert_that_pv_is("SIMPLE:VALUE2:SP.DISP", "1")
예제 #3
0
class SamposTests(unittest.TestCase):
    """
    Tests for the sampos IOC.
    """

    test_values = [0, 10]
    axes = ['X', 'Y', 'Z', 'W', 'S']

    def setUp(self):
        self._ioc = IOCRegister.get_running("SAMPOS")

        self.ca = ChannelAccess(20, device_prefix=DEVICE_PREFIX)
        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_values_are_set_THEN_readbacks_update(self):
        for axis in self.axes:
            for value in self.test_values:
                self.ca.assert_setting_setpoint_sets_readback(
                    value,
                    readback_pv="{}".format(axis),
                    set_point_pv="{}:SP".format(axis))

    def test_WHEN_values_are_set_THEN_setpoint_readbacks_update(self):
        for axis in self.axes:
            for value in self.test_values:
                self.ca.assert_setting_setpoint_sets_readback(
                    value,
                    readback_pv="{}:SP:RBV".format(axis),
                    set_point_pv="{}:SP".format(axis))
 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)
예제 #5
0
class FinsTests(unittest.TestCase):
    """
    Tests for the Fins IOC.
    """
    def setUp(self):
        self._ioc = IOCRegister.get_running("FINS_01")
        self.ca = ChannelAccess(device_prefix=DEVICE_PREFIX)

    def test_GIVEN_fins_THEN_has_flow_pv(self):
        self.ca.assert_that_pv_exists("BENCH:FLOW1")
class FmrTests(unittest.TestCase):
    """
    Tests for the fmr IOC.
    """
    def setUp(self):
        self._ioc = IOCRegister.get_running(DEVICE_PREFIX)
        self.ca = ChannelAccess(20, device_prefix=DEVICE_PREFIX)

    def test_WHEN_ioc_is_started_THEN_PV_exists(self):
        self.ca.assert_that_pv_exists("FMR:ACTIVITY", timeout=30)
예제 #7
0
class Wm323Tests(unittest.TestCase):
    """
    Tests for the wm323 IOC.
    """
    def setUp(self):
        self._lewis, self._ioc = get_running_lewis_and_ioc(
            "wm323", DEVICE_PREFIX)
        self.ca = ChannelAccess(device_prefix=DEVICE_PREFIX,
                                default_timeout=20)
        self.ca.assert_that_pv_exists("DISABLE")

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

    @skip_if_recsim("Requires emulator logic so not supported in RECSIM")
    def test_WHEN_ioc_is_started_THEN_pump_type_pv_correct(self):
        self.ca.assert_that_pv_is("TYPE", "323Du")

    @parameterized.expand([('On low limit', SPEED_LOW_LIMIT),
                           ('Intermediate value', 42),
                           ('On high limit', SPEED_HIGH_LIMIT)])
    def test_WHEN_speed_setpoint_is_sent_THEN_readback_updates(self, _, value):
        self.ca.assert_setting_setpoint_sets_readback(value, 'SPEED')

    @parameterized.expand([('Low limit', SPEED_LOW_LIMIT, SPEED_LOW_LIMIT - 1),
                           ('High limit', SPEED_HIGH_LIMIT,
                            SPEED_HIGH_LIMIT + 1)])
    def test_WHEN_speed_setpoint_is_set_outside_max_limits_THEN_setpoint_within(
            self, _, limit, value):
        self.ca.set_pv_value("SPEED:SP", value)
        self.ca.assert_that_pv_is("SPEED:SP", limit)

    def test_WHEN_direction_setpoint_is_sent_THEN_readback_updates(self):
        for mode in ["Clockwise", "Anti-Clockwise"]:
            self.ca.assert_setting_setpoint_sets_readback(mode, "DIRECTION")

    def test_WHEN_status_setpoint_is_sent_THEN_readback_updates(self):
        for mode in ["Running", "Stopped"]:
            self.ca.assert_setting_setpoint_sets_readback(mode, "STATUS")

    @skip_if_recsim("Requires emulator logic so not supported in RECSIM")
    def test_GIVEN_pump_off_WHEN_set_pump_on_THEN_pump_turned_on(self):
        self.ca.set_pv_value("RUN:SP", "Run")

        self.ca.assert_that_pv_is("STATUS", "Running")

    @skip_if_recsim("Requires emulator logic so not supported in RECSIM")
    def test_GIVEN_pump_on_WHEN_set_pump_off_THEN_pump_paused(self):
        self.ca.set_pv_value("RUN:SP", "Run")
        self.ca.assert_that_pv_is("STATUS", "Running")

        self.ca.set_pv_value("STOP:SP", "Stop")

        self.ca.assert_that_pv_is("STATUS", "Stopped")
class Ag53220ATests(unittest.TestCase):
    """
    Tests for the Ag53220A IOC.
    """
    def setUp(self):
        self._lewis, self._ioc = get_running_lewis_and_ioc(
            "Ag53220A", DEVICE_PREFIX)
        self.ca = ChannelAccess(device_prefix=DEVICE_PREFIX)
        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")
class FermiChopperLifterTests(unittest.TestCase):
    """
    Tests for the fermi chopper lift.

    There isn't any logic to test in this IOC so this is really just a test that the DB records get loaded.
    """
    def setUp(self):
        self._ioc = IOCRegister.get_running("GALIL_01")
        self.ca = ChannelAccess(default_timeout=30)
        self.ca.assert_that_pv_exists("MOT:CHOPLIFT:STATUS", timeout=60)

    def test_WHEN_ioc_is_run_THEN_status_record_exists(self):
        # Simulated galil user variables are initialized to zero which maps to "Unknown".
        self.ca.assert_that_pv_is("MOT:CHOPLIFT:STATUS", "Unknown")
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 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 GalilmulTests(unittest.TestCase):
    """
    Tests for loading multiple motor controllers into a single IOC
    """
    def setUp(self):
        self._ioc = IOCRegister.get_running(DEVICE_PREFIX)
        self.assertIsNotNone(self._ioc)

        self.ca = ChannelAccess(device_prefix=None, default_timeout=20)

    def test_GIVEN_ioc_started_THEN_pvs_for_all_motors_exist(self):
        for controller in ("01", "02"):
            for motor in ["{:02d}".format(mtr) for mtr in range(1, 8 + 1)]:
                self.ca.assert_that_pv_exists("MOT:MTR{}{}".format(
                    controller, motor))

    def test_GIVEN_ioc_started_THEN_axes_for_all_motors_exist(self):
        for controller in (1, 2):
            for motor in range(1, 8 + 1):
                self.ca.assert_that_pv_exists("GALILMUL_01:{}:AXIS{}".format(
                    controller, motor))

    def test_GIVEN_axis_moved_THEN_other_axes_do_not_move(self):
        # This is to check that axes are independent, i.e. they're not accidentally using the same underlying driver

        # Set all motors to zero
        for controller in ("01", "02"):
            for motor in ["{:02d}".format(mtr) for mtr in range(1, 8 + 1)]:
                self.ca.set_pv_value("MOT:MTR{}{}".format(controller, motor),
                                     0)
                self.ca.assert_that_pv_is(
                    "MOT:MTR{}{}".format(controller, motor), 0)

        # Move motor 0101
        self.ca.set_pv_value("MOT:MTR0101", 20)
        self.ca.assert_that_pv_is("MOT:MTR0101", 20)

        # Check all other motors are still at zero
        for controller in ("01", "02"):
            for motor in ["{:02d}".format(mtr) for mtr in range(1, 8 + 1)]:
                if controller == "01" and motor == "01":
                    continue
                self.ca.assert_that_pv_is(
                    "MOT:MTR{}{}".format(controller, motor), 0)
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 Ag3631aTests(unittest.TestCase):
    """
    Tests for the AG3631A.
    """
    def setUp(self):
        self._ioc = IOCRegister.get_running(DEVICE_PREFIX)

        self.ca = ChannelAccess(20, DEVICE_PREFIX)
        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_GIVEN_voltage_set_WHEN_read_THEN_voltage_is_as_set(self):
        set_voltage = 123.456
        self.ca.set_pv_value("VOLT:SP", set_voltage)
        self.ca.assert_that_pv_is("VOLT:SP", set_voltage)

        self.ca.assert_that_pv_is("VOLT", set_voltage)
class CAENv895Tests(unittest.TestCase):
    """
    Tests for CAENv895
    """
    def setUp(self):
        self.ca = ChannelAccess(default_timeout=30,
                                device_prefix=DEVICE_PREFIX)
        self.ca_np = ChannelAccess(
            default_timeout=30)  # no device name prefix in PV
        self._ioc = IOCRegister.get_running(DEVICE_PREFIX)
        self.ca.assert_that_pv_exists("CR0:BOARD_ID")

    def test_GIVEN_ioc_WHEN_startup_complete_THEN_defaults_loaded(self):
        self.ca.assert_that_pv_is("CR0:CRATE", 0)
        self.ca.assert_that_pv_is("CR0:BOARD_ID", 0)
        # check values from configmenu config "ioctestdefaults" file loaded in pre_ioc_launch_hook()
        self.ca.assert_that_pv_is("CR0:C0:CH3:THOLD:SP", 75)
        self.ca.assert_that_pv_is("CR0:C1:CH0:ENABLE:SP", "YES")
        self.ca_np.assert_that_pv_is(
            "AS:{}:vmeconfigMenu:currName".format(DEVICE_PREFIX),
            "ioctestdefaults")
        self.ca_np.assert_that_pv_is(
            "AS:{}:vmeconfigMenu:status".format(DEVICE_PREFIX), "Success")
        self.ca_np.assert_that_pv_is(
            "AS:{}:vmeconfigMenu:busy".format(DEVICE_PREFIX), "Done")

    def test_GIVEN_output_widths_WHEN_output_widths_set_THEN_width_read_back(
            self):
        # GIVEN
        width_0_to_7 = 10
        width_8_to_15 = 13
        # WHEN
        self.ca.set_pv_value("CR0:C0:OUT:WIDTH:0_TO_7:SP", width_0_to_7)
        self.ca.set_pv_value("CR0:C0:OUT:WIDTH:8_TO_15:SP", width_8_to_15)
        # THEN
        self.ca.assert_that_pv_is("SIM:CR0:C0:OUT:WIDTH:0_TO_7", width_0_to_7)
        self.ca.assert_that_pv_is("SIM:CR0:C0:OUT:WIDTH:8_TO_15",
                                  width_8_to_15)
class EgxcolimTests(unittest.TestCase):
    """
    Tests for the egxcolim IOC.
    """

    directions = ["NORTH", "SOUTH"]
    axes = ["X"]

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

        self.ca = ChannelAccess(20, device_prefix=DEVICE_PREFIX)
        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_setpoint_is_set_THEN_readback_updates(self):
        for direction in self.directions:
            for axis in self.axes:
                self.ca.assert_setting_setpoint_sets_readback(
                    123, "{direction}:{axis}".format(direction=direction,
                                                     axis=axis))
예제 #17
0
class Knrk6Tests(unittest.TestCase):
    """
    Tests for the Knrk6 IOC.
    """
    def setUp(self):
        self._lewis, self._ioc = get_running_lewis_and_ioc(
            DEVICE_NAME, DEVICE_PREFIX)
        self.ca = ChannelAccess(device_prefix=DEVICE_PREFIX)
        self.ca.assert_that_pv_exists("POSITION", timeout=30)
        self._lewis.backdoor_run_function_on_device("reset")

    def test_GIVEN_home_position_THEN_home_position_returned(self):
        expected_value = 1
        self.ca.set_pv_value("POSITION:SP", expected_value)

        self.ca.assert_that_pv_is("POSITION", expected_value, timeout=5)

    def test_GIVEN_set_position_THEN_moved_to_correct_position(self):
        expected_value = 6
        self.ca.set_pv_value("POSITION:SP", expected_value)

        self.ca.assert_that_pv_is("POSITION", expected_value, timeout=5)

    @skip_if_recsim("Unable to use lewis backdoor in RECSIM")
    def test_GIVEN_device_not_connected_WHEN_get_error_THEN_alarm(self):
        self._lewis.backdoor_set_on_device('connected', False)
        self.ca.assert_that_pv_alarm_is('POSITION',
                                        ChannelAccess.Alarms.INVALID,
                                        timeout=5)

    @skip_if_recsim("Unable to use lewis backdoor in RECSIM")
    def test_GIVEN_an_input_error_WHEN_open_file_THEN_error_str_returned(self):
        self._lewis.backdoor_set_on_device("input_correct", False)
        expected_value = "Position change slow"
        self.ca.set_pv_value("POSITION:SP", 5)

        self.ca.assert_that_pv_is("ERROR", expected_value, timeout=5)
예제 #18
0
class VerticalJawsTests(unittest.TestCase):
    def set_motor_speeds(self):
        self.ca.set_pv_value(UNDERLYING_MTR_NORTH + ".VMAX", 20)
        self.ca.set_pv_value(UNDERLYING_MTR_NORTH + ".VELO", 20)
        self.ca.set_pv_value(UNDERLYING_MTR_SOUTH + ".VMAX", 20)
        self.ca.set_pv_value(UNDERLYING_MTR_SOUTH + ".VELO", 20)

    """
    Tests for vertical jaws
    """

    def setUp(self):
        self._ioc = IOCRegister.get_running("vertical_jaws")
        self.ca = ChannelAccess(default_timeout=30)

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

    @parameterized.expand(parameterized_list(TEST_POSITIONS))
    def test_WHEN_south_jaw_setpoint_changed_THEN_south_jaw_moves(
            self, _, value):
        self.ca.assert_setting_setpoint_sets_readback(value, MOTOR_S,
                                                      MOTOR_S_SP)

    @parameterized.expand(parameterized_list(TEST_POSITIONS))
    def test_WHEN_north_jaw_setpoint_changed_THEN_north_jaw_moves(
            self, _, value):
        self.ca.assert_setting_setpoint_sets_readback(value, MOTOR_N,
                                                      MOTOR_N_SP)

    def test_GIVEN_jaws_closed_at_centre_WHEN_gap_opened_THEN_north_and_south_jaws_move(
            self):
        # GIVEN
        self.ca.assert_that_pv_is("MOT:JAWS1:VGAP", 0)
        self.ca.assert_that_pv_is("MOT:JAWS1:VCENT", 0)
        # WHEN
        self.ca.set_pv_value("MOT:JAWS1:VGAP:SP", 10)
        # THEN
        self.ca.assert_that_pv_is(MOTOR_S, -5)
        self.ca.assert_that_pv_is(MOTOR_N, 5)

    def test_GIVEN_jaws_open_WHEN_jaws_closed_THEN_jaws_close(self):
        # GIVEN
        self.ca.set_pv_value("MOT:JAWS1:VGAP:SP", 10)
        # WHEN
        self.ca.set_pv_value("MOT:JAWS1:VGAP:SP", 0)
        # THEN
        self.ca.assert_that_pv_is("MOT:JAWS1:VGAP", 0)
예제 #19
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)
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)
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 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)
예제 #23
0
class KepcoTests(object):
    """
    Tests for the KEPCO.
    """

    def setUp(self):
        self._lewis, self._ioc = get_running_lewis_and_ioc("kepco", DEVICE_PREFIX)
        self.ca = ChannelAccess(default_timeout=30, device_prefix=DEVICE_PREFIX)
        self._lewis.backdoor_run_function_on_device("reset")
        self.ca.assert_that_pv_exists("VOLTAGE", timeout=30)
        reset_calibration_file(self.ca, "default_calib.dat")

    def _write_voltage(self, expected_voltage):
        self._lewis.backdoor_set_on_device("voltage", expected_voltage)
        self._ioc.set_simulated_value("SIM:VOLTAGE", expected_voltage)

    def _write_current(self, expected_current):
        self._lewis.backdoor_set_on_device("current", expected_current)
        self._ioc.set_simulated_value("SIM:CURRENT", expected_current)

    def _set_IDN(self, expected_idn_no_firmware, expected_firmware):
        self._lewis.backdoor_set_on_device("idn_no_firmware", expected_idn_no_firmware)
        self._lewis.backdoor_set_on_device("firmware", expected_firmware)
        expected_idn = "{}{}".format(expected_idn_no_firmware, str(expected_firmware))[:39]  # EPICS limited to 40 chars
        self._ioc.set_simulated_value("SIM:IDN", expected_idn)
        self._ioc.set_simulated_value("SIM:FIRMWARE", str(expected_firmware))
        # Both firmware and IDN are passive so must be updated
        self.ca.process_pv("FIRMWARE")
        self.ca.process_pv("IDN")
        return expected_idn

    def _set_output_mode(self, expected_output_mode):
        self._lewis.backdoor_set_on_device("output_mode", expected_output_mode)
        self._ioc.set_simulated_value("SIM:OUTPUTMODE", expected_output_mode)

    def _set_output_status(self, expected_output_status):
        self._lewis.backdoor_set_on_device("output_status", expected_output_status)

    def test_GIVEN_voltage_set_WHEN_read_THEN_voltage_is_as_expected(self):
        expected_voltage = 1.2
        self._write_voltage(expected_voltage)
        self.ca.assert_that_pv_is("VOLTAGE", expected_voltage)

    def test_GIVEN_current_set_WHEN_read_THEN_current_is_as_expected(self):
        expected_current = 1.5
        self._write_current(expected_current)
        self.ca.assert_that_pv_is("CURRENT", expected_current)

    def test_GIVEN_setpoint_voltage_set_WHEN_read_THEN_setpoint_voltage_is_as_expected(self):
        # Get current Voltage
        current_voltage = self.ca.get_pv_value("VOLTAGE")
        # Set new Voltage via SP
        self.ca.set_pv_value("VOLTAGE:SP", current_voltage + 5)
        # Check SP RBV matches new current
        self.ca.assert_that_pv_is("VOLTAGE:SP:RBV", current_voltage + 5)

    @parameterized.expand(parameterized_list([-5.1, 7.8]))
    def test_GIVEN_setpoint_current_set_WHEN_read_THEN_setpoint_current_is_as_expected(self, _, expected_current):
        self.ca.set_pv_value("CURRENT:SP", expected_current)
        # Check SP RBV matches new current
        self.ca.assert_that_pv_is("CURRENT:SP:RBV", expected_current)

    def test_GIVEN_output_mode_set_WHEN_read_THEN_output_mode_is_as_expected(self):
        expected_output_mode_flag = UnitFlags.CURRENT
        expected_output_mode_str = OutputMode.CURRENT
        self._set_output_mode(expected_output_mode_flag)
        # Check OUTPUT MODE matches new OUTPUT MODE
        self.ca.assert_that_pv_is("OUTPUTMODE", expected_output_mode_str)

    def test_GIVEN_output_status_set_WHEN_read_THEN_output_STATUS_is_as_expected(self):
        expected_output_status_flag = UnitFlags.ON
        expected_output_status_str = Status.ON
        self.ca.set_pv_value("OUTPUTSTATUS:SP", expected_output_status_flag)
        self.ca.assert_that_pv_is("OUTPUTSTATUS:SP:RBV", expected_output_status_str)

    @parameterized.expand(parameterized_list(IDN_LIST))
    def test_GIVEN_idn_set_WHEN_read_THEN_idn_is_as_expected(self, _, idn_no_firmware, firmware):
        expected_idn = self._set_IDN(idn_no_firmware, firmware)
        self.ca.process_pv("IDN")
        self.ca.assert_that_pv_is("IDN", expected_idn)

    @skip_if_recsim("In rec sim you can not diconnect the device")
    def test_GIVEN_diconnected_WHEN_read_THEN_alarms_on_readbacks(self):
        self._lewis.backdoor_set_on_device("connected", False)

        self.ca.assert_that_pv_alarm_is("OUTPUTMODE", self.ca.Alarms.INVALID)
        self.ca.assert_that_pv_alarm_is("CURRENT", self.ca.Alarms.INVALID)
        self.ca.assert_that_pv_alarm_is("VOLTAGE", self.ca.Alarms.INVALID)

    def _test_ramp_to_target(self, start_current, target_current, ramp_rate, step_number, wait_between_changes):
        self._write_current(start_current)
        self.ca.set_pv_value("CURRENT:SP", start_current)
        self.ca.assert_that_pv_is("CURRENT:SP:RBV", start_current)
        self.ca.set_pv_value("RAMP:RATE:SP", ramp_rate)
        self.ca.set_pv_value("RAMP:STEPS:SP", step_number)
        self.ca.set_pv_value("RAMPON:SP", "ON")
        self.ca.set_pv_value("CURRENT:SP", target_current, sleep_after_set=0.0)
        if start_current < target_current:
            self.ca.assert_that_pv_value_is_increasing("CURRENT:SP:RBV", wait=wait_between_changes)
        else:
            self.ca.assert_that_pv_value_is_decreasing("CURRENT:SP:RBV", wait=wait_between_changes)
        self.ca.assert_that_pv_is("RAMPING", "YES")
        # Device stops ramping when it gets to target
        self.ca.assert_that_pv_is("CURRENT:SP:RBV", target_current, timeout=40)
        self._write_current(target_current)
        self.ca.assert_that_pv_is("RAMPING", "NO")
        self.ca.assert_that_pv_value_is_unchanged("CURRENT:SP:RBV", wait=wait_between_changes)
        self.ca.set_pv_value("RAMPON:SP", "OFF")

    def test_GIVEN_rampon_WHEN_target_set_THEN_current_ramps_to_target(self):
        self._test_ramp_to_target(1, 2, 2, 20, 7)

    def test_GIVEN_rampon_WHEN_target_set_with_different_step_rate_THEN_current_ramps_to_target_more_finely(self):
        self._test_ramp_to_target(4, 3, 2, 60, 2)

    @parameterized.expand(parameterized_list(IDN_LIST))
    def test_GIVEN_idn_set_AND_firmware_set_THEN_firmware_pv_correct(self, _, idn_no_firmware, firmware):
        self._set_IDN(idn_no_firmware, firmware)
        self.ca.process_pv("FIRMWARE")
        self.ca.assert_that_pv_is("FIRMWARE", firmware)

    @parameterized.expand(parameterized_list([
        ("default_calib.dat", 100, 100),
        ("field_double_amps.dat", 100, 50),
    ]))
    @skip_if_recsim("Calibration lookup does not work in recsim")
    def test_GIVEN_calibration_WHEN_field_set_THEN_current_as_expected(self, _, calibration_file, field, expected_current):
        with use_calibration_file(self.ca, calibration_file, "default_calib.dat"):
            self.ca.set_pv_value("FIELD:SP", field)
            self.ca.assert_that_pv_is("FIELD:SP:RBV", field)
            self.ca.assert_that_pv_is("CURRENT:SP", expected_current)
            self.ca.assert_that_pv_is("CURRENT:SP:RBV", expected_current)

    @parameterized.expand(parameterized_list([
        ("default_calib.dat", 100, 100),
        ("field_double_amps.dat", 100, 200),
    ]))
    @skip_if_recsim("Calibration lookup does not work in recsim")
    def test_GIVEN_calibration_WHEN_current_set_THEN_field_as_expected(self, _, calibration_file, current, expected_field):
        with use_calibration_file(self.ca, calibration_file, "default_calib.dat"):
            self._write_current(current)
            self.ca.assert_that_pv_is("CURRENT", current)
            self.ca.assert_that_pv_is("FIELD", expected_field)

    @skip_if_recsim("Lewis not available in recsim")
    def test_WHEN_sending_setpoint_THEN_only_one_setpoint_sent(self):
        self._lewis.backdoor_set_and_assert_set("current_set_count", 0)
        self.ca.set_pv_value("CURRENT:SP", 100)
        self._lewis.assert_that_emulator_value_is("current_set_count", 1)

        # Wait a short time and make sure count is not being incremented again later.
        time.sleep(5)
        self._lewis.assert_that_emulator_value_is("current_set_count", 1)
class 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 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")
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")
예제 #27
0
class Jsco4180Tests(unittest.TestCase):
    """
    Tests for the Jsco4180 IOC.
    """

    def setUp(self):
        self._lewis, self._ioc = get_running_lewis_and_ioc(DEVICE_NAME, DEVICE_PREFIX)
        self.ca = ChannelAccess(device_prefix=DEVICE_PREFIX, default_timeout=30)
        for pv in required_pvs:
            self.ca.assert_that_pv_exists(pv, timeout=30)
        self._lewis.backdoor_run_function_on_device("reset")

    @skip_if_recsim("Unable to use lewis backdoor in RECSIM")
    def test_GIVEN_wrong_component_on_device_WHEN_running_THEN_retry_run_and_updates_component(self):
        expected_value_A = 30
        expected_value_B = 15
        expected_value_C = 55

        self.ca.set_pv_value("COMP:A:SP", expected_value_A)
        self.ca.set_pv_value("COMP:B:SP", expected_value_B)
        self.ca.set_pv_value("COMP:C:SP", expected_value_C)

        self.ca.set_pv_value("START:SP", 1)

        sleep(10)
        # Setting an incorrect component on the device will result in the state machine attempting
        # to rerun the pump and reset components.
        self._lewis.backdoor_set_on_device("component_A", 25)
        self._lewis.backdoor_set_on_device("component_B", 10)
        self._lewis.backdoor_set_on_device("component_C", 14)

        self.ca.assert_that_pv_is("COMP:A", expected_value_A, timeout=30)
        self.ca.assert_that_pv_is("COMP:B", expected_value_B, timeout=30)
        self.ca.assert_that_pv_is("COMP:C", expected_value_C, timeout=30)

    # there was a previous problem where if setpoint and readback differed a sleep and resend was started,
    # but the old state machine did not look to see if a new sp was issued while it was asleep and so then
    # resent the old out of date SP
    @unstable_test(max_retries=2, wait_between_runs=60)
    @skip_if_recsim("Unable to use lewis backdoor in RECSIM")
    def test_GIVEN_wrong_component_on_device_WHEN_send_new_sp_THEN_state_machine_aborts_resend(self):
        value = 50
        self.ca.set_pv_value("COMP:A:SP", value)
        self.ca.set_pv_value("COMP:B:SP", value)
        self.ca.set_pv_value("START:SP", 1)
        self.ca.assert_that_pv_is("STATUS", "Pumping", timeout=5)
        self.ca.assert_that_pv_is("COMP:A", value, timeout=30)
        self.ca.assert_that_pv_is("COMP:B", value, timeout=30)

        # Setting an incorrect component on the device will result in the state machine attempting
        # to rerun the pump and reset components after a delay
        initial_delay = self.ca.get_pv_value("ERROR:DELAY")  # delay before state machine reset
        delay = 30  # Increase delay to avoid race conditions
        self.ca.set_pv_value("ERROR:DELAY", delay)
        try:
            with self.ca.assert_pv_not_processed("RESET:SP"):
                self._lewis.backdoor_set_on_device("component_A", value - 5)
                self.ca.assert_that_pv_is("COMP:A", value - 5, timeout=5)
                sleep(delay / 2.0)

                # however if we change setpoint, the loop should start again
                self._lewis.backdoor_set_on_device("component_A", value - 5)
                self.ca.set_pv_value("COMP:A:SP", value - 10)
                self.ca.set_pv_value("COMP:B:SP", value + 10)
                # reset should not have happened yet
                self.ca.assert_that_pv_is("COMP:A", value - 5, timeout=delay / 2.0)
                self.ca.assert_that_pv_value_is_unchanged("COMP:A", wait=delay / 2.0)

            # Reset should now happen within a further timeout/2 seconds (but give it longer to avoid races)
            with self.ca.assert_pv_processed("RESET:SP"):
                self.ca.assert_that_pv_is("COMP:A", value - 10, timeout=delay * 2)
        finally:
            # Put error delay back to it's initial value
            self.ca.set_pv_value("ERROR:DELAY", initial_delay)

    @skip_if_recsim("Unable to use lewis backdoor in RECSIM")
    def test_GIVEN_wrong_component_on_device_WHEN_running_continuous_THEN_retry_run_and_updates_component_in_correct_mode(
            self):
        value = 50
        expected_value = "Pumping"
        self.ca.set_pv_value("COMP:A:SP", value)
        self.ca.set_pv_value("COMP:B:SP", value)

        self.ca.set_pv_value("START:SP", 1)

        # Give the device some time running in a good state
        sleep(10)
        # Sabotage! - Setting an incorrect component on the device will result in the state machine attempting
        # to rerun the pump and reset components.
        self._lewis.backdoor_set_on_device("component_A", 33)

        self.ca.assert_that_pv_is("STATUS", expected_value, timeout=30)

    @skip_if_recsim("Unable to use lewis backdoor in RECSIM")
    def test_GIVEN_wrong_component_on_device_WHEN_running_timed_THEN_retry_run_and_updates_component_in_correct_mode(
            self):
        value = 50
        expected_value = "Pumping"
        self.ca.set_pv_value("COMP:A:SP", value)
        self.ca.set_pv_value("COMP:B:SP", value)
        self.ca.set_pv_value("TIME:RUN:SP", 100)
        self.ca.set_pv_value("PUMP_FOR_TIME:SP", 1)

        # Give the device some time running in a good state
        sleep(10)
        # Sabotage! - Setting an incorrect component on the device will result in the state machine attempting
        # to rerun the pump and reset components.
        self._lewis.backdoor_set_on_device("component_A", 33)

        self.ca.assert_that_pv_is("STATUS", expected_value, timeout=30)

    @skip_if_recsim("Flowrate device logic not supported in RECSIM")
    def test_GIVEN_an_ioc_WHEN_set_flowrate_THEN_flowrate_setpoint_is_correct(self):

        error_delay = float(self.ca.get_pv_value("ERROR:DELAY"))
        sleep(2 * error_delay)  # To make sure we're not in the middle of the error-checking state machine

        expected_value = 1.000
        self.ca.set_pv_value("FLOWRATE:SP", expected_value)

        self.ca.assert_that_pv_is("FLOWRATE:SP:RBV", expected_value)

        self.ca.set_pv_value("TIME:RUN:SP", 100)
        self.ca.set_pv_value("START:SP", "Start")

        self.ca.assert_that_pv_is("FLOWRATE", expected_value)

    @skip_if_recsim("LeWIS backdoor not supported in RECSIM")
    def test_GIVEN_an_ioc_WHEN_set_flowrate_and_pump_volume_THEN_ioc_uses_rbv_for_calculation_of_remaining_time(self):
        expected_sp_value = 1.000
        expected_rbv_value = 2.000
        pump_for_volume = 2
        expected_time_value = (pump_for_volume / expected_rbv_value) * 60

        error_delay = float(self.ca.get_pv_value("ERROR:DELAY"))
        sleep(2 * error_delay)  # To make sure we're not in the middle of the error-checking state machine

        # 1. set invalid flowrate setpoint (FLOWRATE:SP)
        self.ca.set_pv_value("FLOWRATE:SP", expected_sp_value)
        self.ca.assert_that_pv_is("FLOWRATE:SP:RBV", expected_sp_value)

        # 2. set valid hardware flowrate (FLOWRATE:SP:RBV) via backdoor command
        self._lewis.backdoor_set_on_device("flowrate_rbv", expected_rbv_value)
        self.ca.assert_that_pv_is("FLOWRATE:SP:RBV", expected_rbv_value)

        # 3. set volume setpoint and start pump
        self.ca.set_pv_value("TIME:VOL:SP", pump_for_volume)
        self.ca.set_pv_value("START:SP", "Start")

        # 4. check calculated time is based on flowrate setpoint readback (:SP:RBV rather than :SP)
        self.ca.assert_that_pv_is("TIME:VOL:CALCRUN", expected_time_value)

    @skip_if_recsim("LeWIS backdoor not supported in RECSIM")
    def test_GIVEN_an_ioc_WHEN_set_flowrate_and_pump_time_THEN_ioc_uses_rbv_for_calculation_of_remaining_volume(self):
        expected_sp_value = 1.000
        expected_rbv_value = 2.000
        pump_for_time = 120
        expected_volume_value = (pump_for_time * expected_rbv_value) / 60

        error_delay = float(self.ca.get_pv_value("ERROR:DELAY"))
        sleep(2 * error_delay)  # To make sure we're not in the middle of the error-checking state machine

        # 1. set invalid flowrate setpoint (FLOWRATE:SP)
        self.ca.set_pv_value("FLOWRATE:SP", expected_sp_value)
        self.ca.assert_that_pv_is("FLOWRATE:SP:RBV", expected_sp_value)

        # 2. set valid hardware flowrate (FLOWRATE:SP:RBV) via backdoor command
        self._lewis.backdoor_set_on_device("flowrate_rbv", expected_rbv_value)
        self.ca.assert_that_pv_is("FLOWRATE:SP:RBV", expected_rbv_value)

        # 3. set time setpoint and start pump
        self.ca.set_pv_value("TIME:RUN:SP", pump_for_time)
        self.ca.set_pv_value("START:SP", "Start")

        # 4. check calculated volume is based on flowrate setpoint readback (:SP:RBV rather than :SP)
        self.ca.assert_that_pv_is("TIME:RUN:CALCVOL", expected_volume_value)

    # test to check that the IOC updates the flowrate RBV quickly enough
    # for the remaining volume calculation to be valid.  simulates operation of a script.
    def test_GIVEN_an_ioc_WHEN_set_flowrate_and_immediately_set_pump_to_start_THEN_ioc_updates_rbv_for_calculation_of_remaining_volume(
            self):
        expected_sp_value = 2.000
        script_sp_value = 3.000
        pump_for_time = 120

        # 1. initialize flowrate
        self.ca.set_pv_value("FLOWRATE:SP", expected_sp_value)
        self.ca.assert_that_pv_is("FLOWRATE:SP:RBV", expected_sp_value, timeout=5)

        # 2. set new flowrate and immediately set pump to run, to simulate script
        self.ca.set_pv_value("FLOWRATE:SP", script_sp_value)
        self.ca.set_pv_value("TIME:RUN:SP", pump_for_time)
        self.ca.set_pv_value("START:SP", "Start")

        # 3. calculate remaining volume
        expected_volume_value = (pump_for_time * self.ca.get_pv_value("FLOWRATE:SP:RBV")) / 60

        # 4. check ioc calculation is as expected
        self.ca.assert_that_pv_is("TIME:RUN:CALCVOL", expected_volume_value)

    @skip_if_recsim("Lewis device logic not supported in RECSIM")
    def test_GIVEN_an_ioc_WHEN_set_maximum_pressure_limit_THEN_maximum_pressure_limit_is_correct(self):
        expected_value = 200
        self.ca.assert_setting_setpoint_sets_readback(expected_value, "PRESSURE:MAX")

    @skip_if_recsim("Lewis device logic not supported in RECSIM")
    def test_GIVEN_an_ioc_WHEN_set_minimum_pressure_limit_THEN_minimum_pressure_limit_is_correct(self):
        expected_value = 100
        self.ca.set_pv_value("PRESSURE:MIN:SP", expected_value)
        self.ca.assert_setting_setpoint_sets_readback(expected_value, "PRESSURE:MIN")

    @skip_if_recsim("Lewis device logic not supported in RECSIM")
    def test_GIVEN_an_ioc_WHEN_continuous_pump_set_THEN_pump_on(self):
        self.ca.set_pv_value("START:SP", 1)

        self.ca.assert_that_pv_is("STATUS", "Pumping")

    @skip_if_recsim("Lewis device logic not supported in RECSIM")
    def test_GIVEN_an_ioc_WHEN_timed_pump_set_THEN_timed_pump_on(self):
        # Set a run time for a timed run
        self.ca.set_pv_value("TIME:RUN:SP", 10000)
        self.ca.set_pv_value("PUMP_FOR_TIME:SP", 1)

        self.ca.assert_that_pv_is("STATUS", "Pumping")

    @skip_if_recsim("Unable to use lewis backdoor in RECSIM")
    def test_GIVEN_an_ioc_WHEN_get_current_pressure_THEN_current_pressure_returned(self):
        expected_value = 300
        self._lewis.backdoor_set_on_device("pressure", expected_value)

        self.ca.assert_that_pv_is("PRESSURE", expected_value)

    @parameterized.expand([
        ("component_{}".format(suffix), suffix) for suffix in ["A", "B", "C", "D"]
    ])
    @skip_if_recsim("Reliant on setUP lewis backdoor call")
    def test_GIVEN_an_ioc_WHEN_get_component_THEN_correct_component_returned(self, component, suffix):
        expected_value = 10.0
        self._lewis.backdoor_set_on_device(component, expected_value)

        self.ca.assert_that_pv_is("COMP:{}".format(suffix), expected_value)

    @parameterized.expand([
        ("COMP:{}".format(suffix), suffix) for suffix in ["A", "B", "C"]
    ])
    @skip_if_recsim("Reliant on setUP lewis backdoor call")
    def test_GIVEN_an_ioc_WHEN_set_component_THEN_correct_component_set(self, component, suffix):
        expected_value = 100.0
        self.ca.set_pv_value("COMP:{}:SP".format(suffix), expected_value)
        if component == "COMP:A":
            self.ca.set_pv_value("COMP:B:SP", 0)
            self.ca.set_pv_value("COMP:C:SP", 0)
        elif component == "COMP:B":
            self.ca.set_pv_value("COMP:A:SP", 0)
            self.ca.set_pv_value("COMP:C:SP", 0)
        elif component == "COMP:C":
            self.ca.set_pv_value("COMP:A:SP", 0)
            self.ca.set_pv_value("COMP:B:SP", 0)
        self.ca.set_pv_value("PUMP_FOR_TIME:SP", "Start")

        self.ca.assert_that_pv_is(component, expected_value)

    def test_GIVEN_ioc_initial_state_WHEN_get_error_THEN_error_returned(self):
        expected_value = "No error"

        self.ca.assert_that_pv_is("ERROR", expected_value)

    @skip_if_recsim("Unable to use lewis backdoor in RECSIM")
    def test_GIVEN_ioc_in_hardware_error_state_WHEN_get_error_THEN_hardware_error_returned(self):
        expected_value = "Hardware error"
        self._lewis.backdoor_set_on_device("error", ERROR_STATE_HARDWARE_FAULT)

        self.ca.assert_that_pv_is("ERROR", expected_value)

    @skip_if_recsim("Unable to use lewis backdoor in RECSIM")
    def test_GIVEN_ioc_in_error_state_WHEN_reset_error_THEN_error_reset(self):
        expected_value = "No error"
        self._lewis.backdoor_set_on_device("error", ERROR_STATE_NO_ERROR)
        self.ca.set_pv_value("ERROR:SP", "Reset")

        self.ca.assert_that_pv_is("ERROR", expected_value)

    @skip_if_recsim("Unable to use lewis backdoor in RECSIM")
    def test_GIVEN_ioc_in_error_state_WHEN_reset_error_THEN_error_reset(self):
        expected_value = "No error"
        self._lewis.backdoor_set_on_device("error", ERROR_STATE_HARDWARE_FAULT)

        self.ca.assert_that_pv_is("ERROR", expected_value)

    @skip_if_recsim("Unable to use lewis backdoor in RECSIM")
    def test_GIVEN_device_not_connected_WHEN_get_error_THEN_alarm(self):
        self._lewis.backdoor_set_on_device('connected', False)

        self.ca.assert_that_pv_alarm_is('ERROR:SP', ChannelAccess.Alarms.INVALID)

    @skip_if_recsim("Reliant on setUP lewis backdoor call")
    def test_GIVEN_timed_pump_WHEN_get_program_runtime_THEN_program_runtime_increments(self):
        self.ca.set_pv_value("TIME:RUN:SP", 10000)
        self.ca.set_pv_value("PUMP_FOR_TIME:SP", 1)

        self.ca.assert_that_pv_value_is_increasing("TIME", wait=2)

    @skip_if_recsim("Lewis device logic not supported in RECSIM")
    def test_GIVEN_timed_pump_WHEN_set_constant_pump_THEN_state_updated_to_constant_pump(self):
        # Set a run time for a timed run
        self.ca.set_pv_value("TIME:RUN:SP", 10000)
        self.ca.process_pv("PUMP_FOR_TIME:SP")
        expected_value = "Pumping"
        self.ca.assert_that_pv_is("STATUS", expected_value)

        self.ca.process_pv("START:SP")
        expected_value = "Pumping"
        self.ca.assert_that_pv_is("STATUS", expected_value)

    @skip_if_recsim("Lewis device logic not supported in RECSIM")
    def test_GIVEN_constant_pump_WHEN_set_timed_pump_THEN_state_updated_to_timed_pump(self):
        expected_value = "Pumping"

        self.ca.process_pv("START:SP")
        self.ca.assert_that_pv_is("STATUS", expected_value)

        # Set a run time for a timed run
        self.ca.set_pv_value("TIME:RUN:SP", 10000)
        self.ca.process_pv("PUMP_FOR_TIME:SP")
        self.ca.assert_that_pv_is("STATUS", expected_value)

    @skip_if_recsim("Lewis device logic not supported in RECSIM")
    def test_GIVEN_input_incorrect_WHEN_set_flowrate_THEN_trouble_message_returned(self):
        self._lewis.backdoor_set_on_device("input_correct", False)
        self.ca.set_pv_value("FLOWRATE:SP", 0.010)

        self.ca.assert_that_pv_is("ERROR:STR", "[Error:stack underflow]")

    @skip_if_recsim("Lewis device logic not supported in RECSIM")
    def test_GIVEN_command_seq_that_would_crash_pump_WHEN_command_seq_called_THEN_pump_crashes(self):
        self.ca.set_pv_value("_TEST_CRASH.PROC", 1)

        self.ca.assert_that_pv_alarm_is("COMP:A", ChannelAccess.Alarms.INVALID, timeout=30)

    @skip_if_recsim("Lewis device logic not supported in RECSIM")
    def test_GIVEN_pump_running_WHEN_set_file_number_command_called_THEN_program_is_busy_error(self):
        expected_value = "[Program is Busy]"
        self.ca.set_pv_value("START:SP", 1)
        self.ca.set_pv_value("FILE:SP", 0)

        self.ca.assert_that_pv_is("ERROR:STR", expected_value)

    @parameterized.expand([("low_set_time", 100, 1, 1),
                           ("high_set_time", 1000, 10, 1),
                           ("non_standard_set_time", 456, 5, 1)])
    @unstable_test(max_retries=5)
    @skip_if_recsim("Lewis device logic not supported in RECSIM")
    def test_GIVEN_pump_for_volume_WHEN_pumping_THEN_device_is_pumping_set_volume(self, _, time, volume, flowrate):
        # Set a target pump time a target pump volume. When we start a pump set volume run, then the remaining
        # time should be related to the target volume, and not the target time (that would be used for a pump for time).
        set_time = time
        set_volume = volume
        set_flowrate = flowrate
        expected_time = set_volume * set_flowrate * 60  # flow rate units = mL/min, so convert to seconds

        self.ca.set_pv_value("TIME:RUN:SP", set_time)
        self.ca.set_pv_value("TIME:VOL:SP", set_volume)
        self.ca.set_pv_value("FLOWRATE:SP", set_flowrate)

        self.ca.process_pv("PUMP_SET_VOLUME:SP")

        self.ca.assert_that_pv_is_within_range("TIME:REMAINING", min_value=expected_time - 20,
                                               max_value=expected_time + 20)
예제 #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)
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 RknpsTests(DanfysikCommon, unittest.TestCase):
    """
    Tests for the RIKEN Multidrop Danfysik Power Supplies.
    """
    def setUp(self):
        self._lewis, self._ioc = get_running_lewis_and_ioc("rknps", PREFIX)
        self.ca = ChannelAccess(device_prefix=PREFIX, default_timeout=60)
        self._lewis.backdoor_set_on_device("connected", True)

        self.current_readback_factor = 1000

        self.id_prefixes = [ID + ":" for ID in IDS]

        for id_prefix in self.id_prefixes:
            self.ca.assert_that_pv_exists("{}ADDRESS".format(id_prefix),
                                          timeout=30)

    def disconnect_device(self):
        """RIKEN PSU emulator disconnects slightly differently"""
        self._lewis.backdoor_set_on_device("connected", False)

    def set_voltage(self, voltage):
        self._lewis.backdoor_set_on_device("set_all_volt_values", voltage)

    def _activate_interlocks(self):
        """
        Activate both interlocks in the emulator.
        """
        self._lewis.backdoor_set_on_device("set_all_interlocks", True)

    def _deactivate_interlocks(self):
        """
        Deactivate both interlocks in the emulator.
        """
        self._lewis.backdoor_set_on_device("set_all_interlocks", False)

    @skip_if_recsim("Interlock statuses depend on emulator")
    def test_WHEN_interlocks_are_active_THEN_ilk_is_Interlocked(self):
        self._activate_interlocks()
        for IDN in IDS:
            self.ca.assert_that_pv_is("{0}:ILK".format(IDN), HAS_TRIPPED[True])

    @skip_if_recsim("Interlock statuses depend on emulator")
    def test_WHEN_interlocks_are_inactive_THEN_ilk_is_not_Interlocked(self):
        self._deactivate_interlocks()
        for IDN in IDS:
            self.ca.assert_that_pv_is("{0}:ILK".format(IDN),
                                      HAS_TRIPPED[False])

    @skip_if_recsim(
        "In rec sim this test fails as the changes are not propagated to all appropriate PVs"
    )
    def test_GIVEN_a_positive_value_and_emulator_in_use_WHEN_current_is_set_THEN_values_are_as_expected(
            self):
        expected_value = 480
        for IDN in IDS:
            self.ca.set_pv_value("{0}:CURR:SP".format(IDN), expected_value)
            self.ca.assert_that_pv_is("{0}:CURR".format(IDN), expected_value)
            self.ca.assert_that_pv_is("{0}:RA".format(IDN), expected_value)
            self.ca.assert_that_pv_is("{0}:POL".format(IDN), "+")

    @skip_if_recsim(
        "In rec sim this test fails as the changes are not propagated to all appropriate PVs"
    )
    def test_GIVEN_a_negative_value_and_emulator_in_use_WHEN_current_is_set_THEN_values_are_as_expected(
            self):
        expected_value = -123
        for IDN in IDS:
            self.ca.set_pv_value("{0}:CURR:SP".format(IDN), expected_value)
            self.ca.assert_that_pv_is("{0}:CURR".format(IDN), expected_value)
            self.ca.assert_that_pv_is("{0}:RA".format(IDN),
                                      abs(expected_value))
            self.ca.assert_that_pv_is("{0}:POL".format(IDN), "-")

    @skip_if_devsim("In dev sim this test fails as the emulator "
                    "handles the difference in values between write and read")
    def test_GIVEN_a_negative_value_and_emulator_not_in_use_WHEN_current_is_set_THEN_values_are_as_expected(
            self):
        set_value = -123
        return_value = set_value * 1000
        for IDN in IDS:
            self.ca.set_pv_value("{0}:CURR:SP".format(IDN), set_value)
            self.ca.assert_that_pv_is("{0}:CURR".format(IDN), return_value)
            self.ca.assert_that_pv_is("{0}:RA".format(IDN), return_value)

    @skip_if_recsim("Power updates through protocol redirection")
    def test_GIVEN_rb3_status_changes_THEN_rb3_banner_pv_updates_correctly(
            self):
        if "RB3" not in IDS:
            self.fail("Didn't find RB3 for test.")

        for powered_on in (True, False):
            self.ca.set_pv_value("RB3:POWER:SP", powered_on)
            self.ca.assert_that_pv_is(
                "RB3:BANNER", "on; beam to ports 1,2"
                if powered_on else "off; ports 1,2 safe")

    @skip_if_recsim("Power updates through protocol redirection")
    def test_GIVEN_rb4_status_changes_THEN_rb4_banner_pv_updates_correctly(
            self):
        if "RB4" not in IDS:
            self.fail("Didn't find RB4 for test.")

        for powered_on in (True, False):
            self.ca.set_pv_value("RB4:POWER:SP", powered_on)
            self.ca.assert_that_pv_is(
                "RB4:BANNER", "on; beam to ports 3,4"
                if powered_on else "off; ports 3,4 safe")

    @parameterized.expand(INTERLOCKS)
    @skip_if_recsim("Test requires emulator to change interlock state")
    def test_GIVEN_interlock_status_WHEN_read_all_status_THEN_status_is_as_expected(
            self, interlock):
        for boolean_value, expected_value in HAS_TRIPPED.items():
            for IDN, ADDR in zip(IDS, PSU_ADDRESSES):
                # GIVEN
                self._lewis.backdoor_run_function_on_device(
                    "set_{0}".format(interlock), (boolean_value, ADDR))

                # THEN
                self.ca.assert_that_pv_is("{0}:ILK:{1}".format(IDN, interlock),
                                          expected_value)
                self.ca.assert_that_pv_alarm_is(
                    "{0}:ILK:{1}".format(IDN, interlock), self.ca.Alarms.NONE)

    @parameterized.expand(INTERLOCKS)
    @skip_if_recsim("Test requires emulator")
    def test_GIVEN_individual_interlock_read_WHEN_device_not_connected_THEN_interlock_PV_in_alarm(
            self, interlock):
        # WHEN
        self._lewis.backdoor_set_on_device("connected", False)

        # THEN
        for IDN, ADDR in zip(IDS, PSU_ADDRESSES):
            self.ca.assert_that_pv_alarm_is(
                "{0}:ILK:{1}".format(IDN, interlock), self.ca.Alarms.INVALID)

    @parameterized.expand(
        parameterized_list([
            ("FAULT STATE", 0, 0),
            ("BEND 1", 1, 0),
            ("BEND 2", 0, 1),
            ("SEPTUM", 1, 1),
        ]))
    @skip_if_devsim("DAQ does not exist in devsim")
    def test_GIVEN_mock_DAQ_inputs_THEN_RB2_mode_is_correct(
            self, _, state, val1, val2):
        self.ca.set_pv_value("DAQ:R04:DATA:SIM", val1)
        self.ca.set_pv_value("DAQ:R05:DATA:SIM", val2)
        self.ca.assert_that_pv_is("RB2:MODE", state)

    @parameterized.expand(
        parameterized_list([
            ("FAULT (LOW)", 0, 0),
            ("PORT 3 (RQ18-20)", 1, 0),
            ("PORT 4 (RQ21-23)", 0, 1),
            ("FAULT (HIGH)", 1, 1),
        ]))
    @skip_if_devsim("DAQ does not exist in devsim")
    def test_GIVEN_mock_DAQ_inputs_THEN_PORT3_4_mode_is_correct(
            self, _, state, val1, val2):
        self.ca.set_pv_value("DAQ:R02:DATA:SIM", val1)
        self.ca.set_pv_value("DAQ:R03:DATA:SIM", val2)
        self.ca.assert_that_pv_is("PORT3_4:MODE", state)

    @skip_if_devsim("DAQ does not exist in devsim")
    def test_GIVEN_fault_condition_THEN_RB2_alarms_correct(self):
        self.ca.set_pv_value("DAQ:R04:DATA:SIM", 0)
        self.ca.set_pv_value("DAQ:R05:DATA:SIM", 0)
        self.ca.assert_that_pv_alarm_is("RB2:MODE", ChannelAccess.Alarms.MAJOR)

    @skip_if_devsim("DAQ does not exist in devsim")
    def test_GIVEN_high_fault_condition_THEN_PORT3_4_alarms_correct(self):
        self.ca.set_pv_value("DAQ:R02:DATA:SIM", 1)
        self.ca.set_pv_value("DAQ:R03:DATA:SIM", 1)
        self.ca.assert_that_pv_alarm_is("PORT3_4:MODE",
                                        ChannelAccess.Alarms.MAJOR)

    @skip_if_devsim("DAQ does not exist in devsim")
    def test_GIVEN_low_fault_condition_THEN_PORT3_4_alarms_correct(self):
        self.ca.set_pv_value("DAQ:R02:DATA:SIM", 0)
        self.ca.set_pv_value("DAQ:R03:DATA:SIM", 0)
        self.ca.assert_that_pv_alarm_is("PORT3_4:MODE",
                                        ChannelAccess.Alarms.MAJOR)