def set_axis_moving(axis):
    ca_motors = ChannelAccess(device_prefix="MOT")
    current_position = ca_motors.get_pv_value(axis)
    low_limit = ca_motors.get_pv_value(axis + ":MTR.LLM")
    high_limit = ca_motors.get_pv_value(axis + ":MTR.HLM")
    if current_position - low_limit < high_limit - current_position:
        ca_motors.set_pv_value(axis + ":SP", high_limit)
    else:
        ca_motors.set_pv_value(axis + ":SP", low_limit)
class GemJawsTests(unittest.TestCase):
    """
    Tests for the gem beamscraper jaws
    """
    def setUp(self):
        self._ioc = IOCRegister.get_running("gem_jaws")
        self.ca = ChannelAccess(default_timeout=30)

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

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

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

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

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

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

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

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

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

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

            self.ca.assert_that_pv_is_number(MOTOR_N + lim, calc_expected_linear_read(underlying_limit),
                                             TOLERANCE)
Exemplo n.º 3
0
def _update_fields_continuously(psu_amps_at_measured_zero):
    """
    This method is run in a background process for some tests which require the measured fields to "respond" to the
    power supply setpoints in a semi-realistic way.

    It makes new channel access objects so that we don't share locks with the existing ones, as that can cause issues
    with the update rate of this loop. In general this loop needs to be about as fast (or ideally faster) than the
    loop speed of the IOC, so that whenever the state machine picks up new magnetometer readings they reflect the
    latest power supply setpoints.
    """
    psu_ca = {
        "X": ChannelAccess(device_prefix=X_KEPCO_DEVICE_PREFIX),
        "Y": ChannelAccess(device_prefix=Y_KEPCO_DEVICE_PREFIX),
        "Z": ChannelAccess(device_prefix=Z_KEPCO_DEVICE_PREFIX),
    }

    controller_ca = ChannelAccess(device_prefix=ZF_DEVICE_PREFIX)
    magnetometer_ca = ChannelAccess(device_prefix=MAGNETOMETER_DEVICE_PREFIX)

    amps_per_mg = {
        "X": controller_ca.get_pv_value("P:X"),
        "Y": controller_ca.get_pv_value("P:Y"),
        "Z": controller_ca.get_pv_value("P:Z"),
    }

    while True:
        outputs = {
            axis: psu_ca[axis].get_pv_value("CURRENT:SP:RBV")
            for axis in FIELD_AXES
        }

        measured = {
            axis: (outputs[axis] - psu_amps_at_measured_zero[axis]) *
            (1.0 / amps_per_mg[axis])
            for axis in FIELD_AXES
        }

        for axis in FIELD_AXES:
            magnetometer_ca.set_pv_value("SIM:DAQ:{}".format(axis),
                                         measured[axis],
                                         sleep_after_set=0)
class DanfysikBase(object):
    """
    Tests for danfysik.
    """
    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)

        self._lewis.backdoor_run_function_on_device("reinitialise")
        self._lewis.backdoor_set_on_device("comms_initialized", True)

        # Used for daisy chained Danfysiks, default is a single Danfysik so we don't need an id
        self.id_prefixes = [""]

        self.current_readback_factor = 1

        self.set_autoonoff(False)

    def set_autoonoff(self, state):
        """
        Sets the status of the AUTOONOFF pv.

        Args:
            state (bool): True to enable AUTOONOFF, false otherwise
        """
        state_desc = "Enabled" if state else "Disabled"

        if self.ca.get_pv_value("AUTOONOFF") != state_desc:
            old_autoonoff_disp = int(self.ca.get_pv_value("AUTOONOFF.DISP"))
            self.ca.set_pv_value("AUTOONOFF.DISP",
                                 0,
                                 wait=True,
                                 sleep_after_set=0)
            self.ca.set_pv_value("AUTOONOFF", state, sleep_after_set=0)
            self.ca.assert_that_pv_is("AUTOONOFF", state_desc)
            self.ca.set_pv_value("AUTOONOFF.DISP",
                                 old_autoonoff_disp,
                                 sleep_after_set=0)
class SimpleTests(unittest.TestCase):
    """
    Tests for the stability checking logic
    """

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        # Adding one more valid reading at the end of the buffer should cause the invalid value at the beginning
        # to be forgotten, meaning it should then be considered stable
        self.ca.set_pv_value("VAL", stable_value)
        self.ca.assert_that_pv_is("STAB:IS_STABLE", True)
Exemplo n.º 6
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 IpsTests(unittest.TestCase):
    """
    Tests for the Ips IOC.
    """
    def setUp(self):
        self._lewis, self._ioc = get_running_lewis_and_ioc(EMULATOR_NAME, DEVICE_PREFIX)
        # Some changes happen on the order of HEATER_WAIT_TIME seconds. Use a significantly longer timeout
        # to capture a few heater wait times plus some time for PVs to update.
        self.ca = ChannelAccess(device_prefix=DEVICE_PREFIX, default_timeout=HEATER_WAIT_TIME*10)

        # Wait for some critical pvs to be connected.
        for pv in ["MAGNET:FIELD:PERSISTENT", "FIELD", "FIELD:SP:RBV", "HEATER:STATUS"]:
            self.ca.assert_that_pv_exists(pv)

        # Ensure in the correct mode
        self.ca.set_pv_value("CONTROL:SP", "Remote & Unlocked")
        self.ca.set_pv_value("ACTIVITY:SP", "To Setpoint")

        # Don't run reset as the sudden change of state confuses the IOC's state machine. No matter what the initial
        # state of the device the SNL should be able to deal with it.
        # self._lewis.backdoor_run_function_on_device("reset")

        self.ca.set_pv_value("HEATER:WAITTIME", HEATER_WAIT_TIME)

        self.ca.set_pv_value("FIELD:RATE:SP", 10)
        # self.ca.assert_that_pv_is_number("FIELD:RATE:SP", 10)

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

        # Wait for statemachine to reach "at field" state before every test.
        self.ca.assert_that_pv_is("STATEMACHINE", "At field")

    def tearDown(self):
        # Wait for statemachine to reach "at field" state after every test.
        self.ca.assert_that_pv_is("STATEMACHINE", "At field")

        self.assertEqual(self._lewis.backdoor_get_from_device("quenched"), False)

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

    def _assert_field_is(self, field, check_stable=False):
        self.ca.assert_that_pv_is_number("FIELD", field, tolerance=TOLERANCE)
        self.ca.assert_that_pv_is_number("FIELD:USER", field, tolerance=TOLERANCE)
        if check_stable:
            self.ca.assert_that_pv_value_is_unchanged("FIELD", wait=30)
            self.ca.assert_that_pv_is_number("FIELD", field, tolerance=TOLERANCE, timeout=10)
            self.ca.assert_that_pv_is_number("FIELD:USER", field, tolerance=TOLERANCE, timeout=10)

    def _assert_heater_is(self, heater_state):
        self.ca.assert_that_pv_is("HEATER:STATUS:SP", "On" if heater_state else "Off")
        if heater_state:
            self.ca.assert_that_pv_is("HEATER:STATUS", "On",)
        else:
            self.ca.assert_that_pv_is_one_of("HEATER:STATUS", HEATER_OFF_STATES)

    def _set_and_check_persistent_mode(self, mode):
        self.ca.assert_setting_setpoint_sets_readback("YES" if mode else "NO", "PERSISTENT")

    @parameterized.expand(val for val in parameterized_list(TEST_VALUES))
    def test_GIVEN_persistent_mode_enabled_WHEN_magnet_told_to_go_to_field_setpoint_THEN_goes_to_that_setpoint_and_psu_ramps_to_zero(self, _, val):

        self._set_and_check_persistent_mode(True)

        # Field in the magnet already from persistent mode.
        persistent_field = float(self.ca.get_pv_value("MAGNET:FIELD:PERSISTENT"))

        # Set the new field. This will cause all of the following events based on the state machine.
        self.ca.set_pv_value("FIELD:SP", val)

        # PSU should be ramped to match the persistent field inside the magnet
        self._assert_field_is(persistent_field)
        self.ca.assert_that_pv_is("ACTIVITY", "To Setpoint")

        # Then it is safe to turn on the heater
        self._assert_heater_is(True)

        # Assert that value gets passed to device by SNL. SNL waits 30s for the heater to cool down/warm up
        # after being set.
        self._assert_field_is(val)

        # Now that the correct current is in the magnet, the SNL should turn the heater off
        self._assert_heater_is(False)

        # Now that the heater is off, can ramp down the PSU to zero (SNL waits some time for heater to be off before
        # ramping PSU to zero)
        self.ca.assert_that_pv_is_number("FIELD", 0, tolerance=TOLERANCE)  # PSU field
        self.ca.assert_that_pv_is_number("MAGNET:FIELD:PERSISTENT", val, tolerance=TOLERANCE)  # Persistent field
        self.ca.assert_that_pv_is_number("FIELD:USER", val, tolerance=TOLERANCE)  # User field should be tracking persistent field here
        self.ca.assert_that_pv_is("ACTIVITY", "To Zero")

        # ...And the magnet should now be in the right state!
        self.ca.assert_that_pv_is("STATEMACHINE", "At field")
        self.ca.assert_that_pv_is_number("MAGNET:FIELD:PERSISTENT", val, tolerance=TOLERANCE)

        # "User" field should take the value put in the setpoint, even when the actual field provided by the supply
        # drops to zero
        self.ca.assert_that_pv_is_number("FIELD", 0, tolerance=TOLERANCE)  # PSU field
        self.ca.assert_that_pv_is_number("MAGNET:FIELD:PERSISTENT", val, tolerance=TOLERANCE)  # Persistent field
        self.ca.assert_that_pv_is_number("FIELD:USER", val, tolerance=TOLERANCE)  # User field should be tracking persistent field here

    @parameterized.expand(val for val in parameterized_list(TEST_VALUES))
    def test_GIVEN_non_persistent_mode_WHEN_magnet_told_to_go_to_field_setpoint_THEN_goes_to_that_setpoint_and_psu_does_not_ramp_to_zero(self, _, val):

        self._set_and_check_persistent_mode(False)

        # Field in the magnet already from persistent mode.
        persistent_field = float(self.ca.get_pv_value("MAGNET:FIELD:PERSISTENT"))

        # Set the new field. This will cause all of the following events based on the state machine.
        self.ca.set_pv_value("FIELD:SP", val)

        # PSU should be ramped to match the persistent field inside the magnet (if there was one)
        self._assert_field_is(persistent_field)

        # Then it is safe to turn on the heater (the heater is explicitly switched on and we wait for it even if it
        # was already on out of an abundance of caution).
        self._assert_heater_is(True)

        # Assert that value gets passed to device by SNL. SNL waits 30s for the heater to cool down/warm up
        # after being set.
        self._assert_field_is(val)

        # ...And the magnet should now be in the right state!
        self.ca.assert_that_pv_is_number("MAGNET:FIELD:PERSISTENT", val, tolerance=TOLERANCE)

        # And the PSU should remain stable providing the required current/field
        self.ca.assert_that_pv_is("STATEMACHINE", "At field")
        self._assert_field_is(val, check_stable=True)

    @contextmanager
    def _backdoor_magnet_quench(self, reason="Test framework quench"):
        self._lewis.backdoor_run_function_on_device("quench", [reason])
        try:
            yield
        finally:
            # Get back out of the quenched state. This is because the tearDown method checks that magnet has not
            # quenched.
            self._lewis.backdoor_run_function_on_device("unquench")
            # Wait for IOC to notice quench state has gone away
            self.ca.assert_that_pv_alarm_is("STS:SYSTEM:FAULT", self.ca.Alarms.NONE)

    @parameterized.expand(field for field in parameterized_list(TEST_VALUES))
    def test_GIVEN_magnet_quenches_while_at_field_THEN_ioc_displays_this_quench_in_statuses(self, _, field):

        self._set_and_check_persistent_mode(False)
        self.ca.set_pv_value("FIELD:SP", field)
        self._assert_field_is(field)
        self.ca.assert_that_pv_is("STATEMACHINE", "At field")

        with self._backdoor_magnet_quench():
            self.ca.assert_that_pv_is("STS:SYSTEM:FAULT", "Quenched")
            self.ca.assert_that_pv_alarm_is("STS:SYSTEM:FAULT", self.ca.Alarms.MAJOR)
            self.ca.assert_that_pv_is("CONTROL", "Auto-Run-Down")
            self.ca.assert_that_pv_alarm_is("CONTROL", self.ca.Alarms.MAJOR)

            # The trip field should be the field at the point when the magnet quenched.
            self.ca.assert_that_pv_is_number("FIELD:TRIP", field, tolerance=TOLERANCE)

            # Field should be set to zero by emulator (mirroring what the field ought to do in the real device)
            self.ca.assert_that_pv_is_number("FIELD", 0, tolerance=TOLERANCE)
            self.ca.assert_that_pv_is_number("FIELD:USER", 0, tolerance=TOLERANCE)
            self.ca.assert_that_pv_is_number("MAGNET:FIELD:PERSISTENT", 0, tolerance=TOLERANCE)

    @parameterized.expand(val for val in parameterized_list(TEST_VALUES))
    def test_WHEN_inductance_set_via_backdoor_THEN_value_in_ioc_updates(self, _, val):
        self._lewis.backdoor_set_on_device("inductance", val)
        self.ca.assert_that_pv_is_number("MAGNET:INDUCTANCE", val, tolerance=TOLERANCE)

    @parameterized.expand(val for val in parameterized_list(TEST_VALUES))
    def test_WHEN_measured_current_set_via_backdoor_THEN_value_in_ioc_updates(self, _, val):
        self._lewis.backdoor_set_on_device("measured_current", val)
        self.ca.assert_that_pv_is_number("MAGNET:CURR:MEAS", val, tolerance=TOLERANCE)

    @parameterized.expand(val for val in parameterized_list(TEST_SWEEP_RATES))
    def test_WHEN_sweep_rate_set_THEN_sweep_rate_on_ioc_updates(self, _, val):
        self.ca.set_pv_value("FIELD:RATE:SP", val)
        self.ca.assert_that_pv_is_number("FIELD:RATE:SP", val, tolerance=TOLERANCE)
        self.ca.assert_that_pv_is_number("FIELD:RATE", val, tolerance=TOLERANCE)
        self.ca.assert_that_pv_alarm_is("FIELD:RATE", self.ca.Alarms.NONE)

    @parameterized.expand(activity_state for activity_state in parameterized_list(ACTIVITY_STATES))
    @unstable_test()
    def test_WHEN_activity_set_via_backdoor_to_clamped_THEN_alarm_major_ELSE_no_alarm(self, _, activity_state):
        self.ca.set_pv_value("ACTIVITY", activity_state)
        if activity_state == "Clamped":
            self.ca.assert_that_pv_alarm_is("ACTIVITY", "MAJOR")
        else:
            self.ca.assert_that_pv_alarm_is("ACTIVITY", "NO_ALARM")

    @parameterized.expand(control_command for control_command in parameterized_list(CONTROL_COMMANDS_WITH_VALUES))
    def test_WHEN_control_command_value_set_THEN_remote_unlocked_set(self, _, control_pv, set_value):
        self.ca.set_pv_value("CONTROL", "Local & Locked")
        self.ca.set_pv_value(control_pv, set_value)
        self.ca.assert_that_pv_is("CONTROL", "Remote & Unlocked")

    @parameterized.expand(control_pv for control_pv in parameterized_list(CONTROL_COMMANDS_WITHOUT_VALUES))
    def test_WHEN_control_command_processed_THEN_remote_unlocked_set(self, _, control_pv):
        self.ca.set_pv_value("CONTROL", "Local & Locked")
        self.ca.process_pv(control_pv)
        self.ca.assert_that_pv_is("CONTROL", "Remote & Unlocked")
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"))
Exemplo n.º 9
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)
Exemplo n.º 10
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)
Exemplo n.º 11
0
class Sans2dVacTankTests(unittest.TestCase):
    """
    Tests for the sans2d vacuum tank motor extensions.
    """
    def setUp(self):
        self.ca = ChannelAccess(device_prefix="MOT")
        self.lower_inhibit_bound = -2
        self.upper_inhibit_bound = 2

    def reset_axis_to_non_inhibit(self, axis):
        self.ca.set_pv_value("{}:SP".format(axis), 0)
        self.ca.assert_that_pv_is_number(axis, 0, tolerance=1.9, timeout=10)
        self.ca.set_pv_value("{}:MTR.STOP".format(axis), 1, wait=True)

    def reset_axes_to_non_inhibit(self, axis_one, axis_two):
        axis_one_val = self.ca.get_pv_value(axis_one)
        axis_two_val = self.ca.get_pv_value(axis_two)
        axis_one_inhibiting = axis_one_val < self.lower_inhibit_bound or axis_one_val > self.upper_inhibit_bound
        axis_two_inhibiting = axis_two_val < self.lower_inhibit_bound or axis_two_val > self.upper_inhibit_bound
        if axis_one_inhibiting and axis_two_inhibiting:
            self.fail(
                "Both {} and {} are inhibiting each other, cannot reliably run test"
                .format(axis_one, axis_two))
        elif axis_one_inhibiting:
            self.reset_axis_to_non_inhibit(axis_one)
        elif axis_two_inhibiting:
            self.reset_axis_to_non_inhibit(axis_two)

    def _set_collision_avoidance_state_with_retries(self, state):
        with ManagerMode(ChannelAccess()):
            for _ in range(5):
                try:
                    self.ca.set_pv_value("SANS2DVAC:COLLISION_AVOIDANCE",
                                         state)
                    break
                except WriteAccessException as e:
                    err = e
                    time.sleep(1)
            else:
                raise err

    @parameterized.expand(AXES_TO_STOP)
    def test_GIVEN_axis_moving_WHEN_stop_all_THEN_axis_stopped(self, axis):
        self._set_collision_avoidance_state_with_retries(1)

        for _ in range(3):
            set_axis_moving(axis)
            assert_axis_moving(axis)
            self.ca.set_pv_value("SANS2DVAC:STOP_MOTORS:ALL", 1)
            assert_axis_not_moving(axis)
        self.reset_axis_to_non_inhibit(axis)

        self._set_collision_avoidance_state_with_retries(0)

    @parameterized.expand([("FRONTBEAMSTOP", "FRONTDETROT"),
                           ("FRONTDETROT", "FRONTBEAMSTOP")])
    def test_GIVEN_axes_in_range_WHEN_axis_goes_out_of_range_THEN_other_axis_inhibited(
            self, inhibiting_axis, inhibited_axis):
        # Arrange
        self.reset_axes_to_non_inhibit(inhibited_axis, inhibiting_axis)
        try:
            # Act
            self.ca.set_pv_value("{}:SP".format(inhibiting_axis),
                                 -3,
                                 wait=True)
            self.ca.assert_that_pv_is_number("{}:SP".format(inhibiting_axis),
                                             -3)
            start_position = self.ca.get_pv_value(inhibited_axis)
            with self.assertRaises(WriteAccessException,
                                   msg="DISP should be set on inhibited axis"):
                set_axis_moving(inhibited_axis)
            # Assert
            self.ca.assert_that_pv_is(
                "SANS2DVAC:INHIBIT_{}".format(inhibited_axis), 1)
            end_position = self.ca.get_pv_value(inhibited_axis)
            self.assertEqual(start_position, end_position)
        finally:
            # Rearrange
            self.reset_axes_to_non_inhibit(inhibited_axis, inhibiting_axis)
class CybamanTests(unittest.TestCase):
    """
    Tests for the cybaman IOC.
    """

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

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

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

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

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

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

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

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

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

        modifier = 12.34

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    def test_GIVEN_a_device_at_a_specific_position_WHEN_setpoint_is_updated_THEN_tm_val_is_calculated_correctly(
            self):

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

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

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

            # Assert that the TM val calculation record contains the correct value
            # Tolerance is 1001 because rounding errors would get multiplied by 1000
            self.ca.assert_that_pv_is_number("{}:_CALC_TM_AND_SET".format(
                case["axis_to_change"].upper()),
                                             case["expected_tm_val"],
                                             tolerance=1001)
class TdkLambdaGenesysTests(unittest.TestCase):
    @contextlib.contextmanager
    def _temporarily_change_offsets(self, read_offset, write_offset):
        self.ca.set_pv_value("1:CURR:_CALC.B", read_offset)
        self.ca.set_pv_value("1:CURR:SP:RBV:_CALC.B", read_offset)
        self.ca.set_pv_value("1:CURR:SP:_CALC.B", write_offset)
        try:
            yield
        finally:
            self.ca.set_pv_value("1:CURR:_CALC.B", DEFAULT_READ_OFFSET)
            self.ca.set_pv_value("1:CURR:SP:RBV:_CALC.B", DEFAULT_READ_OFFSET)
            self.ca.set_pv_value("1:CURR:SP:_CALC.B", DEFAULT_WRITE_OFFSET)

    def setUp(self):
        self._lewis, self._ioc = get_running_lewis_and_ioc(
            "tdk_lambda_genesys", "GENESYS_01")
        self.ca = ChannelAccess(default_timeout=10, device_prefix="GENESYS_01")
        self.ca.assert_that_pv_exists("1:VOLT", timeout=20)

    def _write_voltage(self, expected_voltage):
        self._lewis.backdoor_set_on_device("voltage", expected_voltage)
        self.ca.set_pv_value("1:SIM:VOLT", expected_voltage)

    def _write_current(self, expected_current):
        self._lewis.backdoor_set_on_device("current", expected_current)
        self.ca.set_pv_value("1:SIM:CURR", expected_current)

    def _set_power_state(self, expected_state):
        self._lewis.backdoor_set_on_device("powerstate", expected_state)

    @skip_if_recsim("Uses LeWIS backdoor")
    def test_GIVEN_voltage_set_WHEN_read_THEN_voltage_is_as_expected(self):
        expected_voltage = 4.3
        self._write_voltage(expected_voltage)
        self.ca.assert_that_pv_is("1:VOLT", expected_voltage)

    @skip_if_recsim("Uses LeWIS backdoor")
    def test_GIVEN_current_set_WHEN_read_THEN_current_is_as_expected(self):
        expected_current = 2
        self._write_current(expected_current)
        self.ca.assert_that_pv_is("1:CURR", expected_current)

    def test_GIVEN_setpoint_voltage_set_WHEN_read_THEN_setpoint_voltage_is_as_expected(
            self):
        expected_voltage = self.ca.get_pv_value("1:VOLT:SP") + 2.5
        self.ca.assert_setting_setpoint_sets_readback(expected_voltage,
                                                      "1:VOLT:SP:RBV",
                                                      "1:VOLT:SP")

    def test_GIVEN_setpoint_current_set_when_read_THEN_setpoint_current_is_as_expected(
            self):
        expected_current = self.ca.get_pv_value("1:CURR:SP") + 5
        self.ca.assert_setting_setpoint_sets_readback(expected_current,
                                                      "1:CURR:SP:RBV",
                                                      "1:CURR:SP")

    @skip_if_recsim("Uses LeWIS backdoor")
    def test_GIVEN_state_set_WHEN_read_THEN_state_is_as_expected_ON(self):
        self._set_power_state("ON")
        self.ca.assert_that_pv_is("1:POWER", "ON")

    @skip_if_recsim("Uses LeWIS backdoor")
    def test_GIVEN_state_set_WHEN_read_THEN_state_is_as_expected_OFF(self):
        self._set_power_state("OFF")
        self.ca.assert_that_pv_is("1:POWER", "OFF")

    def test_GIVEN_state_set_via_number_WHEN_read_THEN_state_is_as_expected(
            self):
        self.ca.set_pv_value("1:POWER:SP", 1)
        self.ca.assert_that_pv_is("1:POWER", "ON")

    @skip_if_recsim("Recsim is unable to simulate comms being uninitialized")
    def test_GIVEN_power_supply_comms_become_uninitialized_THEN_ioc_recovers(
            self):
        try:
            for curr in [0.123, 0.456]:
                self._lewis.backdoor_set_on_device("comms_initialized", False)
                self._write_current(curr)
                # Should be able to re-initialize comms and read the new current
                self.ca.assert_that_pv_is_number("1:CURR",
                                                 curr,
                                                 tolerance=0.01,
                                                 timeout=30)

        finally:
            # If test fails, don't want it to affect other tests.
            self._lewis.backdoor_set_on_device("comms_initialized", True)

    def test_GIVEN_field_THEN_able_to_read_back(self):
        self.ca.assert_setting_setpoint_sets_readback(
            123, readback_pv="1:FIELD:SP:RBV", set_point_pv="1:FIELD:SP")

    @skip_if_recsim("Uses LeWIS backdoor")
    def test_GIVEN_current_THEN_able_to_field_correctly(self):
        test_value = 0.456
        self._lewis.backdoor_set_on_device("current", test_value)
        self.ca.assert_that_pv_is_number("1:CURR", test_value)
        self.ca.assert_that_pv_is_number("1:FIELD", test_value * AMPS_TO_GAUSS)

    def test_GIVEN_field_set_point_is_set_THEN_current_set_point_is_scaled_appropriately(
            self):
        test_value = 789
        self.ca.set_pv_value("1:FIELD:SP", test_value)
        self.ca.assert_that_pv_is_number("1:CURR:SP",
                                         test_value / AMPS_TO_GAUSS)

    @skip_if_recsim("Uses LeWIS backdoor")
    def test_GIVEN_current_set_point_THEN_field_set_point_RBV_is_read_correctly(
            self):
        test_value = 112.5
        self._lewis.backdoor_set_on_device("setpoint_current", test_value)
        self.ca.assert_that_pv_is_number("1:CURR:SP:RBV", test_value)
        self.ca.assert_that_pv_is_number("1:FIELD:SP:RBV",
                                         test_value * AMPS_TO_GAUSS)

    @skip_if_recsim("Uses lewis backdoor")
    def test_GIVEN_non_zero_offsets_WHEN_setpoint_sent_to_psu_THEN_adjusted_by_offset(
            self):
        test_value = 25
        write_offset = 10
        with self._temporarily_change_offsets(read_offset=0,
                                              write_offset=write_offset):
            self.ca.set_pv_value("1:CURR:SP", test_value)
            self._lewis.assert_that_emulator_value_is("setpoint_current",
                                                      test_value +
                                                      write_offset,
                                                      cast=float)

    @skip_if_recsim("Uses lewis backdoor")
    def test_GIVEN_non_zero_offsets_WHEN_value_read_back_from_psu_THEN_adjusted_by_offset(
            self):
        test_value = 43
        read_offset = 10
        with self._temporarily_change_offsets(read_offset=read_offset,
                                              write_offset=0):
            self._lewis.backdoor_set_on_device("current", test_value)
            self._lewis.backdoor_set_on_device("setpoint_current", test_value)

            self.ca.assert_that_pv_is_number("1:CURR",
                                             test_value + read_offset)
            self.ca.assert_that_pv_is_number("1:CURR:SP:RBV",
                                             test_value + read_offset)

    def test_GIVEN_voltage_setpoint_higher_than_max_THEN_capped_to_maximum_and_readback_alarm(
            self):
        self.ca.set_pv_value("1:VOLT:SP", MAX_VOLTAGE + 1)
        self.ca.assert_that_pv_is_number("1:VOLT:SP", MAX_VOLTAGE)
        self.ca.assert_that_pv_alarm_is("1:VOLT:SP:RBV", self.ca.Alarms.MINOR)

    def test_GIVEN_current_setpoint_higher_than_max_THEN_capped_to_maximum_and_readback_alarm(
            self):
        self.ca.set_pv_value("1:CURR:SP", MAX_CURRENT + 1)
        self.ca.assert_that_pv_is_number("1:CURR:SP", MAX_CURRENT)
        self.ca.assert_that_pv_alarm_is("1:CURR:SP:RBV", self.ca.Alarms.MINOR)
class JulaboTests(unittest.TestCase):
    """
    Tests for the Julabo IOC.
    """

    TEMP_TOLERANCE = 0.005

    def setUp(self):
        self._lewis, self._ioc = get_running_lewis_and_ioc(
            "julabo", "JULABO_01")
        self.ca = ChannelAccess(device_prefix=DEVICE_PREFIX)
        self.ca.assert_that_pv_exists("TEMP", timeout=30)
        # Turn off circulate
        self.ca.set_pv_value("MODE:SP", 0)

    def test_set_new_temperature_sets_setpoint_readback_correctly(self):
        # Get current temp
        start_t = self.ca.get_pv_value("TEMP")
        # Set new temp via SP
        self.ca.set_pv_value("TEMP:SP", start_t + 5)
        # Check SP RBV matches new temp
        self.ca.assert_that_pv_is_number("TEMP:SP:RBV", start_t + 5)

    @skip_if_recsim("In rec sim this test fails")
    def test_setting_temperature_above_high_limit_does_not_set_value(self):
        # Get current temp sp rbv
        start_t = self.ca.get_pv_value("TEMP:SP:RBV")
        # Get high limit
        high_t = self.ca.get_pv_value("HIGHLIMIT")
        # Set new temp to above high limit
        self.ca.set_pv_value("TEMP:SP", high_t + 5)
        # Check SP RBV hasn't changed
        self.ca.assert_that_pv_is_number("TEMP:SP:RBV", start_t)

    @skip_if_recsim("In rec sim this test fails")
    def test_setting_temperature_below_low_limit_does_not_set_value(self):
        # Get current temp sp rbv
        start_t = self.ca.get_pv_value("TEMP:SP:RBV")
        # Get low limit
        low_t = self.ca.get_pv_value("LOWLIMIT")
        # Set new temp to above high limit
        self.ca.set_pv_value("TEMP:SP", low_t - 5)
        # Check SP RBV hasn't changed
        self.ca.assert_that_pv_is_number("TEMP:SP:RBV", start_t)

    @skip_if_recsim("In rec sim this test fails")
    def test_set_new_temperature_with_circulate_off_means_temperature_remains_unchanged(
            self):
        # Get current temp
        start_t = self.ca.get_pv_value("TEMP")
        # Set new temp via SP
        self.ca.set_pv_value("TEMP:SP", start_t + 5)
        # Check temp hasn't changed
        self.ca.assert_that_pv_is_number("TEMP",
                                         start_t,
                                         tolerance=self.TEMP_TOLERANCE)

    def test_set_new_temperature_with_circulate_on_changes_temperature(self):
        # Get current temp plus a bit
        start_t = self.ca.get_pv_value("TEMP") + 1
        # Set new temp via SP
        self.ca.set_pv_value("TEMP:SP", start_t)
        # Turn on circulate
        self.ca.set_pv_value("MODE:SP", 1)
        # Check temp has changed
        self.ca.assert_that_pv_is_number("TEMP", start_t)

    def test_setting_external_PID_sets_values_correctly(self):
        # Get initial values and add to them
        p = self.ca.get_pv_value("EXTP") + 1
        i = self.ca.get_pv_value("EXTI") + 1
        d = self.ca.get_pv_value("EXTD") + 1
        # Set new values
        self.ca.set_pv_value("EXTP:SP", p)
        self.ca.set_pv_value("EXTI:SP", i)
        self.ca.set_pv_value("EXTD:SP", d)
        # Check values have changed
        self.ca.assert_that_pv_is_number("EXTP", p)
        self.ca.assert_that_pv_is_number("EXTI", i)
        self.ca.assert_that_pv_is_number("EXTD", d)

    def test_setting_internal_PID_sets_values_correctly(self):
        # Get initial values and add to them
        p = self.ca.get_pv_value("INTP") + 1
        i = self.ca.get_pv_value("INTI") + 1
        d = self.ca.get_pv_value("INTD") + 1
        # Set new values
        self.ca.set_pv_value("INTP:SP", p)
        self.ca.set_pv_value("INTI:SP", i)
        self.ca.set_pv_value("INTD:SP", d)
        # Check values have changed
        self.ca.assert_that_pv_is_number("INTP", p)
        self.ca.assert_that_pv_is_number("INTI", i)
        self.ca.assert_that_pv_is_number("INTD", d)

    @skip_if_recsim("In rec sim this test fails")
    def test_setting_internal_PID_above_limit_does_nothing(self):
        # Get initial values
        start_p = self.ca.get_pv_value("INTP")
        start_i = self.ca.get_pv_value("INTI")
        start_d = self.ca.get_pv_value("INTD")
        # Set outside of range
        self.ca.set_pv_value("INTP:SP", TOO_HIGH_PID_VALUE)
        self.ca.set_pv_value("INTI:SP", TOO_HIGH_PID_VALUE)
        self.ca.set_pv_value("INTD:SP", TOO_HIGH_PID_VALUE)
        # Check values have not changed
        self.ca.assert_that_pv_is_number("INTP", start_p)
        self.ca.assert_that_pv_is_number("INTI", start_i)
        self.ca.assert_that_pv_is_number("INTD", start_d)

    @skip_if_recsim("In rec sim this test fails")
    def test_setting_internal_PID_below_limit_does_nothing(self):
        # Get initial values
        start_p = self.ca.get_pv_value("INTP")
        start_i = self.ca.get_pv_value("INTI")
        start_d = self.ca.get_pv_value("INTD")
        # Set outside of range
        self.ca.set_pv_value("INTP:SP", TOO_LOW_PID_VALUE)
        self.ca.set_pv_value("INTI:SP", TOO_LOW_PID_VALUE)
        self.ca.set_pv_value("INTD:SP", TOO_LOW_PID_VALUE)
        # Check values have not changed
        self.ca.assert_that_pv_is_number("INTP", start_p)
        self.ca.assert_that_pv_is_number("INTI", start_i)
        self.ca.assert_that_pv_is_number("INTD", start_d)

    @skip_if_recsim("In rec sim this test fails")
    def test_setting_external_PID_above_limit_does_nothing(self):
        # Get initial values
        start_p = self.ca.get_pv_value("EXTP")
        start_i = self.ca.get_pv_value("EXTI")
        start_d = self.ca.get_pv_value("EXTD")
        # Set outside of range
        self.ca.set_pv_value("EXTP:SP", TOO_HIGH_PID_VALUE)
        self.ca.set_pv_value("EXTI:SP", TOO_HIGH_PID_VALUE)
        self.ca.set_pv_value("EXTD:SP", TOO_HIGH_PID_VALUE)
        # Check values have not changed
        self.ca.assert_that_pv_is_number("EXTP", start_p)
        self.ca.assert_that_pv_is_number("EXTI", start_i)
        self.ca.assert_that_pv_is_number("EXTD", start_d)

    @skip_if_recsim("In rec sim this test fails")
    def test_setting_external_PID_below_limit_does_nothing(self):
        # Get initial values
        start_p = self.ca.get_pv_value("EXTP")
        start_i = self.ca.get_pv_value("EXTI")
        start_d = self.ca.get_pv_value("EXTD")
        # Set outside of range
        self.ca.set_pv_value("EXTP:SP", TOO_LOW_PID_VALUE)
        self.ca.set_pv_value("EXTI:SP", TOO_LOW_PID_VALUE)
        self.ca.set_pv_value("EXTD:SP", TOO_LOW_PID_VALUE)
        # Check values have not changed
        self.ca.assert_that_pv_is_number("EXTP", start_p)
        self.ca.assert_that_pv_is_number("EXTI", start_i)
        self.ca.assert_that_pv_is_number("EXTD", start_d)

    def test_setting_control_mode_on_device_changes_control_mode_readback(
            self):
        for control_mode in ["Internal", "External",
                             "Internal"]:  # Check both transitions
            self.ca.assert_setting_setpoint_sets_readback(
                control_mode, "CONTROLMODE")
class MercuryTests(unittest.TestCase):
    """
    Tests for the Mercury IOC.
    """
    def setUp(self):
        self._lewis, self._ioc = get_running_lewis_and_ioc(
            "mercuryitc", DEVICE_PREFIX)
        self._lewis.backdoor_set_on_device("connected", True)
        self.ca = ChannelAccess(device_prefix=DEVICE_PREFIX,
                                default_timeout=20)
        card_pv_prefix = get_card_pv_prefix(TEMP_CARDS[0])
        self.ca.assert_setting_setpoint_sets_readback(
            "OFF",
            readback_pv="{}:SPC".format(card_pv_prefix),
            expected_alarm=self.ca.Alarms.MAJOR)

    @parameterized.expand(
        parameterized_list(
            itertools.product(PID_PARAMS, PID_TEST_VALUES,
                              TEMP_CARDS + PRESSURE_CARDS)))
    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_pid_params_set_via_backdoor_THEN_readback_updates(
            self, _, param, test_value, card):
        card_pv_prefix = get_card_pv_prefix(card)

        self._lewis.backdoor_run_function_on_device(
            "backdoor_set_channel_property",
            [card, param.lower(), test_value])
        self.ca.assert_that_pv_is("{}:{}".format(card_pv_prefix, param),
                                  test_value)

    @parameterized.expand(
        parameterized_list(
            itertools.product(PID_PARAMS, PID_TEST_VALUES,
                              TEMP_CARDS + PRESSURE_CARDS)))
    def test_WHEN_pid_params_set_THEN_readback_updates(self, _, param,
                                                       test_value, card):
        card_pv_prefix = get_card_pv_prefix(card)

        self.ca.assert_setting_setpoint_sets_readback(
            test_value,
            readback_pv="{}:{}".format(card_pv_prefix, param),
            set_point_pv="{}:{}:SP".format(card_pv_prefix, param))

    @parameterized.expand(
        parameterized_list(
            itertools.product(AUTOPID_MODES, TEMP_CARDS + PRESSURE_CARDS)))
    def test_WHEN_autopid_set_THEN_readback_updates(self, _, test_value, card):
        card_pv_prefix = get_card_pv_prefix(card)

        self.ca.assert_setting_setpoint_sets_readback(
            test_value,
            readback_pv="{}:PID:AUTO".format(card_pv_prefix),
            set_point_pv="{}:PID:AUTO:SP".format(card_pv_prefix))

    @parameterized.expand(
        parameterized_list(
            itertools.product(TEMPERATURE_TEST_VALUES, TEMP_CARDS)))
    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_actual_temp_is_set_via_backdoor_THEN_pv_updates(
            self, _, test_value, card):
        card_pv_prefix = get_card_pv_prefix(card)

        self._lewis.backdoor_run_function_on_device(
            "backdoor_set_channel_property", [card, "temperature", test_value])
        self.ca.assert_that_pv_is("{}:TEMP".format(card_pv_prefix), test_value)

    @parameterized.expand(
        parameterized_list(
            itertools.product(TEMPERATURE_TEST_VALUES, PRESSURE_CARDS)))
    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_actual_pressure_is_set_via_backdoor_THEN_pv_updates(
            self, _, test_value, card):
        card_pv_prefix = get_card_pv_prefix(card)

        self._lewis.backdoor_run_function_on_device(
            "backdoor_set_channel_property", [card, "pressure", test_value])
        self.ca.assert_that_pv_is("{}:PRESSURE".format(card_pv_prefix),
                                  test_value)

    @parameterized.expand(
        parameterized_list(
            itertools.product(RESISTANCE_TEST_VALUES, TEMP_CARDS)))
    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_resistance_is_set_via_backdoor_THEN_pv_updates(
            self, _, test_value, card):
        card_pv_prefix = get_card_pv_prefix(card)

        self._lewis.backdoor_run_function_on_device(
            "backdoor_set_channel_property", [card, "resistance", test_value])
        self.ca.assert_that_pv_is("{}:RESISTANCE".format(card_pv_prefix),
                                  test_value)

    @parameterized.expand(
        parameterized_list(
            itertools.product(RESISTANCE_TEST_VALUES, PRESSURE_CARDS)))
    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_voltage_is_set_via_backdoor_THEN_pv_updates(
            self, _, test_value, card):
        card_pv_prefix = get_card_pv_prefix(card)

        self._lewis.backdoor_run_function_on_device(
            "backdoor_set_channel_property", [card, "voltage", test_value])
        self.ca.assert_that_pv_is("{}:VOLT".format(card_pv_prefix), test_value)

    @parameterized.expand(
        parameterized_list(
            itertools.product(TEMPERATURE_TEST_VALUES, TEMP_CARDS)))
    def test_WHEN_sp_temp_is_set_THEN_pv_updates(self, _, test_value, card):
        card_pv_prefix = get_card_pv_prefix(card)

        self.ca.assert_setting_setpoint_sets_readback(
            test_value,
            set_point_pv="{}:TEMP:SP".format(card_pv_prefix),
            readback_pv="{}:TEMP:SP:RBV".format(card_pv_prefix))

    @parameterized.expand(
        parameterized_list(
            itertools.product(TEMPERATURE_TEST_VALUES, PRESSURE_CARDS)))
    def test_WHEN_sp_pressure_is_set_THEN_pv_updates(self, _, test_value,
                                                     card):
        card_pv_prefix = get_card_pv_prefix(card)

        self.ca.assert_setting_setpoint_sets_readback(
            test_value,
            set_point_pv="{}:PRESSURE:SP".format(card_pv_prefix),
            readback_pv="{}:PRESSURE:SP:RBV".format(card_pv_prefix))

    @parameterized.expand(
        parameterized_list(
            itertools.product(HEATER_MODES, TEMP_CARDS + PRESSURE_CARDS)))
    def test_WHEN_heater_mode_is_set_THEN_pv_updates(self, _, mode, card):
        card_pv_prefix = get_card_pv_prefix(card)

        self.ca.assert_setting_setpoint_sets_readback(
            mode,
            set_point_pv="{}:HEATER:MODE:SP".format(card_pv_prefix),
            readback_pv="{}:HEATER:MODE".format(card_pv_prefix))

    @parameterized.expand(
        parameterized_list(
            itertools.product(GAS_FLOW_MODES, TEMP_CARDS + PRESSURE_CARDS)))
    def test_WHEN_gas_flow_mode_is_set_THEN_pv_updates(self, _, mode, card):
        card_pv_prefix = get_card_pv_prefix(card)

        self.ca.assert_setting_setpoint_sets_readback(
            mode,
            set_point_pv="{}:FLOW:STAT:SP".format(card_pv_prefix),
            readback_pv="{}:FLOW:STAT".format(card_pv_prefix))

    @parameterized.expand(
        parameterized_list(
            itertools.product(GAS_FLOW_TEST_VALUES,
                              TEMP_CARDS + PRESSURE_CARDS)))
    def test_WHEN_gas_flow_is_set_THEN_pv_updates(self, _, mode, card):
        card_pv_prefix = get_card_pv_prefix(card)

        self.ca.assert_setting_setpoint_sets_readback(
            mode,
            set_point_pv="{}:FLOW:SP".format(card_pv_prefix),
            readback_pv="{}:FLOW".format(card_pv_prefix))

    @parameterized.expand(
        parameterized_list(
            itertools.product(HEATER_PERCENT_TEST_VALUES,
                              TEMP_CARDS + PRESSURE_CARDS)))
    def test_WHEN_heater_percent_is_set_THEN_pv_updates(self, _, mode, card):
        card_pv_prefix = get_card_pv_prefix(card)

        self.ca.assert_setting_setpoint_sets_readback(
            mode,
            set_point_pv="{}:HEATER:SP".format(card_pv_prefix),
            readback_pv="{}:HEATER".format(card_pv_prefix))

    @parameterized.expand(
        parameterized_list(
            itertools.product(HEATER_PERCENT_TEST_VALUES,
                              TEMP_CARDS + PRESSURE_CARDS)))
    def test_WHEN_heater_voltage_limit_is_set_THEN_pv_updates(
            self, _, mode, card):
        card_pv_prefix = get_card_pv_prefix(card)

        self.ca.assert_setting_setpoint_sets_readback(
            mode,
            set_point_pv="{}:HEATER:VOLT_LIMIT:SP".format(card_pv_prefix),
            readback_pv="{}:HEATER:VOLT_LIMIT".format(card_pv_prefix))

    @parameterized.expand(
        parameterized_list(
            itertools.product(HEATER_PERCENT_TEST_VALUES,
                              TEMP_CARDS + PRESSURE_CARDS)))
    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_heater_power_is_set_via_backdoor_THEN_pv_updates(
            self, _, test_value, card):
        card_pv_prefix = get_card_pv_prefix(card)

        heater_chan_name = self.ca.get_pv_value(
            "{}:HTRCHAN".format(card_pv_prefix))

        self._lewis.backdoor_run_function_on_device(
            "backdoor_set_channel_property",
            [heater_chan_name, "power", test_value])
        self.ca.assert_that_pv_is("{}:HEATER:POWER".format(card_pv_prefix),
                                  test_value)

    @parameterized.expand(
        parameterized_list(
            itertools.product(HEATER_PERCENT_TEST_VALUES,
                              TEMP_CARDS + PRESSURE_CARDS)))
    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_heater_curr_is_set_via_backdoor_THEN_pv_updates(
            self, _, test_value, card):
        card_pv_prefix = get_card_pv_prefix(card)

        heater_chan_name = self.ca.get_pv_value(
            "{}:HTRCHAN".format(card_pv_prefix))

        self._lewis.backdoor_run_function_on_device(
            "backdoor_set_channel_property",
            [heater_chan_name, "current", test_value])
        self.ca.assert_that_pv_is("{}:HEATER:CURR".format(card_pv_prefix),
                                  test_value)

    @parameterized.expand(
        parameterized_list(
            itertools.product(HEATER_PERCENT_TEST_VALUES,
                              TEMP_CARDS + PRESSURE_CARDS)))
    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_heater_voltage_is_set_via_backdoor_THEN_pv_updates(
            self, _, test_value, card):
        card_pv_prefix = get_card_pv_prefix(card)

        heater_chan_name = self.ca.get_pv_value(
            "{}:HTRCHAN".format(card_pv_prefix))

        self._lewis.backdoor_run_function_on_device(
            "backdoor_set_channel_property",
            [heater_chan_name, "voltage", test_value])
        self.ca.assert_that_pv_is("{}:HEATER:VOLT".format(card_pv_prefix),
                                  test_value)

    @parameterized.expand(
        parameterized_list(
            itertools.product(MOCK_NICKNAMES,
                              TEMP_CARDS + PRESSURE_CARDS + LEVEL_CARDS)))
    def test_WHEN_name_is_set_THEN_pv_updates(self, _, test_value, card):
        card_pv_prefix = get_card_pv_prefix(card)

        self.ca.assert_setting_setpoint_sets_readback(
            test_value,
            readback_pv="{}:NAME".format(card_pv_prefix),
            set_point_pv="{}:NAME:SP".format(card_pv_prefix))

    @parameterized.expand(
        parameterized_list(
            itertools.product(GAS_LEVEL_TEST_VALUES, LEVEL_CARDS)))
    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_helium_level_is_set_via_backdoor_THEN_pv_updates(
            self, _, test_value, card):
        card_pv_prefix = get_card_pv_prefix(card)

        self._lewis.backdoor_run_function_on_device(
            "backdoor_set_channel_property",
            [card, "helium_level", test_value])
        self.ca.assert_that_pv_is_number("{}:HELIUM".format(card_pv_prefix),
                                         test_value,
                                         tolerance=0.01)

    @parameterized.expand(
        parameterized_list(
            itertools.product(GAS_LEVEL_TEST_VALUES, LEVEL_CARDS)))
    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_nitrogen_level_is_set_via_backdoor_THEN_pv_updates(
            self, _, test_value, card):
        card_pv_prefix = get_card_pv_prefix(card)

        self._lewis.backdoor_run_function_on_device(
            "backdoor_set_channel_property",
            [card, "nitrogen_level", test_value])
        self.ca.assert_that_pv_is_number("{}:NITROGEN".format(card_pv_prefix),
                                         test_value,
                                         tolerance=0.01)

    @parameterized.expand(
        parameterized_list(
            itertools.product(TEMP_CARDS + PRESSURE_CARDS, HEATER_CARDS)))
    def test_WHEN_heater_association_is_set_THEN_pv_updates(
            self, _, parent_card, associated_card):
        card_pv_prefix = get_card_pv_prefix(parent_card)

        with ManagerMode(ChannelAccess()):
            self.ca.assert_setting_setpoint_sets_readback(
                associated_card, "{}:HTRCHAN".format(card_pv_prefix))

    @parameterized.expand(
        parameterized_list(
            itertools.product(TEMP_CARDS + PRESSURE_CARDS, AUX_CARDS)))
    def test_WHEN_aux_association_is_set_THEN_pv_updates(
            self, _, parent_card, associated_card):
        card_pv_prefix = get_card_pv_prefix(parent_card)
        with ManagerMode(ChannelAccess()):
            self.ca.assert_setting_setpoint_sets_readback(
                associated_card, "{}:AUXCHAN".format(card_pv_prefix))

    @parameterized.expand(
        parameterized_list(itertools.product(HELIUM_READ_RATES, LEVEL_CARDS)))
    def test_WHEN_he_read_rate_is_set_THEN_pv_updates(self, _, test_value,
                                                      card):
        card_pv_prefix = get_card_pv_prefix(card)
        self.ca.assert_setting_setpoint_sets_readback(
            test_value, "{}:HELIUM:READ_RATE".format(card_pv_prefix))

    @parameterized.expand(
        parameterized_list([
            ("CATALOG:PARSE.VALA", TEMP_CARDS),
            ("CATALOG:PARSE.VALB", PRESSURE_CARDS),
            ("CATALOG:PARSE.VALC", LEVEL_CARDS),
            ("CATALOG:PARSE.VALD", HEATER_CARDS),
            ("CATALOG:PARSE.VALE", AUX_CARDS),
        ]))
    @skip_if_recsim("Complex logic not tested in recsim")
    def test_WHEN_getting_catalog_it_contains_all_cards(self, _, pv, cards):
        for card in cards:
            self.ca.assert_that_pv_value_causes_func_to_return_true(
                pv, lambda val: card in val)

    @parameterized.expand(
        parameterized_list(
            itertools.product(MOCK_CALIB_FILES, TEMP_CARDS + PRESSURE_CARDS)))
    def test_WHEN_setting_calibration_file_THEN_pv_updates(
            self, _, test_value, card):
        card_pv_prefix = get_card_pv_prefix(card)
        with ManagerMode(ChannelAccess()):
            self.ca.assert_setting_setpoint_sets_readback(
                test_value, "{}:CALFILE".format(card_pv_prefix))

    @parameterized.expand(parameterized_list(["O", "R"]))
    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_resistance_suffix_is_changed_THEN_resistance_reads_correctly(
            self, _, resistance_suffix):
        self._lewis.backdoor_set_on_device("resistance_suffix",
                                           resistance_suffix)
        resistance_value = 3
        self._lewis.backdoor_run_function_on_device(
            "backdoor_set_channel_property",
            [PRIMARY_TEMPERATURE_CHANNEL, "resistance", resistance_value])
        self.ca.assert_that_pv_is(
            "{}:RESISTANCE".format(
                get_card_pv_prefix(PRIMARY_TEMPERATURE_CHANNEL)),
            resistance_value)
        self.ca.assert_that_pv_alarm_is(
            "{}:RESISTANCE".format(
                get_card_pv_prefix(PRIMARY_TEMPERATURE_CHANNEL)),
            self.ca.Alarms.NONE)

    def test_WHEN_auto_flow_set_THEN_pv_updates_and_states_are_set(self):
        card_pv_prefix = get_card_pv_prefix(TEMP_CARDS[0])
        pressure_card_pv_prefix = get_card_pv_prefix(PRESSURE_CARDS[0])

        self.ca.set_pv_value("{}:PID:AUTO:SP".format(card_pv_prefix), "OFF")
        self.ca.set_pv_value("{}:FLOW:STAT:SP".format(card_pv_prefix), "Auto")
        self.ca.set_pv_value("{}:HEATER:MODE:SP".format(card_pv_prefix),
                             "Manual")
        self.ca.set_pv_value("{}:FLOW:STAT:SP".format(pressure_card_pv_prefix),
                             "Manual")

        self.ca.assert_setting_setpoint_sets_readback(
            "ON",
            set_point_pv="{}:SPC:SP".format(card_pv_prefix),
            readback_pv="{}:SPC".format(card_pv_prefix))
        self.ca.assert_that_pv_is("{}:PID:AUTO".format(card_pv_prefix), "ON")
        self.ca.assert_that_pv_is("{}:FLOW:STAT".format(card_pv_prefix),
                                  "Manual")
        self.ca.assert_that_pv_is("{}:HEATER:MODE".format(card_pv_prefix),
                                  "Auto")
        self.ca.assert_that_pv_is(
            "{}:FLOW:STAT".format(pressure_card_pv_prefix), "Auto")
        self.ca.assert_that_pv_is("{}:SPC:SP".format(pressure_card_pv_prefix),
                                  "ON")

    def test_WHEN_auto_flow_set_off_THEN_pv_updates_and_states_are_not_set(
            self):
        card_pv_prefix = get_card_pv_prefix(TEMP_CARDS[0])
        pressure_card_pv_prefix = get_card_pv_prefix(PRESSURE_CARDS[0])

        self.ca.set_pv_value("{}:PID:AUTO:SP".format(card_pv_prefix), "OFF")
        self.ca.set_pv_value("{}:FLOW:STAT:SP".format(card_pv_prefix), "Auto")
        self.ca.set_pv_value("{}:HEATER:MODE:SP".format(card_pv_prefix),
                             "Manual")
        self.ca.set_pv_value("{}:FLOW:STAT:SP".format(pressure_card_pv_prefix),
                             "Manual")

        self.ca.assert_setting_setpoint_sets_readback(
            "OFF",
            set_point_pv="{}:SPC:SP".format(card_pv_prefix),
            readback_pv="{}:SPC".format(card_pv_prefix),
            expected_alarm=self.ca.Alarms.MAJOR)
        self.ca.assert_that_pv_is("{}:PID:AUTO".format(card_pv_prefix), "OFF")
        self.ca.assert_that_pv_is("{}:FLOW:STAT".format(card_pv_prefix),
                                  "Auto")
        self.ca.assert_that_pv_is("{}:HEATER:MODE".format(card_pv_prefix),
                                  "Manual")
        self.ca.assert_that_pv_is(
            "{}:FLOW:STAT".format(pressure_card_pv_prefix), "Manual")
        self.ca.assert_that_pv_is("{}:SPC:SP".format(pressure_card_pv_prefix),
                                  "OFF")

    def set_temp_reading_and_sp(self, reading, set_point, spc_state="On"):
        """
        Set the temperature in lewis and the set point on the first card
        :param reading: The reading lewis will return
        :param set_point: The set point to set on the device
        :param spc_state: State to set SPC to (defaults to On)
        """
        card_pv_prefix = get_card_pv_prefix(TEMP_CARDS[0])
        self.ca.set_pv_value("{}:SPC:SP".format(card_pv_prefix), spc_state)
        self.ca.set_pv_value("{}:TEMP:SP".format(card_pv_prefix), set_point)
        self._lewis.backdoor_run_function_on_device(
            "backdoor_set_channel_property",
            [TEMP_CARDS[0], "temperature", reading])

    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_auto_flow_set_on_and_temp_lower_than_2_deadbands_THEN_pressure_set_to_minimum_pressure(
            self):
        set_point = 10
        reading = 10 - SPC_TEMP_DEADBAND * 2.1
        pressure_card_pv_prefix = get_card_pv_prefix(PRESSURE_CARDS[0])
        self.set_temp_reading_and_sp(reading, set_point)

        self.ca.assert_that_pv_is(
            "{}:PRESSURE:SP:RBV".format(pressure_card_pv_prefix),
            SPC_MIN_PRESSURE)

    @parameterized.expand([(10, ), (1, ), (300, ), (12, ), (20, )])
    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_auto_flow_set_on_and_temp_low_but_within_1_to_2_deadbands_THEN_pressure_set_to_pressure_for_setpoint_temp_and_does_not_ramp(
            self, set_point):
        reading = set_point - SPC_TEMP_DEADBAND * 1.5
        pressure_card_pv_prefix = get_card_pv_prefix(PRESSURE_CARDS[0])
        self.set_temp_reading_and_sp(reading, set_point)

        self.ca.assert_that_pv_is(
            "{}:PRESSURE:SP:RBV".format(pressure_card_pv_prefix),
            pressure_for(set_point))
        sleep(1.5)
        self.ca.assert_that_pv_is(
            "{}:PRESSURE:SP:RBV".format(pressure_card_pv_prefix),
            pressure_for(set_point))

    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_auto_flow_set_on_and_temp_at_setpoint_THEN_pressure_set_to_pressure_for_setpoint_temp_and_does_ramp_down(
            self):
        set_point = 10
        reading = set_point
        pressure_card_pv_prefix = get_card_pv_prefix(PRESSURE_CARDS[0])
        self.set_temp_reading_and_sp(reading, set_point)

        self.ca.assert_that_pv_is_number(
            "{}:PRESSURE:SP".format(pressure_card_pv_prefix),
            pressure_for(set_point) + SPC_OFFSET,
            tolerance=SPC_OFFSET / 4)  # should see number in ramp
        self.ca.assert_that_pv_is(
            "{}:PRESSURE:SP".format(pressure_card_pv_prefix),
            pressure_for(set_point))  # final value

    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_auto_flow_set_on_and_temp_above_setpoint_by_more_than_deadband_THEN_pressure_set_to_pressure_for_setpoint_temp_plus_gain_and_does_ramp(
            self):
        diff = SPC_TEMP_DEADBAND * 1.1
        set_point = 10
        reading = set_point + diff
        expected_pressure = pressure_for(set_point) + SPC_OFFSET + (
            abs(reading - set_point - SPC_TEMP_DEADBAND) * SPC_GAIN)**2
        pressure_card_pv_prefix = get_card_pv_prefix(PRESSURE_CARDS[0])
        self.set_temp_reading_and_sp(reading, set_point)

        self.ca.assert_that_pv_is(
            "{}:PRESSURE:SP".format(pressure_card_pv_prefix),
            expected_pressure)  # final value
        sleep(1.5)  # wait for possible ramp
        self.ca.assert_that_pv_is(
            "{}:PRESSURE:SP".format(pressure_card_pv_prefix),
            expected_pressure)  # final value

    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_auto_flow_set_on_and_pressure_would_be_high_THEN_pressure_set_to_maximum_pressure(
            self):
        diff = 1000
        set_point = 10
        reading = set_point + diff
        pressure_card_pv_prefix = get_card_pv_prefix(PRESSURE_CARDS[0])

        self.set_temp_reading_and_sp(reading, set_point)

        self.ca.assert_that_pv_is(
            "{}:PRESSURE:SP".format(pressure_card_pv_prefix),
            SPC_MAX_PRESSURE)  # final value

    def test_WHEN_auto_flow_set_off_THEN_pressure_is_not_updated(self):
        diff = 1000
        set_point = 10
        reading = set_point + diff

        pressure_card_pv_prefix = get_card_pv_prefix(PRESSURE_CARDS[0])
        expected_value = -10
        self.ca.set_pv_value("{}:PRESSURE:SP".format(pressure_card_pv_prefix),
                             expected_value)

        self.set_temp_reading_and_sp(reading, set_point, "OFF")

        self.ca.assert_that_pv_is(
            "{}:PRESSURE:SP".format(pressure_card_pv_prefix), expected_value)

    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_auto_flow_on_but_error_in_temp_readback_THEN_pressure_is_not_updated(
            self):
        set_point = 10

        card_pv_prefix = get_card_pv_prefix(TEMP_CARDS[0])
        pressure_card_pv_prefix = get_card_pv_prefix(PRESSURE_CARDS[0])
        expected_value = -10
        self.ca.set_pv_value("{}:PRESSURE:SP".format(pressure_card_pv_prefix),
                             expected_value)
        self._lewis.backdoor_set_on_device("connected", False)
        self.ca.assert_that_pv_alarm_is(
            "{}:TEMP:SP:RBV".format(card_pv_prefix), self.ca.Alarms.INVALID)

        self.ca.set_pv_value("{}:SPC:SP".format(card_pv_prefix), "ON")
        self.ca.set_pv_value("{}:TEMP:SP".format(card_pv_prefix), set_point)

        self.ca.assert_that_pv_is(
            "{}:PRESSURE:SP".format(pressure_card_pv_prefix), expected_value)
Exemplo n.º 16
0
class LSITests(unittest.TestCase):
    """
    Tests for LSi Correlator
    """
    def setUp(self):
        self._ioc = IOCRegister.get_running("LSI")
        self.ca = ChannelAccess(default_timeout=30,
                                device_prefix=DEVICE_PREFIX)

    def test_GIVEN_setting_pv_WHEN_pv_written_to_THEN_new_value_read_back(
            self):
        pv_name = "MEASUREMENTDURATION"
        pv_value = 1000

        self.ca.set_pv_value(pv_name, pv_value)
        self.ca.assert_that_pv_is_number(pv_name, pv_value)

    def test_GIVEN_setting_pv_WHEN_pv_written_to_with_invalid_value_THEN_value_not_updated(
            self):
        pv_name = "MEASUREMENTDURATION"
        original_value = self.ca.get_pv_value(pv_name)

        self.ca.set_pv_value(pv_name, -1)
        self.ca.assert_that_pv_is_number(pv_name, original_value)

    def test_GIVEN_integer_device_setting_WHEN_pv_written_to_with_a_float_THEN_value_is_rounded_before_setting(
            self):
        pv_name = "MEASUREMENTDURATION"
        new_value = 12.3

        self.ca.set_pv_value(pv_name, new_value)
        self.ca.assert_that_pv_is_number(pv_name, 12)

    def test_GIVEN_monitor_on_setting_pv_WHEN_pv_changed_THEN_monitor_gets_updated(
            self):
        pv_name = "MEASUREMENTDURATION"
        self.ca.set_pv_value(pv_name, 10.0, wait=True)
        new_value = 12.3
        expected_value = 12.0

        with self.ca.assert_that_pv_monitor_is(pv_name, expected_value):
            self.ca.set_pv_value(pv_name + ":SP", new_value)

    def test_GIVEN_invalid_value_for_setting_WHEN_setting_pv_written_THEN_status_pv_updates_with_error(
            self):
        setting_pv = "MEASUREMENTDURATION"
        self.ca.set_pv_value(setting_pv, -1)
        error_message = "LSI --- wrong value assigned to MeasurementDuration"

        self.ca.assert_that_pv_is("ERRORMSG", error_message)

    @parameterized.expand([
        ("NORMALIZATION", ("SYMMETRIC", "COMPENSATED")),
        ("SWAPCHANNELS", ("ChA_ChB", "ChB_ChA")),
        ("CORRELATIONTYPE", ("AUTO", "CROSS")),
        ("TRANSFERRATE", ("ms100", "ms150", "ms200", "ms250", "ms300", "ms400",
                          "ms500", "ms600", "ms700")),
        ("SAMPLINGTIMEMULTIT", ("ns12_5", "ns200", "ns400", "ns800", "ns1600",
                                "ns3200"))
    ])
    def test_GIVEN_enum_setting_WHEN_setting_pv_written_to_THEN_new_value_read_back(
            self, pv, values):
        for value in values:
            self.ca.set_pv_value(pv, value, sleep_after_set=0.0)
            self.ca.assert_that_pv_is(pv, value)

    @parameterized.expand([("OVERLOADLIMIT", "Mcps"),
                           ("SCATTERING_ANGLE", "degree"),
                           ("SAMPLE_TEMP", "K"), ("SOLVENT_VISCOSITY", "mPas"),
                           ("SOLVENT_REFRACTIVE_INDEX", ""),
                           ("LASER_WAVELENGTH", "nm")])
    def test_GIVEN_pv_with_unit_WHEN_EGU_field_read_from_THEN_unit_returned(
            self, pv, expected_unit):
        self.ca.assert_that_pv_is("{pv}.EGU".format(pv=pv), expected_unit)

    @parameterized.expand([
        ("CORRELATION_FUNCTION", 400),
        ("LAGS", 400),
    ])
    def test_GIVEN_array_pv_WHEN_NELM_field_read_THEN_length_of_array_returned(
            self, pv, expected_length):
        self.ca.assert_that_pv_is_number("{pv}.NELM".format(pv=pv),
                                         expected_length)

    @parameterized.expand(parameterized_list(SETTING_PVS))
    def test_GIVEN_pv_name_THEN_setpoint_exists_for_that_pv(
            self, _, pv, value):
        self.ca.assert_setting_setpoint_sets_readback(value, pv)

    @parameterized.expand(parameterized_list(PV_NAMES))
    def test_GIVEN_pv_name_THEN_val_field_exists_for_that_pv(self, _, pv):
        self.ca.assert_that_pv_is("{pv}.VAL".format(pv=pv),
                                  self.ca.get_pv_value(pv))

    @parameterized.expand(parameterized_list(PV_NAMES))
    def test_GIVEN_pv_WHEN_pv_read_THEN_pv_has_no_alarms(self, _, pv):
        self.ca.assert_that_pv_alarm_is(pv, self.ca.Alarms.NONE)

    @parameterized.expand(parameterized_list(["CORRELATION_FUNCTION", "LAGS"]))
    def test_GIVEN_start_pressed_WHEN_measurement_is_possible_THEN_correlation_and_lags_populated(
            self, _, pv):
        self.ca.assert_that_pv_is("RUNNING", "NO", timeout=10)

        self.ca.set_pv_value("START", 1, sleep_after_set=0.0)

        array_size = self.ca.get_pv_value("{pv}.NELM".format(pv=pv))

        test_data = np.linspace(0, array_size, array_size)

        self.ca.assert_that_pv_value_causes_func_to_return_true(
            pv, lambda pv_value: np.allclose(pv_value, test_data))

    def test_GIVEN_start_pressed_WHEN_measurement_already_on_THEN_error_raised(
            self):
        self.ca.set_pv_value("START", 1, sleep_after_set=0.0)
        self.ca.set_pv_value("START", 1, sleep_after_set=0.0)

        error_message = "LSI --- Cannot configure: Measurement active"

        self.ca.assert_that_pv_is("ERRORMSG", error_message)
class TritonTests(unittest.TestCase):
    """
    Tests for the Triton IOC.
    """
    def setUp(self):
        self._lewis, self._ioc = get_running_lewis_and_ioc(
            "triton", DEVICE_PREFIX)
        self.ca = ChannelAccess(device_prefix=DEVICE_PREFIX)

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

    def test_WHEN_P_setpoint_is_set_THEN_readback_updates(self):
        for value in PID_TEST_VALUES:
            self.ca.assert_setting_setpoint_sets_readback(value, "P")

    def test_WHEN_I_setpoint_is_set_THEN_readback_updates(self):
        for value in PID_TEST_VALUES:
            self.ca.assert_setting_setpoint_sets_readback(value, "I")

    def test_WHEN_D_setpoint_is_set_THEN_readback_updates(self):
        for value in PID_TEST_VALUES:
            self.ca.assert_setting_setpoint_sets_readback(value, "D")

    def test_WHEN_temperature_setpoint_is_set_THEN_readback_updates(self):
        for value in TEMPERATURE_TEST_VALUES:
            self.ca.assert_setting_setpoint_sets_readback(
                value, set_point_pv="TEMP:SP", readback_pv="TEMP:SP:RBV")

    @skip_if_recsim(
        "This is implemented at the protocol level, so does not work in recsim"
    )
    def test_WHEN_temperature_setpoint_is_set_THEN_closed_loop_turned_on_automatically(
            self):
        for value in TEMPERATURE_TEST_VALUES:
            self.ca.set_pv_value("CLOSEDLOOP:SP", "Off")
            self.ca.assert_that_pv_is("CLOSEDLOOP", "Off")
            self.ca.assert_setting_setpoint_sets_readback(
                value, set_point_pv="TEMP:SP", readback_pv="TEMP:SP:RBV")
            self.ca.assert_that_pv_is("CLOSEDLOOP", "On")

    def test_GIVEN_closed_loop_already_on_WHEN_temperature_setpoint_is_set_THEN_closed_loop_setpoint_not_reprocessed(
            self):
        for value in TEMPERATURE_TEST_VALUES:
            self.ca.set_pv_value("CLOSEDLOOP:SP", "On")
            self.ca.assert_that_pv_is("CLOSEDLOOP", "On")
            timestamp_before = self.ca.get_pv_value("CLOSEDLOOP:SP.TSEL")
            self.ca.assert_setting_setpoint_sets_readback(
                value, set_point_pv="TEMP:SP", readback_pv="TEMP:SP:RBV")
            self.ca.assert_that_pv_is("CLOSEDLOOP", "On")
            self.ca.assert_that_pv_is("CLOSEDLOOP:SP.TSEL", timestamp_before)
            self.ca.assert_that_pv_value_is_unchanged("CLOSEDLOOP:SP.TSEL",
                                                      wait=5)

    def test_WHEN_heater_range_is_set_THEN_readback_updates(self):
        for value in HEATER_RANGE_TEST_VALUES:
            self.ca.assert_setting_setpoint_sets_readback(
                value, "HEATER:RANGE")

    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_heater_power_is_set_via_backdoor_THEN_pv_has_the_value_just_set(
            self):
        for value in HEATER_RANGE_TEST_VALUES:
            self._lewis.backdoor_set_on_device("heater_power", value)
            self.ca.assert_that_pv_is("HEATER:POWER", value)
            self.ca.assert_that_pv_alarm_is("HEATER:POWER",
                                            self.ca.Alarms.NONE)

    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_closed_loop_mode_is_set_via_backdoor_THEN_the_closed_loop_pv_updates_with_value_just_set(
            self):
        for value in [False, True,
                      False]:  # Need to check both transitions work properly
            self._lewis.backdoor_set_on_device("closed_loop", value)
            self.ca.assert_that_pv_is("CLOSEDLOOP", "On" if value else "Off")

    @skip_if_recsim("Behaviour too complex for recsim")
    def test_WHEN_channels_are_enabled_and_disabled_via_pv_THEN_the_readback_pv_updates_with_value_just_set(
            self):
        for chan in VALID_TEMPERATURE_SENSORS:
            for enabled in [False, True, False
                            ]:  # Need to check both transitions work properly
                self.ca.assert_setting_setpoint_sets_readback(
                    "ON" if enabled else "OFF",
                    "CHANNELS:T{}:STATE".format(chan))

    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_a_short_status_is_set_on_device_THEN_displayed_status_is_identical(
            self):
        # Status message that could be contained in an EPICS string type
        short_status = "Device status"
        assert 0 < len(short_status) < 40

        # Status message that device is likely to return - longer than EPICS string type but reasonable for a protocol
        medium_status = "This is a device status that contains a bit more information"
        assert 40 < len(medium_status) < 256

        # Short and medium statuses should be displayed in full.
        for status in [short_status, medium_status]:
            self._lewis.backdoor_set_on_device("status", status)
            self.ca.assert_that_pv_is("STATUS", status)

    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_long_status_is_set_on_device_THEN_displayed_status_truncated_but_displays_at_least_500_chars(
            self):

        # Somewhat arbitrary, but decide on a minimum number of characters that should be displayed in a
        # status message to the user if the status message is very long. This seems to be a reasonable
        # number given the messages expected, but the manual does not provide an exhaustive list.
        minimum_characters_in_pv = 500

        # Very long status message, used to check that very long messages can be handled gracefully
        long_status = "This device status is quite long:" + " (here is a load of information)" * 50

        assert minimum_characters_in_pv < len(long_status)

        # Allow truncation for long status, but it should still display as many characters as possible
        self._lewis.backdoor_set_on_device("status", long_status)
        self.ca.assert_that_pv_value_causes_func_to_return_true(
            "STATUS", lambda val: long_status.startswith(val) and len(val) >=
            minimum_characters_in_pv)

    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_automation_is_set_on_device_THEN_displayed_automation_is_identical(
            self):
        automations = [
            "Warming up to 200K",
            "Cooling down to 1K",
        ]

        for automation in automations:
            self._lewis.backdoor_set_on_device("automation", automation)
            self.ca.assert_that_pv_is("AUTOMATION", automation)

    def _set_temp_via_backdoor(self, channel, temp):
        self._lewis.backdoor_command([
            "device", "set_temperature_backdoor", "'{}'".format(channel),
            "{}".format(temp)
        ])

    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_stil_temp_is_set_via_backdoor_THEN_pv_updates(self):
        for temp in TEMPERATURE_TEST_VALUES:
            self._set_temp_via_backdoor("STIL", temp)
            self.ca.assert_that_pv_is("STIL:TEMP", temp)

    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_mc_temp_is_set_via_backdoor_THEN_pv_updates(self):
        for temp in TEMPERATURE_TEST_VALUES:
            self._set_temp_via_backdoor("MC", temp)
            self.ca.assert_that_pv_is("MC:TEMP", temp)

    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_sorb_temp_is_set_via_backdoor_THEN_pv_updates(self):
        for temp in TEMPERATURE_TEST_VALUES:
            self._set_temp_via_backdoor("SORB", temp)
            self.ca.assert_that_pv_is("SORB:TEMP", temp)

    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_4KHX_temp_is_set_via_backdoor_THEN_pv_updates(self):
        for temp in TEMPERATURE_TEST_VALUES:
            self._set_temp_via_backdoor("PT2", temp)
            self.ca.assert_that_pv_is("4KHX:TEMP", temp)

    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_jthx_temp_is_set_via_backdoor_THEN_pv_updates(self):
        for temp in TEMPERATURE_TEST_VALUES:
            self._set_temp_via_backdoor("PT1", temp)
            self.ca.assert_that_pv_is("JTHX:TEMP", temp)

    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_pressure_is_set_via_backdoor_THEN_pressure_pv_updates(self):
        for sensor, pressure in itertools.product(VALID_PRESSURE_SENSORS,
                                                  PRESSURE_TEST_VALUES):
            self._lewis.backdoor_command([
                "device", "set_pressure_backdoor",
                str(sensor),
                str(pressure)
            ])
            self.ca.assert_that_pv_is("PRESSURE:P{}".format(sensor), pressure)

    def test_WHEN_closed_loop_is_set_via_pv_THEN_readback_updates(self):
        for state in [False, True, False]:
            self.ca.assert_setting_setpoint_sets_readback(
                "On" if state else "Off", "CLOSEDLOOP")

    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_read_mc_id_is_issued_via_arbitrary_command_THEN_response_is_in_format_device_uses(
            self):
        self.ca.set_pv_value("ARBITRARY:SP", "READ:SYS:DR:CHAN:MC")
        self.ca.assert_that_pv_value_causes_func_to_return_true(
            "ARBITRARY", lambda val: val.startswith("STAT:SYS:DR:CHAN:MC:"))

    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_channel_temperature_is_set_via_backdoor_THEN_the_pvs_update_with_values_just_written(
            self):
        for chan, value in itertools.product(VALID_TEMPERATURE_SENSORS,
                                             TEMPERATURE_TEST_VALUES):
            self._lewis.backdoor_command([
                "device", "set_sensor_property_backdoor",
                str(chan), "temperature",
                str(value)
            ])
            self.ca.assert_that_pv_is("CHANNELS:T{}:TEMP".format(chan), value)

    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_channel_resistance_is_set_via_backdoor_THEN_the_pvs_update_with_values_just_written(
            self):
        for chan, value in itertools.product(VALID_TEMPERATURE_SENSORS,
                                             RESISTANCE_TEST_VALUES):
            self._lewis.backdoor_command([
                "device", "set_sensor_property_backdoor",
                str(chan), "resistance",
                str(value)
            ])
            self.ca.assert_that_pv_is("CHANNELS:T{}:RES".format(chan), value)

    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_channel_excitation_is_set_via_backdoor_THEN_the_pvs_update_with_values_just_written(
            self):
        for chan, value in itertools.product(VALID_TEMPERATURE_SENSORS,
                                             EXCITATION_TEST_VALUES):
            self._lewis.backdoor_command([
                "device", "set_sensor_property_backdoor",
                str(chan), "excitation",
                str(value)
            ])
            self.ca.assert_that_pv_is("CHANNELS:T{}:EXCITATION".format(chan),
                                      value)

    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_channel_pause_is_set_via_backdoor_THEN_the_pvs_update_with_values_just_written(
            self):
        for chan, value in itertools.product(VALID_TEMPERATURE_SENSORS,
                                             TIME_DELAY_TEST_VALUES):
            self._lewis.backdoor_command([
                "device", "set_sensor_property_backdoor",
                str(chan), "pause",
                str(value)
            ])
            self.ca.assert_that_pv_is("CHANNELS:T{}:PAUSE".format(chan), value)

    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_channel_dwell_is_set_via_backdoor_THEN_the_pvs_update_with_values_just_written(
            self):
        for chan, value in itertools.product(VALID_TEMPERATURE_SENSORS,
                                             TIME_DELAY_TEST_VALUES):
            self._lewis.backdoor_command([
                "device", "set_sensor_property_backdoor",
                str(chan), "dwell",
                str(value)
            ])
            self.ca.assert_that_pv_is("CHANNELS:T{}:DWELL".format(chan), value)

    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_heater_resistance_is_changed_THEN_heater_heater_resistance_pv_updates(
            self):
        for heater_resistance in RESISTANCE_TEST_VALUES:
            self._lewis.backdoor_set_on_device("heater_resistance",
                                               heater_resistance)
            self.ca.assert_that_pv_is_number("HEATER:RES", heater_resistance)

    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_heater_resistance_and_power_are_changed_THEN_heater_current_is_calculated_correctly(
            self):
        for res, power in itertools.product(RESISTANCE_TEST_VALUES,
                                            POWER_TEST_VALUES):
            self._lewis.backdoor_set_on_device("heater_resistance", res)
            self._lewis.backdoor_set_on_device("heater_power", power)

            self.ca.assert_that_pv_is_number(
                "HEATER:CURR", (power / res)**0.5,
                tolerance=0.01)  # Ohm's law P = RI^2

    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_heater_current_and_range_are_changed_THEN_heater_percent_power_is_calculated_correctly(
            self):

        for rang, res, power in itertools.product(HEATER_RANGE_TEST_VALUES,
                                                  RESISTANCE_TEST_VALUES,
                                                  POWER_TEST_VALUES):
            self._lewis.backdoor_set_on_device("heater_resistance", res)
            self._lewis.backdoor_set_on_device("heater_power", power)
            self._lewis.backdoor_set_on_device("heater_range", rang)

            assert rang != 0, "Heater range of zero will cause a zero division error!"

            self.ca.assert_that_pv_is_number("HEATER:PERCENT",
                                             100 * ((power / res)**0.5) / rang,
                                             tolerance=0.05)
Exemplo n.º 18
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)
class JawsTestsBase(object):
    """
    Base class for jaws tests
    """
    def setUp(self):
        self.setup_jaws()
        self._ioc = IOCRegister.get_running("jaws")
        self.ca = ChannelAccess(default_timeout=30)
        for mtr in self.UNDERLYING_MTRS.values():
            self.ca.set_pv_value("{}.DISP".format(mtr), 0)
            self.ca.set_pv_value("{}.VMAX".format(mtr), 100)
            self.ca.set_pv_value("{}.VELO".format(mtr), 100)

        self.ca.set_pv_value("{}:ABLE:SP".format(JAWS_BASE_PV), 0)
        self.ca.set_pv_value("{}:LOCK:SP".format(JAWS_BASE_PV), 0)

        self.ca.set_pv_value("MOT:JAWS1:HGAP:SP", 0)
        self.ca.set_pv_value("MOT:JAWS1:VGAP:SP", 0)

    @parameterized.expand(parameterized_list(DIRECTIONS))
    def test_GIVEN_ioc_started_THEN_underlying_mtr_fields_can_be_read(
            self, _, direction):
        underlying_mtr = self.UNDERLYING_MTRS[direction]

        expected = self.ca.get_pv_value("{}.VELO".format(underlying_mtr))
        jaw_blade_pv = "{}:J{}".format(JAWS_BASE_PV, direction)

        actual = self.ca.get_pv_value("{}:MTR.VELO".format(jaw_blade_pv))

        self.assertEqual(expected, actual)

    @parameterized.expand(parameterized_list(TEST_POSITIONS))
    def test_WHEN_jaw_blade_setpoint_changed_THEN_jaw_blade_moves(
            self, _, value):
        for direction in DIRECTIONS:
            rbv_pv = "{}:J{}".format(JAWS_BASE_PV, direction)
            sp_pv = "{}:J{}:SP".format(JAWS_BASE_PV, direction)
            self.ca.assert_setting_setpoint_sets_readback(value, rbv_pv, sp_pv)

    def test_GIVEN_jaws_closed_at_centre_WHEN_vgap_opened_THEN_north_and_south_jaws_move(
            self):
        n_pv = "{}:JN".format(JAWS_BASE_PV)
        s_pv = "{}:JS".format(JAWS_BASE_PV)
        # GIVEN
        self.ca.assert_that_pv_is("MOT:JAWS1:VGAP", 0)
        self.ca.assert_that_pv_is("MOT:JAWS1:VCENT", 0)
        # WHEN
        self.ca.set_pv_value("MOT:JAWS1:VGAP:SP", 1)
        # THEN
        self.ca.assert_that_pv_is(n_pv, 0.5)
        self.ca.assert_that_pv_is(s_pv, -0.5)

    def test_GIVEN_jaws_closed_at_centre_WHEN_hgap_opened_THEN_east_and_west_jaws_move(
            self):
        e_pv = "{}:JE".format(JAWS_BASE_PV)
        w_pv = "{}:JW".format(JAWS_BASE_PV)
        # GIVEN
        self.ca.assert_that_pv_is("MOT:JAWS1:HGAP", 0)
        self.ca.assert_that_pv_is("MOT:JAWS1:HCENT", 0)
        # WHEN
        self.ca.set_pv_value("MOT:JAWS1:HGAP:SP", 1)

        # THEN
        self.ca.assert_that_pv_is(e_pv, 0.5)
        self.ca.assert_that_pv_is(w_pv, -0.5)

    @parameterized.expand([("lock", "Unlocked"), ("able", "Enable")])
    def test_GIVEN_all_jaws_have_state_set_THEN_overall_state_is_set(
            self, key, expected):
        enabled_val = 0
        for mtr in self.UNDERLYING_MTRS.values():
            mtr_status_pv = "{}_{}".format(mtr, key)
            self.ca.set_pv_value(mtr_status_pv, enabled_val)

        jaws_status_readback_pv = "{}:{}".format(JAWS_BASE_PV, key.upper())
        actual = self.ca.get_pv_value(jaws_status_readback_pv)

        self.assertEqual(expected, actual)

    @parameterized.expand([("lock", "Locked"), ("able", "Disable")])
    def test_GIVEN_no_jaws_have_state_set_THEN_overall_state_is_not_set(
            self, key, expected):
        disabled_val = 1
        for mtr in self.UNDERLYING_MTRS.values():
            mtr_status_pv = "{}_{}".format(mtr, key)
            self.ca.set_pv_value(mtr_status_pv, disabled_val)

        jaws_status_readback_pv = "{}:{}".format(JAWS_BASE_PV, key.upper())
        actual = self.ca.get_pv_value(jaws_status_readback_pv)

        self.assertEqual(expected, actual)

    @parameterized.expand([("lock", "Unknown"), ("able", "Unknown")])
    def test_GIVEN_some_jaws_have_state_set_THEN_overall_state_is_unknown(
            self, key, expected):
        disabled_val = 0
        enabled_val = 1
        for mtr in list(self.UNDERLYING_MTRS.values())[:2]:
            mtr_status_pv = "{}_{}".format(mtr, key)
            self.ca.set_pv_value(mtr_status_pv, enabled_val)
        for mtr in list(self.UNDERLYING_MTRS.values())[2:]:
            mtr_status_pv = "{}_{}".format(mtr, key)
            self.ca.set_pv_value(mtr_status_pv, disabled_val)

        jaws_status_readback_pv = "{}:{}".format(JAWS_BASE_PV, key.upper())
        actual = self.ca.get_pv_value(jaws_status_readback_pv)

        self.assertEqual(expected, actual)

    @parameterized.expand(parameterized_list(DIRECTIONS))
    def test_GIVEN_underlying_mtr_adel_value_THEN_jaws_ADEL_field_mirrored(
            self, _, direction):
        motor_adel_pv = "{}.ADEL".format(self.UNDERLYING_MTRS[direction])
        jaw_adel_pv = "{}:J{}.ADEL".format(JAWS_BASE_PV, direction)

        self.ca.set_pv_value(motor_adel_pv, 0.0)
        self.ca.assert_that_pv_is(motor_adel_pv, 0.0)

        test_values = [1e-4, 1.2, 12.3]

        for test_value in test_values:
            self.ca.set_pv_value(motor_adel_pv, test_value)

            self.ca.assert_that_pv_is_number(motor_adel_pv, test_value)
            self.ca.assert_that_pv_is_number(jaw_adel_pv, test_value)

    @parameterized.expand([("V", "N"), ("H", "E")])
    def test_GIVEN_underlying_mtr_adel_THEN_jaws_centre_and_gap_adel_mirrored(
            self, axis, underlying_mtr_direction):
        underlying_mtr = self.UNDERLYING_MTRS[underlying_mtr_direction]
        motor_pv = "{}.ADEL".format(underlying_mtr)

        test_values = [1e-4, 1.2, 12.3]
        for test_value in test_values:
            self.ca.set_pv_value(motor_pv, test_value)

            self.ca.assert_that_pv_is_number(motor_pv, test_value)

            self.ca.assert_that_pv_is_number(
                "{}:{}CENT.ADEL".format(JAWS_BASE_PV, axis), test_value)
            self.ca.assert_that_pv_is_number(
                "{}:{}GAP.ADEL".format(JAWS_BASE_PV, axis), test_value)
class Itc503Tests(unittest.TestCase):
    """
    Tests for the Itc503 IOC.
    """
    def setUp(self):
        self._lewis, self._ioc = get_running_lewis_and_ioc(
            "itc503", DEVICE_PREFIX)
        self.ca = ChannelAccess(device_prefix=DEVICE_PREFIX,
                                default_timeout=20)
        self.ca.assert_that_pv_exists("DISABLE")
        self._make_device_scan_faster()

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    @skip_if_recsim(
        "Test is to tell whether data from emulator is correctly received")
    def test_GIVEN_output_nonzero_WHEN_units_changed_THEN_output_raw_adjusts(
            self):
        # Check that it is currently working correctly in Amps
        self._lewis.backdoor_set_on_device("is_paused", True)
        self._lewis.backdoor_set_on_device("output",
                                           1 / 0.037)  # 1T (0.037 = T_TO_A)
        self.ca.assert_that_pv_is_number("OUTPUT:RAW", 1 / 0.037, 0.001)
        self.ca.assert_that_pv_is_number("OUTPUT", 10000,
                                         1)  # OUTPUT should remain in Gauss
        # Set outputmode to tesla
        self.ca.set_pv_value("OUTPUTMODE:SP", "TESLA")
        self.ca.assert_that_pv_is_number("OUTPUT:RAW", 1, 0.001)
        self.ca.assert_that_pv_is_number("OUTPUT", 10000, 1)
        # Confirm functionality returns to normal when going back to Amps
        self.ca.set_pv_value("OUTPUTMODE:SP", "AMPS")
        self.ca.assert_that_pv_is_number("OUTPUT:RAW", 1 / 0.037, 0.001)
        self.ca.assert_that_pv_is_number("OUTPUT", 10000, 1)
Exemplo n.º 22
0
class KepcoTests(object):
    """
    Tests for the KEPCO.
    """

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        # Wait a short time and make sure count is not being incremented again later.
        time.sleep(5)
        self._lewis.assert_that_emulator_value_is("current_set_count", 1)
class BufferTests(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)

    def _set_buffer_size(self, buff_size):
        self.ca.set_pv_value("BUFF:SIZE:SP", buff_size)
        self.ca.assert_that_pv_is("BUFF:SIZE", buff_size)

    @skip_if_recsim("Cannot use lewis backdoor in recsim")
    def test_GIVEN_empty_buffer_WHEN_readings_added_to_fill_THEN_buffer_clears_and_index_PVs_correct(
            self):
        buffer_size = 10
        self._set_buffer_size(buffer_size)
        reads = _generate_readings(25, 1)

        for i in range(len(reads)):
            # WHEN
            _insert_reading(self, [reads[i]])
            # THEN
            self.ca.assert_that_pv_is("BUFF:NEXT", (i + 1) % buffer_size)
            self.ca.assert_that_pv_is("INDEX:START", (i + 1) % buffer_size)

    @skip_if_recsim("Cannot use lewis backdoor in recsim")
    def test_GIVEN_empty_buffer_WHEN_new_reads_added_until_buffer_full_THEN_buffer_and_index_pvs_behave_correctly(
            self):
        buffer_size = 5
        self._set_buffer_size(buffer_size)
        reads = _generate_readings(6, 5)

        self.ca.assert_that_pv_is("BUFF:NEXT", 0)
        self.ca.assert_that_pv_is("INDEX:START", 0)

        for i in range(buffer_size - 1):  # -1 so that buffer has 1 free space
            _insert_reading(self, [reads[i]])
            self.ca.assert_that_pv_is("BUFF:NEXT", i + 1)
            self.ca.assert_that_pv_is("INDEX:START", i + 1)
        # insert final reading
        _insert_reading(self, [reads[5]])
        self.ca.assert_that_pv_is("BUFF:NEXT", 0)
        self.ca.assert_that_pv_is("INDEX:START", 0)

    @skip_if_recsim("Cannot use lewis backdoor in recsim")
    def test_GIVEN_buffer_full_WHEN_new_reading_added_THEN_new_reading_added_at_index_0_AND_buffer_still_used(
            self):
        buffer_size = 10
        self._set_buffer_size(buffer_size)
        reads = _generate_readings(15, 5)

        _insert_reading(self, reads[:buffer_size])
        # GIVEN
        self.ca.assert_that_pv_is("BUFF:NEXT", 0)
        # WHEN
        _insert_reading(self, [reads[11]])
        # THEN
        self.ca.assert_that_pv_is("BUFF:NEXT", 1)
        # AND
        self.ca.assert_that_pv_is("BUFF:CONTROLMODE", "ALW")

    @skip_if_recsim("Cannot use lewis backdoor in recsim")
    def test_GIVEN_buffer_almost_full_WHEN_multiple_remaining_locations_written_to_THEN_correct_readings_returned(
            self):
        self._set_buffer_size(10)
        reads = _generate_readings(15, 5)
        expected_reads = reads[7:10]

        _insert_reading(self, reads[:7])
        self.ca.assert_that_pv_is("BUFF:NEXT", 7)

        _insert_reading(self, reads[7:10])  # This fills the buffer
        self.ca.assert_that_pv_is("BUFF:NEXT", 0)

        # retrieved_readings contains 3 readings each with (3 values, format READ,TST,CHAN).
        # The buffer stores them as contiguous comma separated strings, hence the need to pull out 9 array positions.
        retrieved_readings = self.ca.get_pv_value("BUFF:READ")[:9]
        retrieved_readings = map(int,
                                 retrieved_readings)  # map from float to int
        retrieved_readings = map(str,
                                 retrieved_readings)  # map from int to str
        # compare inserted reads with retrieved reads
        self.assertEqual(",".join(expected_reads).replace("+", ""),
                         ",".join(retrieved_readings))

    @skip_if_recsim("Cannot use lewis backdoor in recsim")
    def test_GIVEN_buffer_almost_full_WHEN_buffer_fills_and_overflows_THEN_correct_readings_returned(
            self):
        self._set_buffer_size(10)
        reads = _generate_readings(15, 5)
        expected_read = reads[10]

        _insert_reading(self, reads[:7])
        self.ca.assert_that_pv_is("BUFF:NEXT", 7)

        # This overfills the buffer and causes a clear, and then writes the final read into buffer location 0
        _insert_reading(self, reads[7:11])
        self.ca.assert_that_pv_is("BUFF:NEXT", 1)

        retrieved_readings = self.ca.get_pv_value("BUFF:READ")[:3]
        retrieved_readings = map(int,
                                 retrieved_readings)  # map from float to int
        retrieved_readings = map(str,
                                 retrieved_readings)  # map from int to str
        self.assertEqual(expected_read.replace("+", ""),
                         ",".join(retrieved_readings))
Exemplo n.º 24
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")
Exemplo n.º 25
0
class Knr1050Tests(unittest.TestCase):
    """
    Tests for the Knr1050 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_wait_time=0.0)
        self._lewis.backdoor_run_function_on_device("reset")
        # Set the device in remote mode ready to receive instructions
        self.ca.set_pv_value("MODE:SP", "REMOTE")
        self.ca.set_pv_value("MODE.PROC", 1)
        # Set the flow and concentrations to a default state that enable pump switch on
        self.ca.set_pv_value("STOP:SP", 1)
        self.ca.set_pv_value("STATUS", "OFF")
        self.ca.set_pv_value("FLOWRATE:SP", 0.01)
        self.ca.set_pv_value("PRESSURE:MIN:SP", 0)
        self.ca.set_pv_value("PRESSURE:MAX:SP", 100)
        self.ca.set_pv_value("COMP:A:SP", 100)
        self.ca.set_pv_value("COMP:B:SP", 0)
        self.ca.set_pv_value("COMP:C:SP", 0)
        self.ca.set_pv_value("COMP:D:SP", 0)
        self.ca.set_pv_value("STATUS:GET.PROC", 1)
        self.ca.set_pv_value("DISABLE:CHECK.PROC", 1)

    def _set_pressure_limit_low(self, limit):
        self._lewis.backdoor_set_on_device("pressure_limit_low", limit)

    def _set_pressure_limit_high(self, limit):
        self._lewis.backdoor_set_on_device("pressure_limit_high", limit)

    @skip_if_recsim("Recsim simulation not implemented")
    def test_GIVEN_an_ioc_WHEN_start_pump_sent_THEN_pump_starts(self):
        self.ca.set_pv_value("START:SP", 1)

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

    @skip_if_recsim("Recsim simulation not implemented")
    def test_GIVEN_an_ioc_WHEN_timed_pump_sent_THEN_pump_starts(self):
        self.ca.set_pv_value("TIMED:SP", 1)

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

    @skip_if_recsim("Recsim simulation not implemented")
    def test_GIVEN_an_ioc_WHEN_stop_pump_sent_via_ioc_THEN_device_state_off(self):
        expected_dev_state = "OFF"
        self.ca.set_pv_value("STOP:SP", 1)

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

    @skip_if_recsim("Recsim simulation not implemented")
    def test_GIVEN_an_ioc_WHEN_pump_is_turned_on_via_ioc_THEN_pump_is_on(self):
        self.ca.set_pv_value("START:SP", 1)
        pump_status = self._lewis.backdoor_get_from_device("pump_on")

        self.assertEqual(pump_status, True)

    @skip_if_recsim("Recsim simulation not implemented")
    def test_GIVEN_set_concentration_via_ioc_WHEN_ramp_command_sent_via_ioc_THEN_correct_concentration_set(self):
        expected_concentrations = [0, 50, 35, 15]
        self.ca.set_pv_value("COMP:A:SP", expected_concentrations[0])
        self.ca.set_pv_value("COMP:B:SP", expected_concentrations[1])
        self.ca.set_pv_value("COMP:C:SP", expected_concentrations[2])
        self.ca.set_pv_value("COMP:D:SP", expected_concentrations[3])
        self.ca.set_pv_value("START:SP", 1)

        sleep(1.0) # allow emulator to process above data

        concentrations = [self.ca.get_pv_value("COMP:A"),
                          self.ca.get_pv_value("COMP:B"),
                          self.ca.get_pv_value("COMP:C"),
                          self.ca.get_pv_value("COMP:D")]
        self.assertEqual(expected_concentrations, concentrations)

    @skip_if_recsim("Recsim simulation not implemented")
    def test_GIVEN_an_ioc_WHEN_stop_pump_sent_THEN_lewis_pump_stops(self):
        expected_pump_status = False
        self.ca.set_pv_value("STOP:SP", 1)
        pump_status = self._lewis.backdoor_get_from_device("pump_on")

        self.assertEqual(pump_status, expected_pump_status)

    @skip_if_recsim("Recsim simulation not implemented")
    def test_GIVEN_an_ioc_WHEN_stop2_command_sent_THEN_expected_stop_type(self):
        self._lewis.backdoor_set_on_device("keep_last_values", False)
        stopped_status = self._lewis.backdoor_get_from_device("keep_last_values")
        self.assertEqual(stopped_status, False)
        self.ca.set_pv_value("_STOP:KLV:SP", 1)

        stopped_status = self._lewis.backdoor_get_from_device("keep_last_values")
        self.assertEqual(stopped_status, True)

    @skip_if_recsim("Recsim simulation not implemented")
    def test_GIVEN_an_ioc_WHEN_pump_switched_on_then_back_to_off_THEN_device_state_off(self):
        expected_dev_state = "OFF"
        self.ca.set_pv_value("START:SP", 1)
        self.ca.set_pv_value("STOP:SP", 1)

        sleep(1.0) # allow emulator to process above data

        state = self._lewis.backdoor_get_from_device("state")

        self.assertEqual(expected_dev_state, state)

    @skip_if_recsim("Recsim simulation not implemented")
    def test_GIVEN_set_low_pressure_limit_via_backdoor_WHEN_get_low_pressure_limits_via_IOC_THEN_get_expected_pressure_limit(self):
        expected_pressure = 10
        self._set_pressure_limit_low(expected_pressure)
        self.ca.set_pv_value("PRESSURE:LIMITS.PROC", 1)

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

    @skip_if_recsim("Recsim simulation not implemented")
    def test_GIVEN_set_high_pressure_limit_via_backdoor_WHEN_get_high_pressure_limits_via_IOC_THEN_get_expected_pressure_limit(self):
        expected_pressure = 100
        self._set_pressure_limit_high(expected_pressure)
        self.ca.set_pv_value("PRESSURE:LIMITS.PROC", 1)

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

    @skip_if_recsim("Recsim simulation not implemented")
    def test_GIVEN_set_low_pressure_limit_via_ioc_WHEN_get_low_pressure_limit_THEN_get_expected_pressure_limit(self):
        expected_pressure = 10
        self.ca.set_pv_value("PRESSURE:MIN:SP", expected_pressure)
        self.ca.set_pv_value("PRESSURE:LIMITS.PROC", 1)
        self.ca.assert_that_pv_is("PRESSURE:MIN", expected_pressure)

    @skip_if_recsim("Recsim simulation not implemented")
    def test_GIVEN_set_high_pressure_limit_via_ioc_WHEN_get_high_pressure_limit_via_backdoor_THEN_get_expected_pressure_limit(self):
        expected_pressure = 200
        self.ca.set_pv_value("PRESSURE:MAX:SP", expected_pressure)
        self.ca.set_pv_value("PRESSURE:LIMITS.PROC", 1)
        self.ca.assert_that_pv_is("PRESSURE:MAX", expected_pressure)

        self.assertEqual(self._lewis.backdoor_get_from_device("pressure_limit_high"), expected_pressure)

    @skip_if_recsim("Recsim simulation not implemented")
    def test_GIVEN_set_low_pressure_limit_via_ioc_WHEN_get_low_pressure_limit_via_IOC_THEN_get_expected_value(self):
        expected_pressure = 45
        self.ca.set_pv_value("PRESSURE:MIN:SP", expected_pressure)
        self.ca.set_pv_value("PRESSURE:LIMITS.PROC", 1)
        self.ca.assert_that_pv_is("PRESSURE:MIN", expected_pressure)

    @skip_if_recsim("Recsim simulation not implemented")
    def test_GIVEN_set_high_pressure_limit_via_ioc_WHEN_get_high_pressure_limit_via_IOC_THEN_get_expected_value(self):
        expected_pressure = 500

        self.ca.set_pv_value("PRESSURE:MAX:SP", expected_pressure)
        self.ca.set_pv_value("PRESSURE:LIMITS.PROC", 1)
        self.ca.assert_that_pv_is("PRESSURE:MAX", expected_pressure)

    @skip_if_recsim("Recsim simulation not implemented")
    def test_GIVEN_set_flow_limit_min_via_ioc_WHEN_ramp_command_sent_via_IOC_THEN_correct_flow_limit_set(self):
        expected_flow = 0.01
        self.ca.set_pv_value("FLOWRATE:SP", expected_flow)
        self.ca.set_pv_value("START:SP", 1)

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

    @skip_if_recsim("Recsim simulation not implemented")
    def test_GIVEN_set_flow_limit_min_via_ioc_WHEN_get_flow_via_IOC_THEN_correct_flow_limit(self):
        expected_flow = 0.01
        self.ca.set_pv_value("FLOWRATE:SP", expected_flow)

        self.assertEqual(self.ca.get_pv_value("FLOWRATE:SP:RBV"), expected_flow)

    @skip_if_recsim("Recsim simulation not implemented")
    def test_GIVEN_ioc_turned_on_WHEN_get_dev_state_via_ioc_THEN_off_state_returned(self):
        expected_dev_state = 'OFF'
        state = self.ca.get_pv_value("STATUS")

        self.assertEqual(expected_dev_state, state)

    @skip_if_recsim("Recsim simulation not implemented")
    def test_GIVEN_ioc_turned_on_WHEN_set_local_mode_via_IOC_THEN_disabled_mode(self):
        expected_mode = 'Disabled'
        self.ca.set_pv_value("MODE:SP", "LOCAL")

        self.ca.assert_that_pv_is("DISABLE", expected_mode)

    @skip_if_recsim("Recsim simulation not implemented")
    def test_GIVEN_local_mode_WHEN_set_pump_on_via_IOC_THEN_pump_disabled(self):
        self.ca.set_pv_value("MODE:SP", "LOCAL")
        self.ca.set_pv_value("START:SP", 1)

        self.ca.assert_that_pv_is("STATUS", 'OFF')

    @skip_if_recsim("Recsim simulation not implemented")
    def test_GIVEN_incorrect_gradients_WHEN_set_pump_on_via_IOC_THEN_pump_disabled(self):
        self.ca.set_pv_value("COMP:A:SP", 50)  # sum of gradients =/= 100%
        self.ca.set_pv_value("START:SP", 1)

        self.ca.assert_that_pv_is("STATUS", 'OFF')

    @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('PRESSURE:LIMITS', ChannelAccess.Alarms.INVALID)

    @skip_if_recsim("Can not test disconnection in rec sim")
    def test_GIVEN_timed_run_started_THEN_remaining_time_decreases(self):
        self.ca.set_pv_value("TIME:SP", 10)
        self.ca.set_pv_value("TIMED:SP", 1)

        self.ca.assert_that_pv_value_is_decreasing("TIME:REMAINING", wait=5)

    @skip_if_recsim("Can not test disconnection in rec sim")
    def test_GIVEN_timed_run_started_THEN_pump_stopped_once_finished_run(self):
        self.ca.set_pv_value("TIME:SP", 10)
        self.ca.set_pv_value("TIMED:SP", 1)

        self.ca.assert_that_pv_is("STATUS", 'OFF', timeout=15)

    @skip_if_recsim("Can not test disconnection in rec sim")
    def test_GIVEN_long_timed_run_started_THEN_if_remaining_time_checked_then_not_finished(self):
        self.ca.set_pv_value("TIME:SP", 100)
        self.ca.set_pv_value("TIMED:SP", 1)

        self.ca.assert_that_pv_is("TIME:CHECK", 0)

    @skip_if_recsim("Can not test disconnection in rec sim")
    def test_GIVEN_set_volume_run_started_THEN_remaining_volume_decreases(self):
        self.ca.set_pv_value("FLOWRATE:SP", 0.02)
        self.ca.set_pv_value("VOL:SP", 0.05)
        self.ca.set_pv_value("TIMED:SP", 1)
        self.ca.assert_that_pv_is_not("VOL:REMAINING", 0.0, timeout=5)

        self.ca.assert_that_pv_value_is_decreasing("VOL:REMAINING", wait=5)

    @skip_if_recsim("Can't use lewis backdoor in RECSIM")
    def test_GIVEN_input_error_THEN_error_string_captured(self):
        expected_error = "20,Instrument in standalone mode"
        self._lewis.backdoor_set_on_device("input_correct", False)

        self.ca.assert_that_pv_is("ERROR:STR", expected_error, timeout=5)
class GemorcTests(unittest.TestCase):
    """
    Tests for the Gemorc IOC.
    """
    def reset_emulator(self):
        self._lewis.backdoor_set_on_device("reset", True)
        sleep(
            1
        )  # Wait for reset to finish so we don't jump the gun. No external indicator from emulator

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

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

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

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

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

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

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

            total_time += interval
            sleep(interval)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    @skip_if_recsim("Initialisation logic not performed in Recsim")
    def test_WHEN_oscillating_THEN_auto_reinitialisation_triggers_after_counter_reaches_auto_trigger_value(
            self):
        initialisation_interval = 100
        initial_status_string = "Sequence not run since IOC startup"
        self.ca.set_pv_value("INIT:AUTO", initialisation_interval)
        self.start_oscillating()
        while self.ca.get_pv_value("CYCLES") < initialisation_interval:
            self.ca.assert_that_pv_is("INIT:PROGRESS", "No")
            self.ca.assert_that_pv_is("INIT:STAT", initial_status_string)
            sleep(1)
        self.ca.assert_that_pv_is_not("INIT:STAT", initial_status_string)
        self.ca.assert_that_pv_is(
            "STAT:OSC", "No",
            timeout=10)  # Initialisation seq has a 5s wait at the start
class Sm300Tests(unittest.TestCase):
    """
    Tests for the Samsm300 IOC.
    """
    def setUp(self):
        self._lewis, self._ioc = get_running_lewis_and_ioc("sm300", SM300_DEVICE_PREFIX)
        self.ca = ChannelAccess(device_prefix="MOT")
        self.ioc_ca = ChannelAccess(device_prefix=SM300_DEVICE_PREFIX)
        self.ioc_ca.assert_that_pv_exists("RESET_AND_HOME", timeout=30)
        self._lewis.backdoor_run_function_on_device("reset")

    def set_starting_position(self, starting_pos, axis="x"):
        """
        Set the starting position of the motor and check it is there.
        Args:
            starting_pos: position to start at in steps
            axis: axis to set position on

        """
        if IOCRegister.uses_rec_sim:
            resolution = self.ca.get_pv_value("{}.MRES".format(MTRPV[axis]))
            self.ca.set_pv_value(MTRPV[axis], starting_pos * resolution)
        self._lewis.backdoor_set_on_device(axis + "_axis_rbv", starting_pos)
        self._lewis.backdoor_set_on_device(axis + "_axis_sp", starting_pos)
        self.ca.assert_that_pv_is("{}.RRBV".format(MTRPV[axis]), starting_pos)

    @skip_if_recsim("Needs to set x read back explicitly")
    def test_GIVEN_motor_at_position_WHEN_get_axis_x_ioc_position_THEN_position_is_as_expected(self):
        expected_value = 100
        resolution = self.ca.get_pv_value("MTR0101.MRES")
        self._lewis.backdoor_set_on_device("x_axis_rbv", expected_value / resolution)
        self._lewis.backdoor_set_on_device("x_axis_sp", expected_value / resolution)

        self.ca.assert_that_pv_is("MTR0101.RBV", expected_value)

    @skip_if_recsim("Needs to set y read back explicitly")
    def test_GIVEN_motor_at_position_WHEN_get_axis_y_ioc_position_THEN_position_is_as_expected(self):
        expected_value = 95
        resolution = self.ca.get_pv_value("MTR0102.MRES")
        self._lewis.backdoor_set_on_device("y_axis_rbv", expected_value / resolution)
        self._lewis.backdoor_set_on_device("y_axis_sp", expected_value / resolution)

        self.ca.assert_that_pv_is("MTR0102.RBV", expected_value)

    @skip_if_recsim("Needs to set error")
    def test_GIVEN_error_on_axis_WHEN_get_axis_x_THEN_error_returned(self):
        self._lewis.backdoor_set_on_device("x_axis_rbv_error", "B10")

        # It doesn't appear that the motor can be made to go into an invlaid state so major alarm will have to do
        self.ca.assert_that_pv_alarm_is("MTR0101", self.ca.Alarms.INVALID)

    @skip_if_recsim("Needs to set error")
    def test_GIVEN_malformed_motor_position_WHEN_get_axis_x_THEN_error_returned(self):
        self._lewis.backdoor_set_on_device("x_axis_rbv_error", "Xrubbish")

        # It doesn't appear that the motor can be made to go into an invlaid state so major alarm will have to do
        self.ca.assert_that_pv_alarm_is("MTR0101", self.ca.Alarms.INVALID)

    @skip_if_recsim("Needs to set moving on motor")
    def test_GIVEN_a_motor_is_moving_WHEN_get_moving_THEN_both_axis_are_moving(self):

        expected_value = 0
        self._lewis.backdoor_set_on_device("is_moving", True)

        self.ca.assert_that_pv_is("MTR0101.DMOV", expected_value)
        self.ca.assert_that_pv_is("MTR0102.DMOV", expected_value)

    @skip_if_recsim("Needs to set moving on motor")
    def test_GIVEN_a_motor_is_not_moving_WHEN_get_moving_THEN_both_axis_are_not_moving(self):
        expected_value = 1
        self._lewis.backdoor_set_on_device("is_moving", False)

        self.ca.assert_that_pv_is("MTR0101.DMOV", expected_value)
        self.ca.assert_that_pv_is("MTR0102.DMOV", expected_value)

    @skip_if_recsim("Needs to set moving error")
    def test_GIVEN_a_motor_is_in_error_WHEN_get_moving_THEN_both_axis_are_in_error(self):
        self._lewis.backdoor_set_on_device("is_moving_error", True)

        self.ca.assert_that_pv_alarm_is("MTR0101", self.ca.Alarms.MAJOR)
        self.ca.assert_that_pv_alarm_is("MTR0102", self.ca.Alarms.MAJOR)

    def test_GIVEN_motor_at_position_WHEN_set_postion_THEN_motor_moves_to_the_position(self):
        expected_value = 10
        self.set_starting_position(0)
        self.ca.set_pv_value("MTR0101", expected_value)

        self.ca.assert_that_pv_is("MTR0101.RBV", expected_value)

    @skip_if_recsim("Sim motor doesn't home")
    def test_GIVEN_a_motor_WHEN_homed_THEN_motor_moves_to_home(self):
        expected_home = 0
        self.set_starting_position(10)
        self.ca.set_pv_value("MTR0101.HOMF", 1)

        self.ca.assert_that_pv_is("MTR0101.RBV", expected_home)

    @skip_if_recsim("Sim motor doesn't home")
    def test_GIVEN_a_motor_WHEN_homed_in_reverse_THEN_motor_moves_to_home(self):
        expected_home = 0
        self.set_starting_position(10)
        self.ca.set_pv_value("MTR0101.HOMR", 1)

        self.ca.assert_that_pv_is("MTR0101.RBV", expected_home)

    @skip_if_recsim("Needs to get reset values set from lewis")
    def test_GIVEN_a_motor_WHEN_reset_pressed_THEN_initial_values_sent(self):
        self.ioc_ca.assert_that_pv_exists("RESET", 30)
        self.ioc_ca.set_pv_value("RESET", 1)

        reset_codes = self._lewis.backdoor_get_from_device("reset_codes")

        for reset_code in expected_reset_codes:
            assert_that(reset_codes, contains_string(reset_code))
        self.ioc_ca.assert_that_pv_is("RESET", "Done")

    @skip_if_recsim("Sim doesn't return until move is finished")
    def test_GIVEN_a_motor_moving_to_set_point_WHEN_told_to_move_to_another_set_point_THEN_motor_goes_to_new_setpoint(self):
        self.set_starting_position(0)
        first_position = 20
        final_position = 30
        self.ca.set_pv_value("MTR0101", first_position)
        self.ca.assert_that_pv_is("MTR0101.DMOV", 0)

        self.ca.set_pv_value("MTR0101", final_position)

        self.ca.assert_that_pv_is("MTR0101.RBV", final_position)

    @skip_if_recsim("Sim doesn't return until move is finished")
    def test_GIVEN_an_axis_moving_to_set_point_WHEN_other_axis_told_to_move_THEN_motor_goes_to_setpoint(self):
        self.set_starting_position(0, axis="x")
        self.set_starting_position(0, axis="y")
        x_position = 10
        y_position = 5
        self.ca.set_pv_value("MTR0101.VAL", x_position)
        self.ca.assert_that_pv_is("MTR0101.DMOV", 0)

        self.ca.set_pv_value("MTR0102.VAL", y_position)

        self.ca.assert_that_pv_is("MTR0101.RBV", x_position, timeout=30)
        self.ca.assert_that_pv_is("MTR0102.RBV", y_position, timeout=30)

    @skip_if_recsim("Needs to get reset code from lewis")
    def test_GIVEN_a_motor_WHEN_disconnect_THEN_M77_is_sent(self):
        self.ioc_ca.assert_that_pv_exists("DISCONNECT", 30)
        self.ioc_ca.set_pv_value("DISCONNECT", 1)

        reset_codes = self._lewis.backdoor_get_from_device("disconnect")

        assert_that(reset_codes, is_("77"))

    @skip_if_recsim("Needs to set error code in lewis")
    def test_GIVEN_normal_error_WHEN_query_THEN_error_is_set(self):
        self._lewis.backdoor_set_on_device("error_code", 1)

        self.ioc_ca.assert_that_pv_is("ERROR", "Servo error")
        self.ioc_ca.assert_that_pv_alarm_is("ERROR", self.ca.Alarms.MAJOR)

    @skip_if_recsim("Needs to set error code in lewis")
    def test_GIVEN_no_error_WHEN_query_THEN_error_is_blank(self):
        self._lewis.backdoor_set_on_device("error_code", 0)

        self.ioc_ca.assert_that_pv_is("ERROR", "")
        self.ioc_ca.assert_that_pv_alarm_is("ERROR", self.ca.Alarms.NONE)

    @skip_if_recsim("Needs to set error code in lewis")
    def test_GIVEN_command_send_error_WHEN_query_THEN_error_is_set(self):
        self._lewis.backdoor_set_on_device("error_code", 0x10)

        self.ioc_ca.assert_that_pv_is("ERROR", "Cmd error code")
        self.ioc_ca.assert_that_pv_alarm_is("ERROR", self.ca.Alarms.MAJOR)

    @skip_if_recsim("Needs to set error code in lewis")
    def test_GIVEN_cnc_command_send_CNC_error_WHEN_query_THEN_error_is_set(self):
        self._lewis.backdoor_set_on_device("error_code", 0x20)

        self.ioc_ca.assert_that_pv_is("ERROR", "CNC cmd error code")
        self.ioc_ca.assert_that_pv_alarm_is("ERROR", self.ca.Alarms.MAJOR)

    @skip_if_recsim("Needs to set error code in lewis")
    def test_GIVEN_a_motor_WHEN_reset_and_homed_THEN_motor_moves_to_home_and_resets(self):
        expected_home = 0
        self.set_starting_position(20, axis="x")
        self.set_starting_position(20, axis="y")

        self.ioc_ca.set_pv_value("RESET_AND_HOME", 1)

        reset_codes = self._lewis.backdoor_get_from_device("reset_codes")
        for reset_code in expected_reset_codes:
            assert_that(reset_codes, contains_string(reset_code))
            self.ioc_ca.assert_that_pv_is("RESET", "Done")
        self.ca.assert_that_pv_is("MTR0101.RBV", expected_home)
        self.ca.assert_that_pv_is("MTR0102.RBV", expected_home)

    @skip_if_recsim("Needs to set disconnected")
    def test_GIVEN_motor_is_disconnected_WHEN_get_axis_x_ioc_position_THEN_alarm_is_disconnected(self):
        self._lewis.backdoor_set_on_device("is_disconnected", True)

        self.ca.assert_that_pv_alarm_is("MTR0101", self.ca.Alarms.INVALID)
Exemplo n.º 28
0
class JawsManagerBase(object):
    """
    Base classes for all jaws manager tests.
    """
    def setUp(self):
        self._ioc = IOCRegister.get_running("GALIL_01")
        self.ca = ChannelAccess()
        self.ca.assert_that_pv_exists("MOT:MTR0101", timeout=30)
        for jaw in range(1, self.get_num_of_jaws() + 1):
            self.ca.assert_that_pv_exists(UNDERLYING_GAP_SP.format(jaw, "V"),
                                          timeout=30)
            self.ca.assert_that_pv_exists(UNDERLYING_GAP_SP.format(jaw, "H"),
                                          timeout=30)
        self.ca.assert_that_pv_exists(self.get_sample_pv() +
                                      ":{}GAP:SP".format("V"),
                                      timeout=30)

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

    @abc.abstractmethod
    def get_num_of_jaws(self):
        pass

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

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

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

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

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

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

    def _test_WHEN_sample_gap_set_THEN_other_jaws_as_expected(
            self, direction, sample_gap, expected):
        self.ca.set_pv_value(
            self.get_sample_pv() + ":{}GAP:SP".format(direction), sample_gap)
        for i, exp in enumerate(expected):
            self.ca.assert_that_pv_is_number(UNDERLYING_GAP_SP.format(
                i + 1, direction),
                                             exp,
                                             0.1,
                                             timeout=1)
Exemplo n.º 29
0
class Ilm200Tests(unittest.TestCase):
    """
    Tests for the Ilm200 IOC.
    """
    DEFAULT_SCAN_RATE = 1
    SLOW = "Slow"
    FAST = "Fast"
    LEVEL_TOLERANCE = 0.1

    FULL = 100.0
    LOW = 10.0
    FILL = 5.0

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

            self.set_level_via_backdoor(channel, ALARM_THRESHOLDS[channel] - 0.1)
            self.ca.assert_that_pv_alarm_is(self.ch_pv(channel, "LEVEL"), self.ca.Alarms.MAJOR)