Ejemplo n.º 1
0
class MezfliprTests(unittest.TestCase):
    def setUp(self):
        self._lewis, self._ioc = get_running_lewis_and_ioc(
            EMULATOR_NAME, DEVICE_PREFIX)

        self.ca = ChannelAccess(device_prefix=DEVICE_PREFIX)

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

    @skip_if_recsim("Disconnection simulation not implemented in recsim")
    def test_GIVEN_device_is_connected_THEN_can_read_id(self):
        self.ca.assert_that_pv_is("ID", "Flipper Control")
        self.ca.assert_that_pv_alarm_is("ID", self.ca.Alarms.NONE)

    @parameterized.expand(["ANALYSER", "POLARISER"])
    def test_GIVEN_amplitude_is_set_THEN_amplitude_can_be_read_back(
            self, flipper):
        for val in [0., 0.12, 2.99]:  # Amplitude should be limited to 3A
            self.ca.assert_setting_setpoint_sets_readback(
                val, readback_pv="{}:AMPLITUDE".format(flipper))

    @parameterized.expand(["ANALYSER", "POLARISER"])
    def test_GIVEN_compensation_is_set_THEN_compensation_can_be_read_back(
            self, flipper):
        for val in [0., 0.12, 5000.5]:
            self.ca.assert_setting_setpoint_sets_readback(
                val, readback_pv="{}:COMPENSATION".format(flipper))

    @parameterized.expand(["ANALYSER", "POLARISER"])
    def test_GIVEN_dt_is_set_THEN_dt_can_be_read_back(self, flipper):
        for val in [0., -0.12, -5000.5]:  # DT only accepts negative values.
            self.ca.assert_setting_setpoint_sets_readback(
                val, readback_pv="{}:DT".format(flipper))

    @parameterized.expand(["ANALYSER", "POLARISER"])
    def test_GIVEN_constant_is_set_THEN_constant_can_be_read_back(
            self, flipper):
        for val in [0., 0.12, 5000.5]:
            self.ca.assert_setting_setpoint_sets_readback(
                val, readback_pv="{}:CONSTANT".format(flipper))

    @parameterized.expand(["ANALYSER", "POLARISER"])
    def test_GIVEN_constant_is_set_THEN_constant_can_be_read_back(
            self, flipper):
        for filename in [r"C:\a.txt", r"C:\b.txt"]:
            self.ca.assert_setting_setpoint_sets_readback(
                filename, readback_pv="{}:FILENAME".format(flipper))

    @parameterized.expand([
        "Analyser Off Pol. Off",
        "Analyser Off Pol. On",
        "Analyser On Pol. Off",
        "Analyser On Pol. On",
    ])
    def test_GIVEN_toggle_state_is_set_THEN_toggle_state_can_be_read_back(
            self, toggle_state):
        self.ca.set_pv_value("TOGGLE:SP", toggle_state)
        self.ca.assert_that_pv_is("TOGGLE", toggle_state)
        self.ca.assert_that_pv_alarm_is("TOGGLE", self.ca.Alarms.NONE)
Ejemplo n.º 2
0
class CurrentTests(unittest.TestCase):
    # These current testing values are uncalibrated values from the DAQ lying between 0 and 10.
    current_values = [0, 1.33333, 5e1, 10e-3, 10]
    current_values_which_give_alarms = [10, 11]

    def setUp(self):
        self.ca = ChannelAccess(20, device_prefix=DEVICE_PREFIX)
        shared_setup(self.ca)
        self._simulate_current(0)

    def _simulate_current(self, current):
        curr_array = [current] * 1000
        self.ca.set_pv_value("DAQ:CURR:WV:SIM", curr_array)

    @parameterized.expand(parameterized_list(current_values))
    def test_that_GIVEN_current_value_THEN_calibrated_current_readback_changes(
            self, _, value):
        # GIVEN
        self._simulate_current(value)

        self.ca.assert_that_pv_is_number("CURR",
                                         value * DAQ_CURR_READ_SCALE_FACTOR,
                                         MARGIN_OF_ERROR)

    @parameterized.expand(parameterized_list(current_values_which_give_alarms))
    def test_that_WHEN_current_is_out_of_range_THEN_alarm_raised(
            self, _, value):
        # WHEN
        self._simulate_current(value)

        # THEN
        self.ca.assert_that_pv_alarm_is("CURR", ChannelAccess.Alarms.MAJOR)
class CP2800StatusTests(unittest.TestCase):
    def setUp(self):
        _, self._ioc = get_running_lewis_and_ioc(None, DEVICE_PREFIX)
        self.assertIsNotNone(self._ioc)
        self.ca = ChannelAccess(device_prefix=DEVICE_PREFIX)

    def test_GIVEN_compressor_elapsed_minutes_THEN_alarm_correct(self):
        tests = [
            (-1, self.ca.Alarms.MAJOR),
            (0, self.ca.Alarms.NONE),
            (1, self.ca.Alarms.NONE),
            (500001, self.ca.Alarms.MINOR),
            (1000001, self.ca.Alarms.MAJOR),
        ]
        for test in tests:
            elapsed_time, expected_alarm = test
            self.ca.set_pv_value("SIM:ELAPSED", elapsed_time)
            self.ca.assert_that_pv_alarm_is("ELAPSED", expected_alarm, 10)

    def test_GIVEN_compressor_state_on_or_off_THEN_readback_correct(self):
        states = [(1, "On"), (0, "Off")]
        for state in states:
            send_val, expected_response = state
            self.ca.set_pv_value("SIM:POWER", send_val)
            self.ca.assert_that_pv_is("POWER", expected_response)

    def test_GIVEN_error_value_THEN_readback_correct(self):
        self.ca.set_pv_value("SIM:ERR", 1)
        self.ca.assert_that_pv_is("ERR", 1, timeout=10)

    def test_GIVEN_negative_error_value_THEN_alarm_correct(self):
        self.ca.set_pv_value("SIM:ERR", -1)
        self.ca.assert_that_pv_alarm_is("ERR",
                                        self.ca.Alarms.MAJOR,
                                        timeout=10)
Ejemplo n.º 4
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)
Ejemplo n.º 5
0
class FlipprpsTests(unittest.TestCase):
    """
    Tests for the Flipprps IOC.
    """
    def setUp(self):
        self._lewis, self._ioc = get_running_lewis_and_ioc(
            "flipprps", DEVICE_PREFIX)
        self.ca = ChannelAccess(device_prefix=DEVICE_PREFIX)
        self._lewis.backdoor_set_on_device("connected", True)

    @skip_if_recsim("Lewis backdoor commands not available in RecSim")
    def test_SET_polarity(self):
        self.ca.set_pv_value("POLARITY", "Down")
        self._lewis.assert_that_emulator_value_is("polarity", 0)
        self.ca.set_pv_value("POLARITY", "Up")
        self._lewis.assert_that_emulator_value_is("polarity", 1)

    def test_GET_id(self):
        self.ca.assert_that_pv_is("ID", "Flipper")

    @skip_if_recsim("Lewis backdoor commands not available in RecSim")
    def test_GIVEN_device_not_connected_THEN_id_is_in_alarm(self):
        self._lewis.backdoor_set_on_device("connected", False)
        self.ca.assert_that_pv_alarm_is("ID", self.ca.Alarms.INVALID, 20)

    @skip_if_recsim("Lewis backdoor commands not available in RecSim")
    def test_GIVEN_device_not_connected_THEN_polarity_raises_timeout_alarm_after_set(
            self):
        self._lewis.backdoor_set_on_device("connected", False)
        self.ca.set_pv_value("POLARITY", "Up")
        self.ca.assert_that_pv_alarm_is("POLARITY", self.ca.Alarms.INVALID)
Ejemplo n.º 6
0
class DAQmxTests(object):
    """
    General tests for the DAQmx.
    """
    def setUp(self):
        self.emulator, self._ioc = get_running_lewis_and_ioc(
            DEVICE_PREFIX, DEVICE_PREFIX)

        self.ca = ChannelAccess(device_prefix=DEVICE_PREFIX)

    def test_WHEN_emulator_disconnected_THEN_data_in_alarm_and_valid_on_reconnect(
            self):
        self.ca.assert_that_pv_alarm_is_not("DATA",
                                            ChannelAccess.Alarms.INVALID)
        self.emulator.disconnect_device()
        self.ca.assert_that_pv_alarm_is("DATA", ChannelAccess.Alarms.INVALID)

        # Check we don't get excessive numbers of messages if we stay disconnected for 15s (up to 15 messages seems
        # reasonable - 1 per second on average)
        with assert_log_messages(self._ioc, number_of_messages=15):
            sleep(15)
            # Double-check we are still in alarm
            self.ca.assert_that_pv_alarm_is("DATA",
                                            ChannelAccess.Alarms.INVALID)

        self.emulator.reconnect_device()
        self.ca.assert_that_pv_alarm_is_not("DATA",
                                            ChannelAccess.Alarms.INVALID,
                                            timeout=5)
        self.ca.assert_that_pv_value_is_changing("DATA", 1)
class Tpg300Tests(unittest.TestCase):
    """
    Tests for the TPG300.
    """

    def setUp(self):
        self._lewis, self._ioc = get_running_lewis_and_ioc("tpg300", DEVICE_PREFIX)
        self.ca = ChannelAccess(20, device_prefix=DEVICE_PREFIX)

    def tearDown(self):
        self._connect_emulator()

    def _set_pressure(self, expected_pressure, channel):
        prop = "pressure_{}".format(channel.lower())
        pv = "SIM:PRESSURE"
        self._lewis.backdoor_set_on_device(prop, expected_pressure)
        self._ioc.set_simulated_value(pv, expected_pressure)

    def _set_units(self, unit):
        self._lewis.backdoor_run_function_on_device("backdoor_set_unit", [unit.value])
        self._ioc.set_simulated_value("SIM:UNITS", unit.name)

    def _connect_emulator(self):
        self._lewis.backdoor_run_function_on_device("connect")

    def _disconnect_emulator(self):
        self._lewis.backdoor_run_function_on_device("disconnect")

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

    def test_that_GIVEN_a_connected_emulator_WHEN_units_are_set_THEN_unit_is_the_same_as_backdoor(self):
        for unit in Units:
            self._set_units(unit)

            expected_unit = unit.name
            self.ca.assert_that_pv_is("UNITS", expected_unit)

    def test_that_GIVEN_a_connected_emulator_and_pressure_value_WHEN_set_pressure_is_set_THEN_the_ioc_is_updated(self):
        for expected_pressure, channel in product(TEST_PRESSURES, CHANNELS):
            pv = "PRESSURE_{}".format(channel)
            self._set_pressure(expected_pressure, channel)
            self.ca.assert_that_pv_is(pv, expected_pressure)

    @skip_if_recsim("Recsim is unable to simulate a disconnected device")
    def test_that_GIVEN_a_disconnected_emulator_WHEN_getting_pressure_THEN_INVALID_alarm_shows(self):
        self._disconnect_emulator()

        for channel in CHANNELS:
            pv = "PRESSURE_{}".format(channel)
            self.ca.assert_that_pv_alarm_is(pv, self.ca.Alarms.INVALID)
class ChannelTests(unittest.TestCase):
    def setUp(self):
        self._lewis, self._ioc = get_running_lewis_and_ioc(
            "keithley_2700", DEVICE_PREFIX)
        self.ca = ChannelAccess(default_timeout=30,
                                device_prefix=DEVICE_PREFIX)
        self.ca.assert_that_pv_exists("IDN")
        self.ca.set_pv_value("BUFF:CLEAR:SP", "")
        self.ca.assert_that_pv_is("BUFF:AUTOCLEAR", "ON")
        if not IOCRegister.uses_rec_sim:
            self._lewis.backdoor_set_on_device("simulate_readings", False)

    @unstable_test()
    @skip_if_recsim("Cannot use lewis backdoor in recsim")
    def test_GIVEN_empty_buffer_WHEN_reading_inserted_THEN_channel_PVs_get_correct_values(
            self):
        _reset_drift_channels(self)
        reading = "+1386.05,+4000,101"
        expected_values = {
            'read': 1386.05,
            'time': 4000,
            'temp101': 47.424,
            'drift': 0,
        }
        # GIVEN
        self.ca.set_pv_value("BUFF:CLEAR:SP", "")
        # WHEN
        _insert_reading(self, [reading])
        # THEN
        self.ca.assert_that_pv_is_number("CHNL:101:READ",
                                         expected_values['read'],
                                         tolerance=READ_TOLERANCE)
        self.ca.assert_that_pv_is_number("CHNL:101:TIME",
                                         expected_values['time'],
                                         tolerance=TIME_TOLERANCE)
        self.ca.assert_that_pv_is_number("CHNL:101:TEMP",
                                         expected_values['temp101'],
                                         tolerance=TEMP_TOLERANCE)
        self.ca.assert_that_pv_is_number("CHNL:101:DRIFT",
                                         expected_values['drift'],
                                         tolerance=DRIFT_TOLERANCE)

    @skip_if_recsim("Alarm invalid in recsim")
    def test_GIVEN_temperature_out_of_bounds_THEN_alarm_is_major(self):
        #GIVEN
        reading = "+939,+10,101"
        _insert_reading(self, [reading])
        #THEN
        self.ca.assert_that_pv_alarm_is("CHNL:101:TEMP:CHECK",
                                        self.ca.Alarms.MAJOR)
Ejemplo n.º 9
0
class WbvalveTests(unittest.TestCase):
    """
    Tests for the Wbvalve IOC.
    """
    def setUp(self):
        self._lewis, self._ioc = get_running_lewis_and_ioc(
            emulator_name, DEVICE_PREFIX)
        self.ca = ChannelAccess(device_prefix=DEVICE_PREFIX)
        self._lewis.backdoor_run_function_on_device('reset')

    @parameterized.expand(['J1', 'J2'])
    @skip_if_recsim("Recsim behaviour not defined")
    def test_GIVEN_an_ioc_WHEN_set_valve_to_wb1on_THEN_status_is_wb1on(
            self, expected_value):
        self.ca.assert_setting_setpoint_sets_readback(expected_value, 'POS')

    @skip_if_recsim("Can not test disconnection in rec sim")
    def test_GIVEN_device_not_connected_WHEN_get_status_THEN_alarm(self):
        self._lewis.backdoor_set_on_device('connected', False)
        self.ca.assert_that_pv_alarm_is('POS', ChannelAccess.Alarms.INVALID)
class Sans2dVacuumTankTest(unittest.TestCase):
    """
    Tests for the SANS2D vacuum tank, based on a FINS PLC.
    """
    def setUp(self):
        self._ioc = IOCRegister.get_running("FINS_01")
        self.ca = ChannelAccess(device_prefix=ioc_prefix)

    @parameterized.expand(parameterized_list([-5, 0, 3, 5, 7, 9, 16]))
    def test_WHEN_set_tank_status_to_unknown_value_THEN_error_status(
            self, _, status_rval):
        self.ca.set_pv_value("SIM:TANK:STATUS", status_rval)
        self.ca.assert_that_pv_is("TANK:STATUS", "ERROR: STATUS UNKNOWN")
        self.ca.assert_that_pv_alarm_is("TANK:STATUS", "MAJOR")

    @parameterized.expand([(1, "ATMOSPHERE"), (2, "VAC DOWN"),
                           (4, "AT VACUUM"), (8, "VENTING")])
    def test_WHEN_set_tank_status_to_known_value_THEN_no_error(
            self, status_rval, status_val):
        self.ca.set_pv_value("SIM:TANK:STATUS", status_rval)
        self.ca.assert_that_pv_is("TANK:STATUS", status_val)
        self.ca.assert_that_pv_alarm_is("TANK:STATUS", "NO_ALARM")
Ejemplo n.º 11
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)
class MecfrfTests(unittest.TestCase):
    def setUp(self):
        self._lewis, self._ioc = get_running_lewis_and_ioc(
            _EMULATOR_NAME, DEVICE_PREFIX)
        self.ca = ChannelAccess(device_prefix=DEVICE_PREFIX,
                                default_timeout=30)

        self._lewis.backdoor_set_on_device("connected", True)
        self._lewis.backdoor_set_on_device("corrupted_messages", False)

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

    @parameterized.expand(
        parameterized_list(itertools.product(SENSORS, TEST_LENGTHS)))
    @skip_if_recsim("Uses Lewis backdoor")
    def test_WHEN_value_is_written_to_emulator_THEN_record_updates(
            self, _, sensor, length):
        self._lewis.backdoor_set_on_device("sensor{}".format(sensor),
                                           length * RAW_READING_SCALING)
        self.ca.assert_that_pv_is("SENSOR{}".format(sensor), length)
        self.ca.assert_that_pv_alarm_is("SENSOR{}".format(sensor),
                                        self.ca.Alarms.NONE)

    @skip_if_recsim("Uses Lewis backdoor")
    def test_WHEN_emulator_sends_corrupt_packets_THEN_records_go_into_alarm(
            self):
        with self.ca.assert_pv_processed("_RESET_CONNECTION"):
            self._lewis.backdoor_set_on_device("corrupted_messages", True)
            self.ca.assert_that_pv_is("_GETTING_INVALID_MESSAGES", 1)

        self._lewis.backdoor_set_on_device("corrupted_messages", False)
        self.ca.assert_that_pv_is("_GETTING_INVALID_MESSAGES", 0)

    @parameterized.expand(parameterized_list(SENSORS))
    @skip_if_recsim("Uses Lewis backdoor")
    def test_WHEN_emulator_disconnected_THEN_records_go_into_alarm(
            self, _, sensor):
        self.ca.assert_that_pv_is("_READINGS_OUTDATED", "No")
        self.ca.assert_that_pv_alarm_is("SENSOR{}".format(sensor),
                                        self.ca.Alarms.NONE)

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

        self.ca.assert_that_pv_is("_READINGS_OUTDATED", "Yes")
        self.ca.assert_that_pv_alarm_is("SENSOR{}".format(sensor),
                                        self.ca.Alarms.INVALID)
Ejemplo n.º 13
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 InstronStressRigTests(unittest.TestCase):
    """
    Tests for the Instron IOC.
    """
    def _change_channel(self, name):
        # Setpoint is zero-indexed
        self.ca.set_pv_value("CHANNEL:SP", CHANNELS[name] - 1)
        self.ca.assert_that_pv_is("CHANNEL.RVAL", CHANNELS[name])
        self.ca.assert_that_pv_alarm_is("CHANNEL", self.ca.Alarms.NONE)
        self.ca.assert_that_pv_is("CHANNEL:GUI", name)
        self.ca.assert_that_pv_alarm_is("CHANNEL:GUI", self.ca.Alarms.NONE)

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

        self.ca = ChannelAccess(15, device_prefix=DEVICE_PREFIX)
        self.ca.assert_that_pv_exists("CHANNEL", timeout=30)

        # Can't use lewis backdoor commands in recsim
        # All of the below commands apply to devsim only.
        if not IOCRegister.uses_rec_sim:
            # Reinitialize the emulator state
            self._lewis.backdoor_command(["device", "reset"])

            self._lewis.backdoor_set_on_device("status", 7680)

            for index, chan_type2 in enumerate((3, 2, 4)):
                self._lewis.backdoor_command([
                    "device", "set_channel_param",
                    str(index + 1), "channel_type",
                    str(chan_type2)
                ])

            self.ca.assert_that_pv_is("CHANNEL:SP.ZRST", "Position")

            self._change_channel("Position")

            # Ensure the rig is stopped
            self._lewis.backdoor_command(["device", "movement_type", "0"])
            self.ca.assert_that_pv_is("GOING", "NO")

            # Ensure stress area and strain length are sensible values (i.e. not zero)
            self._lewis.backdoor_command(
                ["device", "set_channel_param", "2", "area", "10"])
            self._lewis.backdoor_command(
                ["device", "set_channel_param", "3", "length", "10"])
            self.ca.assert_that_pv_is_number("STRESS:AREA",
                                             10,
                                             tolerance=0.001)
            self.ca.assert_that_pv_is_number("STRAIN:LENGTH",
                                             10,
                                             tolerance=0.001)

            # Ensure that all the scales are sensible values (i.e. not zero)
            for index, channel in enumerate(POS_STRESS_STRAIN, 1):
                self._lewis.backdoor_command(
                    ["device", "set_channel_param",
                     str(index), "scale", "10"])
                self.ca.assert_that_pv_is_number(channel + ":SCALE",
                                                 10,
                                                 tolerance=0.001)

            # Always set the waveform generator to run for lots of cycles so it only stops if we want it to
            self.ca.set_pv_value(quart_prefixed("CYCLE:SP"), LOTS_OF_CYCLES)

    @skip_if_recsim("In rec sim we can not set the code easily")
    def test_WHEN_the_rig_has_no_error_THEN_the_status_is_ok(self):
        self._lewis.backdoor_set_on_device("status", 7680)

        self.ca.assert_that_pv_is("STAT:DISP", "System OK")
        self.ca.assert_that_pv_alarm_is("STAT:DISP", ChannelAccess.Alarms.NONE)

    @skip_if_recsim("In rec sim we can not set the code easily")
    def test_WHEN_the_rig_has_other_no_error_THEN_the_status_is_ok(self):
        self._lewis.backdoor_set_on_device("status", 0)

        self.ca.assert_that_pv_is("STAT:DISP", "System OK")
        self.ca.assert_that_pv_alarm_is("STAT:DISP", ChannelAccess.Alarms.NONE)

    @skip_if_recsim("In rec sim we can not set the code easily")
    def test_WHEN_the_rig_has_error_THEN_the_status_is_emergency_stop_pushed(
            self):
        code_and_errors = [([0, 1, 1, 0], "Emergency stop pushed"),
                           ([0, 0, 0, 1], "Travel limit exceeded"),
                           ([0, 0, 1, 0], "Power amplifier too hot"),
                           ([0, 1, 1, 0], "Emergency stop pushed"),
                           ([0, 1, 0, 1], "Invalid status from rig"),
                           ([0, 1, 0, 0], "Invalid status from rig"),
                           ([0, 1, 1, 0], "Emergency stop pushed"),
                           ([0, 1, 1, 1], "Oil too hot"),
                           ([1, 0, 0, 0], "Oil level too low"),
                           ([1, 0, 0, 1], "Motor too hot"),
                           ([1, 0, 1, 0], "Oil pressure too high"),
                           ([1, 0, 1, 1], "Oil pressure too low"),
                           ([1, 1, 0, 0], "Manifold/pump blocked"),
                           ([1, 1, 0, 1], "Oil level going too low"),
                           ([1, 1, 1, 0], "Manifold low pressure")]

        for code, error in code_and_errors:
            code_val = code[0] * 2**12
            code_val += code[1] * 2**11
            code_val += code[2] * 2**10
            code_val += code[3] * 2**9

            self._lewis.backdoor_set_on_device("status", code_val)

            self.ca.assert_that_pv_is("STAT:DISP",
                                      error,
                                      msg="code set {0} = {code_val}".format(
                                          code, code_val=code_val))
            self.ca.assert_that_pv_alarm_is("STAT:DISP",
                                            ChannelAccess.Alarms.MAJOR)

    def test_WHEN_the_rig_is_initialized_THEN_it_is_not_going(self):
        self.ca.assert_that_pv_is("GOING", "NO")

    def test_WHEN_the_rig_is_initialized_THEN_it_is_not_panic_stopping(self):
        self.ca.assert_that_pv_is("PANIC:SP", "READY")

    def test_WHEN_the_rig_is_initialized_THEN_it_is_not_stopping(self):
        self.ca.assert_that_pv_is("STOP:SP", "READY")

    def test_that_the_rig_is_not_normally_in_control_mode(self):
        self.ca.assert_that_pv_is("STOP:SP", "READY")

    def test_WHEN_init_sequence_run_THEN_waveform_ramp_is_set_the_status_is_ok(
            self):
        for chan in POS_STRESS_STRAIN:
            self.ca.set_pv_value("{0}:RAMP:WFTYP:SP".format(chan), 0)

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

        for chan in POS_STRESS_STRAIN:
            self.ca.assert_that_pv_is("{0}:RAMP:WFTYP".format(chan),
                                      RAMP_WAVEFORM_TYPES[3])

    def _switch_to_position_channel_and_change_setpoint(self):

        # It has to be big or the set point will be reached before the test completes
        _big_set_point = 999999999999

        # Select position as control channel
        self._change_channel("Position")
        # Change the setpoint so that movement can be started
        self.ca.set_pv_value("POS:SP", _big_set_point)
        self.ca.assert_that_pv_is_number("POS:SP", _big_set_point, tolerance=1)
        self.ca.assert_that_pv_is_number("POS:SP:RBV",
                                         _big_set_point,
                                         tolerance=1)

    @skip_if_recsim("Dynamic behaviour not captured in RECSIM")
    def test_WHEN_going_and_then_stopping_THEN_going_pv_reflects_the_expected_state(
            self):
        self.ca.assert_that_pv_is("GOING", "NO")
        self._switch_to_position_channel_and_change_setpoint()
        self.ca.set_pv_value("MOVE:GO:SP", 1)
        self.ca.assert_that_pv_is("GOING", "YES")
        self.ca.set_pv_value("STOP:SP", 1)
        self.ca.assert_that_pv_is("GOING", "NO")
        self.ca.set_pv_value("STOP:SP", 0)

    @skip_if_recsim("Dynamic behaviour not captured in RECSIM")
    def test_WHEN_going_and_then_panic_stopping_THEN_going_pv_reflects_the_expected_state(
            self):
        self.ca.assert_that_pv_is("GOING", "NO")
        self._switch_to_position_channel_and_change_setpoint()
        self.ca.set_pv_value("MOVE:GO:SP", 1)
        self.ca.assert_that_pv_is("GOING", "YES")
        self.ca.set_pv_value("PANIC:SP", 1)
        self.ca.assert_that_pv_is("GOING", "NO")
        self.ca.set_pv_value("PANIC:SP", 0)

    @skip_if_recsim("In rec sim this test fails")
    def test_WHEN_arbitrary_command_Q22_is_sent_THEN_the_response_is_a_status_code(
            self):
        self.ca.set_pv_value("ARBITRARY:SP", "Q22")
        # Assert that the response to Q22 is a status code
        self.ca.assert_that_pv_is_within_range("ARBITRARY",
                                               min_value=0,
                                               max_value=65535)

    @skip_if_recsim("In rec sim this test fails")
    def test_WHEN_arbitrary_command_Q300_is_sent_THEN_the_response_is_a_number_between_1_and_3(
            self):
        self.ca.set_pv_value("ARBITRARY:SP", "Q300")
        # Assert that the response to Q300 is between 1 and 3
        self.ca.assert_that_pv_is_within_range("ARBITRARY",
                                               min_value=1,
                                               max_value=3)

    @skip_if_recsim("In rec sim this test fails")
    def test_WHEN_arbitrary_command_C4_is_sent_THEN_Q4_gives_back_the_value_that_was_just_set(
            self):
        def _set_and_check(value):
            # Put the record into a non-alarm state. This is needed so that we can wait until the record is in alarm
            # later, when we do a command which (expectedly) puts the record into a timeout alarm.
            self.ca.set_pv_value("ARBITRARY:SP", "Q4,1")
            self.ca.assert_that_pv_alarm_is("ARBITRARY", self.ca.Alarms.NONE)

            self.ca.set_pv_value("ARBITRARY:SP", "C4,1," + str(value))
            self.ca.assert_that_pv_is("ARBITRARY:SP", "C4,1," + str(value))
            # No response from arbitrary command causes record to be TIMEOUT INVALID - this is expected.
            self.ca.assert_that_pv_alarm_is("ARBITRARY",
                                            self.ca.Alarms.INVALID)

            self.ca.set_pv_value("ARBITRARY:SP", "Q4,1")
            self.ca.assert_that_pv_is_number("ARBITRARY",
                                             value,
                                             tolerance=0.001,
                                             timeout=60)

        for v in [0, 1, 0]:
            _set_and_check(v)

    def test_WHEN_control_channel_is_requested_THEN_an_allowed_value_is_returned(
            self):
        self.ca.assert_that_pv_is_one_of("CHANNEL", CHANNELS.keys())

    @skip_if_recsim("In rec sim this test fails")
    def test_WHEN_control_channel_setpoint_is_requested_THEN_it_is_one_of_the_allowed_values(
            self):
        self.ca.assert_that_pv_is_one_of("CHANNEL:SP", CHANNELS.keys())

    @skip_if_recsim("In rec sim this test fails")
    def test_WHEN_the_control_channel_is_set_THEN_the_readback_contains_the_value_that_was_just_set(
            self):
        for channel in CHANNELS.keys():
            # change channel function contains the relevant assertions.
            self._change_channel(channel)

    def test_WHEN_the_step_time_for_various_channels_is_set_as_an_integer_THEN_the_readback_contains_the_value_that_was_just_set(
            self):
        for chan, val in [("POS", 123), ("STRESS", 456), ("STRAIN", 789)]:
            pv_name = chan + ":STEP:TIME"
            self.ca.assert_setting_setpoint_sets_readback(val, pv_name)

    def test_WHEN_the_step_time_for_various_channels_is_set_as_a_float_THEN_the_readback_contains_the_value_that_was_just_set(
            self):
        for chan, val in [("POS", 111.111), ("STRESS", 222.222),
                          ("STRAIN", 333.333)]:
            pv_name = chan + ":STEP:TIME"
            self.ca.assert_setting_setpoint_sets_readback(val, pv_name)

    def test_WHEN_the_ramp_waveform_for_a_channel_is_set_THEN_the_readback_contains_the_value_that_was_just_set(
            self):
        pv_name = "{0}:RAMP:WFTYP"
        for chan in POS_STRESS_STRAIN:
            for set_value, return_value in enumerate(RAMP_WAVEFORM_TYPES):
                self.ca.assert_setting_setpoint_sets_readback(
                    set_value,
                    pv_name.format(chan),
                    expected_value=return_value)

    def test_WHEN_the_ramp_amplitude_for_a_channel_is_set_as_an_integer_THEN_the_readback_contains_the_value_that_was_just_set(
            self):
        for chan in POS_STRESS_STRAIN:
            for val in [0, 10, 1000, 1000000]:
                pv_name = chan + ":RAW:SP"
                pv_name_rbv = pv_name + ":RBV"
                self.ca.assert_setting_setpoint_sets_readback(
                    val, readback_pv=pv_name_rbv, set_point_pv=pv_name)

    def test_WHEN_the_ramp_amplitude_for_a_channel_is_set_as_a_float_THEN_the_readback_contains_the_value_that_was_just_set(
            self):
        for chan in POS_STRESS_STRAIN:
            for val in [1.0, 5.5, 1.000001, 9.999999, 10000.1]:
                pv_name = chan + ":RAW:SP"
                pv_name_rbv = pv_name + ":RBV"
                self.ca.assert_setting_setpoint_sets_readback(
                    val, readback_pv=pv_name_rbv, set_point_pv=pv_name)

    @skip_if_recsim("In rec sim this test fails")
    def test_WHEN_the_setpoint_for_a_channel_is_set_THEN_the_readback_contains_the_value_that_was_just_set(
            self):
        def _set_and_check(chan, value):
            self.ca.set_pv_value(chan + ":SP", value)
            self.ca.assert_that_pv_is_number(chan + ":SP",
                                             value,
                                             tolerance=0.001)
            self.ca.assert_that_pv_is_number(chan + ":SP:RBV",
                                             value,
                                             tolerance=0.05,
                                             timeout=30)

        for chan in POS_STRESS_STRAIN:
            for i in [1.0, 123.456, 555.555, 1000]:
                _set_and_check(chan, i)

    def test_WHEN_channel_tolerance_is_set_THEN_it_changes_limits_on_SP_RBV(
            self):
        for chan in POS_STRESS_STRAIN:
            sp_val = 1
            self.ca.set_pv_value(chan + ":SP", sp_val)
            for val in [0.1, 1.0, 2.5]:
                pv_name = chan + ":TOLERANCE"
                pv_name_high = chan + ":SP:RBV.HIGH"
                pv_name_low = chan + ":SP:RBV.LOW"
                self.ca.assert_setting_setpoint_sets_readback(
                    val,
                    readback_pv=pv_name_high,
                    set_point_pv=pv_name,
                    expected_value=val + sp_val,
                    expected_alarm=None)
                self.ca.assert_setting_setpoint_sets_readback(
                    val,
                    readback_pv=pv_name_low,
                    set_point_pv=pv_name,
                    expected_value=sp_val - val,
                    expected_alarm=None)

    @skip_if_recsim("Alarms not properly emulated in recsim")
    def test_GIVEN_a_big_tolerance_WHEN_the_setpoint_is_set_THEN_the_setpoint_has_no_alarms(
            self):
        def _set_and_check(chan, value):
            self.ca.set_pv_value(chan + ":SP", value)
            self.ca.set_pv_value(chan + ":TOLERANCE", 9999)
            self.ca.assert_that_pv_alarm_is(chan + ":SP:RBV",
                                            ChannelAccess.Alarms.NONE)

        for chan in POS_STRESS_STRAIN:
            for i in [0.123, 567]:
                _set_and_check(chan, i)

    @skip_if_recsim("Alarms not properly emulated in recsim")
    def test_GIVEN_a_tolerance_of_minus_one_WHEN_the_setpoint_is_set_THEN_the_setpoint_readback_has_alarms(
            self):
        def _set_and_check(chan, value):
            self.ca.set_pv_value(chan + ":SP", value)
            self.ca.set_pv_value(chan + ":TOLERANCE", -1)
            self.ca.assert_that_pv_alarm_is(chan + ":SP:RBV",
                                            ChannelAccess.Alarms.MINOR)

        for chan in POS_STRESS_STRAIN:
            for i in [0.234, 789]:
                _set_and_check(chan, i)

    @skip_if_recsim("In rec sim this test fails")
    def test_WHEN_ioc_gets_a_raw_position_reading_from_the_device_THEN_it_is_converted_correctly(
            self):

        for chan_scale in [0.1, 10.0]:
            self._lewis.backdoor_command(
                ["device", "set_channel_param", "1", "scale",
                 str(chan_scale)])
            self.ca.assert_that_pv_is("POS:SCALE", chan_scale)

            for raw_value in [0, 123]:
                self._lewis.backdoor_command([
                    "device", "set_channel_param", "1", "value",
                    str(raw_value)
                ])
                self.ca.assert_that_pv_is_number("POS:RAW",
                                                 raw_value,
                                                 tolerance=0.01)
                self.ca.assert_that_pv_is_number("POS",
                                                 raw_value * chan_scale * 1000,
                                                 tolerance=(0.01 * chan_scale *
                                                            1000))

    @skip_if_recsim("In rec sim this test fails")
    def test_WHEN_ioc_gets_a_raw_stress_reading_from_the_device_THEN_it_is_converted_correctly(
            self):

        for chan_area in [0.1, 10.0]:
            self._lewis.backdoor_command(
                ["device", "set_channel_param", "2", "area",
                 str(chan_area)])
            self.ca.assert_that_pv_is("STRESS:AREA", chan_area)

            for chan_scale in [0.1, 10.0]:
                self._lewis.backdoor_command([
                    "device", "set_channel_param", "2", "scale",
                    str(chan_scale)
                ])
                self.ca.assert_that_pv_is("STRESS:SCALE", chan_scale)

                for raw_value in [0, 123]:
                    self._lewis.backdoor_command([
                        "device", "set_channel_param", "2", "value",
                        str(raw_value)
                    ])
                    self.ca.assert_that_pv_is("STRESS:RAW", raw_value)
                    self.ca.assert_that_pv_is(
                        "STRESS", raw_value * chan_scale * (1.0 / chan_area))

    @skip_if_recsim("In rec sim this test fails")
    def test_WHEN_strain_length_updates_on_device_THEN_pv_updates(self):
        for value in [1, 123]:
            self._lewis.backdoor_command(
                ["device", "set_channel_param", "3", "length",
                 str(value)])
            self.ca.assert_that_pv_is("STRAIN:LENGTH", value)

    @skip_if_recsim("In rec sim this test fails")
    def test_WHEN_ioc_gets_a_raw_strain_reading_from_the_device_THEN_it_is_converted_correctly(
            self):
        for chan_scale in [0.1, 10.0]:
            self._lewis.backdoor_command(
                ["device", "set_channel_param", "3", "scale",
                 str(chan_scale)])

            for chan_length in [0.1, 10.0]:
                self._lewis.backdoor_command([
                    "device", "set_channel_param", "3", "length",
                    str(chan_length)
                ])

                for raw_value in [0, 0.001]:
                    self._lewis.backdoor_command([
                        "device", "set_channel_param", "3", "value",
                        str(raw_value)
                    ])

                    self.ca.assert_that_pv_is("STRAIN:SCALE", chan_scale)
                    self.ca.assert_that_pv_is("STRAIN:LENGTH", chan_length)
                    self.ca.assert_that_pv_is("STRAIN:RAW", raw_value)

                    self.ca.assert_that_pv_is(
                        "STRAIN",
                        (raw_value * chan_scale * 100000 * (1 / chan_length)))

    def test_WHEN_the_area_setpoint_is_set_THEN_the_area_readback_updates(
            self):
        def _set_and_check(value):
            self.ca.set_pv_value("STRESS:AREA:SP", value)
            self.ca.assert_that_pv_is_number("STRESS:AREA",
                                             value,
                                             tolerance=0.01)
            self.ca.assert_that_pv_alarm_is("STRESS:AREA",
                                            ChannelAccess.Alarms.NONE)

        for val in [0.234, 789]:
            _set_and_check(val)

    def test_WHEN_the_area_setpoint_is_set_THEN_the_diameter_readback_updates(
            self):
        def _set_and_check(value):
            self.ca.set_pv_value("STRESS:AREA:SP", value)
            self.ca.assert_that_pv_is_number("STRESS:DIAMETER",
                                             (2 * math.sqrt(value / math.pi)),
                                             tolerance=0.01)
            self.ca.assert_that_pv_alarm_is("STRESS:DIAMETER",
                                            ChannelAccess.Alarms.NONE)

        for val in [0.234, 789]:
            _set_and_check(val)

    def test_WHEN_the_diameter_setpoint_is_set_THEN_the_diameter_readback_updates(
            self):
        def _set_and_check(value):
            self.ca.set_pv_value("STRESS:DIAMETER:SP", value)
            self.ca.assert_that_pv_is_number("STRESS:DIAMETER",
                                             value,
                                             tolerance=0.0005)
            self.ca.assert_that_pv_alarm_is("STRESS:DIAMETER",
                                            ChannelAccess.Alarms.NONE)

        for val in [0.234, 789]:
            _set_and_check(val)

    def test_WHEN_the_diameter_setpoint_is_set_THEN_the_area_readback_updates(
            self):
        def _set_and_check(value):
            self.ca.set_pv_value("STRESS:DIAMETER:SP", value)
            self.ca.assert_that_pv_is_number("STRESS:AREA",
                                             ((value / 2.0)**2 * math.pi),
                                             tolerance=0.0005)
            self.ca.assert_that_pv_alarm_is("STRESS:AREA",
                                            ChannelAccess.Alarms.NONE)

        for val in [0.234, 789]:
            _set_and_check(val)

    @skip_if_recsim("In rec sim this test fails")
    def test_WHEN_a_position_setpoint_is_set_THEN_it_is_converted_correctly(
            self):
        for scale in [2.34, 456.78]:
            self._lewis.backdoor_command(
                ["device", "set_channel_param", "1", "scale",
                 str(scale)])
            self.ca.assert_that_pv_is("POS:SCALE", scale)

            for val in [1.23, 123.45]:
                self.ca.set_pv_value("POS:SP", val)
                self.ca.assert_that_pv_is_number("POS:RAW:SP",
                                                 val * (1.0 / 1000.0) *
                                                 (1 / scale),
                                                 tolerance=0.0000000001)
                self.ca.assert_that_pv_alarm_is("POS:RAW:SP",
                                                ChannelAccess.Alarms.NONE)

    @skip_if_recsim("In rec sim this test fails")
    def test_WHEN_a_stress_setpoint_is_set_THEN_it_is_converted_correctly(
            self):

        for area in [789, 543.21]:
            self._lewis.backdoor_command(
                ["device", "set_channel_param", "2", "area",
                 str(area)])
            self.ca.assert_that_pv_is("STRESS:AREA", area)

            for chan_scale in [2.34, 456.78]:
                self._lewis.backdoor_command([
                    "device", "set_channel_param", "2", "scale",
                    str(chan_scale)
                ])
                self.ca.assert_that_pv_is("STRESS:SCALE", chan_scale)

                for val in [1.23, 123.45]:
                    self.ca.set_pv_value("STRESS:SP", val)
                    self.ca.assert_that_pv_is_number("STRESS:RAW:SP",
                                                     val * (1 / chan_scale) *
                                                     area,
                                                     tolerance=0.0000000001)
                    self.ca.assert_that_pv_alarm_is("STRESS:RAW:SP",
                                                    ChannelAccess.Alarms.NONE)

    @skip_if_recsim("In rec sim this test fails")
    def test_WHEN_a_strain_setpoint_is_set_THEN_it_is_converted_correctly(
            self):

        for length in [789, 543.21]:
            self._lewis.backdoor_command(
                ["device", "set_channel_param", "3", "length",
                 str(length)])
            self.ca.assert_that_pv_is("STRAIN:LENGTH", length)

            for chan_scale in [2.34, 456.78]:
                self._lewis.backdoor_command([
                    "device", "set_channel_param", "3", "scale",
                    str(chan_scale)
                ])
                self.ca.assert_that_pv_is("STRAIN:SCALE", chan_scale)

                for val in [1.23, 123.45]:
                    self.ca.set_pv_value("STRAIN:SP", val)
                    self.ca.assert_that_pv_is_number("STRAIN:RAW:SP",
                                                     val * (1 / chan_scale) *
                                                     length * (1.0 / 100000.0),
                                                     tolerance=0.0000000001)
                    self.ca.assert_that_pv_alarm_is("STRAIN:RAW:SP",
                                                    ChannelAccess.Alarms.NONE)

    @skip_if_recsim("In rec sim this test fails")
    def test_WHEN_the_channel_type_updates_on_the_device_THEN_the_pv_updates(
            self):

        for chan_name, chan_num in [("POS", 1), ("STRESS", 2), ("STRAIN", 3)]:
            for value_1, value_2, return_value_1, return_value_2 in [
                (0, 1, "Standard transducer", "Unrecognized"),
                (1, 10, "User transducer", "Ext. waveform generator")
            ]:

                self._lewis.backdoor_command([
                    "device", "set_channel_param",
                    str(chan_num), "transducer_type",
                    str(value_1)
                ])
                self._lewis.backdoor_command([
                    "device", "set_channel_param",
                    str(chan_num), "channel_type",
                    str(value_2)
                ])
                self.ca.assert_that_pv_is("" + chan_name + ":TYPE:STANDARD",
                                          return_value_1)
                self.ca.assert_that_pv_is("" + chan_name + ":TYPE",
                                          return_value_2)

    def test_WHEN_waveform_type_abs_set_on_axes_THEN_all_axes_are_set(self):
        def _set_and_check(set_value, return_value):
            self.ca.set_pv_value("AXES:RAMP:WFTYP:SP", set_value)
            for chan in POS_STRESS_STRAIN:
                self.ca.assert_that_pv_is("{0}:RAMP:WFTYP".format(chan),
                                          return_value)
                self.ca.assert_that_pv_alarm_is("{0}:RAMP:WFTYP".format(chan),
                                                ChannelAccess.Alarms.NONE)

        for set_value, return_value in enumerate(RAMP_WAVEFORM_TYPES):
            _set_and_check(set_value, return_value)

    @skip_if_recsim("In rec sim this test fails")
    def test_WHEN_channel_fails_check_THEN_channel_mbbi_record_is_invalid_and_has_tag_disabled(
            self):

        for index, (chan_name, type, index_as_name,
                    channel_as_name) in enumerate(
                        zip(POS_STRESS_STRAIN, (1, 1, 1), ("ZR", "ON", "TW"),
                            ("Position", "Stress", "Strain"))):

            self._lewis.backdoor_command([
                "device", "set_channel_param",
                str(index + 1), "channel_type",
                str(type)
            ])

            self.ca.assert_that_pv_is("" + chan_name + ":TYPE:CHECK", "FAIL")
            self.ca.assert_that_pv_is("CHANNEL:SP.{}ST".format(index_as_name),
                                      "{0} - disabled".format(channel_as_name))

            self.ca.set_pv_value("CHANNEL:SP", index)

            self.ca.assert_that_pv_alarm_is("CHANNEL:SP",
                                            ChannelAccess.Alarms.INVALID)

    @skip_if_recsim("In rec sim this test fails")
    def test_WHEN_channel_succeeds_check_THEN_channel_mbbi_record_is_invalid_and_has_tag_disabled(
            self):
        self.ca.set_pv_value("CHANNEL:SP", 3)
        for index, (chan_name, type, index_as_name,
                    channel_as_name) in enumerate(
                        zip(POS_STRESS_STRAIN, (3, 2, 4), ("ZR", "ON", "TW"),
                            ("Position", "Stress", "Strain"))):

            self._lewis.backdoor_command([
                "device", "set_channel_param",
                str(index + 1), "channel_type",
                str(type)
            ])

            self.ca.assert_that_pv_is("" + chan_name + ":TYPE:CHECK",
                                      "PASS",
                                      timeout=30)

            self.ca.assert_that_pv_is("CHANNEL:SP.{}ST".format(index_as_name),
                                      channel_as_name)
            self.ca.assert_that_pv_is("CHANNEL:SP.{}SV".format(index_as_name),
                                      ChannelAccess.Alarms.NONE)

            self.ca.set_pv_value("CHANNEL:SP", index)
            self.ca.assert_that_pv_alarm_is("CHANNEL:SP",
                                            ChannelAccess.Alarms.NONE)

    @skip_if_recsim("In rec sim we can not disconnect the device from the IOC")
    def test_WHEN_the_rig_is_not_connected_THEN_the_status_has_alarm(self):
        self._lewis.backdoor_set_on_device("status", None)

        self.ca.assert_that_pv_alarm_is("STAT:DISP",
                                        ChannelAccess.Alarms.INVALID)

    # Waveform tests

    def check_running_state(self, status, running, continuing, timeout=None):
        self.ca.assert_that_pv_is(wave_prefixed("STATUS"), status, timeout)
        self.ca.assert_that_pv_is(wave_prefixed("RUNNING"),
                                  "Running" if running else "Not running",
                                  timeout)
        self.ca.assert_that_pv_is(
            wave_prefixed("CONTINUING"),
            "Continuing" if continuing else "Not continuing", timeout)

    @skip_if_recsim("No backdoor in LewisNone")
    def test_GIVEN_the_waveform_generator_is_stopped_WHEN_it_is_started_THEN_it_is_running(
            self):
        self.check_running_state(status="Stopped",
                                 running=False,
                                 continuing=False)
        self.ca.set_pv_value(wave_prefixed("START"), 1)
        self.check_running_state(status="Running",
                                 running=True,
                                 continuing=True)

    @skip_if_recsim("No backdoor in LewisNone")
    def test_GIVEN_the_waveform_generator_is_stopped_WHEN_it_is_aborted_THEN_it_is_stopped(
            self):
        self.check_running_state(status="Stopped",
                                 running=False,
                                 continuing=False)
        self.ca.set_pv_value(wave_prefixed("ABORT"), 1)
        self.check_running_state(status="Stopped",
                                 running=False,
                                 continuing=False)

    @skip_if_recsim("No backdoor in LewisNone")
    def test_GIVEN_the_waveform_generator_is_stopped_WHEN_it_is_stopped_THEN_it_is_stopped(
            self):
        self.check_running_state(status="Stopped",
                                 running=False,
                                 continuing=False)
        self.ca.set_pv_value(wave_prefixed("STOP"), 1)
        self.check_running_state(status="Stopped",
                                 running=False,
                                 continuing=False)

    @skip_if_recsim("No backdoor in LewisNone")
    def test_GIVEN_the_waveform_generator_is_running_WHEN_it_is_started_THEN_it_is_running(
            self):
        self._lewis.backdoor_command(
            ["device", "set_waveform_state", WAVEFORM_RUNNING])
        self.check_running_state(status="Running",
                                 running=True,
                                 continuing=True)
        self.ca.set_pv_value(wave_prefixed("START"), 1)
        self.check_running_state(status="Running",
                                 running=True,
                                 continuing=True)

    @skip_if_recsim("No backdoor in LewisNone")
    def test_GIVEN_the_waveform_generator_is_running_WHEN_it_is_aborted_THEN_it_is_aborted(
            self):
        self._lewis.backdoor_command(
            ["device", "set_waveform_state", WAVEFORM_RUNNING])
        self.check_running_state(status="Running",
                                 running=True,
                                 continuing=True)
        self.ca.set_pv_value(wave_prefixed("ABORT"), 1)
        self.check_running_state(status="Aborted",
                                 running=False,
                                 continuing=False)

    @skip_if_recsim("No backdoor in LewisNone")
    def test_GIVEN_the_waveform_generator_is_running_WHEN_it_is_stopped_THEN_it_is_finishing_and_then_stops_within_5_seconds(
            self):
        self._lewis.backdoor_command(
            ["device", "set_waveform_state", WAVEFORM_RUNNING])
        self.check_running_state(status="Running",
                                 running=True,
                                 continuing=True)
        self.ca.set_pv_value(wave_prefixed("STOP"), 1)
        self.check_running_state(status="Finishing",
                                 running=True,
                                 continuing=False)
        self.check_running_state(status="Stopped",
                                 running=False,
                                 continuing=False,
                                 timeout=5)

    @skip_if_recsim("No backdoor in LewisNone")
    def test_GIVEN_the_waveform_generator_is_aborted_WHEN_it_is_started_THEN_it_is_running_and_keeps_running(
            self):
        self._lewis.backdoor_command(
            ["device", "set_waveform_state", WAVEFORM_ABORTED])
        self.check_running_state(status="Aborted",
                                 running=False,
                                 continuing=False)
        self.ca.set_pv_value(wave_prefixed("START"), 1)
        self.check_running_state(status="Running",
                                 running=True,
                                 continuing=True)

        # We need to make sure it can keep running for a few scans. The IOC could feasibly stop the generator shortly
        # after it is started
        time.sleep(5)
        self.check_running_state(status="Running",
                                 running=True,
                                 continuing=True)

    @skip_if_recsim("No backdoor in LewisNone")
    def test_GIVEN_the_waveform_generator_is_aborted_WHEN_it_is_aborted_THEN_it_is_aborted(
            self):
        self._lewis.backdoor_command(
            ["device", "set_waveform_state", WAVEFORM_ABORTED])
        self.check_running_state(status="Aborted",
                                 running=False,
                                 continuing=False)
        self.ca.set_pv_value(wave_prefixed("ABORT"), 1)
        self.check_running_state(status="Aborted",
                                 running=False,
                                 continuing=False)

    @skip_if_recsim("No backdoor in LewisNone")
    def test_GIVEN_the_waveform_generator_is_aborted_WHEN_it_is_stopped_THEN_it_is_aborted(
            self):
        self._lewis.backdoor_command(
            ["device", "set_waveform_state", WAVEFORM_ABORTED])
        self.check_running_state(status="Aborted",
                                 running=False,
                                 continuing=False)
        self.ca.set_pv_value(wave_prefixed("STOP"), 1)
        self.check_running_state(status="Aborted",
                                 running=False,
                                 continuing=False)

    def test_WHEN_waveform_type_is_set_THEN_the_device_reports_it_has_changed(
            self):
        for index, wave_type in enumerate([
                "Sine", "Triangle", "Square", "Haversine", "Havetriangle",
                "Haversquare", "Sensor", "Aux", "Sawtooth"
        ]):
            self.ca.assert_setting_setpoint_sets_readback(
                index, wave_prefixed("TYPE"), expected_value=wave_type)

    @skip_if_recsim("Recsim record does not handle multiple channels ")
    def test_GIVEN_multiple_channels_WHEN_waveform_frequency_is_set_THEN_the_device_is_updated_to_that_value(
            self):
        expected_values = [123.456, 789.012, 345.678]
        assert len(expected_values) == NUMBER_OF_CHANNELS

        # Do this as two separate loops so that we can verify that all 3 channel values are stored independently
        for device_channel in range(NUMBER_OF_CHANNELS):
            self.ca.set_pv_value("CHANNEL:SP.VAL", device_channel)
            self.ca.set_pv_value(wave_prefixed("FREQ:SP"),
                                 expected_values[device_channel])

        for device_channel in range(NUMBER_OF_CHANNELS):
            self.ca.set_pv_value("CHANNEL:SP.VAL", device_channel)
            self.ca.assert_that_pv_is(wave_prefixed("FREQ"),
                                      expected_values[device_channel])

    @unstable_test()
    @skip_if_recsim("Conversion factors initialized to 0")
    def test_GIVEN_multiple_channels_WHEN_waveform_amplitude_is_set_THEN_the_device_is_updated_to_that_value_with_channel_conversion_factor_applied(
            self):
        input_values = [123.4, 567.8, 91.2]
        conversion_factors = [
            float(self.ca.get_pv_value("POS:SCALE")) * 1000,
            float(self.ca.get_pv_value("STRESS:SCALE")) /
            float(self.ca.get_pv_value("STRESS:AREA")),
            float(self.ca.get_pv_value("STRAIN:SCALE")) * 100000 *
            float(self.ca.get_pv_value("STRAIN:LENGTH"))
        ]

        for i in range(len(conversion_factors)):
            self.assertNotEqual(0, conversion_factors[i],
                                "Factor {} was zero".format(i))

        expected_values = [
            input_values[i] / conversion_factors[i]
            for i in range(NUMBER_OF_CHANNELS)
        ]
        assert len(expected_values) == len(conversion_factors) == len(
            input_values) == NUMBER_OF_CHANNELS

        # Do this as two separate loops so that we can verify that all 3 channel values are stored independently
        for device_channel in range(NUMBER_OF_CHANNELS):
            self.ca.set_pv_value("CHANNEL:SP.VAL", device_channel)
            self.ca.set_pv_value(wave_prefixed("AMP:SP"),
                                 input_values[device_channel])
            self.ca.assert_that_pv_is(wave_prefixed("AMP"),
                                      expected_values[device_channel])
            self.ca.assert_that_pv_is(wave_prefixed("AMP:SP:RBV"),
                                      input_values[device_channel])

        for device_channel in range(NUMBER_OF_CHANNELS):
            self.ca.set_pv_value("CHANNEL:SP.VAL", device_channel)
            self.ca.assert_that_pv_is(wave_prefixed("AMP"),
                                      expected_values[device_channel])
            self.ca.assert_that_pv_is(wave_prefixed("AMP:SP:RBV"),
                                      input_values[device_channel])

    @skip_if_recsim("RECSIM does not capture dynamic behaviour")
    def test_WHEN_the_quarter_counter_is_off_THEN_the_number_of_counts_is_and_remains_zero(
            self):
        self.ca.set_pv_value(quart_prefixed("OFF"), 1)
        self.ca.assert_that_pv_is("QUART", 0)
        self.ca.assert_that_pv_is("QUART", 0, timeout=5)

    @skip_if_recsim("Status more complicated than RECSIM can handle")
    def test_WHEN_the_quarter_counter_is_armed_THEN_the_status_is_armed(self):
        self.ca.set_pv_value(quart_prefixed("ARM"), 1)
        self.ca.assert_that_pv_is(quart_prefixed("STATUS"), "Armed")

    @skip_if_recsim("Counting part of dynamic device behaviour")
    def test_WHEN_the_waveform_generator_is_started_THEN_the_quarter_counter_starts_counting_and_keeps_increasing(
            self):
        self.ca.set_pv_value(wave_prefixed("START"), 1)
        self.ca.assert_that_pv_value_is_increasing("QUART", 5)

    @skip_if_recsim("Status more complicated than RECSIM can handle")
    def test_WHEN_the_quarter_counter_is_armed_THEN_the_number_of_quarts_never_exceeds_the_requested_maximum(
            self):
        cycles = 5
        self.ca.set_pv_value(quart_prefixed("CYCLE:SP"), cycles)
        self.ca.assert_that_pv_is(quart_prefixed("SP"), cycles * 4)
        self.ca.set_pv_value(wave_prefixed("START"), 1)
        while self.ca.get_pv_value(quart_prefixed("STATUS")) == "Armed":
            self.assertLessEqual(float(self.ca.get_pv_value("QUART") / 4.0),
                                 cycles)
        self.ca.assert_that_pv_is(quart_prefixed("STATUS"), "Tripped")

    def test_GIVEN_the_waveform_generator_is_stopped_WHEN_instructed_to_hold_THEN_status_is_stopped(
            self):
        self.ca.assert_that_pv_is(wave_prefixed("STATUS"), "Stopped")
        self.ca.set_pv_value(wave_prefixed("HOLD"), 1)
        self.ca.assert_that_pv_is(wave_prefixed("STATUS"), "Stopped")

    @skip_if_recsim("No backdoor in LewisNone")
    def test_GIVEN_the_waveform_generator_is_running_WHEN_instructed_to_hold_THEN_status_is_holding(
            self):
        self._lewis.backdoor_command(
            ["device", "set_waveform_state", WAVEFORM_RUNNING])
        self.ca.assert_that_pv_is(wave_prefixed("STATUS"), "Running")
        self.ca.set_pv_value(wave_prefixed("HOLD"), 1)
        self.ca.assert_that_pv_is(wave_prefixed("STATUS"), "Holding")

    @skip_if_recsim("No backdoor in LewisNone")
    def test_GIVEN_the_waveform_generator_is_holding_WHEN_instructed_to_hold_THEN_status_is_holding(
            self):
        self._lewis.backdoor_command(
            ["device", "set_waveform_state", WAVEFORM_HOLDING])
        self.ca.assert_that_pv_is(wave_prefixed("STATUS"), "Holding")
        self.ca.set_pv_value(wave_prefixed("HOLD"), 1)
        self.ca.assert_that_pv_is(wave_prefixed("STATUS"), "Holding")

    @skip_if_recsim("No backdoor in LewisNone")
    def test_GIVEN_the_waveform_generator_is_finishing_WHEN_instructed_to_hold_THEN_status_is_finishing(
            self):
        self._lewis.backdoor_command(
            ["device", "set_waveform_state", WAVEFORM_FINISHING])
        self.ca.assert_that_pv_is(wave_prefixed("STATUS"), "Finishing")
        self.ca.set_pv_value(wave_prefixed("HOLD"), 1)
        self.ca.assert_that_pv_is(wave_prefixed("STATUS"), "Finishing")

    def verify_channel_abs(self, expected_value):
        self.ca.assert_that_pv_is("POS:RAMP:WFTYP", expected_value)
        self.ca.assert_that_pv_is("STRAIN:RAMP:WFTYP", expected_value)
        self.ca.assert_that_pv_is("STRESS:RAMP:WFTYP", expected_value)

    def test_WHEN_the_waveform_generator_is_started_THEN_every_axis_is_set_to_ramp(
            self):
        self.ca.set_pv_value(wave_prefixed("START"), 1)
        self.verify_channel_abs(RAMP_WAVEFORM_TYPES[0])

    def test_WHEN_the_waveform_generator_is_stopped_THEN_every_axis_is_set_to_absolute_ramp(
            self):
        self.ca.set_pv_value(wave_prefixed("STOP"), 1)
        self.verify_channel_abs(RAMP_WAVEFORM_TYPES[3])

    @skip_if_recsim("Different statuses don't interact in RECSIM")
    def test_WHEN_the_waveform_generator_is_started_THEN_the_quarter_counter_is_armed(
            self):
        self.ca.set_pv_value(wave_prefixed("START"), 1)
        self.ca.assert_that_pv_is(quart_prefixed("STATUS"), "Armed")

    def test_WHEN_the_max_cyles_is_set_THEN_the_readback_matches_setpoint(
            self):
        value = 7
        self.ca.assert_setting_setpoint_sets_readback(
            value=value,
            set_point_pv=quart_prefixed("CYCLE:SP"),
            readback_pv=quart_prefixed("CYCLE:SP:RBV"))
Ejemplo n.º 15
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)
Ejemplo n.º 16
0
class Dh2000Tests(unittest.TestCase):
    """
    Tests for the Dh2000 IOC.
    """
    def setUp(self):
        self._lewis, self._ioc = get_running_lewis_and_ioc(
            "dh2000", DEVICE_PREFIX)
        self.ca = ChannelAccess(device_prefix=DEVICE_PREFIX)

        self._lewis.backdoor_set_on_device("shutter_is_open", False)
        self._lewis.backdoor_set_on_device("interlock_is_triggered", False)
        self._lewis.backdoor_set_on_device("is_connected", True)

        self.ca.assert_that_pv_is("SHUTTER:A", "Closed")
        self.ca.assert_that_pv_is("INTERLOCK", "OK")

    @parameterized.expand([("shutter_open_interlock_off", True, False),
                           ("shutter_closed_interlock_off", False, False),
                           ("shutter_closed_interlock_on", False, True)])
    def test_GIVEN_device_in_a_state_WHEN_status_requested_THEN_shutter_and_interlock_status_returned(
            self, _, shutter_is_open, interlock_is_triggered):
        # GIVEN
        self._lewis.backdoor_set_on_device("shutter_is_open", shutter_is_open)
        self._lewis.backdoor_set_on_device("interlock_is_triggered",
                                           interlock_is_triggered)

        # THEN
        self.ca.assert_that_pv_is(
            "INTERLOCK", "TRIGGERED" if interlock_is_triggered else "OK")
        self.ca.assert_that_pv_is("SHUTTER:A",
                                  "Open" if shutter_is_open else "Closed")

    def test_GIVEN_shutter_open_WHEN_interlock_triggered_THEN_shutter_closes(
            self):
        self._lewis.backdoor_set_on_device("shutter_is_open", True)
        self._lewis.backdoor_set_on_device("interlock_is_triggered", False)

        # GIVEN
        self.ca.assert_that_pv_is("SHUTTER:A", "Open")
        self.ca.assert_that_pv_is("INTERLOCK", "OK")

        # WHEN
        self._lewis.backdoor_set_on_device("interlock_is_triggered", True)

        # THEN
        self.ca.assert_that_pv_is("INTERLOCK", "TRIGGERED")
        self.ca.assert_that_pv_is("SHUTTER:A", "Closed")

    def test_GIVEN_shutter_open_WHEN_shutter_close_requested_THEN_shutter_closes(
            self):
        # GIVEN
        self._lewis.backdoor_set_on_device("shutter_is_open", True)
        self.ca.assert_that_pv_is("SHUTTER:A", "Open")

        # WHEN
        self.ca.set_pv_value("SHUTTER:A:SP", "Close")

        # THEN
        self.ca.assert_that_pv_is("SHUTTER:A", "Closed")

    def test_GIVEN_shutter_closed_and_interlock_not_triggered_WHEN_shutter_open_requested_THEN_shutter_opens(
            self):
        # GIVEN
        self.ca.assert_that_pv_is("INTERLOCK", "OK")
        self.ca.assert_that_pv_is("SHUTTER:A", "Closed")

        # WHEN
        self.ca.set_pv_value("SHUTTER:A:SP", "Open")

        # THEN
        self.ca.assert_that_pv_is("SHUTTER:A", "Open")

    def test_GIVEN_shutter_closed_and_interlock_triggered_WHEN_shutter_open_requested_THEN_shutter_does_not_open(
            self):
        # GIVEN
        self._lewis.backdoor_set_on_device("interlock_is_triggered", True)
        self.ca.assert_that_pv_is("INTERLOCK", "TRIGGERED")
        self.ca.assert_that_pv_is("SHUTTER:A", "Closed")

        # WHEN
        self.ca.set_pv_value("SHUTTER:A:SP", "Open")

        # THEN
        self.ca.assert_that_pv_is("SHUTTER:A", "Closed")

    def test_GIVEN_interlock_triggered_THEN_interlock_PV_has_major_alarm(self):
        # GIVEN
        self._lewis.backdoor_set_on_device("interlock_is_triggered", True)

        self.ca.assert_that_pv_alarm_is("INTERLOCK", self.ca.Alarms.MAJOR)

    def test_GIVEN_disconnected_device_THEN_interlock_and_shutter_status_show_INVALID(
            self):
        self._lewis.backdoor_set_on_device("is_connected", False)

        self.ca.assert_that_pv_alarm_is("SHUTTER:A", self.ca.Alarms.INVALID)
        self.ca.assert_that_pv_alarm_is("INTERLOCK", self.ca.Alarms.INVALID)
class Amint2lTests(unittest.TestCase):
    """
    Tests for the AM Int2-L.
    """
    def setUp(self):
        self._lewis, self._ioc = get_running_lewis_and_ioc(
            "amint2l", DEVICE_PREFIX)
        self.assertIsNotNone(self._lewis)
        self.assertIsNotNone(self._ioc)

        self.ca = ChannelAccess(device_prefix=DEVICE_PREFIX)
        self._lewis.backdoor_set_on_device('connected', True)
        self._lewis.backdoor_set_on_device("address", ADDRESS)

    def _set_pressure(self, expected_pressure):
        self._lewis.backdoor_set_on_device("pressure", expected_pressure)
        self._ioc.set_simulated_value("SIM:PRESSURE", expected_pressure)

    def test_GIVEN_pressure_set_WHEN_read_THEN_pressure_is_as_expected(self):
        expected_pressure = 1.23
        self._set_pressure(expected_pressure)

        self.ca.assert_that_pv_is("PRESSURE", expected_pressure)
        self.ca.assert_that_pv_alarm_is("PRESSURE", self.ca.Alarms.NONE)
        self.ca.assert_that_pv_is("RANGE:ERROR", "No Error")

    def test_GIVEN_negative_pressure_set_WHEN_read_THEN_pressure_is_as_expected(
            self):
        expected_pressure = -123.34
        self._set_pressure(expected_pressure)

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

    def test_GIVEN_pressure_with_no_decimal_places_set_WHEN_read_THEN_pressure_is_as_expected(
            self):
        expected_pressure = 7
        self._set_pressure(expected_pressure)

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

    @skip_if_recsim("In rec sim this test fails")
    def test_GIVEN_pressure_over_range_set_WHEN_read_THEN_error(self):
        expected_pressure = "OR"
        self._set_pressure(expected_pressure)

        self.ca.assert_that_pv_alarm_is("PRESSURE", self.ca.Alarms.INVALID)
        self.ca.assert_that_pv_is("RANGE:ERROR", "Over Range")

    @skip_if_recsim("In rec sim this test fails")
    def test_GIVEN_pressure_under_range_set_WHEN_read_THEN_error(self):
        expected_pressure = "UR"
        self._set_pressure(expected_pressure)

        self.ca.assert_that_pv_alarm_is("PRESSURE", self.ca.Alarms.INVALID)
        self.ca.assert_that_pv_is("RANGE:ERROR", "Under Range")

    @skip_if_recsim("In rec sim this test fails")
    def test_GIVEN_device_disconnected_WHEN_read_THEN_pv_shows_disconnect(
            self):
        self._lewis.backdoor_set_on_device("pressure", None)
        # Setting none simulates no response from device which is like pulling the serial cable. Disconnecting the
        # emulator using the backdoor makes the record go udf not timeout which is what the actual device does.

        self.ca.assert_that_pv_alarm_is("PRESSURE", self.ca.Alarms.INVALID)

    @skip_if_recsim("Can not test disconnection in recsim")
    def test_GIVEN_device_not_connected_WHEN_get_pressure_THEN_alarm(self):
        self._lewis.backdoor_set_on_device('connected', False)
        self.ca.assert_that_pv_alarm_is('PRESSURE',
                                        ChannelAccess.Alarms.INVALID)
Ejemplo n.º 18
0
class IceFridgeTests(unittest.TestCase):
    """
    Tests for the IceFrdge IOC.
    """
    def setUp(self):
        self._lewis, self._ioc = get_running_lewis_and_ioc(IOCS[0]["emulator"], DEVICE_PREFIX)
        self.ca = ChannelAccess(device_prefix=DEVICE_PREFIX, default_timeout=25)

        if not IOCRegister.uses_rec_sim:
            self._lewis.backdoor_run_function_on_device("reset")
            self._lewis.backdoor_set_on_device("connected", True)

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

    @parameterized.expand(parameterized_list(VTI_TEMP_SUFFIXES))
    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_VTI_temp_set_backdoor_THEN_ioc_read_correctly(self, _, temp_num):
        self._lewis.backdoor_run_function_on_device("set_cryo_temp", (temp_num, 3.6))
        self.ca.assert_that_pv_is_number("VTI:TEMP{}".format(temp_num), 3.6, 0.001)

    @parameterized.expand(parameterized_list(itertools.product(VTI_LOOPS, VTI_LOOP_TEST_INPUTS)))
    def test_WHEN_vti_loop_setpoint_THEN_readback_identical(self, _, loop_num, temp):
        self.ca.assert_setting_setpoint_sets_readback(temp, "VTI:LOOP{}:TSET".format(loop_num),
                                                      "VTI:LOOP{}:TSET:SP".format(loop_num))

    @parameterized.expand(parameterized_list(itertools.product(VTI_LOOPS, VTI_LOOP_TEST_INPUTS)))
    def test_WHEN_vti_loop_proportional_THEN_readback_identical(self, _, loop_num, temp):
        self.ca.assert_setting_setpoint_sets_readback(temp, "VTI:LOOP{}:P".format(loop_num),
                                                      "VTI:LOOP{}:P:SP".format(loop_num))

    @parameterized.expand(parameterized_list(itertools.product(VTI_LOOPS, VTI_LOOP_TEST_INPUTS)))
    def test_WHEN_vti_loop_integral_THEN_readback_identical(self, _, loop_num, temp):
        self.ca.assert_setting_setpoint_sets_readback(temp, "VTI:LOOP{}:I".format(loop_num),
                                                      "VTI:LOOP{}:I:SP".format(loop_num))

    @parameterized.expand(parameterized_list(itertools.product(VTI_LOOPS, VTI_LOOP_TEST_INPUTS)))
    def test_WHEN_vti_loop_derivative_THEN_readback_identical(self, _, loop_num, temp):
        self.ca.assert_setting_setpoint_sets_readback(temp, "VTI:LOOP{}:D".format(loop_num),
                                                      "VTI:LOOP{}:D:SP".format(loop_num))

    @parameterized.expand(parameterized_list(itertools.product(VTI_LOOPS, VTI_LOOP_TEST_INPUTS)))
    def test_WHEN_vti_loop_ramp_rate_THEN_readback_identical(self, _, loop_num, temp):
        self.ca.assert_setting_setpoint_sets_readback(temp, "VTI:LOOP{}:RAMPRATE".format(loop_num),
                                                      "VTI:LOOP{}:RAMPRATE:SP".format(loop_num))

    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_Lakeshore_MC_Cernox_set_backdoor_THEN_ioc_read_correctly(self):
        self._lewis.backdoor_set_on_device("lakeshore_mc_cernox", 0.5)
        self.ca.assert_that_pv_is_number("LS:MC:CERNOX", 0.5, 0.001)

    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_Lakeshore_MC_RuO_set_backdoor_THEN_ioc_read_correctly(self):
        self._lewis.backdoor_set_on_device("lakeshore_mc_ruo", 0.6)
        self.ca.assert_that_pv_is_number("LS:MC:RUO", 0.6, 0.001)

    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_Lakeshore_still_temp_set_backdoor_THEN_ioc_read_correctly(self):
        self._lewis.backdoor_set_on_device("lakeshore_still_temp", 0.7)
        self.ca.assert_that_pv_is_number("LS:STILL:TEMP", 0.7, 0.001)

    def test_WHEN_Lakeshore_MC_setpoint_THEN_readback_identical(self):
        self.ca.assert_setting_setpoint_sets_readback(0.8, "LS:MC:TEMP", "LS:MC:TEMP:SP")

    @skip_if_recsim("Lewis assertion not working in recsim")
    def test_WHEN_Lakeshore_MC_setpoint_is_zero_THEN_scan_correct(self):
        self.ca.set_pv_value("LS:MC:TEMP:SP", 0)
        self._lewis.assert_that_emulator_value_is("lakeshore_scan", 1, 15)
        self._lewis.assert_that_emulator_value_is("lakeshore_cmode", 4, 15)

    @skip_if_recsim("Lewis assertion not working in recsim")
    def test_WHEN_Lakeshore_MC_setpoint_is_larger_than_zero_THEN_scan_correct(self):
        self.ca.set_pv_value("LS:MC:TEMP:SP", 4)
        self._lewis.assert_that_emulator_value_is("lakeshore_scan", 0, 15)
        self._lewis.assert_that_emulator_value_is("lakeshore_cmode", 1, 15)

    def test_WHEN_Lakeshore_MC_setpoint_negative_THEN_readback_zero(self):
        self.ca.set_pv_value("LS:MC:TEMP:SP", -1)

        self.ca.assert_that_pv_is("LS:MC:TEMP", 0)

    def test_WHEN_Lakeshore_MC_setpoint_over_limit_THEN_readback_at_limit(self):
        self.ca.set_pv_value("LS:MC:TEMP:SP", 301)

        self.ca.assert_that_pv_is("LS:MC:TEMP", 300)

    def test_WHEN_Lakeshore_MC_proportional_THEN_readback_identical(self):
        self.ca.assert_setting_setpoint_sets_readback(0.9, "LS:MC:P", "LS:MC:P:SP")

    @skip_if_recsim("pv updated when other pv processes, has no scan field")
    def test_WHEN_Lakeshore_MC_integral_THEN_readback_identical(self):
        self.ca.assert_setting_setpoint_sets_readback(11, "LS:MC:I", "LS:MC:I:SP")

    @skip_if_recsim("pv updated when other pv processes, has no scan field")
    def test_WHEN_Lakeshore_MC_derivative_THEN_readback_identical(self):
        self.ca.assert_setting_setpoint_sets_readback(12, "LS:MC:D", "LS:MC:D:SP")

    @parameterized.expand(parameterized_list(LS_MC_HTR_RANGE_VALUES))
    def test_WHEN_Lakeshore_MC_heater_range_THEN_readback_identical(self, _, heater_range):
        self.ca.assert_setting_setpoint_sets_readback(heater_range, "LS:MC:HTR:RANGE", "LS:MC:HTR:RANGE:SP")

    @parameterized.expand(parameterized_list(LS_MC_HTR_RANGE_INVALID_VALUES))
    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_lakeshore_MC_heater_range_invalid_setpoint_THEN_pv_in_alarm(self, _, invalid_range):
        self.ca.assert_that_pv_alarm_is("LS:MC:HTR:RANGE", self.ca.Alarms.NONE, timeout=15)

        self._lewis.backdoor_set_on_device("lakeshore_mc_heater_range", invalid_range)
        self.ca.assert_that_pv_alarm_is("LS:MC:HTR:RANGE", self.ca.Alarms.INVALID, timeout=15)

    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_Lakeshore_MC_heater_percentage_set_backdoor_THEN_ioc_read_correctly(self):
        self._lewis.backdoor_set_on_device("lakeshore_mc_heater_percentage", 50)
        self.ca.assert_that_pv_is_number("LS:MC:HTR:PERCENT", 50, 0.001)

    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_Lakeshore_MC_still_output_set_backdoor_THEN_ioc_read_correctly(self):
        self._lewis.backdoor_set_on_device("lakeshore_still_output", 1.3)
        self.ca.assert_that_pv_is_number("LS:STILL", 1.3, 0.001)

    @parameterized.expand(parameterized_list(itertools.product(LS_VOLTAGE_CHANNELS, LS_VOLTAGE_RANGE_VALUES)))
    def test_WHEN_Lakeshore_voltage_range_THEN_readback_identical(self, _, voltage_channel, voltage_value):
        self.ca.assert_setting_setpoint_sets_readback(voltage_value, "LS:VLTG:RANGE:CH{}".format(voltage_channel),
                                                      "LS:VLTG:RANGE:SP")

    @parameterized.expand(parameterized_list(itertools.product(LS_VOLTAGE_CHANNELS, LS_VOLTAGE_RANGE_INVALID_VALUES)))
    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_Lakeshore_voltage_range_invalid_setpoint_THEN_pv_in_alarm(self, _, voltage_channel, invalid_range):
        self.ca.assert_that_pv_alarm_is("LS:VLTG:RANGE:CH{}".format(voltage_channel), self.ca.Alarms.NONE,
                                        timeout=15)

        self._lewis.backdoor_set_on_device("lakeshore_exc_voltage_range_ch{}".format(voltage_channel), invalid_range)
        self.ca.assert_that_pv_alarm_is("LS:VLTG:RANGE:CH{}".format(voltage_channel), self.ca.Alarms.INVALID,
                                        timeout=15)

    @parameterized.expand(parameterized_list(MIMIC_PRESSURE_SUFFIXES))
    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_pressure_set_backdoor_THEN_ioc_read_correctly(self, _, pressure_num):
        self._lewis.backdoor_run_function_on_device("set_pressure", (pressure_num, 1.4))
        self.ca.assert_that_pv_is_number("PRESSURE{}".format(pressure_num), 1.4, 0.001)

    @parameterized.expand(parameterized_list(MIMIC_VALVE_NUMBERS))
    @skip_if_recsim("pv updated when other pv processes, has no scan field")
    def test_WHEN_valve_status_open_THEN_readback_identical(self, _, valve_num):
        self.ca.assert_setting_setpoint_sets_readback("OPEN", "VALVE{}".format(valve_num),
                                                      "VALVE{}:SP".format(valve_num))

    @parameterized.expand(parameterized_list(MIMIC_VALVE_NUMBERS))
    @skip_if_recsim("pv updated when other pv processes, has no scan field")
    def test_WHEN_valve_status_closed_THEN_readback_identical(self, _, valve_num):
        self.ca.assert_setting_setpoint_sets_readback("OPEN", "VALVE{}".format(valve_num),
                                                      "VALVE{}:SP".format(valve_num))

        self.ca.assert_setting_setpoint_sets_readback("CLOSED", "VALVE{}".format(valve_num),
                                                      "VALVE{}:SP".format(valve_num))

    @parameterized.expand(parameterized_list(MIMIC_PROPORTIONAL_VALVES_NUMBERS))
    @skip_if_recsim("pv updated when other pv processes, has no scan field")
    def test_WHEN_proportional_valve_THEN_readback_identical(self, _, proportional_valve_num):
        self.ca.assert_setting_setpoint_sets_readback(1.5, "PROPORTIONAL_VALVE{}".format(proportional_valve_num),
                                                      "PROPORTIONAL_VALVE{}:SP".format(proportional_valve_num))

    @parameterized.expand(parameterized_list(itertools.product(MIMIC_PROPORTIONAL_VALVES_NUMBERS, [0.001, 2])))
    @skip_if_recsim("pv updated when other pv processes, has no scan field")
    def test_WHEN_proportional_valve_not_0_THEN_calc_is_one(self, _, proportional_valve_num, test_value):
        self.ca.set_pv_value("PROPORTIONAL_VALVE{}:SP".format(proportional_valve_num), test_value)

        self.ca.assert_that_pv_is("PROPORTIONAL_VALVE{}:_CALC".format(proportional_valve_num), 1)

    @parameterized.expand(parameterized_list(MIMIC_PROPORTIONAL_VALVES_NUMBERS))
    @skip_if_recsim("pv updated when other pv processes, has no scan field")
    def test_WHEN_proportional_valve_0_THEN_calc_is_zero(self, _, proportional_valve_num):
        self.ca.set_pv_value("PROPORTIONAL_VALVE{}:SP".format(proportional_valve_num), 1)
        self.ca.assert_that_pv_is("PROPORTIONAL_VALVE{}:_CALC".format(proportional_valve_num), 1)

        self.ca.set_pv_value("PROPORTIONAL_VALVE{}:SP".format(proportional_valve_num), 0)
        self.ca.assert_that_pv_is("PROPORTIONAL_VALVE{}:_CALC".format(proportional_valve_num), 0)

    @parameterized.expand(parameterized_list(MIMIC_PROPORTIONAL_VALVES_NUMBERS))
    @skip_if_recsim("pv updated when other pv processes, has no scan field")
    def test_WHEN_proportional_valve_sp_negative_THEN_readback_zero(self, _, proportional_valve_num):
        self.ca.set_pv_value("PROPORTIONAL_VALVE{}:SP".format(proportional_valve_num), -1)

        self.ca.assert_that_pv_is("PROPORTIONAL_VALVE{}".format(proportional_valve_num), 0)

    @parameterized.expand(parameterized_list(MIMIC_PROPORTIONAL_VALVES_NUMBERS))
    @skip_if_recsim("pv updated when other pv processes, has no scan field")
    def test_WHEN_proportional_valve_sp_over_limit_THEN_readback_at_limit(self, _, proportional_valve_num):
        self.ca.set_pv_value("PROPORTIONAL_VALVE{}:SP".format(proportional_valve_num), 101)

        self.ca.assert_that_pv_is("PROPORTIONAL_VALVE{}".format(proportional_valve_num), 100)

    @skip_if_recsim("pv updated when other pv processes, has no scan field")
    def test_WHEN_needle_valve_THEN_readback_identical(self):
        self.ca.assert_setting_setpoint_sets_readback(1.6, "NEEDLE_VALVE", "NEEDLE_VALVE:SP")

    @parameterized.expand(parameterized_list([0.001, 2]))
    @skip_if_recsim("pv updated when other pv processes, has no scan field")
    def test_WHEN_needle_valve_not_0_THEN_calc_is_one(self, _, test_value):
        self.ca.set_pv_value("NEEDLE_VALVE:SP", test_value)
        self.ca.assert_that_pv_is("NEEDLE_VALVE:_CALC", 1)

    @skip_if_recsim("pv updated when other pv processes, has no scan field")
    def test_WHEN_needle_valve_0_THEN_calc_is_zero(self):
        self.ca.set_pv_value("NEEDLE_VALVE:SP", 0)
        self.ca.assert_that_pv_is("NEEDLE_VALVE:_CALC", 0)

    @skip_if_recsim("pv updated when other pv processes, has no scan field")
    def test_WHEN_needle_valve_sp_negative_THEN_readback_zero(self):
        self.ca.set_pv_value("NEEDLE_VALVE:SP", -1)

        self.ca.assert_that_pv_is("NEEDLE_VALVE", 0)

    @skip_if_recsim("pv updated when other pv processes, has no scan field")
    def test_WHEN_needle_valve_sp_over_limit_THEN_readback_at_limit(self):
        self.ca.set_pv_value("NEEDLE_VALVE:SP", 101)

        self.ca.assert_that_pv_is("NEEDLE_VALVE", 100)

    @parameterized.expand(parameterized_list(MIMIC_SOLENOID_VALVES_NUMBERS))
    @skip_if_recsim("pv updated when other pv processes, has no scan field")
    def test_WHEN_solenoid_valve_open_THEN_readback_identical(self, _, solenoid_valve_num):
        self.ca.assert_setting_setpoint_sets_readback("OPEN", "SOLENOID_VALVE{}".format(solenoid_valve_num),
                                                      "SOLENOID_VALVE{}:SP".format(solenoid_valve_num))

    @parameterized.expand(parameterized_list(MIMIC_SOLENOID_VALVES_NUMBERS))
    @skip_if_recsim("pv updated when other pv processes, has no scan field")
    def test_WHEN_solenoid_valve_close_THEN_readback_identical(self, _, solenoid_valve_num):
        self.ca.assert_setting_setpoint_sets_readback("OPEN", "SOLENOID_VALVE{}".format(solenoid_valve_num),
                                                      "SOLENOID_VALVE{}:SP".format(solenoid_valve_num))

        self.ca.assert_setting_setpoint_sets_readback("CLOSED", "SOLENOID_VALVE{}".format(solenoid_valve_num),
                                                      "SOLENOID_VALVE{}:SP".format(solenoid_valve_num))

    @skip_if_recsim("lewis backdoor not available in recsim")
    def test_WHEN_1K_stage_temp_THEN_ioc_read_correctly(self):
        self._lewis.backdoor_set_on_device("temp_1K_stage", 1.7)
        self.ca.assert_that_pv_is_number("1K:TEMP", 1.7, 0.001)

    @skip_if_recsim("lewis backdoor not available in recsim")
    def test_WHEN_MC_temperature_THEN_ioc_read_correctly(self):
        self._lewis.backdoor_set_on_device("mixing_chamber_temp", 1.8)
        self.ca.assert_that_pv_is_number("MC:TEMP", 1.8, 0.001)

    @skip_if_recsim("lewis backdoor not available in recsim")
    def test_WHEN_MC_resistance_THEN_ioc_read_correctly(self):
        self._lewis.backdoor_set_on_device("mixing_chamber_resistance", 1.9)
        self.ca.assert_that_pv_is_number("MC:_RESISTANCE", 1.9, 0.001)

    @skip_if_recsim("lewis backdoor not available in recsim")
    def test_WHEN_MC_resistance_calc_THEN_calculation_correct(self):
        self._lewis.backdoor_set_on_device("mixing_chamber_resistance", 1918)
        self.ca.assert_that_pv_is_number("MC:RESISTANCE:CALC", 1.918, 0.001)

    def test_WHEN_mimic_mode_manual_THEN_buttons_disabled(self):
        self.ca.set_pv_value("MIMIC:MODE:SP", "MANUAL")

        self.ca.assert_that_pv_is("MIMIC:START:SP.DISP", '1')
        self.ca.assert_that_pv_is("MIMIC:SKIP:SP.DISP", '1')
        self.ca.assert_that_pv_is("MIMIC:STOP:SP.DISP", '1')

    def test_WHEN_mimic_mode_automatic_THEN_buttons_disabled(self):
        self.ca.set_pv_value("MIMIC:MODE:SP", "AUTOMATIC")

        self.ca.assert_that_pv_is("MIMIC:START:SP.DISP", '1')
        self.ca.assert_that_pv_is("MIMIC:SKIP:SP.DISP", '1')
        self.ca.assert_that_pv_is("MIMIC:STOP:SP.DISP", '1')

    def test_WHEN_mimic_mode_semi_automatic_THEN_buttons_enabled(self):
        self.ca.set_pv_value("MIMIC:MODE:SP", "SEMI AUTOMATIC")

        self.ca.assert_that_pv_is("MIMIC:START:SP.DISP", '0')
        self.ca.assert_that_pv_is("MIMIC:SKIP:SP.DISP", '0')
        self.ca.assert_that_pv_is("MIMIC:STOP:SP.DISP", '0')

    @skip_if_recsim("Lewis assertion not working in recsim")
    def test_WHEN_mimic_skip_THEN_skipped(self):
        self._lewis.assert_that_emulator_value_is("skipped", False, 15)

        self.ca.set_pv_value("MIMIC:MODE:SP", "SEMI AUTOMATIC")
        # does not matter what value the pv is set to, only that it processes
        self.ca.set_pv_value("MIMIC:SKIP:SP", "SKIP")

        self._lewis.assert_that_emulator_value_is("skipped", True, 15)

    @skip_if_recsim("Lewis assertion not working in recsim")
    def test_WHEN_mimic_stop_THEN_stopped(self):
        self._lewis.assert_that_emulator_value_is("stopped", False, 15)

        self.ca.set_pv_value("MIMIC:MODE:SP", "SEMI AUTOMATIC")
        # does not matter what value the pv is set to, only that it processes
        self.ca.set_pv_value("MIMIC:STOP:SP", "STOP")

        self._lewis.assert_that_emulator_value_is("stopped", True, 15)

    @skip_if_recsim("Lewis assertion not working in recsim")
    def test_WHEN_mimic_sequence_condense_THEN_condense(self):
        self._lewis.assert_that_emulator_value_is("condense", False, 15)

        self.ca.set_pv_value("MIMIC:MODE:SP", "SEMI AUTOMATIC")
        self.ca.set_pv_value("MIMIC:SEQUENCE:SP", "Condense")
        # does not matter what value the pv is set to, only that it processes
        self.ca.set_pv_value("MIMIC:START:SP", "START")

        self._lewis.assert_that_emulator_value_is("condense", True, 15)

    @skip_if_recsim("Lewis assertion not working in recsim")
    def test_WHEN_mimic_sequence_circulate_THEN_circulate(self):
        self._lewis.assert_that_emulator_value_is("circulate", False, 15)

        self.ca.set_pv_value("MIMIC:MODE:SP", "SEMI AUTOMATIC")
        self.ca.set_pv_value("MIMIC:SEQUENCE:SP", "Circulate")
        # does not matter what value the pv is set to, only that it processes
        self.ca.set_pv_value("MIMIC:START:SP", "START")

        self._lewis.assert_that_emulator_value_is("circulate", True, 15)

    @skip_if_recsim("Lewis assertion not working in recsim")
    def test_WHEN_mimic_sequence_condense_and_circulate_THEN_condense_and_circulate(self):
        self._lewis.assert_that_emulator_value_is("condense", False, 15)
        self._lewis.assert_that_emulator_value_is("circulate", False, 15)

        self.ca.set_pv_value("MIMIC:MODE:SP", "SEMI AUTOMATIC")
        self.ca.set_pv_value("MIMIC:SEQUENCE:SP", "Condense & Circulate")
        # does not matter what value the pv is set to, only that it processes
        self.ca.set_pv_value("MIMIC:START:SP", "START")

        self._lewis.assert_that_emulator_value_is("condense", True, 15)
        self._lewis.assert_that_emulator_value_is("circulate", True, 15)

    @skip_if_recsim("Lewis assertion not working in recsim")
    def test_WHEN_mimic_sequence_temp_control_THEN_readback_identical(self):
        self._lewis.assert_that_emulator_value_is("temp_control", 0, 15)

        self.ca.set_pv_value("MIMIC:MODE:SP", "SEMI AUTOMATIC")
        self.ca.set_pv_value("MIMIC:SEQUENCE:SP", "Temperature Control")
        self.ca.set_pv_value("MIMIC:SEQUENCE:TEMP:SP", 2.3)
        # does not matter what value the pv is set to, only that it processes
        self.ca.set_pv_value("MIMIC:START:SP", "START")

        self.ca.assert_that_pv_is("MIMIC:SEQUENCE:TEMP", 2.3)

    @skip_if_recsim("Lewis assertion not working in recsim")
    def test_WHEN_mimic_sequence_make_safe_THEN_make_safe(self):
        self._lewis.assert_that_emulator_value_is("make_safe", False, 15)

        self.ca.set_pv_value("MIMIC:MODE:SP", "SEMI AUTOMATIC")
        self.ca.set_pv_value("MIMIC:SEQUENCE:SP", "Make Safe")
        # does not matter what value the pv is set to, only that it processes
        self.ca.set_pv_value("MIMIC:START:SP", "START")

        self._lewis.assert_that_emulator_value_is("make_safe", True, 15)

    @skip_if_recsim("Lewis assertion not working in recsim")
    def test_WHEN_mimic_sequence_warm_up_THEN_warm_up(self):
        self._lewis.assert_that_emulator_value_is("warm_up", False, 15)

        self.ca.set_pv_value("MIMIC:MODE:SP", "SEMI AUTOMATIC")
        self.ca.set_pv_value("MIMIC:SEQUENCE:SP", "Warm Up")
        # does not matter what value the pv is set to, only that it processes
        self.ca.set_pv_value("MIMIC:START:SP", "START")

        self._lewis.assert_that_emulator_value_is("warm_up", True, 15)

    @skip_if_recsim("Lewis backdoor not working in recsim")
    def test_WHEN_mimic_info_THEN_ioc_read_correctly(self):
        self._lewis.backdoor_set_on_device("mimic_info", "RBMK reactors do not explode!")
        self.ca.assert_that_pv_is("MIMIC:INFO", "RBMK reactors do not explode!")

    @skip_if_recsim("Lewis backdoor not working in recsim")
    def test_WHEN_state_THEN_ioc_read_correctly(self):
        self._lewis.backdoor_set_on_device("state", "It\\'s disgraceful, really!")
        self.ca.assert_that_pv_is("STATE", "It's disgraceful, really!")

    def test_WHEN_nv_mode_setpoint_manual_THEN_readback_identical(self):
        self.ca.assert_setting_setpoint_sets_readback("MANUAL", "NVMODE", "NVMODE:SP")

    def test_WHEN_nv_mode_setpoint_auto_THEN_readback_identical(self):
        self.ca.assert_setting_setpoint_sets_readback("MANUAL", "NVMODE", "NVMODE:SP")

        self.ca.assert_setting_setpoint_sets_readback("AUTO", "NVMODE", "NVMODE:SP")

    def test_WHEN_1K_pump_off_THEN_readback_identical(self):
        self.ca.assert_setting_setpoint_sets_readback("ON", "1K:PUMP", "1K:PUMP:SP")

        self.ca.assert_setting_setpoint_sets_readback("OFF", "1K:PUMP", "1K:PUMP:SP")

    def test_WHEN_1K_pump_on_THEN_readback_identical(self):
        self.ca.assert_setting_setpoint_sets_readback("OFF", "1K:PUMP", "1K:PUMP:SP")

        self.ca.assert_setting_setpoint_sets_readback("ON", "1K:PUMP", "1K:PUMP:SP")

    @skip_if_recsim("pv updated when other pv processes, has no scan field")
    def test_WHEN_He3_pump_off_THEN_readback_identical(self):
        self.ca.assert_setting_setpoint_sets_readback("OFF", "HE3:PUMP", "HE3:PUMP:SP")

    @skip_if_recsim("pv updated when other pv processes, has no scan field")
    def test_WHEN_He3_pump_on_THEN_readback_identical(self):
        self.ca.assert_setting_setpoint_sets_readback("OFF", "HE3:PUMP", "HE3:PUMP:SP")

        self.ca.assert_setting_setpoint_sets_readback("ON", "HE3:PUMP", "HE3:PUMP:SP")

    @skip_if_recsim("pv updated when other pv processes, has no scan field")
    def test_WHEN_roots_pump_off_THEN_readback_identical(self):
        self.ca.assert_setting_setpoint_sets_readback("OFF", "ROOTS", "ROOTS:SP")

    @skip_if_recsim("pv updated when other pv processes, has no scan field")
    def test_WHEN_roots_pump_on_THEN_readback_identical(self):
        self.ca.assert_setting_setpoint_sets_readback("OFF", "ROOTS", "ROOTS:SP")

        self.ca.assert_setting_setpoint_sets_readback("ON",  "ROOTS", "ROOTS:SP")

    @skip_if_recsim("testing lack of connection to device makes no sense in recsim")
    def test_WHEN_ioc_disconnected_THEN_all_pvs_in_alarm(self):
        for pv in TEST_ALARM_STATUS_PVS:
            self.ca.assert_that_pv_alarm_is(pv, self.ca.Alarms.NONE)

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

        for pv in TEST_ALARM_STATUS_PVS:
            self.ca.assert_that_pv_alarm_is(pv, self.ca.Alarms.INVALID)
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)
Ejemplo n.º 20
0
class VoltageTests(unittest.TestCase):
    voltage_values = [0, 10.1111111, 10e1, 20e-2, 200]
    voltage_values_which_give_alarms = [
        -50, MIN_SEPARATOR_VOLT, MAX_SEPARATOR_VOLT, 250
    ]

    def setUp(self):
        self.ca = ChannelAccess(20, device_prefix=DEVICE_PREFIX)
        shared_setup(self.ca)

    def test_that_GIVEN_sim_val_0_and_data_0_WHEN_voltage_set_point_changed_THEN_data_changed(
            self):
        # GIVEN
        self.ca.set_pv_value("DAQ:VOLT:SIM", 0)
        self.ca.assert_that_pv_is("DAQ:VOLT:SP", 0)

        # WHEN
        self.ca.set_pv_value("VOLT:SP", 20.)

        # THEN
        self.ca.assert_that_pv_is("DAQ:VOLT:SP",
                                  20. * DAQ_VOLT_WRITE_SCALE_FACTOR)

    @parameterized.expand(parameterized_list(voltage_values))
    def test_that_WHEN_set_THEN_the_voltage_changes(self, _, value):
        # WHEN
        self.ca.set_pv_value("VOLT:SP", value)

        # THEN
        self.ca.assert_that_pv_is_number("VOLT", value, MARGIN_OF_ERROR)

    @parameterized.expand(parameterized_list(voltage_values_which_give_alarms))
    def test_that_WHEN_voltage_out_of_range_THEN_alarm_raised(self, _, value):
        # WHEN

        self.ca.set_pv_value("VOLT:SP", value)

        # THEN
        self.ca.assert_that_pv_alarm_is("VOLT", ChannelAccess.Alarms.MAJOR)

    def test_that_GIVEN_voltage_in_range_WHEN_setpoint_is_above_range_THEN_setpoint_is_set_to_max_value(
            self):
        # GIVEN
        self.ca.set_pv_value("VOLT:SP", 30)
        self.ca.assert_that_pv_is("VOLT", 30)

        # WHEN
        self.ca.set_pv_value("VOLT:SP", 215.)

        # THEN
        self.ca.assert_that_pv_is("VOLT:SP", MAX_SEPARATOR_VOLT)
        self.ca.assert_that_pv_is("VOLT", MAX_SEPARATOR_VOLT)

    def test_that_GIVEN_voltage_in_range_WHEN_setpoint_is_below_range_THEN_setpoint_is_set_to_min_value(
            self):
        # GIVEN
        self.ca.set_pv_value("VOLT:SP", 30)
        self.ca.assert_that_pv_is("VOLT", 30)

        # WHEN
        self.ca.set_pv_value("VOLT:SP", -50)

        # THEN
        self.ca.assert_that_pv_is("VOLT", MIN_SEPARATOR_VOLT)
        self.ca.assert_that_pv_is("VOLT:SP", MIN_SEPARATOR_VOLT)

    def test_GIVEN_data_to_be_filtered_WHEN_filtering_applied_THEN_returned_data_has_correct_shape(
            self):
        # stride_length = 1
        # GIVEN
        self.ca.set_pv_value("DAQ:VOLT:WV:SIM", DAQ_DATA)

        # THEN
        returned_data_shape = self.ca.get_pv_value("FILTERED:VOLT.NORD")

        # THEN
        self.assertEqual(returned_data_shape, len(DAQ_DATA) - STRIDE_LENGTH)

    def test_GIVEN_unfiltered_data_WHEN_filtering_applied_THEN_corrected_data_is_returned(
            self):
        # GIVEN
        # Writing directly to DAQ:VOLT, need to remove the scaling factor
        self.ca.set_pv_value("DAQ:VOLT:WV:SIM",
                             DAQ_DATA * DAQ_VOLT_WRITE_SCALE_FACTOR)

        # THEN
        returned_data_shape = int(self.ca.get_pv_value("FILTERED:VOLT.NORD"))
        returned_data = self.ca.get_pv_value(
            "FILTERED:VOLT")[:returned_data_shape]

        filtered_data = apply_average_filter(DAQ_DATA)

        self.assertEqual(len(returned_data), len(filtered_data))

        for filtered_value, reference_value in zip(returned_data,
                                                   filtered_data):
            self.assertAlmostEqual(filtered_value, reference_value, places=3)
class Sans2dVacuumSystemTests(unittest.TestCase):
    """
    Tests for the SANS2D vacuum system, based on a FINS PLC.
    """
    def setUp(self):
        self._ioc = IOCRegister.get_running("FINS_01")
        self.ca = ChannelAccess(device_prefix=ioc_prefix)

    def test_WHEN_ioc_is_run_THEN_heartbeat_record_exists(self):
        self.ca.assert_setting_setpoint_sets_readback(1, "HEARTBEAT",
                                                      "SIM:HEARTBEAT")

    @parameterized.expand([(1, "IN", ChannelAccess.Alarms.NONE),
                           (2, "OUT", ChannelAccess.Alarms.NONE),
                           (3, "UNKNOWN", ChannelAccess.Alarms.MAJOR),
                           (4, "ERROR", ChannelAccess.Alarms.MAJOR),
                           (5, "ERROR(IN)", ChannelAccess.Alarms.MAJOR),
                           (6, "ERROR(OUT)", ChannelAccess.Alarms.MAJOR)])
    def test_WHEN_data_changes_THEN_monitor_status_correct(
            self, raw_value, enum_string, alarm):
        self.ca.set_pv_value("SIM:ADDR:1001", raw_value)
        self.ca.assert_that_pv_is("MONITOR3:STATUS", enum_string)
        self.ca.assert_that_pv_alarm_is("MONITOR3:STATUS", alarm)

    @parameterized.expand([(7, "CLOSED", ChannelAccess.Alarms.NONE),
                           (8, "OPEN", ChannelAccess.Alarms.NONE),
                           (16, "ERROR", ChannelAccess.Alarms.MAJOR),
                           (24, "ERROR(OPEN)", ChannelAccess.Alarms.MAJOR)])
    def test_WHEN_data_changes_THEN_shutter_status_correct(
            self, raw_value, enum_string, alarm):
        self.ca.set_pv_value("SIM:ADDR:1001", raw_value)
        self.ca.assert_that_pv_is("SHUTTER:STATUS", enum_string)
        self.ca.assert_that_pv_alarm_is("SHUTTER:STATUS", alarm)

    @parameterized.expand([(127, "CLOSED", ChannelAccess.Alarms.NONE),
                           (128, "OPEN", ChannelAccess.Alarms.NONE),
                           (256, "ERROR", ChannelAccess.Alarms.MAJOR),
                           (384, "ERROR(OPEN)", ChannelAccess.Alarms.MAJOR)])
    def test_WHEN_data_changes_THEN_v8_status_correct(self, raw_value,
                                                      enum_string, alarm):
        self.ca.set_pv_value("SIM:ADDR:1001", raw_value)
        self.ca.assert_that_pv_is("V8:STATUS", enum_string)
        self.ca.assert_that_pv_alarm_is("V8:STATUS", alarm)

    def test_WHEN_common_alarm_low_THEN_common_alarm_bad_high_and_alarm(self):
        self.ca.set_pv_value("SIM:ADDR:1001", 0)
        self.ca.assert_that_pv_is("COMMON_ALARM:BAD", 1)
        self.ca.assert_that_pv_alarm_is("COMMON_ALARM:BAD",
                                        ChannelAccess.Alarms.MAJOR)

    def test_WHEN_common_alarm_high_THEN_common_alarm_bad_low_and_no_alarm(
            self):
        self.ca.set_pv_value("SIM:ADDR:1001", 32768)
        self.ca.assert_that_pv_is("COMMON_ALARM:BAD", 0)
        self.ca.assert_that_pv_alarm_is("COMMON_ALARM:BAD",
                                        ChannelAccess.Alarms.NONE)

    @parameterized.expand([(1, "DEFLATED", ChannelAccess.Alarms.NONE),
                           (2, "INFLATING", ChannelAccess.Alarms.NONE),
                           (4, "INFLATED", ChannelAccess.Alarms.NONE),
                           (8, "DEFLATING", ChannelAccess.Alarms.NONE)])
    def test_WHEN_data_changes_THEN_seal_status_correct(
            self, raw_value, enum_string, alarm):
        self.ca.set_pv_value("SIM:ADDR:1004", raw_value)
        self.ca.assert_that_pv_is("SEAL:STATUS", enum_string)
        self.ca.assert_that_pv_alarm_is("SEAL:STATUS", alarm)

    @parameterized.expand([(0, 0), (4000, 10000), (2000, 5000)])
    def test_WHEN_seal_supply_pressure_changes_THEN_correctly_converted(
            self, raw_value, expected_converted_val):
        self.ca.set_pv_value("SIM:SEAL:SUPPLY:PRESS:RAW", raw_value)
        self.ca.assert_that_pv_is("SEAL:SUPPLY:PRESS", expected_converted_val)

    def _set_sp_and_assert(self,
                           set_pv,
                           state,
                           expected_state=None,
                           int_state=None):
        if int_state is None:
            int_state = state
        if expected_state is None:
            expected_state = state
        self.ca.set_pv_value("{}:STATUS:SP".format(set_pv), int_state)
        self.ca.assert_that_pv_monitor_gets_values(
            "{}:{}:SP".format(set_pv, expected_state), [expected_state, "..."])

    def test_WHEN_opening_and_closing_shutter_THEN_propogates(self):
        self._set_sp_and_assert("SHUTTER", "OPEN")
        self._set_sp_and_assert("SHUTTER", "CLOSE")
        self._set_sp_and_assert("SHUTTER", "OPEN")

    def test_WHEN_opening_and_closing_shutter_with_numbers_THEN_propogates(
            self):
        self._set_sp_and_assert("SHUTTER", "OPEN", 1)
        self._set_sp_and_assert("SHUTTER", "CLOSE", 0)
        self._set_sp_and_assert("SHUTTER", "OPEN", 1)

    def test_WHEN_insert_and_extract_monitor_THEN_propogates(self):
        self._set_sp_and_assert("MONITOR3", "IN", "INSERT")
        self._set_sp_and_assert("MONITOR3", "OUT", "EXTRACT")
        self._set_sp_and_assert("MONITOR3", "IN", "INSERT")

    def test_WHEN_insert_and_extract_monitor_with_numbers_THEN_propogates(
            self):
        self._set_sp_and_assert("MONITOR3", "IN", "INSERT", 1)
        self._set_sp_and_assert("MONITOR3", "OUT", "EXTRACT", 0)
        self._set_sp_and_assert("MONITOR3", "IN", "INSERT", 1)

    def test_WHEN_start_and_stop_guide_THEN_propogates(self):
        self._set_sp_and_assert("GUIDE", "START")
        self._set_sp_and_assert("GUIDE", "STOP")
        self._set_sp_and_assert("GUIDE", "START")

    def test_WHEN_start_and_stop_guide_with_numbers_THEN_propogates(self):
        self._set_sp_and_assert("GUIDE", "START", 1)
        self._set_sp_and_assert("GUIDE", "STOP", 0)
        self._set_sp_and_assert("GUIDE", "START", 1)

    def test_WHEN_begin_run_in_auto_shutter_mode_THEN_shutter_opened(self):
        self.ca.set_pv_value("SHUTTER:STATUS:SP", "CLOSE", wait=True)
        self.ca.set_pv_value("SHUTTER:AUTO", 1, wait=True)

        self.ca.process_pv("SHUTTER:OPEN_IF_AUTO")

        self.ca.assert_that_pv_is("SHUTTER:STATUS:SP", "OPEN")

    def test_WHEN_begin_run_in_manual_shutter_mode_THEN_shutter_opened(self):
        self.ca.set_pv_value("SHUTTER:STATUS:SP", "CLOSE", wait=True)
        self.ca.set_pv_value("SHUTTER:AUTO", 0, wait=True)

        self.ca.process_pv("SHUTTER:OPEN_IF_AUTO")

        self.ca.assert_that_pv_is_not("SHUTTER:STATUS:SP", "OPEN", timeout=5)

    def test_WHEN_end_run_in_auto_shutter_mode_THEN_shutter_opened(self):
        self.ca.set_pv_value("SHUTTER:STATUS:SP", "OPEN", wait=True)
        self.ca.set_pv_value("SHUTTER:AUTO", 1, wait=True)

        self.ca.process_pv("SHUTTER:CLOSE_IF_AUTO")

        self.ca.assert_that_pv_is("SHUTTER:STATUS:SP", "CLOSE")

    def test_WHEN_end_run_in_manual_shutter_mode_THEN_shutter_opened(self):
        self.ca.set_pv_value("SHUTTER:STATUS:SP", "OPEN", wait=True)
        self.ca.set_pv_value("SHUTTER:AUTO", 0, wait=True)

        self.ca.process_pv("SHUTTER:CLOSE_IF_AUTO")

        self.ca.assert_that_pv_is_not("SHUTTER:STATUS:SP", "CLOSE", timeout=5)
Ejemplo n.º 22
0
class StabilityTests(unittest.TestCase):
    STOP_DATA_THREAD = threading.Event()

    def setUp(self):
        self.ca = ChannelAccess(20, device_prefix=DEVICE_PREFIX)
        self.STOP_DATA_THREAD.set()
        shared_setup(self.ca)

    @staticmethod
    def evaluate_current_instability(current_values):
        """
        Evaluates the input current values against the stability criterion.

        Args:
            current_values: Array of input currents

        Returns:
            current_instability: Boolean array of len(current_values). True where element is unstable, else False

        """

        current_instability = [
            curr_measured >= (CURR_STEADY + CURR_LIMIT)
            for curr_measured in current_values
        ]

        return current_instability

    @staticmethod
    def evaluate_voltage_instability(voltage_values):
        """
        Evaluates the input voltages against the stability criterion.

        Args:
            voltage_values: Array of input voltages

        Returns:
            voltage_instability: Boolean array of len(voltage_values). True where element is unstable, else False

        """

        voltage_instability = [(VOLT_LOWERLIM >= volt_measured)
                               or (volt_measured >= VOLT_UPPERLIM)
                               for volt_measured in voltage_values]

        return voltage_instability

    def get_out_of_range_samples(self, current_values, voltage_values):
        """
        Calculates the number of points which lie out of stability limits for a current and voltage dataset.
        Args:
            current_values: Array of input current values
            voltage_values: Array of input voltage values

        Returns:
            out_of_range_count: Integer, the number of samples in the dataset which are out of range

        """

        current_instability = self.evaluate_current_instability(current_values)
        voltage_instability = self.evaluate_voltage_instability(voltage_values)

        overall_instability = [
            curr or volt
            for curr, volt in zip(current_instability, voltage_instability)
        ]

        out_of_range_count = sum(overall_instability)

        return out_of_range_count

    def write_simulated_current(self, curr_data):
        """ Scales a current waveform to the DAQ voltage range and writes to current simulation PV

        Inputs:
            curr_data: list of floats containing the current data in engineering units
        """
        scaled_data = [x / DAQ_CURR_READ_SCALE_FACTOR for x in curr_data]
        self.ca.set_pv_value("DAQ:CURR:WV:SIM",
                             scaled_data,
                             wait=True,
                             sleep_after_set=0.0)

    def write_simulated_voltage(self, volt_data):
        """ Scales a voltage waveform to the DAQ voltage range and writes to voltage simulation PV

        Inputs:
            volt_data: list of floats containing the voltage data in engineering units
        """
        scaled_data = [x * DAQ_VOLT_WRITE_SCALE_FACTOR for x in volt_data]

        self.ca.set_pv_value("DAQ:VOLT:WV:SIM",
                             scaled_data,
                             wait=True,
                             sleep_after_set=0.0)

    @parameterized.expand([
        ("steady_current_steady_voltage", [CURR_STEADY] * SAMPLE_LEN,
         [VOLT_STEADY] * SAMPLE_LEN),
        ("steady_current_unsteady_voltage", [CURR_STEADY] * SAMPLE_LEN,
         VOLTAGE_DATA),
        ("unsteady_current_steady_voltage", CURRENT_DATA,
         [VOLT_STEADY] * SAMPLE_LEN),
        ("unsteady_current_and_voltage", CURRENT_DATA, VOLTAGE_DATA)
    ])
    def test_GIVEN_current_and_voltage_data_WHEN_limits_are_tested_THEN_number_of_samples_out_of_range_returned(
            self, _, curr_data, volt_data):
        self.write_simulated_current(curr_data)
        self.write_simulated_voltage(volt_data)

        averaged_volt_data = apply_average_filter(volt_data,
                                                  stride=STRIDE_LENGTH)

        expected_out_of_range_samples = self.get_out_of_range_samples(
            curr_data, averaged_volt_data)

        self.ca.assert_that_pv_is_number("_STABILITYCHECK",
                                         expected_out_of_range_samples,
                                         tolerance=0.05 *
                                         expected_out_of_range_samples)

    def test_GIVEN_noisy_voltage_data_WHEN_moving_average_is_applied_THEN_averaged_data_has_fewer_out_of_range_points(
            self):
        curr_data = [CURR_STEADY] * SAMPLE_LEN
        self.write_simulated_current(curr_data)
        self.write_simulated_voltage(DAQ_DATA)

        out_of_range_samples_before_average = self.get_out_of_range_samples(
            curr_data, DAQ_DATA)

        out_of_range_samples_after_average = self.ca.get_pv_value(
            "_STABILITYCHECK")

        self.assertLess(out_of_range_samples_after_average,
                        out_of_range_samples_before_average)

    @parameterized.expand([("unsteady_current", simulate_current_data(),
                            [VOLT_STEADY] * SAMPLE_LEN),
                           ("unsteady_voltage", [CURR_STEADY] * SAMPLE_LEN,
                            simulate_voltage_data())])
    def test_GIVEN_multiple_samples_in_one_second_WHEN_buffer_read_THEN_buffer_reads_all_out_of_range_samples(
            self, _, curr_data, volt_data):

        # Setting this to 3 as channel access takes ~0.25s. May need reducing for slow machines.
        writes_per_second = 3

        # Change buffer length is 1 so UNSTABLETIME PV only reports 1 second of data
        length_of_buffer = 1

        self.ca.set_pv_value("WINDOWSIZE", length_of_buffer)
        self.ca.set_pv_value("RESETWINDOW", 1)

        averaged_volt_data = apply_average_filter(volt_data,
                                                  stride=STRIDE_LENGTH)

        expected_out_of_range_samples = self.get_out_of_range_samples(
            curr_data, averaged_volt_data) * writes_per_second

        self.STOP_DATA_THREAD.clear()

        scaled_curr_data = [x / DAQ_CURR_READ_SCALE_FACTOR for x in curr_data]
        scaled_volt_data = [x * DAQ_VOLT_WRITE_SCALE_FACTOR for x in volt_data]

        data_supply_thread = threading.Thread(
            target=stream_data,
            args=(self.ca, writes_per_second, scaled_curr_data,
                  scaled_volt_data, self.STOP_DATA_THREAD))

        # GIVEN
        data_supply_thread.start()

        self.assertGreater(writes_per_second, 1)

        # THEN
        self.ca.assert_that_pv_is_number("UNSTABLETIME",
                                         expected_out_of_range_samples *
                                         SAMPLETIME,
                                         timeout=60.0,
                                         tolerance=0.1 * SAMPLETIME)

        self.STOP_DATA_THREAD.set()

    def test_GIVEN_buffer_with_data_WHEN_resetwindow_PV_processed_THEN_buffer_is_cleared(
            self):
        number_of_writes = 50

        averaged_volt_data = apply_average_filter(DAQ_DATA,
                                                  stride=STRIDE_LENGTH)

        expected_out_of_range_samples = self.get_out_of_range_samples(
            CURRENT_DATA, averaged_volt_data)

        expected_out_of_range_samples *= number_of_writes * SAMPLETIME

        # GIVEN
        for i in range(number_of_writes):
            self.write_simulated_current(CURRENT_DATA)
            self.write_simulated_voltage(DAQ_DATA)

        self.ca.assert_that_pv_is_number("UNSTABLETIME",
                                         expected_out_of_range_samples)

        # WHEN
        self.ca.set_pv_value("RESETWINDOW", 1)

        # THEN
        self.ca.assert_that_pv_is_number("UNSTABLETIME", 0)

    def test_GIVEN_full_buffer_WHEN_more_data_added_to_buffer_THEN_oldest_values_overwritten(
            self):
        length_of_buffer = 10
        testvalue = 50.0

        self.assertNotEqual(testvalue, 1.0)

        self.ca.set_pv_value("WINDOWSIZE", length_of_buffer)
        self.ca.set_pv_value("RESETWINDOW", 1)

        self.ca.set_pv_value("_COUNTERTIMING.SCAN", "Passive")

        # GIVEN

        for i in range(length_of_buffer):
            self.ca.set_pv_value("_ADDCOUNTS",
                                 1.0 / SAMPLETIME,
                                 wait=True,
                                 sleep_after_set=0.0)

            self.ca.set_pv_value("_COUNTERTIMING.PROC",
                                 1,
                                 wait=True,
                                 sleep_after_set=0.0)

        self.ca.assert_that_pv_is_number("UNSTABLETIME", length_of_buffer)

        # WHEN
        self.ca.set_pv_value("_ADDCOUNTS",
                             testvalue / SAMPLETIME,
                             wait=True,
                             sleep_after_set=0.0)
        self.ca.set_pv_value("_COUNTERTIMING.PROC",
                             1,
                             wait=True,
                             sleep_after_set=0.0)

        # THEN
        self.ca.assert_that_pv_is_number("UNSTABLETIME",
                                         (length_of_buffer - 1.) + testvalue)

    def test_GIVEN_input_data_over_several_seconds_WHEN_stability_PV_read_THEN_all_unstable_time_counted(
            self):
        # This number needs to be large enough to write over several seconds. Writing over multiple seconds is asserted.
        number_of_writes = 50

        time1 = perf_counter()

        expected_out_of_range_samples = self.get_out_of_range_samples(
            CURRENT_DATA, VOLTAGE_DATA) * number_of_writes * SAMPLETIME

        # GIVEN
        for i in range(number_of_writes):
            self.write_simulated_current(CURRENT_DATA)
            self.write_simulated_voltage(VOLTAGE_DATA)

        processtime = perf_counter() - time1
        self.assertGreater(processtime, 1.)

        # THEN
        self.ca.assert_that_pv_is_number("UNSTABLETIME",
                                         expected_out_of_range_samples,
                                         tolerance=0.025 *
                                         expected_out_of_range_samples)

    def test_GIVEN_power_supply_switched_on_WHEN_power_supply_out_of_stability_threshold_THEN_stability_PV_equals_zero_and_goes_into_alarm(
            self):
        # GIVEN
        self.ca.set_pv_value("VOLT:SP", 10)
        self.ca.assert_that_pv_is("POWER:STAT", "ON")

        # WHEN
        stability_threshold = self.ca.get_pv_value("THRESHOLD")

        testvalue = stability_threshold * 100

        self.ca.set_pv_value("_ADDCOUNTS",
                             testvalue / SAMPLETIME,
                             wait=True,
                             sleep_after_set=0.0)

        self.ca.assert_that_pv_is("STABILITY", "Unstable")

        # THEN
        self.ca.assert_that_pv_alarm_is("STABILITY",
                                        ChannelAccess.Alarms.MAJOR)

    def test_GIVEN_power_supply_switched_off_WHEN_power_supply_out_of_stability_threshold_THEN_no_alarms_raised(
            self):
        # GIVEN
        self.ca.set_pv_value("VOLT:SP", 0)
        self.ca.assert_that_pv_is("POWER:STAT", "OFF")

        # WHEN
        stability_threshold = self.ca.get_pv_value("THRESHOLD")

        testvalue = stability_threshold * 100

        self.ca.set_pv_value("_ADDCOUNTS",
                             testvalue / SAMPLETIME,
                             wait=True,
                             sleep_after_set=0.0)

        self.ca.assert_that_pv_is("STABILITY", "Unstable")

        # THEN
        self.ca.assert_that_pv_alarm_is("STABILITY", ChannelAccess.Alarms.NONE)

    def test_GIVEN_separator_stable_THEN_separator_stability_text_label_is_stable(
            self):
        # GIVEN
        # asserted in setup that separator is stable

        # THEN
        self.ca.assert_that_pv_is("STABILITY", "Stable")
Ejemplo n.º 23
0
class Lakeshore372Tests(unittest.TestCase):
    """
    Tests for the lakeshore 372 IOC.
    """
    def setUp(self):
        self._lewis, self._ioc = get_running_lewis_and_ioc(
            _EMULATOR_NAME, DEVICE_PREFIX)
        self.ca = ChannelAccess(device_prefix=DEVICE_PREFIX,
                                default_timeout=15)

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

    def _assert_readback_alarm_states(self, alarm):
        for readback_pv in [
                "TEMP", "TEMP:SP:RBV", "P", "I", "D", "HEATER:POWER",
                "RESISTANCE", "HEATER:RANGE"
        ]:
            self.ca.assert_that_pv_alarm_is(readback_pv, alarm)

    @contextlib.contextmanager
    def _simulate_disconnected_device(self):
        self._lewis.backdoor_set_on_device("connected", False)
        try:
            yield
        finally:
            self._lewis.backdoor_set_on_device("connected", True)

    @parameterized.expand(parameterized_list(TEST_TEMPERATURES))
    def test_WHEN_temp_setpoint_is_set_THEN_actual_temperature_updates(
            self, _, temperature):
        self.ca.assert_setting_setpoint_sets_readback(temperature,
                                                      set_point_pv="TEMP:SP",
                                                      readback_pv="TEMP")

    @parameterized.expand(parameterized_list(TEST_TEMPERATURES))
    def test_WHEN_temp_setpoint_is_set_THEN_setpoint_readback_updates(
            self, _, temperature):
        self.ca.assert_setting_setpoint_sets_readback(
            temperature, set_point_pv="TEMP:SP", readback_pv="TEMP:SP:RBV")

    @parameterized.expand(parameterized_list(HEATER_RANGES))
    def test_WHEN_heater_range_is_set_THEN_heater_range_readback_updates(
            self, _, rng):
        self.ca.assert_setting_setpoint_sets_readback(
            rng, set_point_pv="HEATER:RANGE:SP", readback_pv="HEATER:RANGE")

    @parameterized.expand(parameterized_list(TEST_HEATER_POWER_PERCENTAGES))
    @skip_if_recsim("Uses lewis backdoor")
    def test_WHEN_heater_power_is_set_via_backdoor_THEN_heater_power_pv_updates(
            self, _, pwr):
        self._lewis.backdoor_set_on_device("heater_power", pwr)
        self.ca.assert_that_pv_is_number("HEATER:POWER", pwr, tolerance=0.001)

    @parameterized.expand(parameterized_list(TEST_SENSOR_RESISTANCES))
    @skip_if_recsim("Uses lewis backdoor")
    def test_WHEN_sensor_resistance_is_set_via_backdoor_THEN_resistance_pv_updates(
            self, _, res):
        self._lewis.backdoor_set_on_device("sensor_resistance", res)
        self.ca.assert_that_pv_is_number("RESISTANCE", res, tolerance=0.000001)

    @parameterized.expand(parameterized_list(TEST_PID_PARAMS))
    def test_WHEN_pid_parameters_are_set_THEN_readbacks_update(
            self, _, p, i, d):
        # Simulate a script by writing PIDs all in one go without waiting for update first.
        self.ca.set_pv_value("P:SP", p)
        self.ca.set_pv_value("I:SP", i)
        self.ca.set_pv_value("D:SP", d)
        self.ca.assert_that_pv_is("P", p)
        self.ca.assert_that_pv_is("I", i)
        self.ca.assert_that_pv_is("D", d)

    @skip_if_recsim("Recsim does not support simulated disconnection")
    def test_WHEN_device_does_not_respond_THEN_pvs_go_into_invalid_alarm(self):
        self._assert_readback_alarm_states(self.ca.Alarms.NONE)
        with self._simulate_disconnected_device():
            self._assert_readback_alarm_states(self.ca.Alarms.INVALID)
        # Assert alarms clear on reconnection
        self._assert_readback_alarm_states(self.ca.Alarms.NONE)

    @skip_if_recsim("Complex logic not testable in recsim")
    def test_WHEN_temperature_setpoint_is_sent_THEN_control_mode_changed_to_5(
            self):
        # 5 is the control mode for closed loop PID control, which should always be sent along with a temperature set.
        self._lewis.backdoor_set_on_device("control_mode", 0)
        self._lewis.assert_that_emulator_value_is("control_mode", 0, cast=int)
        self.ca.set_pv_value("TEMP:SP", 0)
        self._lewis.assert_that_emulator_value_is("control_mode", 5, cast=int)
Ejemplo n.º 24
0
class Sans2dVacCollisionAvoidanceTests(unittest.TestCase):
    """
    Tests for the sans2d vacuum tank motor extensions.
    """
    def setUp(self):
        self.ca = ChannelAccess(device_prefix="MOT", default_timeout=30)
        with ManagerMode(ChannelAccess()):
            self._disable_collision_avoidance()

            for axis in BAFFLES_AND_DETECTORS_Z_AXES:
                current_position = self.ca.get_pv_value("{}".format(axis))

                new_position = self._get_axis_default_position(
                    "{}".format(axis))

                self.ca.set_pv_value("{}:MTR.VMAX".format(axis),
                                     TEST_SPEED,
                                     sleep_after_set=0)
                self.ca.set_pv_value("{}:MTR.VELO".format(axis),
                                     TEST_SPEED,
                                     sleep_after_set=0)
                self.ca.set_pv_value("{}:MTR.ACCL".format(axis),
                                     TEST_ACCELERATION,
                                     sleep_after_set=0)

                if current_position != new_position:
                    self.ca.set_pv_value("{}:SP".format(axis),
                                         new_position,
                                         sleep_after_set=0)

                timeout = self._get_timeout_for_moving_to_position(
                    axis, new_position)
                self.ca.assert_that_pv_is("{}".format(axis),
                                          new_position,
                                          timeout=timeout)

            # re-enable collision avoidance
            self._enable_collision_avoidance()

    def _disable_collision_avoidance(self):
        self._set_collision_avoidance_state(1, "DISABLED")

    def _enable_collision_avoidance(self):
        self._set_collision_avoidance_state(0, "ENABLED")

    def _set_collision_avoidance_state(self, write_value, read_value):

        # Do nothing if manager mode is already in correct state
        if ChannelAccess().get_pv_value(ManagerMode.MANAGER_MODE_PV) != "Yes":
            cm = ManagerMode(ChannelAccess())
        else:
            cm = nullcontext()

        with cm:
            err = None
            for _ in range(20):
                try:
                    self.ca.set_pv_value("SANS2DVAC:COLLISION_AVOIDANCE",
                                         write_value,
                                         sleep_after_set=0)
                    break
                except WriteAccessException as e:
                    err = e
                    sleep(1)
            else:
                raise err
            self.ca.assert_that_pv_is("SANS2DVAC:COLLISION_AVOIDANCE",
                                      read_value)

    @contextlib.contextmanager
    def _assert_last_stop_time_updated(self, timeout=5):
        initial_stop_time = self.ca.get_pv_value("SANS2DVAC:_LAST_STOP_TIME")
        try:
            yield
        except Exception as e:
            raise e
        else:
            self.ca.assert_that_pv_is_not("SANS2DVAC:_LAST_STOP_TIME",
                                          initial_stop_time,
                                          timeout=timeout)

    @parameterized.expand(parameterized_list(AXIS_PAIRS))
    def test_GIVEN_motor_interval_above_minor_warning_threshold_THEN_interval_is_correct_and_not_in_alarm(
            self, _, axis_pair):
        # disable collision avoidance so it does not interfere with checking the intervals and their alarm status
        self._disable_collision_avoidance()

        rear_axis_position = self.ca.get_pv_value(axis_pair.rear_axis)
        front_axis_position = rear_axis_position - 50 - MINOR_ALARM_INTERVAL_THRESHOLD
        expected_interval = rear_axis_position - front_axis_position

        self.ca.set_pv_value(axis_pair.front_axis_sp,
                             front_axis_position,
                             sleep_after_set=0)

        timeout = self._get_timeout_for_moving_to_position(
            axis_pair.front_axis, front_axis_position)
        self.ca.assert_that_pv_is_number("SANS2DVAC:{}:INTERVAL".format(
            axis_pair.name),
                                         expected_interval,
                                         timeout=timeout,
                                         tolerance=0.1)
        self.ca.assert_that_pv_alarm_is(
            "SANS2DVAC:{}:INTERVAL".format(axis_pair.name),
            self.ca.Alarms.NONE)

    @parameterized.expand(parameterized_list(AXIS_PAIRS))
    def test_GIVEN_setpoint_interval_above_minor_warning_threshold_THEN_interval_is_correct_and_not_in_alarm(
            self, _, axis_pair):
        # disable collision avoidance so it does not interfere with checking the intervals and their alarm status
        self._disable_collision_avoidance()

        rear_axis_position = 1000
        front_axis_position = rear_axis_position - 50 - MINOR_ALARM_INTERVAL_THRESHOLD
        expected_interval = rear_axis_position - front_axis_position

        self.ca.set_pv_value(axis_pair.front_axis_sp,
                             front_axis_position,
                             sleep_after_set=0)
        self.ca.set_pv_value(axis_pair.rear_axis_sp,
                             rear_axis_position,
                             sleep_after_set=0)
        self._assert_axis_position_reached(axis_pair.front_axis,
                                           front_axis_position)
        self._assert_axis_position_reached(axis_pair.rear_axis,
                                           rear_axis_position)

        self.ca.assert_that_pv_is_number("SANS2DVAC:{}:INTERVAL".format(
            axis_pair.name),
                                         expected_interval,
                                         timeout=5,
                                         tolerance=0.1)
        self.ca.assert_that_pv_alarm_is(
            "SANS2DVAC:{}:INTERVAL".format(axis_pair.name),
            self.ca.Alarms.NONE)

    @parameterized.expand(parameterized_list(AXIS_PAIRS))
    def test_GIVEN_motor_interval_under_minor_warning_threshold_THEN_interval_is_correct_and_in_minor_alarm(
            self, _, axis_pair):
        # disable collision avoidance so it does not interfere with checking the intervals and their alarm status
        self._disable_collision_avoidance()

        rear_position = self.ca.get_pv_value(axis_pair.rear_axis)
        front_new_position = rear_position - MINOR_ALARM_INTERVAL_THRESHOLD + 1
        expected_interval = rear_position - front_new_position

        self.ca.set_pv_value(axis_pair.front_axis_sp,
                             front_new_position,
                             sleep_after_set=0)

        timeout = self._get_timeout_for_moving_to_position(
            axis_pair.front_axis, front_new_position)
        self.ca.assert_that_pv_is_number("SANS2DVAC:{}:INTERVAL".format(
            axis_pair.name),
                                         expected_interval,
                                         timeout=timeout,
                                         tolerance=0.1)
        self.ca.assert_that_pv_alarm_is(
            "SANS2DVAC:{}:INTERVAL".format(axis_pair.name),
            self.ca.Alarms.MINOR)

    @parameterized.expand(parameterized_list(AXIS_PAIRS))
    def test_GIVEN_setpoint_interval_under_minor_warning_threshold_THEN_interval_is_correct_and_in_minor_alarm(
            self, _, axis_pair):
        # disable collision avoidance so it does not interfere with checking the intervals and their alarm status
        self._disable_collision_avoidance()

        rear_axis_position = 1000
        front_axis_position = rear_axis_position - MINOR_ALARM_INTERVAL_THRESHOLD + 1
        expected_interval = rear_axis_position - front_axis_position

        self.ca.set_pv_value(axis_pair.front_axis_sp,
                             front_axis_position,
                             sleep_after_set=0)
        self.ca.set_pv_value(axis_pair.rear_axis_sp,
                             rear_axis_position,
                             sleep_after_set=0)
        self._assert_axis_position_reached(axis_pair.front_axis,
                                           front_axis_position)
        self._assert_axis_position_reached(axis_pair.rear_axis,
                                           rear_axis_position)

        self.ca.assert_that_pv_is_number("SANS2DVAC:{}:INTERVAL".format(
            axis_pair.name),
                                         expected_interval,
                                         timeout=5,
                                         tolerance=0.1)
        self.ca.assert_that_pv_alarm_is(
            "SANS2DVAC:{}:INTERVAL".format(axis_pair.name),
            self.ca.Alarms.MINOR)

    @parameterized.expand(parameterized_list(AXIS_PAIRS))
    def test_GIVEN_motor_interval_under_major_warning_threshold_THEN_interval_is_correct_and_in_major_alarm(
            self, _, axis_pair):
        # disable collision avoidance so it does not interfere with checking the intervals and their alarm status
        self._disable_collision_avoidance()

        rear_axis_position = self.ca.get_pv_value(axis_pair.rear_axis)
        front_axis_position = rear_axis_position - MAJOR_ALARM_INTERVAL_THRESHOLD + 1
        expected_interval = rear_axis_position - front_axis_position

        self.ca.set_pv_value(axis_pair.front_axis_sp,
                             front_axis_position,
                             sleep_after_set=0)

        timeout = self._get_timeout_for_moving_to_position(
            axis_pair.front_axis, front_axis_position)
        self.ca.assert_that_pv_is_number("SANS2DVAC:{}:INTERVAL".format(
            axis_pair.name),
                                         expected_interval,
                                         timeout=timeout,
                                         tolerance=0.1)
        self.ca.assert_that_pv_alarm_is(
            "SANS2DVAC:{}:INTERVAL".format(axis_pair.name),
            self.ca.Alarms.MAJOR)

    @parameterized.expand(parameterized_list(AXIS_PAIRS))
    def test_GIVEN_setpoint_interval_under_major_warning_threshold_THEN_interval_is_correct_and_in_major_alarm(
            self, _, axis_pair):
        # disable collision avoidance so it does not interfere with checking the intervals and their alarm status
        self._disable_collision_avoidance()

        rear_axis_position = 1000
        front_axis_position = rear_axis_position - MAJOR_ALARM_INTERVAL_THRESHOLD + 1
        expected_interval = rear_axis_position - front_axis_position

        self.ca.set_pv_value(axis_pair.front_axis_sp,
                             front_axis_position,
                             sleep_after_set=0)
        self.ca.set_pv_value(axis_pair.rear_axis_sp,
                             rear_axis_position,
                             sleep_after_set=0)

        self._assert_axis_position_reached(axis_pair.front_axis,
                                           front_axis_position)
        self._assert_axis_position_reached(axis_pair.rear_axis,
                                           rear_axis_position)

        self.ca.assert_that_pv_is_number("SANS2DVAC:{}:INTERVAL".format(
            axis_pair.name),
                                         expected_interval,
                                         timeout=5,
                                         tolerance=0.1)
        self.ca.assert_that_pv_alarm_is(
            "SANS2DVAC:{}:INTERVAL".format(axis_pair.name),
            self.ca.Alarms.MAJOR)

    @parameterized.expand(parameterized_list(AXIS_PAIRS))
    def test_GIVEN_front_axis_moves_towards_rear_axis_WHEN_setpoint_interval_greater_than_threshold_THEN_motor_not_stopped(
            self, _, axis_pair):
        front_axis_new_position = (self.ca.get_pv_value(axis_pair.rear_axis) -
                                   axis_pair.minimum_interval) - 50
        self.ca.set_pv_value(axis_pair.front_axis_sp,
                             front_axis_new_position,
                             sleep_after_set=0)

        self._assert_axis_position_reached(axis_pair.front_axis,
                                           front_axis_new_position)

    @parameterized.expand(parameterized_list(AXIS_PAIRS))
    def test_GIVEN_front_axis_moves_towards_rear_axis_WHEN_setpoint_interval_smaller_than_threshold_THEN_motor_stops(
            self, _, axis_pair):

        with self._assert_last_stop_time_updated():
            front_axis_new_position = (self.ca.get_pv_value(
                axis_pair.rear_axis) - axis_pair.minimum_interval) + 50
            self.ca.set_pv_value(axis_pair.front_axis_sp,
                                 front_axis_new_position,
                                 sleep_after_set=0)

            self.ca.assert_that_pv_is("{}:MTR.MOVN".format(
                axis_pair.front_axis),
                                      1,
                                      timeout=1)
            self.ca.assert_that_pv_is("{}:MTR.TDIR".format(
                axis_pair.front_axis),
                                      1,
                                      timeout=1)

            timeout = self._get_timeout_for_moving_to_position(
                axis_pair.front_axis, front_axis_new_position)
            assert_axis_not_moving(axis_pair.front_axis, timeout=timeout)
            self.ca.assert_that_pv_is_not(axis_pair.front_axis,
                                          front_axis_new_position,
                                          timeout=timeout)

    @parameterized.expand(parameterized_list(AXIS_PAIRS))
    def test_GIVEN_front_axis_within_threhsold_distance_to_rear_axis_WHEN_set_to_move_away_THEN_motor_not_stopped(
            self, _, axis_pair):
        front_axis_new_position = (self.ca.get_pv_value(axis_pair.rear_axis) -
                                   axis_pair.minimum_interval) + 50
        self.ca.set_pv_value(axis_pair.front_axis_sp,
                             front_axis_new_position,
                             sleep_after_set=0)

        self.ca.assert_that_pv_is("{}:MTR.MOVN".format(axis_pair.front_axis),
                                  1,
                                  timeout=1)
        self.ca.assert_that_pv_is("{}:MTR.TDIR".format(axis_pair.front_axis),
                                  1,
                                  timeout=1)

        timeout = self._get_timeout_for_moving_to_position(
            axis_pair.front_axis, front_axis_new_position)
        assert_axis_not_moving(axis_pair.front_axis, timeout=timeout)
        self.ca.assert_that_pv_is_not(axis_pair.front_axis,
                                      front_axis_new_position,
                                      timeout=timeout)

        front_axis_new_position = self.ca.get_pv_value(
            axis_pair.front_axis) - 200
        self.ca.set_pv_value(axis_pair.front_axis_sp,
                             front_axis_new_position,
                             sleep_after_set=0)

        assert_axis_moving(axis_pair.front_axis, timeout=1)
        self.ca.assert_that_pv_is("{}:MTR.TDIR".format(axis_pair.front_axis),
                                  0,
                                  timeout=1)
        self._assert_axis_position_reached(axis_pair.front_axis,
                                           front_axis_new_position)

    @parameterized.expand(parameterized_list(AXIS_PAIRS))
    def test_GIVEN_rear_axis_moves_towards_front_axis_WHEN_setpoint_interval_greater_than_threshold_THEN_motor_not_stopped(
            self, _, axis_pair):
        rear_axis_position = (self.ca.get_pv_value(axis_pair.front_axis) +
                              axis_pair.minimum_interval) + 50
        self.ca.set_pv_value(axis_pair.rear_axis_sp,
                             rear_axis_position,
                             sleep_after_set=0)

        self._assert_axis_position_reached(axis_pair.rear_axis,
                                           rear_axis_position)

    @parameterized.expand(parameterized_list(AXIS_PAIRS))
    def test_GIVEN_rear_axis_moves_towards_front_axis_WHEN_setpoint_interval_smaller_than_threshold_THEN_motor_stops(
            self, _, axis_pair):
        with self._assert_last_stop_time_updated():
            rear_axis_new_position = (self.ca.get_pv_value(
                axis_pair.front_axis) + axis_pair.minimum_interval) - 50
            self.ca.set_pv_value(axis_pair.rear_axis_sp,
                                 rear_axis_new_position,
                                 sleep_after_set=0)

            self.ca.assert_that_pv_is("{}:MTR.MOVN".format(
                axis_pair.rear_axis),
                                      1,
                                      timeout=1)
            self.ca.assert_that_pv_is("{}:MTR.TDIR".format(
                axis_pair.rear_axis),
                                      0,
                                      timeout=1)

            timeout = self._get_timeout_for_moving_to_position(
                axis_pair.rear_axis, rear_axis_new_position)
            assert_axis_not_moving(axis_pair.rear_axis, timeout=timeout)
            self.ca.assert_that_pv_is_not(axis_pair.rear_axis,
                                          rear_axis_new_position,
                                          timeout=timeout)

    @parameterized.expand(parameterized_list(AXIS_PAIRS))
    def test_GIVEN_rear_axis_within_threhsold_distance_to_front_axis_WHEN_set_to_move_away_THEN_motor_not_stopped(
            self, _, axis_pair):
        rear_axis_new_position = (self.ca.get_pv_value(axis_pair.front_axis) +
                                  axis_pair.minimum_interval) - 50
        self.ca.set_pv_value(axis_pair.rear_axis_sp,
                             rear_axis_new_position,
                             sleep_after_set=0)

        self.ca.assert_that_pv_is("{}:MTR.MOVN".format(axis_pair.rear_axis),
                                  1,
                                  timeout=1)
        self.ca.assert_that_pv_is("{}:MTR.TDIR".format(axis_pair.rear_axis),
                                  0,
                                  timeout=1)

        timeout = self._get_timeout_for_moving_to_position(
            axis_pair.rear_axis, rear_axis_new_position)
        assert_axis_not_moving(axis_pair.rear_axis, timeout=timeout)
        self.ca.assert_that_pv_is_not(axis_pair.rear_axis,
                                      rear_axis_new_position,
                                      timeout=timeout)

        rear_axis_new_position = self.ca.get_pv_value(
            axis_pair.rear_axis) + 200
        self.ca.set_pv_value(axis_pair.rear_axis_sp,
                             rear_axis_new_position,
                             sleep_after_set=0)

        assert_axis_moving(axis_pair.rear_axis, timeout=1)
        self.ca.assert_that_pv_is("{}:MTR.TDIR".format(axis_pair.rear_axis),
                                  1,
                                  timeout=1)
        self._assert_axis_position_reached(axis_pair.rear_axis,
                                           rear_axis_new_position)

    def _invalid_alarm_on_motor(self, motor, invalid):
        """
        Puts a motor into invalid alarm. The simulated motor record doesn't respect SIMS so instead use a readback
        link pointing at an invalid PV and tell the motor to use the readback link. This causes an invalid alarm as
        desired.
        """
        if invalid:
            self.ca.set_pv_value("{}:MTR.RDBL".format(motor),
                                 "fake_input_link_doesnt_connect",
                                 sleep_after_set=0)

        self.ca.set_pv_value("{}:MTR.URIP".format(motor),
                             "Yes" if invalid else "No",
                             sleep_after_set=0)

        self.ca.assert_that_pv_alarm_is(
            motor, self.ca.Alarms.INVALID if invalid else self.ca.Alarms.NONE)

    @parameterized.expand(parameterized_list(AXIS_PAIRS))
    def test_GIVEN_axis_has_comms_error_THEN_axes_are_stopped(
            self, _, axis_pair):
        with self._assert_last_stop_time_updated(timeout=10):
            self._invalid_alarm_on_motor(axis_pair.front_axis, True)
            self.ca.assert_that_pv_alarm_is(
                "SANS2DVAC:{}:INTERVAL".format(axis_pair.name),
                self.ca.Alarms.INVALID)
        self._invalid_alarm_on_motor(axis_pair.front_axis, False)
        self.ca.assert_that_pv_alarm_is(
            "SANS2DVAC:{}:INTERVAL".format(axis_pair.name),
            self.ca.Alarms.NONE)

        with self._assert_last_stop_time_updated(timeout=10):
            self._invalid_alarm_on_motor(axis_pair.rear_axis, True)
            self.ca.assert_that_pv_alarm_is(
                "SANS2DVAC:{}:INTERVAL".format(axis_pair.name),
                self.ca.Alarms.INVALID)
        self._invalid_alarm_on_motor(axis_pair.rear_axis, False)
        self.ca.assert_that_pv_alarm_is(
            "SANS2DVAC:{}:INTERVAL".format(axis_pair.name),
            self.ca.Alarms.NONE)

    def _get_axis_default_position(self, axis):
        if axis == "FRONTDETZ":
            new_position = 1000
        elif axis == "FRONTBAFFLEZ":
            new_position = 3000
        elif axis == "REARBAFFLEZ":
            new_position = 5000
        elif axis == "REARDETZ":
            new_position = 7000
        else:
            raise ValueError("invalid axis!")

        return new_position

    def _get_timeout_for_moving_to_position(self, moving_axis, new_position):
        distance_to_travel = abs(new_position -
                                 self.ca.get_pv_value(moving_axis))

        time_to_accelerate_and_decelerate = 2 * TEST_ACCELERATION

        # between 0 and full speed, the average speed is half the full speed, same for when decelerating.
        # Therefore, the distance traveled when accelerating and decelerating is
        # 2 * (full_speed/2 * acceleration_time), so full_speed / acceleration_time
        time_at_full_speed = (distance_to_travel -
                              TEST_SPEED * TEST_ACCELERATION) / TEST_SPEED

        total_time = ceil(time_to_accelerate_and_decelerate +
                          time_at_full_speed)

        return total_time + 10  # +10 as a small tolerance to avoid instability

    def _assert_axis_position_reached(self, axis, position):
        timeout = self._get_timeout_for_moving_to_position(axis, position)
        self.ca.assert_that_pv_is_number(axis,
                                         position,
                                         tolerance=0.1,
                                         timeout=timeout)
Ejemplo n.º 25
0
class KeylkgTests(unittest.TestCase):
    """
    Tests for the Keylkg IOC.
    """
    def setUp(self):
        self._lewis, self._ioc = get_running_lewis_and_ioc(
            EMULATOR_NAME, DEVICE_PREFIX)
        self.ca = ChannelAccess(device_prefix=DEVICE_PREFIX)
        self._lewis.backdoor_run_function_on_device("reset")

    def test_GIVEN_running_ioc_WHEN_change_to_communication_mode_THEN_mode_changed(
            self):
        expected_value = "SET-UP"
        self.ca.set_pv_value("MODE:SP", expected_value)

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

    def test_GIVEN_running_ioc_WHEN_change_to_normal_mode_THEN_mode_changed(
            self):
        expected_value = "MEASURE"
        self.ca.set_pv_value("MODE:SP", expected_value)

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

    @parameterized.expand([('low limit', -99.9999), ('test_value_1', -2.3122),
                           ('test_value_2', 12.3423), ('high limit', 99.9999)])
    def test_GIVEN_running_ioc_WHEN_set_output1_offset_THEN_output1_offset_updated(
            self, _, mock_offset):
        expected_value = mock_offset
        self.ca.set_pv_value("OFFSET:OUTPUT:1:SP", expected_value)

        self.ca.assert_that_pv_is_number("OFFSET:OUTPUT:1",
                                         expected_value,
                                         tolerance=0.001)

    @parameterized.expand([('exceeds low limit', -100.0000),
                           ('exceeds high limit', 100.000)])
    def test_GIVEN_running_ioc_WHEN_set_output1_offset_outside_of_limits_THEN_output1_offset_within_limits(
            self, _, mock_offset):
        expected_value = mock_offset
        self.ca.set_pv_value("OFFSET:OUTPUT:1:SP", expected_value)

        self.ca.assert_that_pv_is_within_range("OFFSET:OUTPUT:1", -99.9999,
                                               99.9999)

    @parameterized.expand([('low limit', -99.9999), ('test_value_1', -2.3122),
                           ('test_value_2', 12.3423), ('high limit', 99.9999)])
    def test_GIVEN_running_ioc_WHEN_set_output2_offset_THEN_output1_offset_updated(
            self, _, mock_offset):
        expected_value = mock_offset
        self.ca.set_pv_value("OFFSET:OUTPUT:2:SP", expected_value)

        self.ca.assert_that_pv_is_number("OFFSET:OUTPUT:2",
                                         expected_value,
                                         tolerance=0.001)

    @parameterized.expand([('exceeds low limit', -100.0000),
                           ('exceeds high limit', 100.000)])
    def test_GIVEN_running_ioc_WHEN_set_output2_offset_outside_of_limits_THEN_output2_offset_within_limits(
            self, _, mock_offset):
        expected_value = mock_offset
        self.ca.set_pv_value("OFFSET:OUTPUT:1:SP", expected_value)

        self.ca.assert_that_pv_is_within_range("OFFSET:OUTPUT:2", -99.9999,
                                               99.9999)

    def test_GIVEN_running_ioc_WHEN_change_to_head1_measurement_mode_THEN_mode_changed(
            self):
        expected_value = "MULTI-REFLECTIVE"
        self.ca.set_pv_value("MEASUREMODE:HEAD:A:SP", expected_value)

        self.ca.assert_that_pv_is("MEASUREMODE:HEAD:A", expected_value)

    def test_GIVEN_running_ioc_WHEN_change_to_head2_measurement_mode_THEN_mode_changed(
            self):
        expected_value = "TRANSPARENT OBJ 1"
        self.ca.set_pv_value("MEASUREMODE:HEAD:B:SP", expected_value)

        self.ca.assert_that_pv_is("MEASUREMODE:HEAD:B", expected_value)

    @skip_if_recsim('Cannot use lewis backdoor in RECSIM')
    def test_GIVEN_running_ioc_WHEN_in_measure_mode_THEN_output1_takes_data(
            self):
        expected_value = 0.1234
        self._lewis.backdoor_set_on_device("detector_1_raw_value",
                                           expected_value)
        self.ca.set_pv_value("MODE:SP", "MEASURE")

        self.ca.assert_that_pv_is("VALUE:OUTPUT:1", expected_value)

    @skip_if_recsim('No emulation of data capture in RECSIM')
    def test_GIVEN_running_ioc_WHEN_in_measure_mode_THEN_output2_takes_data(
            self):
        expected_value = 0.1234
        self._lewis.backdoor_set_on_device("detector_2_raw_value",
                                           expected_value)
        self.ca.set_pv_value("MODE:SP", "MEASURE")

        self.ca.assert_that_pv_is("VALUE:OUTPUT:2", expected_value)

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

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

    @skip_if_recsim("Unable to use lewis backdoor in RECSIM")
    def test_GIVEN_device_not_connected_WHEN_get_error_THEN_alarm(self):
        expected_value = "Command error"
        self._lewis.backdoor_set_on_device('input_correct', False)

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

    @skip_if_recsim("Unable to use lewis backdoor in RECSIM")
    def test_GIVEN_running_ioc_WHEN_in_measure_mode_and_reset_output1_THEN_output1_reset(
            self):
        expected_value = 0.0000
        test_value = 0.1234
        self._lewis.backdoor_set_on_device("detector_1_raw_value", test_value)
        self.ca.set_pv_value("MODE:SP", "MEASURE")
        self.ca.assert_that_pv_is("VALUE:OUTPUT:1", test_value)
        self.ca.set_pv_value("RESET:OUTPUT:1:SP", "RESET")

        self.ca.assert_that_pv_is("VALUE:OUTPUT:1", expected_value)

    @skip_if_recsim("Unable to use lewis backdoor in RECSIM")
    def test_GIVEN_running_ioc_WHEN_in_measure_mode_and_reset_output2_THEN_output2_reset(
            self):
        expected_value = 0.0000
        test_value = 0.1234
        self._lewis.backdoor_set_on_device("detector_2_raw_value", test_value)
        self.ca.set_pv_value("MODE:SP", "MEASURE")
        self.ca.assert_that_pv_is("VALUE:OUTPUT:2", test_value)
        self.ca.set_pv_value("RESET:OUTPUT:2:SP", "RESET")

        self.ca.assert_that_pv_is("VALUE:OUTPUT:2", expected_value)

    @skip_if_recsim('Cannot use lewis backdoor in RECSIM')
    def test_GIVEN_running_ioc_WHEN_in_setup_mode_THEN_output1_switches_to_measurement_mode_and_takes_data(
            self):
        expected_value = 0.1234
        self.ca.set_pv_value("MODE:SP", "SET-UP")
        self._lewis.backdoor_set_on_device("detector_1_raw_value",
                                           expected_value)

        self.ca.assert_that_pv_is("VALUE:OUTPUT:1", expected_value)
class Lksh218Tests(unittest.TestCase):
    """
    Tests for the Lksh218 IOC.
    """
    def setUp(self):
        self._lewis, self._ioc = get_running_lewis_and_ioc(
            "Lksh218", DEVICE_PREFIX)
        self.ca = ChannelAccess(device_prefix=DEVICE_PREFIX)
        self._lewis.backdoor_set_on_device("connected", True)

    def tearDown(self):
        self._lewis.backdoor_set_on_device("connected", True)

    def _set_temperature(self, number, temperature):
        pv = "SIM:TEMP{}".format(number)
        self._lewis.backdoor_run_function_on_device("set_temp",
                                                    [number, temperature])
        self._ioc.set_simulated_value(pv, temperature)

    def _set_sensor(self, number, value):
        pv = "SIM:SENSOR{}".format(number)
        self._lewis.backdoor_run_function_on_device("set_sensor",
                                                    [number, value])
        self._ioc.set_simulated_value(pv, value)

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

    def test_that_GIVEN_temp_float_WHEN_temp_pvs_are_read_THEN_temp_is_as_expected(
            self):
        expected_value = 10.586

        for index in range(1, 9):
            pv = "TEMP{}".format(index)
            self._set_temperature(index, expected_value)
            self.ca.assert_that_pv_is(pv, expected_value)

    def test_that_GIVEN_sensor_float_WHEN_sensor_pvs_are_read_THEN_sensor_is_as_expected(
            self):
        expected_value = 11.386

        for index in range(1, 9):
            pv = "SENSOR{}".format(index)
            self._set_sensor(index, expected_value)
            self.ca.assert_that_pv_is(pv, expected_value)

    def test_that_WHEN_reading_all_temps_pv_THEN_all_temp_pv_are_as_expected(
            self):
        expected_string = "10.4869"
        self._lewis.backdoor_set_on_device("temp_all", expected_string)
        self._ioc.set_simulated_value("SIM:TEMPALL", expected_string)

        self.ca.process_pv("TEMPALL")
        self.ca.assert_that_pv_is("TEMPALL", expected_string)

    def test_that_WHEN_reading_sensor_all_pv_THEN_sensor_all_pv_returns_as_expected(
            self):
        expected_string = "12.129"
        self._lewis.backdoor_set_on_device("sensor_all", expected_string)
        self._ioc.set_simulated_value("SIM:SENSORALL", expected_string)

        self.ca.process_pv("SENSORALL")
        self.ca.assert_that_pv_is("SENSORALL", expected_string)

    @skip_if_recsim("Recsim is unable to simulate a disconnected device.")
    def test_that_WHEN_the_emulator_is_disconnected_THEN_an_alarm_is_raised_on_TEMP_and_SENSOR(
            self):
        self._lewis.backdoor_set_on_device("connected", False)

        for i in range(1, 9):
            self.ca.assert_that_pv_alarm_is("TEMP{}".format(i),
                                            ChannelAccess.Alarms.INVALID)
            self.ca.assert_that_pv_alarm_is("SENSOR{}".format(i),
                                            ChannelAccess.Alarms.INVALID)

    @skip_if_recsim("Recsim is unable to simulate a disconnected device.")
    def test_that_WHEN_the_emulator_is_disconnected_THEN_an_alarm_is_raised_on_SENSORALL(
            self):
        self._lewis.backdoor_set_on_device("connected", False)

        self.ca.process_pv("SENSORALL")
        self.ca.assert_that_pv_alarm_is("SENSORALL",
                                        ChannelAccess.Alarms.INVALID)

    @unstable_test()
    @skip_if_recsim("Recsim is unable to simulate a disconnected device.")
    def test_that_WHEN_the_emulator_is_disconnected_THEN_an_alarm_is_raised_on_TEMPALL(
            self):
        self._lewis.backdoor_set_on_device("connected", False)

        self.ca.process_pv("TEMPALL")
        self.ca.assert_that_pv_alarm_is("TEMPALL",
                                        ChannelAccess.Alarms.INVALID)
Ejemplo n.º 27
0
class ZeroFieldTests(unittest.TestCase):
    """
    Tests for the muon zero field controller IOC.
    """
    def _set_simulated_measured_fields(self,
                                       fields,
                                       overload=False,
                                       wait_for_update=True):
        """
        Args:
            fields (dict[AnyStr, float]): A dictionary with the same keys as FIELD_AXES and values corresponding to the
              required fields
            overload (bool): whether to simulate the magnetometer being overloaded
            wait_for_update (bool): whether to wait for the statemachine to pick up the new readings
        """
        for axis in FIELD_AXES:
            self.magnetometer_ca.set_pv_value("SIM:DAQ:{}".format(axis),
                                              fields[axis],
                                              sleep_after_set=0)

        # Just overwrite the calculation to return a constant as we are not interested in testing the
        # overload logic in the magnetometer in these tests (that logic is tested separately).
        self.magnetometer_ca.set_pv_value("OVERLOAD:_CALC.CALC",
                                          "1" if overload else "0",
                                          sleep_after_set=0)

        if wait_for_update:
            for axis in FIELD_AXES:
                self.zfcntrl_ca.assert_that_pv_is("FIELD:{}".format(axis),
                                                  fields[axis])
                self.zfcntrl_ca.assert_that_pv_is("FIELD:{}:MEAS".format(axis),
                                                  fields[axis])

    def _set_user_setpoints(self, fields):
        """
        Args:
            fields (dict[AnyStr, float]): A dictionary with the same keys as FIELD_AXES and values corresponding to the
              required fields 
        """
        for axis in FIELD_AXES:
            self.zfcntrl_ca.set_pv_value("FIELD:{}:SP".format(axis),
                                         fields[axis],
                                         sleep_after_set=0)

    def _set_simulated_power_supply_currents(self,
                                             currents,
                                             wait_for_update=True):
        """
        Args:
            currents (dict[AnyStr, float]): A dictionary with the same keys as FIELD_AXES and values corresponding to
              the required currents
            wait_for_update (bool): whether to wait for the readback and setpoint readbacks to update
        """
        for axis in FIELD_AXES:
            self.zfcntrl_ca.set_pv_value("OUTPUT:{}:CURR:SP".format(axis),
                                         currents[axis],
                                         sleep_after_set=0)

        if wait_for_update:
            for axis in FIELD_AXES:
                self.zfcntrl_ca.assert_that_pv_is(
                    "OUTPUT:{}:CURR".format(axis), currents[axis])
                self.zfcntrl_ca.assert_that_pv_is(
                    "OUTPUT:{}:CURR:SP:RBV".format(axis), currents[axis])

    def _set_simulated_power_supply_voltages(self,
                                             voltages,
                                             wait_for_update=True):
        """
        Args:
            voltages (dict[AnyStr, float]): A dictionary with the same keys as FIELD_AXES and values corresponding to
              the required voltages
            wait_for_update (bool): whether to wait for the readback and setpoint readbacks to update
        """
        for axis in FIELD_AXES:
            self.zfcntrl_ca.set_pv_value("OUTPUT:{}:VOLT:SP".format(axis),
                                         voltages[axis],
                                         sleep_after_set=0)

        if wait_for_update:
            for axis in FIELD_AXES:
                self.zfcntrl_ca.assert_that_pv_is(
                    "OUTPUT:{}:VOLT".format(axis), voltages[axis])
                self.zfcntrl_ca.assert_that_pv_is(
                    "OUTPUT:{}:VOLT:SP:RBV".format(axis), voltages[axis])

    def _assert_at_setpoint(self, status):
        """
        Args:
            status (string): value of AT_SETPOINT PV (either Yes, No or N/A)
        """
        self.zfcntrl_ca.assert_that_pv_is("AT_SETPOINT", status)

    def _assert_status(self, status):
        """
        Args:
            status (Tuple[str, str]): the controller status and error to assert.
        """
        name, expected_alarm = status

        # Special case - this alarm should be suppressed in manual mode. This is because, in manual mode, the
        # scientists will intentionally apply large fields (which overload the magnetometer), but they do not want
        # alarms for this case as it is a "normal" mode of operation.
        if name == Statuses.MAGNETOMETER_OVERLOAD[
                0] and self.zfcntrl_ca.get_pv_value(
                    "AUTOFEEDBACK") == "Manual":
            expected_alarm = self.zfcntrl_ca.Alarms.NONE

        self.zfcntrl_ca.assert_that_pv_is("STATUS", name)
        self.zfcntrl_ca.assert_that_pv_alarm_is("STATUS", expected_alarm)

    def _set_autofeedback(self, autofeedback):
        self.zfcntrl_ca.set_pv_value(
            "AUTOFEEDBACK", "Auto-feedback" if autofeedback else "Manual")

    def _set_scaling_factors(self, px, py, pz, fiddle):
        """
        Args:
            px (float): Amps per mG for the X axis.
            py (float): Amps per mG for the Y axis.
            pz (float): Amps per mG for the Z axis.
            fiddle (float): The feedback (sometimes called "fiddle") factor.
        """
        self.zfcntrl_ca.set_pv_value("P:X", px, sleep_after_set=0)
        self.zfcntrl_ca.set_pv_value("P:Y", py, sleep_after_set=0)
        self.zfcntrl_ca.set_pv_value("P:Z", pz, sleep_after_set=0)
        self.zfcntrl_ca.set_pv_value("P:FEEDBACK", fiddle, sleep_after_set=0)

    def _set_output_limits(self, lower_limits, upper_limits):
        """
        Args:
            lower_limits (dict[AnyStr, float]): A dictionary with the same keys as FIELD_AXES and values corresponding to
              the required output lower limits
            upper_limits (dict[AnyStr, float]): A dictionary with the same keys as FIELD_AXES and values corresponding to
              the required output upper limits
        """
        for axis in FIELD_AXES:
            self.zfcntrl_ca.set_pv_value("OUTPUT:{}:CURR:SP.DRVL".format(axis),
                                         lower_limits[axis],
                                         sleep_after_set=0)
            self.zfcntrl_ca.set_pv_value("OUTPUT:{}:CURR:SP.LOLO".format(axis),
                                         lower_limits[axis],
                                         sleep_after_set=0)
            self.zfcntrl_ca.set_pv_value("OUTPUT:{}:CURR:SP.DRVH".format(axis),
                                         upper_limits[axis],
                                         sleep_after_set=0)
            self.zfcntrl_ca.set_pv_value("OUTPUT:{}:CURR:SP.HIHI".format(axis),
                                         upper_limits[axis],
                                         sleep_after_set=0)

            self.zfcntrl_ca.set_pv_value("OUTPUT:{}:CURR.LOLO".format(axis),
                                         lower_limits[axis],
                                         sleep_after_set=0)
            self.zfcntrl_ca.set_pv_value("OUTPUT:{}:CURR.HIHI".format(axis),
                                         upper_limits[axis],
                                         sleep_after_set=0)

            self.zfcntrl_ca.set_pv_value(
                "OUTPUT:{}:CURR:SP:RBV.LOLO".format(axis),
                lower_limits[axis],
                sleep_after_set=0)
            self.zfcntrl_ca.set_pv_value(
                "OUTPUT:{}:CURR:SP:RBV.HIHI".format(axis),
                upper_limits[axis],
                sleep_after_set=0)

    @contextlib.contextmanager
    def _simulate_disconnected_magnetometer(self):
        """
        While this context manager is active, the magnetometer IOC will fail to take any new readings or process any PVs
        """
        self.magnetometer_ca.set_pv_value("DISABLE", 1, sleep_after_set=0)
        try:
            yield
        finally:
            self.magnetometer_ca.set_pv_value("DISABLE", 0, sleep_after_set=0)

    @contextlib.contextmanager
    def _simulate_invalid_magnetometer_readings(self):
        """
        While this context manager is active, any new readings from the magnetometer will be marked as INVALID
        """
        for axis in FIELD_AXES:
            self.magnetometer_ca.set_pv_value(
                "DAQ:{}:_RAW.SIMS".format(axis),
                self.magnetometer_ca.Alarms.INVALID,
                sleep_after_set=0)

        # Wait for RAW PVs to process
        for axis in FIELD_AXES:
            self.magnetometer_ca.assert_that_pv_alarm_is(
                "DAQ:{}:_RAW.SEVR".format(axis),
                self.magnetometer_ca.Alarms.INVALID)
        try:
            yield
        finally:
            for axis in FIELD_AXES:
                self.magnetometer_ca.set_pv_value(
                    "DAQ:{}:_RAW.SIMS".format(axis),
                    self.magnetometer_ca.Alarms.NONE,
                    sleep_after_set=0)
            # Wait for RAW PVs to process
            for axis in FIELD_AXES:
                self.magnetometer_ca.assert_that_pv_alarm_is(
                    "DAQ:{}:_RAW.SEVR".format(axis),
                    self.magnetometer_ca.Alarms.NONE)

    @contextlib.contextmanager
    def _simulate_invalid_power_supply(self):
        """
        While this context manager is active, the readback values from all power supplies will be marked as INVALID
        (this simulates the device not being plugged in, for example)
        """
        pvs_to_make_invalid = ("CURRENT", "_CURRENT:SP:RBV", "OUTPUTMODE",
                               "OUTPUTSTATUS", "VOLTAGE", "VOLTAGE:SP:RBV")

        for ca, pv in itertools.product(
            (self.x_psu_ca, self.y_psu_ca, self.z_psu_ca),
                pvs_to_make_invalid):
            # 3 is the Enum value for an invalid alarm
            ca.set_pv_value("{}.SIMS".format(pv), 3, sleep_after_set=0)

        # Use a separate loop to avoid needing to wait for a 1-second scan 6 times.
        for ca, pv in itertools.product(
            (self.x_psu_ca, self.y_psu_ca, self.z_psu_ca),
                pvs_to_make_invalid):
            ca.assert_that_pv_alarm_is(pv, ca.Alarms.INVALID)

        try:
            yield
        finally:
            for ca, pv in itertools.product(
                (self.x_psu_ca, self.y_psu_ca, self.z_psu_ca),
                    pvs_to_make_invalid):
                ca.set_pv_value("{}.SIMS".format(pv), 0, sleep_after_set=0)

            # Use a separate loop to avoid needing to wait for a 1-second scan 6 times.
            for ca, pv in itertools.product(
                (self.x_psu_ca, self.y_psu_ca, self.z_psu_ca),
                    pvs_to_make_invalid):
                ca.assert_that_pv_alarm_is(pv, ca.Alarms.NONE)

    @contextlib.contextmanager
    def _simulate_failing_power_supply_writes(self):
        """
        While this context manager is active, any writes to the power supply PVs will be ignored. This simulates the
        device being in local mode, for example. Note that this does not mark readbacks as invalid (for that, use
        _simulate_invalid_power_supply instead).
        """
        pvs = [
            "CURRENT:SP.DISP", "VOLTAGE:SP.DISP", "OUTPUTMODE:SP.DISP",
            "OUTPUTSTATUS:SP.DISP"
        ]

        for ca, pv in itertools.product(
            (self.x_psu_ca, self.y_psu_ca, self.z_psu_ca), pvs):
            ca.set_pv_value(pv, 1, sleep_after_set=0)
        try:
            yield
        finally:
            for ca, pv in itertools.product(
                (self.x_psu_ca, self.y_psu_ca, self.z_psu_ca), pvs):
                ca.set_pv_value(pv, 0, sleep_after_set=0)

    @contextlib.contextmanager
    def _simulate_measured_fields_changing_with_outputs(
            self, psu_amps_at_measured_zero):
        """
        Calculates and sets somewhat realistic simulated measured fields based on the current values of power supplies.

        Args:
            psu_amps_at_measured_zero: Dictionary containing the Amps of the power supplies when the measured field
              corresponds to zero. i.e. if the system is told to go to zero field, these are the power supply readings
              it will require to get there.
        """
        # Always start at zero current
        self._set_simulated_power_supply_currents({"X": 0, "Y": 0, "Z": 0})

        thread = multiprocessing.Process(target=_update_fields_continuously,
                                         args=(psu_amps_at_measured_zero, ))
        thread.start()
        try:
            yield
        finally:
            thread.terminate()

    def _wait_for_all_iocs_up(self):
        """
        Waits for the "primary" pv(s) from each ioc to be available
        """
        for ca in (self.x_psu_ca, self.y_psu_ca, self.z_psu_ca):
            ca.assert_that_pv_exists("CURRENT")
            ca.assert_that_pv_exists("CURRENT:SP")
            ca.assert_that_pv_exists("CURRENT:SP:RBV")

        for axis in FIELD_AXES:
            self.zfcntrl_ca.assert_that_pv_exists("FIELD:{}".format(axis))
            self.magnetometer_ca.assert_that_pv_exists(
                "CORRECTEDFIELD:{}".format(axis))

    def setUp(self):
        _, self._ioc = get_running_lewis_and_ioc(None, ZF_DEVICE_PREFIX)

        timeout = 20
        self.zfcntrl_ca = ChannelAccess(device_prefix=ZF_DEVICE_PREFIX,
                                        default_timeout=timeout)
        self.magnetometer_ca = ChannelAccess(
            device_prefix=MAGNETOMETER_DEVICE_PREFIX, default_timeout=timeout)
        self.x_psu_ca = ChannelAccess(default_timeout=timeout,
                                      device_prefix=X_KEPCO_DEVICE_PREFIX)
        self.y_psu_ca = ChannelAccess(default_timeout=timeout,
                                      device_prefix=Y_KEPCO_DEVICE_PREFIX)
        self.z_psu_ca = ChannelAccess(default_timeout=timeout,
                                      device_prefix=Z_KEPCO_DEVICE_PREFIX)

        self._wait_for_all_iocs_up()

        self.zfcntrl_ca.set_pv_value("TOLERANCE",
                                     STABILITY_TOLERANCE,
                                     sleep_after_set=0)
        self.zfcntrl_ca.set_pv_value("STATEMACHINE:LOOP_DELAY",
                                     LOOP_DELAY_MS,
                                     sleep_after_set=0)
        self._set_autofeedback(False)

        # Set the magnetometer calibration to the 3x3 identity matrix
        for x, y in itertools.product(range(1, 3 + 1), range(1, 3 + 1)):
            self.magnetometer_ca.set_pv_value("SENSORMATRIX:{}{}".format(x, y),
                                              1 if x == y else 0,
                                              sleep_after_set=0)

        self._set_simulated_measured_fields(ZERO_FIELD, overload=False)
        self._set_user_setpoints(ZERO_FIELD)
        self._set_simulated_power_supply_currents(ZERO_FIELD,
                                                  wait_for_update=True)
        self._set_scaling_factors(1, 1, 1, 1)
        self._set_output_limits(
            lower_limits={
                "X": DEFAULT_LOW_OUTPUT_LIMIT,
                "Y": DEFAULT_LOW_OUTPUT_LIMIT,
                "Z": DEFAULT_LOW_OUTPUT_LIMIT
            },
            upper_limits={
                "X": DEFAULT_HIGH_OUTPUT_LIMIT,
                "Y": DEFAULT_HIGH_OUTPUT_LIMIT,
                "Z": DEFAULT_HIGH_OUTPUT_LIMIT
            },
        )

        self._assert_at_setpoint(AtSetpointStatuses.NA)
        self._assert_status(Statuses.NO_ERROR)

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

    @parameterized.expand(parameterized_list(FIELD_AXES))
    def test_WHEN_manual_mode_and_any_readback_value_is_not_equal_to_setpoint_THEN_at_setpoint_field_is_na(
            self, _, axis_to_vary):
        fields = {"X": 10, "Y": 20, "Z": 30}
        self._set_simulated_measured_fields(fields, overload=False)
        self._set_user_setpoints(fields)

        # Set one of the parameters to a completely different value
        self.zfcntrl_ca.set_pv_value("FIELD:{}:SP".format(axis_to_vary),
                                     100,
                                     sleep_after_set=0)

        self._assert_at_setpoint(AtSetpointStatuses.NA)
        self._assert_status(Statuses.NO_ERROR)

    def test_GIVEN_manual_mode_and_magnetometer_not_overloaded_WHEN_readback_values_are_equal_to_setpoints_THEN_at_setpoint_field_is_na(
            self):
        fields = {"X": 55, "Y": 66, "Z": 77}
        self._set_simulated_measured_fields(fields, overload=False)
        self._set_user_setpoints(fields)

        self._assert_at_setpoint(AtSetpointStatuses.NA)
        self._assert_status(Statuses.NO_ERROR)

    def test_GIVEN_manual_mode_and_within_tolerance_WHEN_magnetometer_is_overloaded_THEN_status_overloaded_and_setpoint_field_is_na(
            self):
        fields = {"X": 55, "Y": 66, "Z": 77}
        self._set_simulated_measured_fields(fields, overload=True)
        self._set_user_setpoints(fields)

        self._assert_at_setpoint(AtSetpointStatuses.NA)
        self._assert_status(Statuses.MAGNETOMETER_OVERLOAD)

    def test_GIVEN_manual_mode_and_just_outside_tolerance_WHEN_magnetometer_is_overloaded_THEN_status_overloaded_and_setpoint_field_is_na(
            self):
        fields = {"X": 55, "Y": 66, "Z": 77}
        self._set_simulated_measured_fields(fields, overload=True)
        self._set_user_setpoints({
            k: v + 1.01 * STABILITY_TOLERANCE
            for k, v in six.iteritems(fields)
        })

        self._assert_at_setpoint(AtSetpointStatuses.NA)
        self._assert_status(Statuses.MAGNETOMETER_OVERLOAD)

    def test_GIVEN_manual_mode_and_just_within_tolerance_WHEN_magnetometer_is_overloaded_THEN_status_overloaded_and_setpoint_field_is_na(
            self):
        fields = {"X": 55, "Y": 66, "Z": 77}
        self._set_simulated_measured_fields(fields, overload=True)
        self._set_user_setpoints({
            k: v + 0.99 * STABILITY_TOLERANCE
            for k, v in six.iteritems(fields)
        })

        self._assert_at_setpoint(AtSetpointStatuses.NA)
        self._assert_status(Statuses.MAGNETOMETER_OVERLOAD)

    def test_WHEN_magnetometer_ioc_does_not_respond_THEN_status_is_magnetometer_read_error(
            self):
        fields = {"X": 1, "Y": 2, "Z": 3}
        self._set_simulated_measured_fields(fields, overload=False)
        self._set_user_setpoints(fields)

        with self._simulate_disconnected_magnetometer():
            self._assert_status(Statuses.MAGNETOMETER_READ_ERROR)
            for axis in FIELD_AXES:
                self.zfcntrl_ca.assert_that_pv_alarm_is(
                    "FIELD:{}".format(axis), self.zfcntrl_ca.Alarms.INVALID)
                self.zfcntrl_ca.assert_that_pv_alarm_is(
                    "FIELD:{}:MEAS".format(axis),
                    self.zfcntrl_ca.Alarms.INVALID)

        # Now simulate recovery and assert error gets cleared correctly
        self._assert_status(Statuses.NO_ERROR)
        for axis in FIELD_AXES:
            self.zfcntrl_ca.assert_that_pv_alarm_is(
                "FIELD:{}".format(axis), self.zfcntrl_ca.Alarms.NONE)
            self.zfcntrl_ca.assert_that_pv_alarm_is(
                "FIELD:{}:MEAS".format(axis), self.zfcntrl_ca.Alarms.NONE)

    def test_WHEN_magnetometer_ioc_readings_are_invalid_THEN_status_is_magnetometer_invalid(
            self):
        fields = {"X": 1, "Y": 2, "Z": 3}
        self._set_simulated_measured_fields(fields, overload=False)
        self._set_user_setpoints(fields)

        with self._simulate_invalid_magnetometer_readings():
            self._assert_status(Statuses.MAGNETOMETER_DATA_INVALID)
            for axis in FIELD_AXES:
                self.zfcntrl_ca.assert_that_pv_alarm_is(
                    "FIELD:{}".format(axis), self.zfcntrl_ca.Alarms.INVALID)
                self.zfcntrl_ca.assert_that_pv_alarm_is(
                    "FIELD:{}:MEAS".format(axis),
                    self.zfcntrl_ca.Alarms.INVALID)

        # Now simulate recovery and assert error gets cleared correctly
        self._assert_status(Statuses.NO_ERROR)
        for axis in FIELD_AXES:
            self.zfcntrl_ca.assert_that_pv_alarm_is(
                "FIELD:{}".format(axis), self.zfcntrl_ca.Alarms.NONE)
            self.zfcntrl_ca.assert_that_pv_alarm_is(
                "FIELD:{}:MEAS".format(axis), self.zfcntrl_ca.Alarms.NONE)

    def test_WHEN_power_supplies_are_invalid_THEN_status_is_power_supplies_invalid(
            self):
        fields = {"X": 1, "Y": 2, "Z": 3}
        self._set_simulated_measured_fields(fields, overload=False)
        self._set_user_setpoints(fields)
        self._set_autofeedback(True)

        with self._simulate_invalid_power_supply():
            self._assert_at_setpoint(
                AtSetpointStatuses.TRUE
            )  # Invalid power supplies do not mark the field as "not at setpoint"
            self._assert_status(Statuses.PSU_INVALID)

        # Now simulate recovery and assert error gets cleared correctly
        self._assert_at_setpoint(AtSetpointStatuses.TRUE)
        self._assert_status(Statuses.NO_ERROR)

    def test_WHEN_power_supplies_writes_fail_THEN_status_is_power_supply_writes_failed(
            self):
        fields = {"X": 1, "Y": 2, "Z": 3}
        self._set_simulated_measured_fields(fields, overload=False)

        # For this test we need changing fields so that we can detect that the writes failed
        self._set_user_setpoints({
            k: v + 10 * STABILITY_TOLERANCE
            for k, v in six.iteritems(fields)
        })
        # ... and we also need large limits so that we see that the writes failed as opposed to a limits error
        self._set_output_limits(lower_limits={k: -999999
                                              for k in FIELD_AXES},
                                upper_limits={k: 999999
                                              for k in FIELD_AXES})
        self._set_autofeedback(True)

        with self._simulate_failing_power_supply_writes():
            self._assert_status(Statuses.PSU_WRITE_FAILED)

        # Now simulate recovery and assert error gets cleared correctly
        self._assert_status(Statuses.NO_ERROR)

    def test_GIVEN_measured_field_and_setpoints_are_identical_THEN_setpoints_remain_unchanged(
            self):
        fields = {"X": 5, "Y": 10, "Z": -5}
        outputs = {"X": -1, "Y": -2, "Z": -3}

        self._set_simulated_measured_fields(fields, overload=False)
        self._set_user_setpoints(fields)
        self._set_simulated_power_supply_currents(outputs,
                                                  wait_for_update=True)

        self._set_autofeedback(True)

        for axis in FIELD_AXES:
            self.zfcntrl_ca.assert_that_pv_is_number(
                "OUTPUT:{}:CURR".format(axis), outputs[axis], tolerance=0.0001)
            self.zfcntrl_ca.assert_that_pv_value_is_unchanged(
                "OUTPUT:{}:CURR".format(axis), wait=5)

    @parameterized.expand(
        parameterized_list([
            # If measured field is smaller than the setpoint, we want to adjust the output upwards to compensate
            (operator.sub, operator.gt, 1),
            # If measured field is larger than the setpoint, we want to adjust the output downwards to compensate
            (operator.add, operator.lt, 1),
            # If measured field is smaller than the setpoint, and A/mg is negative, we want to adjust the output downwards
            # to compensate
            (operator.sub, operator.lt, -1),
            # If measured field is larger than the setpoint, and A/mg is negative, we want to adjust the output upwards
            # to compensate
            (operator.add, operator.gt, -1),
            # If measured field is smaller than the setpoint, and A/mg is zero, then power supply output should remain
            # unchanged
            (operator.sub, operator.eq, 0),
            # If measured field is larger than the setpoint, and A/mg is zero, then power supply output should remain
            # unchanged
            (operator.add, operator.eq, 0),
        ]))
    def test_GIVEN_autofeedback_WHEN_measured_field_different_from_setpoints_THEN_power_supply_outputs_move_in_correct_direction(
            self, _, measured_field_modifier, output_comparator,
            scaling_factor):

        fields = {"X": 5, "Y": 0, "Z": -5}

        adjustment_amount = 10 * STABILITY_TOLERANCE  # To ensure that it is not considered stable to start with
        measured_fields = {
            k: measured_field_modifier(v, adjustment_amount)
            for k, v in six.iteritems(fields)
        }

        self._set_scaling_factors(scaling_factor,
                                  scaling_factor,
                                  scaling_factor,
                                  fiddle=1)
        self._set_simulated_measured_fields(measured_fields, overload=False)
        self._set_user_setpoints(fields)
        self._set_simulated_power_supply_currents({
            "X": 0,
            "Y": 0,
            "Z": 0
        },
                                                  wait_for_update=True)
        self._set_output_limits(lower_limits={k: -999999
                                              for k in FIELD_AXES},
                                upper_limits={k: 999999
                                              for k in FIELD_AXES})

        self._assert_status(Statuses.NO_ERROR)

        self._set_autofeedback(True)
        self._assert_at_setpoint(AtSetpointStatuses.FALSE)

        for axis in FIELD_AXES:
            self.zfcntrl_ca.assert_that_pv_value_over_time_satisfies_comparator(
                "OUTPUT:{}:CURR".format(axis),
                wait=5,
                comparator=output_comparator)

        # In this happy-path case, we shouldn't be hitting any long timeouts, so loop times should remain fairly quick
        self.zfcntrl_ca.assert_that_pv_is_within_range(
            "STATEMACHINE:LOOP_TIME", min_value=0, max_value=2 * LOOP_DELAY_MS)

    def test_GIVEN_output_limits_too_small_for_required_field_THEN_status_error_and_alarm(
            self):
        self._set_output_limits(
            lower_limits={
                "X": -0.1,
                "Y": -0.1,
                "Z": -0.1
            },
            upper_limits={
                "X": 0.1,
                "Y": 0.1,
                "Z": 0.1
            },
        )

        # The measured field is smaller than the setpoint, i.e. the output needs to go up to the limits
        self._set_simulated_measured_fields({"X": -1, "Y": -1, "Z": -1})
        self._set_user_setpoints(ZERO_FIELD)
        self._set_simulated_power_supply_currents(ZERO_FIELD)

        self._set_autofeedback(True)

        self._assert_status(Statuses.PSU_ON_LIMITS)
        for axis in FIELD_AXES:
            # Value should be on one of the limits
            self.zfcntrl_ca.assert_that_pv_is_one_of(
                "OUTPUT:{}:CURR:SP".format(axis), [-0.1, 0.1])
            # ...and in alarm
            self.zfcntrl_ca.assert_that_pv_alarm_is(
                "OUTPUT:{}:CURR:SP".format(axis), self.zfcntrl_ca.Alarms.MAJOR)

    def test_GIVEN_limits_wrong_way_around_THEN_appropriate_error_raised(self):
        # Set upper limits < lower limits
        self._set_output_limits(
            lower_limits={
                "X": 0.1,
                "Y": 0.1,
                "Z": 0.1
            },
            upper_limits={
                "X": -0.1,
                "Y": -0.1,
                "Z": -0.1
            },
        )
        self._set_autofeedback(True)
        self._assert_status(Statuses.INVALID_PSU_LIMITS)

    @parameterized.expand(
        parameterized_list([
            {
                "X": 45.678,
                "Y": 0.123,
                "Z": 12.345
            },
            {
                "X": 0,
                "Y": 0,
                "Z": 0
            },
            {
                "X": -45.678,
                "Y": -0.123,
                "Z": -12.345
            },
        ]))
    def test_GIVEN_measured_values_updating_realistically_WHEN_in_auto_mode_THEN_converges_to_correct_answer(
            self, _, psu_amps_at_zero_field):
        self._set_output_limits(lower_limits={k: -100
                                              for k in FIELD_AXES},
                                upper_limits={k: 100
                                              for k in FIELD_AXES})
        self._set_user_setpoints({"X": 0, "Y": 0, "Z": 0})
        self._set_simulated_power_supply_currents({"X": 0, "Y": 0, "Z": 0})

        # Set fiddle small to get a relatively slow response, which should theoretically be stable
        self._set_scaling_factors(0.001, 0.001, 0.001, fiddle=0.05)

        with self._simulate_measured_fields_changing_with_outputs(
                psu_amps_at_measured_zero=psu_amps_at_zero_field):
            self._set_autofeedback(True)
            for axis in FIELD_AXES:
                self.zfcntrl_ca.assert_that_pv_is_number(
                    "OUTPUT:{}:CURR:SP:RBV".format(axis),
                    psu_amps_at_zero_field[axis],
                    tolerance=STABILITY_TOLERANCE * 0.001,
                    timeout=60)
                self.zfcntrl_ca.assert_that_pv_is_number(
                    "FIELD:{}".format(axis),
                    0.0,
                    tolerance=STABILITY_TOLERANCE)

            self._assert_at_setpoint(AtSetpointStatuses.TRUE)
            self.zfcntrl_ca.assert_that_pv_value_is_unchanged("AT_SETPOINT",
                                                              wait=20)
            self._assert_status(Statuses.NO_ERROR)

    @parameterized.expand(parameterized_list(FIELD_AXES))
    def test_GIVEN_output_is_off_WHEN_autofeedback_switched_on_THEN_psu_is_switched_back_on(
            self, _, axis):
        self.zfcntrl_ca.assert_setting_setpoint_sets_readback(
            "Off", "OUTPUT:{}:STATUS".format(axis))
        self._set_autofeedback(True)
        self.zfcntrl_ca.assert_that_pv_is("OUTPUT:{}:STATUS".format(axis),
                                          "On")

    @parameterized.expand(parameterized_list(FIELD_AXES))
    def test_GIVEN_output_mode_is_voltage_WHEN_autofeedback_switched_on_THEN_psu_is_switched_to_current_mode(
            self, _, axis):
        self.zfcntrl_ca.assert_setting_setpoint_sets_readback(
            "Voltage",
            "OUTPUT:{}:MODE".format(axis),
            expected_alarm=self.zfcntrl_ca.Alarms.MAJOR)
        self._set_autofeedback(True)
        self.zfcntrl_ca.assert_that_pv_is("OUTPUT:{}:MODE".format(axis),
                                          "Current")

    @parameterized.expand(parameterized_list(FIELD_AXES))
    def test_GIVEN_output_is_off_and_cannot_write_to_psu_WHEN_autofeedback_switched_on_THEN_get_psu_write_error(
            self, _, axis):
        self.zfcntrl_ca.assert_setting_setpoint_sets_readback(
            "Off", "OUTPUT:{}:STATUS".format(axis))
        with self._simulate_failing_power_supply_writes():
            self._set_autofeedback(True)
            self._assert_status(Statuses.PSU_WRITE_FAILED)

        # Check it can recover when writes work again
        self._assert_status(Statuses.NO_ERROR)
        self.zfcntrl_ca.assert_that_pv_is("OUTPUT:{}:STATUS".format(axis),
                                          "On")

    @parameterized.expand(parameterized_list(FIELD_AXES))
    def test_GIVEN_output_mode_is_voltage_and_cannot_write_to_psu_WHEN_autofeedback_switched_on_THEN_get_psu_write_error(
            self, _, axis):
        self.zfcntrl_ca.assert_setting_setpoint_sets_readback(
            "Voltage",
            "OUTPUT:{}:MODE".format(axis),
            expected_alarm=self.zfcntrl_ca.Alarms.MAJOR)

        with self._simulate_failing_power_supply_writes():
            self._set_autofeedback(True)
            self._assert_status(Statuses.PSU_WRITE_FAILED)

        # Check it can recover when writes work again
        self._assert_status(Statuses.NO_ERROR)
        self.zfcntrl_ca.assert_that_pv_is("OUTPUT:{}:MODE".format(axis),
                                          "Current")

    @parameterized.expand(
        parameterized_list([
            (True, True),
            (False, True),
            (True, False),
            (False, False),
        ]))
    def test_GIVEN_magnetometer_overloaded_THEN_error_suppressed_if_in_manual_mode(
            self, _, autofeedback, overloaded):
        self._set_autofeedback(autofeedback)
        self._set_simulated_measured_fields(ZERO_FIELD,
                                            overload=overloaded,
                                            wait_for_update=True)
        self._assert_status(Statuses.MAGNETOMETER_OVERLOAD
                            if overloaded else Statuses.NO_ERROR)
        self.zfcntrl_ca.assert_that_pv_alarm_is(
            "STATUS", self.zfcntrl_ca.Alarms.MAJOR
            if overloaded and autofeedback else self.zfcntrl_ca.Alarms.NONE)

    def test_GIVEN_power_supply_voltage_limit_is_set_incorrectly_WHEN_going_into_auto_mode_THEN_correct_limits_applied(
            self):
        self._set_simulated_power_supply_voltages({"X": 0, "Y": 0, "Z": 0})

        self._set_autofeedback(True)

        self.zfcntrl_ca.assert_that_pv_is("OUTPUT:X:VOLT:SP:RBV",
                                          X_KEPCO_VOLTAGE_LIMIT)
        self.zfcntrl_ca.assert_that_pv_is("OUTPUT:Y:VOLT:SP:RBV",
                                          Y_KEPCO_VOLTAGE_LIMIT)
        self.zfcntrl_ca.assert_that_pv_is("OUTPUT:Z:VOLT:SP:RBV",
                                          Z_KEPCO_VOLTAGE_LIMIT)
class RikenChangeover(object):
    """
    Tests for a riken changeover.

    This class is inherited by the riken port changeover tests and also the RB2 mode change tests as they are very
    similar (just the PSUs that they look at / control are different)
    """
    @abstractmethod
    def get_input_pv(self):
        return ""

    @abstractmethod
    def get_acknowledgement_pv(self):
        return ""

    @abstractmethod
    def get_power_supplies(self):
        return []

    @abstractmethod
    def get_coord_prefix(self):
        return ""

    @abstractmethod
    def get_prefix(self):
        return ""

    def _set_input_pv(self, ok_to_run_psus):
        self.ca.set_pv_value("{}:SIM".format(self.get_input_pv()), 1 if ok_to_run_psus else 0)
        self.ca.assert_that_pv_is("{}:SIM".format(self.get_input_pv()), 1 if ok_to_run_psus else 0)
        self.ca.assert_that_pv_alarm_is("{}".format(self.get_input_pv()), self.ca.Alarms.NONE)
        self.ca.assert_that_pv_is("{}".format(self.get_input_pv()), 1 if ok_to_run_psus else 0)

    def _set_power_supply_state(self, supply, on):
        self.ca.set_pv_value("{}:POWER:SP".format(supply), 1 if on else 0)
        self.ca.assert_that_pv_is("{}:POWER".format(supply), "On" if on else "Off")

    def _assert_power_supply_disabled(self, supply, disabled):
        self.ca.assert_that_pv_is_number("{}:POWER:SP.DISP".format(supply), 1 if disabled else 0)

    def _set_all_power_supply_states(self, on):
        for supply in self.get_power_supplies():
            self._set_power_supply_state(supply, on)

    def _assert_all_power_supplies_disabled(self, disabled):
        for supply in self.get_power_supplies():
            self._assert_power_supply_disabled(supply, disabled)

    def _assert_necessary_pvs_exist(self):
        self.ca.assert_that_pv_exists("{}:PSUS:DISABLE".format(self.get_coord_prefix()))
        self.ca.assert_that_pv_exists(self.get_input_pv())
        self.ca.assert_that_pv_exists(self.get_acknowledgement_pv())
        for id in self.get_power_supplies():
            self.ca.assert_that_pv_exists("{}:POWER".format(id))

    def setUp(self):
        self.ca = ChannelAccess(device_prefix=self.get_prefix(), default_timeout=10)

        self._assert_necessary_pvs_exist()

        self._set_input_pv(False)  # Set it to false so that the sequencer see the change,
                                   # probably to do with it being in RECSIM
        self._set_input_pv(True)
        self._assert_all_power_supplies_disabled(False)
        self._set_all_power_supply_states(False)

    def test_GIVEN_value_on_input_ioc_changes_THEN_coord_psus_disable_pv_updates_with_the_same_value(self):
        def _set_and_check(ok_to_run_psus):
            self._set_input_pv(ok_to_run_psus)
            self.ca.assert_that_pv_is("{}:PSUS:DISABLE".format(self.get_coord_prefix()),
                                      "ENABLED" if ok_to_run_psus else "DISABLED")

        for ok_to_run_psus in [True, False, True]:  # Check both transitions
            _set_and_check(ok_to_run_psus)

    def test_GIVEN_all_power_supplies_off_WHEN_value_on_input_ioc_changes_THEN_power_supplies_have_their_disp_field_set(self):
        def _set_and_check_disabled_status(ok_to_run_psus):
            self._set_input_pv(ok_to_run_psus)
            self._assert_all_power_supplies_disabled(not ok_to_run_psus)

        for ok_to_run_psus in [True, False, True]:  # Check both transitions
            _set_and_check_disabled_status(ok_to_run_psus)

    def test_WHEN_any_power_supply_is_on_THEN_power_all_pv_is_high(self):

        self._set_all_power_supply_states(False)

        self.ca.assert_that_pv_is_number("{}:PSUS:POWER".format(self.get_coord_prefix()), 0)

        for psu in self.get_power_supplies():
            self._set_power_supply_state(psu, True)
            self.ca.assert_that_pv_is_number("{}:PSUS:POWER".format(self.get_coord_prefix()), 1)

            self._set_power_supply_state(psu, False)
            self.ca.assert_that_pv_is_number("{}:PSUS:POWER".format(self.get_coord_prefix()), 0)

    def test_GIVEN_power_supplies_on_WHEN_value_on_input_ioc_changes_THEN_power_supplies_are_not_disabled_until_they_are_switched_off(self):
        self._set_all_power_supply_states(True)
        self._set_input_pv(False)
        self._assert_all_power_supplies_disabled(False)
        self._set_all_power_supply_states(False)
        self._assert_all_power_supplies_disabled(True)

    def test_GIVEN_plc_cancels_changeover_before_psus_are_all_switched_off_WHEN_psus_become_switched_off_THEN_they_do_not_get_disabled(self):
        self._set_all_power_supply_states(True)
        self._set_input_pv(False)
        self._assert_all_power_supplies_disabled(False)  # Power supplies not disabled because still powered on
        self._set_input_pv(True)  # PLC now cancels request to do a changeover
        self._set_all_power_supply_states(False)
        self._assert_all_power_supplies_disabled(False)

    def test_GIVEN_a_power_supply_is_in_alarm_THEN_the_power_any_pv_is_also_in_alarm(self):
        for supply in self.get_power_supplies():
            with self.ca.put_simulated_record_into_alarm("{}:POWER".format(supply), self.ca.Alarms.INVALID):
                self.ca.assert_that_pv_alarm_is("{}:PSUS:POWER".format(self.get_coord_prefix()), self.ca.Alarms.INVALID)
            self.ca.assert_that_pv_alarm_is("{}:PSUS:POWER".format(self.get_coord_prefix()), self.ca.Alarms.NONE)

    def test_GIVEN_all_power_supply_are_in_alarm_THEN_the_power_any_pv_is_also_in_alarm(self):
        with ExitStack() as stack:
            for supply in self.get_power_supplies():
                stack.enter_context(
                    self.ca.put_simulated_record_into_alarm("{}:POWER".format(supply), self.ca.Alarms.INVALID)
                )
            self.ca.assert_that_pv_alarm_is("{}:PSUS:POWER".format(self.get_coord_prefix()), self.ca.Alarms.INVALID)
        self.ca.assert_that_pv_alarm_is("{}:PSUS:POWER".format(self.get_coord_prefix()), self.ca.Alarms.NONE)

    def test_GIVEN_a_power_supply_is_in_alarm_THEN_the_power_any_pv_reports_that_psus_are_active(self):
        for supply in self.get_power_supplies():
            with self.ca.put_simulated_record_into_alarm("{}:POWER".format(supply), self.ca.Alarms.INVALID):
                self.ca.assert_that_pv_is_number("{}:PSUS:POWER".format(self.get_coord_prefix()), 1)
            self.ca.assert_that_pv_is_number("{}:PSUS:POWER".format(self.get_coord_prefix()), 0)

    def test_GIVEN_all_power_supply_are_in_alarm_THEN_the_power_any_pv_reports_that_psus_are_active(self):
        with ExitStack() as stack:
            for supply in self.get_power_supplies():
                stack.enter_context(
                    self.ca.put_simulated_record_into_alarm("{}:POWER".format(supply), self.ca.Alarms.INVALID)
                )
            self.ca.assert_that_pv_is_number("{}:PSUS:POWER".format(self.get_coord_prefix()), 1)
        self.ca.assert_that_pv_is_number("{}:PSUS:POWER".format(self.get_coord_prefix()), 0)

    def test_GIVEN_changeover_initiated_WHEN_power_supplies_off_THEN_acknowledgement_pv_true(self):
        self._set_all_power_supply_states(False)
        self._set_input_pv(False)

        self.ca.assert_that_pv_is_not_number(self.get_acknowledgement_pv(), 0)

        self._set_input_pv(True)  # Some time later the PLC sends signal to say it has finished the changeover sequence
        self.ca.assert_that_pv_is_number(self.get_acknowledgement_pv(), 0)

    def test_GIVEN_changeover_sequence_completes_THEN_power_supplies_are_reenabled_after_sequence(self):
        self._set_all_power_supply_states(True)
        self._set_input_pv(False)
        self._assert_all_power_supplies_disabled(False)  # Power supplies not disabled because still powered on
        self._set_all_power_supply_states(False)  # Power supplies now switched off so changeover can continue
        self._assert_all_power_supplies_disabled(True)  # All power supplies are now disabled
        self.ca.assert_that_pv_is_not_number(self.get_acknowledgement_pv(), 0)
        self._set_input_pv(True)  # Some time later, changeover is finished
        self._assert_all_power_supplies_disabled(False)  # Power supplies should now be reenabled
        self.ca.assert_that_pv_is(self.get_acknowledgement_pv(), 0)  # And "ok to run changeover" line should be cleared
class Lakeshore340Tests(unittest.TestCase):
    """
    Tests for the lakeshore 340 IOC.
    """
    def setUp(self):
        self._lewis, self._ioc = get_running_lewis_and_ioc(
            _EMULATOR_NAME, DEVICE_PREFIX)
        self.ca = ChannelAccess(device_prefix=DEVICE_PREFIX,
                                default_timeout=15)

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

    @parameterized.expand(
        parameterized_list(itertools.product(SENSORS, TEST_TEMPERATURES)))
    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_temperature_set_via_backdoor_THEN_it_can_be_read_back(
            self, _, sensor, value):
        self._lewis.backdoor_set_on_device("temp_{}".format(sensor.lower()),
                                           value)
        self.ca.assert_that_pv_is_number("{}:TEMP".format(sensor.upper()),
                                         value)

    @parameterized.expand(
        parameterized_list(itertools.product(SENSORS, TEST_TEMPERATURES)))
    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_measurement_set_via_backdoor_THEN_it_can_be_read_back(
            self, _, sensor, value):
        self._lewis.backdoor_set_on_device(
            "measurement_{}".format(sensor.lower()), value)
        self.ca.assert_that_pv_is_number("{}:RDG".format(sensor.upper()),
                                         value)

    @parameterized.expand(parameterized_list(TEST_TEMPERATURES))
    def test_WHEN_tset_is_changed_THEN_readback_updates(self, _, val):
        self.ca.assert_setting_setpoint_sets_readback(
            val, readback_pv="A:TEMP:SP:RBV", set_point_pv="A:TEMP:SP")

    @parameterized.expand(
        parameterized_list(itertools.product(PID_SETTINGS, PID_TEST_VALUES)))
    def test_WHEN_pid_settings_changed_THEN_can_be_read_back(
            self, _, setting, value):
        if setting == "D":
            value = int(
                value)  # Derivative is only allowed to take integer values.

        self.ca.assert_setting_setpoint_sets_readback(value, setting)

    @parameterized.expand(parameterized_list(PID_MODES))
    def test_WHEN_pid_settings_changed_THEN_can_be_read_back(self, _, mode):
        self.ca.assert_setting_setpoint_sets_readback(mode, "PIDMODE")

    @parameterized.expand(parameterized_list(LOOP_STATES))
    def test_WHEN_loop_turned_on_or_off_THEN_can_be_read_back(
            self, _, loopstate):
        self.ca.assert_setting_setpoint_sets_readback(loopstate, "LOOP")

    @parameterized.expand(parameterized_list(TEST_TEMPERATURES))
    def test_WHEN_max_temperature_set_THEN_can_be_read_back(self, _, temp):
        self.ca.assert_setting_setpoint_sets_readback(temp, "TEMP:MAX")

    @parameterized.expand(parameterized_list(HEATER_PERCENTAGES))
    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_heater_power_set_via_backdoor_THEN_can_be_read_back(
            self, _, output):
        self._lewis.backdoor_set_on_device("heater_output", output)
        self.ca.assert_that_pv_is_number("OUTPUT", output)

    @parameterized.expand(parameterized_list(RANGES))
    def test_WHEN_heater_range_set_THEN_can_be_read_back(self, _, range):
        self.ca.assert_setting_setpoint_sets_readback(range, "RANGE")

    @parameterized.expand(parameterized_list(EXCITATIONS))
    def test_WHEN_excitation_a_set_THEN_can_be_read_back(self, _, excitation):
        self.ca.assert_setting_setpoint_sets_readback(excitation,
                                                      "EXCITATIONA")

    @parameterized.expand(parameterized_list(EXCITATIONS))
    @skip_if_recsim
    def test_WHEN_excitation_set_by_backdoor_THEN_can_be_read_back(
            self, _, excitation):
        self._lewis.backdoor_set_on_device("excitationa",
                                           EXCITATIONS.index(excitation))
        self.ca.assert_that_pv_is("EXCITATIONA", excitation)

    def test_WHEN_use_valid_file_THEN_threshold_file_is_none(self):
        self.ca.assert_that_pv_is_path(THRESHOLD_FILE_PV,
                                       THRESHOLD_FILES_DIR + THRESHOLDS_FILE)
        self.ca.assert_that_pv_is(THRESHOLDS_ERROR_PV, "No Error")
        self.ca.assert_that_pv_is_path("THRESHOLDS:USE", "YES")

    def test_WHEN_do_not_use_file_THEN_threshold_file_is_not_set(self):
        with self._ioc.start_with_macros(
            {"USE_EXCITATION_THRESHOLD_FILE": "NO"},
                pv_to_wait_for=THRESHOLD_FILE_PV):
            self.ca.assert_that_pv_is_path("THRESHOLDS:USE", "NO")
            self.ca.assert_that_pv_is(THRESHOLDS_ERROR_PV, "No Error")

    def test_WHEN_initialise_with_incorrect_macro_THEN_pv_is_in_alarm(self):
        filename = "DoesNotExist.txt"
        with self._ioc.start_with_macros(
            {
                "EXCITATION_THRESHOLD_FILE": filename,
                "USE_EXCITATION_THRESHOLD_FILE": "YES"
            },
                pv_to_wait_for=THRESHOLD_FILE_PV):
            self.ca.assert_that_pv_is(THRESHOLDS_ERROR_PV, "File Not Found")
            self.ca.assert_that_pv_is_path("THRESHOLDS:USE", "YES")

    def test_WHEN_initialise_with_invalid_file_THEN_pv_is_in_alarm(self):
        filename = "InvalidLines.txt"
        with self._ioc.start_with_macros(
            {
                "EXCITATION_THRESHOLD_FILE": filename,
                "USE_EXCITATION_THRESHOLD_FILE": "YES"
            },
                pv_to_wait_for=THRESHOLD_FILE_PV):
            self.ca.assert_that_pv_is(THRESHOLDS_ERROR_PV,
                                      "Invalid Lines In File")
            self.ca.assert_that_pv_is_path("THRESHOLDS:USE", "YES")

    def reset_thresholds_values(self, thresholds_excitations, thresholds_temp,
                                excitationa, error, delay_change, temp):
        self.ca.assert_setting_setpoint_sets_readback(excitationa,
                                                      EXCITATIONA_PV)
        self.ca.set_pv_value(THRESHOLD_TEMP_PV, thresholds_temp)
        self.ca.set_pv_value(THRESHOLD_EXCITATIONS_PV, thresholds_excitations)
        self.ca.set_pv_value(THRESHOLDS_DELAY_CHANGE_PV, delay_change)
        self.ca.set_pv_value(THRESHOLDS_ERROR_PV, error)
        self._lewis.backdoor_set_on_device("temp_a", temp)

    def assert_threshold_values(self, thresholds_excitations, thresholds_temp,
                                excitationa, error, error_severity,
                                delay_change):
        self.ca.assert_that_pv_is(THRESHOLD_EXCITATIONS_PV,
                                  thresholds_excitations)
        self.ca.assert_that_pv_is(THRESHOLD_TEMP_PV, thresholds_temp)
        self.ca.assert_that_pv_is(THRESHOLDS_DELAY_CHANGE_PV, delay_change)
        self.ca.assert_that_pv_is(THRESHOLDS_ERROR_PV, error)
        self.ca.assert_that_pv_alarm_is(THRESHOLDS_ERROR_PV, error_severity)
        self.ca.assert_that_pv_is(EXCITATIONA_PV, excitationa)

    @parameterized.expand(parameterized_list(TEMP_SP_EXCITATIONS))
    @skip_if_recsim
    def test_WHEN_set_temp_sp_THEN_thresholds_recalculated(
            self, _, temp_sp_excitations_map):
        new_temp_sp = temp_sp_excitations_map["TEMP:SP"]
        expected_thresholds_temp = temp_sp_excitations_map["THRESHOLDS:TEMP"]
        expected_thresholds_excitation = temp_sp_excitations_map[
            "THRESHOLDS:EXCITATION"]
        # Reset pv values to test
        self.reset_thresholds_values("Off", 0, "Off", "No Error", "NO",
                                     new_temp_sp - 10)
        # Set setpoint
        self.ca.assert_setting_setpoint_sets_readback(
            new_temp_sp, readback_pv="A:TEMP:SP:RBV", set_point_pv="A:TEMP:SP")
        # Confirm change is delayed but threshold temp is set
        self.assert_threshold_values(expected_thresholds_excitation,
                                     expected_thresholds_temp, "Off",
                                     "No Error", "NO_ALARM", "YES")
        # Make temperature equal setpoint
        self._lewis.backdoor_set_on_device("temp_a", new_temp_sp)
        # Confirm Excitations is set correctly
        self.assert_threshold_values(expected_thresholds_excitation,
                                     expected_thresholds_temp,
                                     expected_thresholds_excitation,
                                     "No Error", "NO_ALARM", "NO")

    @parameterized.expand(
        parameterized_list([("None.txt", "NO_ALARM", "No Error"),
                            ("DoesNotExist.txt", "MINOR", "File Not Found"),
                            ("InvalidLines.txt", "MINOR",
                             "Invalid Lines In File")]))
    @skip_if_recsim
    def test_GIVEN_not_using_excitations_OR_invalid_file_WHEN_set_temp_sp_THEN_thresholds_not_recalculated(
            self, _, filename, expected_error_severity, expected_error):
        with self._ioc.start_with_macros(
            {"EXCITATION_THRESHOLD_FILE": filename},
                pv_to_wait_for=THRESHOLD_FILE_PV):
            for temp_sp, temp, excitation in [(5.2, 3.1, "30 nA"),
                                              (16.4, 18.2, "100 nA"),
                                              (20.9, 0, "Off"),
                                              (400.2, 20.3, "1 mV")]:
                # Reset pv values to test
                self.reset_thresholds_values(excitation, temp, excitation,
                                             "No Error", "NO", temp_sp - 10)
                # Set temp
                self.ca.assert_setting_setpoint_sets_readback(
                    temp_sp,
                    readback_pv="A:TEMP:SP:RBV",
                    set_point_pv="A:TEMP:SP")
                self._lewis.backdoor_set_on_device("temp_a", temp_sp)
                # Assert nothing has changed
                self.assert_threshold_values(excitation, temp, excitation,
                                             expected_error,
                                             expected_error_severity, "NO")
Ejemplo n.º 30
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)