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)
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)
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"))
class VoltageTests(unittest.TestCase): voltage_values = [0, 10.1111111, 10e1, 20e-2, 200] voltage_values_which_give_alarms = [ -50, MIN_SEPARATOR_VOLT, MAX_SEPARATOR_VOLT, 250 ] def setUp(self): self.ca = ChannelAccess(20, device_prefix=DEVICE_PREFIX) shared_setup(self.ca) def test_that_GIVEN_sim_val_0_and_data_0_WHEN_voltage_set_point_changed_THEN_data_changed( self): # GIVEN self.ca.set_pv_value("DAQ:VOLT:SIM", 0) self.ca.assert_that_pv_is("DAQ:VOLT:SP", 0) # WHEN self.ca.set_pv_value("VOLT:SP", 20.) # THEN self.ca.assert_that_pv_is("DAQ:VOLT:SP", 20. * DAQ_VOLT_WRITE_SCALE_FACTOR) @parameterized.expand(parameterized_list(voltage_values)) def test_that_WHEN_set_THEN_the_voltage_changes(self, _, value): # WHEN self.ca.set_pv_value("VOLT:SP", value) # THEN self.ca.assert_that_pv_is_number("VOLT", value, MARGIN_OF_ERROR) @parameterized.expand(parameterized_list(voltage_values_which_give_alarms)) def test_that_WHEN_voltage_out_of_range_THEN_alarm_raised(self, _, value): # WHEN self.ca.set_pv_value("VOLT:SP", value) # THEN self.ca.assert_that_pv_alarm_is("VOLT", ChannelAccess.Alarms.MAJOR) def test_that_GIVEN_voltage_in_range_WHEN_setpoint_is_above_range_THEN_setpoint_is_set_to_max_value( self): # GIVEN self.ca.set_pv_value("VOLT:SP", 30) self.ca.assert_that_pv_is("VOLT", 30) # WHEN self.ca.set_pv_value("VOLT:SP", 215.) # THEN self.ca.assert_that_pv_is("VOLT:SP", MAX_SEPARATOR_VOLT) self.ca.assert_that_pv_is("VOLT", MAX_SEPARATOR_VOLT) def test_that_GIVEN_voltage_in_range_WHEN_setpoint_is_below_range_THEN_setpoint_is_set_to_min_value( self): # GIVEN self.ca.set_pv_value("VOLT:SP", 30) self.ca.assert_that_pv_is("VOLT", 30) # WHEN self.ca.set_pv_value("VOLT:SP", -50) # THEN self.ca.assert_that_pv_is("VOLT", MIN_SEPARATOR_VOLT) self.ca.assert_that_pv_is("VOLT:SP", MIN_SEPARATOR_VOLT) def test_GIVEN_data_to_be_filtered_WHEN_filtering_applied_THEN_returned_data_has_correct_shape( self): # stride_length = 1 # GIVEN self.ca.set_pv_value("DAQ:VOLT:WV:SIM", DAQ_DATA) # THEN returned_data_shape = self.ca.get_pv_value("FILTERED:VOLT.NORD") # THEN self.assertEqual(returned_data_shape, len(DAQ_DATA) - STRIDE_LENGTH) def test_GIVEN_unfiltered_data_WHEN_filtering_applied_THEN_corrected_data_is_returned( self): # GIVEN # Writing directly to DAQ:VOLT, need to remove the scaling factor self.ca.set_pv_value("DAQ:VOLT:WV:SIM", DAQ_DATA * DAQ_VOLT_WRITE_SCALE_FACTOR) # THEN returned_data_shape = int(self.ca.get_pv_value("FILTERED:VOLT.NORD")) returned_data = self.ca.get_pv_value( "FILTERED:VOLT")[:returned_data_shape] filtered_data = apply_average_filter(DAQ_DATA) self.assertEqual(len(returned_data), len(filtered_data)) for filtered_value, reference_value in zip(returned_data, filtered_data): self.assertAlmostEqual(filtered_value, reference_value, places=3)
class Sans2dVacCollisionAvoidanceTests(unittest.TestCase): """ Tests for the sans2d vacuum tank motor extensions. """ def setUp(self): self.ca = ChannelAccess(device_prefix="MOT", default_timeout=30) with ManagerMode(ChannelAccess()): self._disable_collision_avoidance() for axis in BAFFLES_AND_DETECTORS_Z_AXES: current_position = self.ca.get_pv_value("{}".format(axis)) new_position = self._get_axis_default_position( "{}".format(axis)) self.ca.set_pv_value("{}:MTR.VMAX".format(axis), TEST_SPEED, sleep_after_set=0) self.ca.set_pv_value("{}:MTR.VELO".format(axis), TEST_SPEED, sleep_after_set=0) self.ca.set_pv_value("{}:MTR.ACCL".format(axis), TEST_ACCELERATION, sleep_after_set=0) if current_position != new_position: self.ca.set_pv_value("{}:SP".format(axis), new_position, sleep_after_set=0) timeout = self._get_timeout_for_moving_to_position( axis, new_position) self.ca.assert_that_pv_is("{}".format(axis), new_position, timeout=timeout) # re-enable collision avoidance self._enable_collision_avoidance() def _disable_collision_avoidance(self): self._set_collision_avoidance_state(1, "DISABLED") def _enable_collision_avoidance(self): self._set_collision_avoidance_state(0, "ENABLED") def _set_collision_avoidance_state(self, write_value, read_value): # Do nothing if manager mode is already in correct state if ChannelAccess().get_pv_value(ManagerMode.MANAGER_MODE_PV) != "Yes": cm = ManagerMode(ChannelAccess()) else: cm = nullcontext() with cm: err = None for _ in range(20): try: self.ca.set_pv_value("SANS2DVAC:COLLISION_AVOIDANCE", write_value, sleep_after_set=0) break except WriteAccessException as e: err = e sleep(1) else: raise err self.ca.assert_that_pv_is("SANS2DVAC:COLLISION_AVOIDANCE", read_value) @contextlib.contextmanager def _assert_last_stop_time_updated(self, timeout=5): initial_stop_time = self.ca.get_pv_value("SANS2DVAC:_LAST_STOP_TIME") try: yield except Exception as e: raise e else: self.ca.assert_that_pv_is_not("SANS2DVAC:_LAST_STOP_TIME", initial_stop_time, timeout=timeout) @parameterized.expand(parameterized_list(AXIS_PAIRS)) def test_GIVEN_motor_interval_above_minor_warning_threshold_THEN_interval_is_correct_and_not_in_alarm( self, _, axis_pair): # disable collision avoidance so it does not interfere with checking the intervals and their alarm status self._disable_collision_avoidance() rear_axis_position = self.ca.get_pv_value(axis_pair.rear_axis) front_axis_position = rear_axis_position - 50 - MINOR_ALARM_INTERVAL_THRESHOLD expected_interval = rear_axis_position - front_axis_position self.ca.set_pv_value(axis_pair.front_axis_sp, front_axis_position, sleep_after_set=0) timeout = self._get_timeout_for_moving_to_position( axis_pair.front_axis, front_axis_position) self.ca.assert_that_pv_is_number("SANS2DVAC:{}:INTERVAL".format( axis_pair.name), expected_interval, timeout=timeout, tolerance=0.1) self.ca.assert_that_pv_alarm_is( "SANS2DVAC:{}:INTERVAL".format(axis_pair.name), self.ca.Alarms.NONE) @parameterized.expand(parameterized_list(AXIS_PAIRS)) def test_GIVEN_setpoint_interval_above_minor_warning_threshold_THEN_interval_is_correct_and_not_in_alarm( self, _, axis_pair): # disable collision avoidance so it does not interfere with checking the intervals and their alarm status self._disable_collision_avoidance() rear_axis_position = 1000 front_axis_position = rear_axis_position - 50 - MINOR_ALARM_INTERVAL_THRESHOLD expected_interval = rear_axis_position - front_axis_position self.ca.set_pv_value(axis_pair.front_axis_sp, front_axis_position, sleep_after_set=0) self.ca.set_pv_value(axis_pair.rear_axis_sp, rear_axis_position, sleep_after_set=0) self._assert_axis_position_reached(axis_pair.front_axis, front_axis_position) self._assert_axis_position_reached(axis_pair.rear_axis, rear_axis_position) self.ca.assert_that_pv_is_number("SANS2DVAC:{}:INTERVAL".format( axis_pair.name), expected_interval, timeout=5, tolerance=0.1) self.ca.assert_that_pv_alarm_is( "SANS2DVAC:{}:INTERVAL".format(axis_pair.name), self.ca.Alarms.NONE) @parameterized.expand(parameterized_list(AXIS_PAIRS)) def test_GIVEN_motor_interval_under_minor_warning_threshold_THEN_interval_is_correct_and_in_minor_alarm( self, _, axis_pair): # disable collision avoidance so it does not interfere with checking the intervals and their alarm status self._disable_collision_avoidance() rear_position = self.ca.get_pv_value(axis_pair.rear_axis) front_new_position = rear_position - MINOR_ALARM_INTERVAL_THRESHOLD + 1 expected_interval = rear_position - front_new_position self.ca.set_pv_value(axis_pair.front_axis_sp, front_new_position, sleep_after_set=0) timeout = self._get_timeout_for_moving_to_position( axis_pair.front_axis, front_new_position) self.ca.assert_that_pv_is_number("SANS2DVAC:{}:INTERVAL".format( axis_pair.name), expected_interval, timeout=timeout, tolerance=0.1) self.ca.assert_that_pv_alarm_is( "SANS2DVAC:{}:INTERVAL".format(axis_pair.name), self.ca.Alarms.MINOR) @parameterized.expand(parameterized_list(AXIS_PAIRS)) def test_GIVEN_setpoint_interval_under_minor_warning_threshold_THEN_interval_is_correct_and_in_minor_alarm( self, _, axis_pair): # disable collision avoidance so it does not interfere with checking the intervals and their alarm status self._disable_collision_avoidance() rear_axis_position = 1000 front_axis_position = rear_axis_position - MINOR_ALARM_INTERVAL_THRESHOLD + 1 expected_interval = rear_axis_position - front_axis_position self.ca.set_pv_value(axis_pair.front_axis_sp, front_axis_position, sleep_after_set=0) self.ca.set_pv_value(axis_pair.rear_axis_sp, rear_axis_position, sleep_after_set=0) self._assert_axis_position_reached(axis_pair.front_axis, front_axis_position) self._assert_axis_position_reached(axis_pair.rear_axis, rear_axis_position) self.ca.assert_that_pv_is_number("SANS2DVAC:{}:INTERVAL".format( axis_pair.name), expected_interval, timeout=5, tolerance=0.1) self.ca.assert_that_pv_alarm_is( "SANS2DVAC:{}:INTERVAL".format(axis_pair.name), self.ca.Alarms.MINOR) @parameterized.expand(parameterized_list(AXIS_PAIRS)) def test_GIVEN_motor_interval_under_major_warning_threshold_THEN_interval_is_correct_and_in_major_alarm( self, _, axis_pair): # disable collision avoidance so it does not interfere with checking the intervals and their alarm status self._disable_collision_avoidance() rear_axis_position = self.ca.get_pv_value(axis_pair.rear_axis) front_axis_position = rear_axis_position - MAJOR_ALARM_INTERVAL_THRESHOLD + 1 expected_interval = rear_axis_position - front_axis_position self.ca.set_pv_value(axis_pair.front_axis_sp, front_axis_position, sleep_after_set=0) timeout = self._get_timeout_for_moving_to_position( axis_pair.front_axis, front_axis_position) self.ca.assert_that_pv_is_number("SANS2DVAC:{}:INTERVAL".format( axis_pair.name), expected_interval, timeout=timeout, tolerance=0.1) self.ca.assert_that_pv_alarm_is( "SANS2DVAC:{}:INTERVAL".format(axis_pair.name), self.ca.Alarms.MAJOR) @parameterized.expand(parameterized_list(AXIS_PAIRS)) def test_GIVEN_setpoint_interval_under_major_warning_threshold_THEN_interval_is_correct_and_in_major_alarm( self, _, axis_pair): # disable collision avoidance so it does not interfere with checking the intervals and their alarm status self._disable_collision_avoidance() rear_axis_position = 1000 front_axis_position = rear_axis_position - MAJOR_ALARM_INTERVAL_THRESHOLD + 1 expected_interval = rear_axis_position - front_axis_position self.ca.set_pv_value(axis_pair.front_axis_sp, front_axis_position, sleep_after_set=0) self.ca.set_pv_value(axis_pair.rear_axis_sp, rear_axis_position, sleep_after_set=0) self._assert_axis_position_reached(axis_pair.front_axis, front_axis_position) self._assert_axis_position_reached(axis_pair.rear_axis, rear_axis_position) self.ca.assert_that_pv_is_number("SANS2DVAC:{}:INTERVAL".format( axis_pair.name), expected_interval, timeout=5, tolerance=0.1) self.ca.assert_that_pv_alarm_is( "SANS2DVAC:{}:INTERVAL".format(axis_pair.name), self.ca.Alarms.MAJOR) @parameterized.expand(parameterized_list(AXIS_PAIRS)) def test_GIVEN_front_axis_moves_towards_rear_axis_WHEN_setpoint_interval_greater_than_threshold_THEN_motor_not_stopped( self, _, axis_pair): front_axis_new_position = (self.ca.get_pv_value(axis_pair.rear_axis) - axis_pair.minimum_interval) - 50 self.ca.set_pv_value(axis_pair.front_axis_sp, front_axis_new_position, sleep_after_set=0) self._assert_axis_position_reached(axis_pair.front_axis, front_axis_new_position) @parameterized.expand(parameterized_list(AXIS_PAIRS)) def test_GIVEN_front_axis_moves_towards_rear_axis_WHEN_setpoint_interval_smaller_than_threshold_THEN_motor_stops( self, _, axis_pair): with self._assert_last_stop_time_updated(): front_axis_new_position = (self.ca.get_pv_value( axis_pair.rear_axis) - axis_pair.minimum_interval) + 50 self.ca.set_pv_value(axis_pair.front_axis_sp, front_axis_new_position, sleep_after_set=0) self.ca.assert_that_pv_is("{}:MTR.MOVN".format( axis_pair.front_axis), 1, timeout=1) self.ca.assert_that_pv_is("{}:MTR.TDIR".format( axis_pair.front_axis), 1, timeout=1) timeout = self._get_timeout_for_moving_to_position( axis_pair.front_axis, front_axis_new_position) assert_axis_not_moving(axis_pair.front_axis, timeout=timeout) self.ca.assert_that_pv_is_not(axis_pair.front_axis, front_axis_new_position, timeout=timeout) @parameterized.expand(parameterized_list(AXIS_PAIRS)) def test_GIVEN_front_axis_within_threhsold_distance_to_rear_axis_WHEN_set_to_move_away_THEN_motor_not_stopped( self, _, axis_pair): front_axis_new_position = (self.ca.get_pv_value(axis_pair.rear_axis) - axis_pair.minimum_interval) + 50 self.ca.set_pv_value(axis_pair.front_axis_sp, front_axis_new_position, sleep_after_set=0) self.ca.assert_that_pv_is("{}:MTR.MOVN".format(axis_pair.front_axis), 1, timeout=1) self.ca.assert_that_pv_is("{}:MTR.TDIR".format(axis_pair.front_axis), 1, timeout=1) timeout = self._get_timeout_for_moving_to_position( axis_pair.front_axis, front_axis_new_position) assert_axis_not_moving(axis_pair.front_axis, timeout=timeout) self.ca.assert_that_pv_is_not(axis_pair.front_axis, front_axis_new_position, timeout=timeout) front_axis_new_position = self.ca.get_pv_value( axis_pair.front_axis) - 200 self.ca.set_pv_value(axis_pair.front_axis_sp, front_axis_new_position, sleep_after_set=0) assert_axis_moving(axis_pair.front_axis, timeout=1) self.ca.assert_that_pv_is("{}:MTR.TDIR".format(axis_pair.front_axis), 0, timeout=1) self._assert_axis_position_reached(axis_pair.front_axis, front_axis_new_position) @parameterized.expand(parameterized_list(AXIS_PAIRS)) def test_GIVEN_rear_axis_moves_towards_front_axis_WHEN_setpoint_interval_greater_than_threshold_THEN_motor_not_stopped( self, _, axis_pair): rear_axis_position = (self.ca.get_pv_value(axis_pair.front_axis) + axis_pair.minimum_interval) + 50 self.ca.set_pv_value(axis_pair.rear_axis_sp, rear_axis_position, sleep_after_set=0) self._assert_axis_position_reached(axis_pair.rear_axis, rear_axis_position) @parameterized.expand(parameterized_list(AXIS_PAIRS)) def test_GIVEN_rear_axis_moves_towards_front_axis_WHEN_setpoint_interval_smaller_than_threshold_THEN_motor_stops( self, _, axis_pair): with self._assert_last_stop_time_updated(): rear_axis_new_position = (self.ca.get_pv_value( axis_pair.front_axis) + axis_pair.minimum_interval) - 50 self.ca.set_pv_value(axis_pair.rear_axis_sp, rear_axis_new_position, sleep_after_set=0) self.ca.assert_that_pv_is("{}:MTR.MOVN".format( axis_pair.rear_axis), 1, timeout=1) self.ca.assert_that_pv_is("{}:MTR.TDIR".format( axis_pair.rear_axis), 0, timeout=1) timeout = self._get_timeout_for_moving_to_position( axis_pair.rear_axis, rear_axis_new_position) assert_axis_not_moving(axis_pair.rear_axis, timeout=timeout) self.ca.assert_that_pv_is_not(axis_pair.rear_axis, rear_axis_new_position, timeout=timeout) @parameterized.expand(parameterized_list(AXIS_PAIRS)) def test_GIVEN_rear_axis_within_threhsold_distance_to_front_axis_WHEN_set_to_move_away_THEN_motor_not_stopped( self, _, axis_pair): rear_axis_new_position = (self.ca.get_pv_value(axis_pair.front_axis) + axis_pair.minimum_interval) - 50 self.ca.set_pv_value(axis_pair.rear_axis_sp, rear_axis_new_position, sleep_after_set=0) self.ca.assert_that_pv_is("{}:MTR.MOVN".format(axis_pair.rear_axis), 1, timeout=1) self.ca.assert_that_pv_is("{}:MTR.TDIR".format(axis_pair.rear_axis), 0, timeout=1) timeout = self._get_timeout_for_moving_to_position( axis_pair.rear_axis, rear_axis_new_position) assert_axis_not_moving(axis_pair.rear_axis, timeout=timeout) self.ca.assert_that_pv_is_not(axis_pair.rear_axis, rear_axis_new_position, timeout=timeout) rear_axis_new_position = self.ca.get_pv_value( axis_pair.rear_axis) + 200 self.ca.set_pv_value(axis_pair.rear_axis_sp, rear_axis_new_position, sleep_after_set=0) assert_axis_moving(axis_pair.rear_axis, timeout=1) self.ca.assert_that_pv_is("{}:MTR.TDIR".format(axis_pair.rear_axis), 1, timeout=1) self._assert_axis_position_reached(axis_pair.rear_axis, rear_axis_new_position) def _invalid_alarm_on_motor(self, motor, invalid): """ Puts a motor into invalid alarm. The simulated motor record doesn't respect SIMS so instead use a readback link pointing at an invalid PV and tell the motor to use the readback link. This causes an invalid alarm as desired. """ if invalid: self.ca.set_pv_value("{}:MTR.RDBL".format(motor), "fake_input_link_doesnt_connect", sleep_after_set=0) self.ca.set_pv_value("{}:MTR.URIP".format(motor), "Yes" if invalid else "No", sleep_after_set=0) self.ca.assert_that_pv_alarm_is( motor, self.ca.Alarms.INVALID if invalid else self.ca.Alarms.NONE) @parameterized.expand(parameterized_list(AXIS_PAIRS)) def test_GIVEN_axis_has_comms_error_THEN_axes_are_stopped( self, _, axis_pair): with self._assert_last_stop_time_updated(timeout=10): self._invalid_alarm_on_motor(axis_pair.front_axis, True) self.ca.assert_that_pv_alarm_is( "SANS2DVAC:{}:INTERVAL".format(axis_pair.name), self.ca.Alarms.INVALID) self._invalid_alarm_on_motor(axis_pair.front_axis, False) self.ca.assert_that_pv_alarm_is( "SANS2DVAC:{}:INTERVAL".format(axis_pair.name), self.ca.Alarms.NONE) with self._assert_last_stop_time_updated(timeout=10): self._invalid_alarm_on_motor(axis_pair.rear_axis, True) self.ca.assert_that_pv_alarm_is( "SANS2DVAC:{}:INTERVAL".format(axis_pair.name), self.ca.Alarms.INVALID) self._invalid_alarm_on_motor(axis_pair.rear_axis, False) self.ca.assert_that_pv_alarm_is( "SANS2DVAC:{}:INTERVAL".format(axis_pair.name), self.ca.Alarms.NONE) def _get_axis_default_position(self, axis): if axis == "FRONTDETZ": new_position = 1000 elif axis == "FRONTBAFFLEZ": new_position = 3000 elif axis == "REARBAFFLEZ": new_position = 5000 elif axis == "REARDETZ": new_position = 7000 else: raise ValueError("invalid axis!") return new_position def _get_timeout_for_moving_to_position(self, moving_axis, new_position): distance_to_travel = abs(new_position - self.ca.get_pv_value(moving_axis)) time_to_accelerate_and_decelerate = 2 * TEST_ACCELERATION # between 0 and full speed, the average speed is half the full speed, same for when decelerating. # Therefore, the distance traveled when accelerating and decelerating is # 2 * (full_speed/2 * acceleration_time), so full_speed / acceleration_time time_at_full_speed = (distance_to_travel - TEST_SPEED * TEST_ACCELERATION) / TEST_SPEED total_time = ceil(time_to_accelerate_and_decelerate + time_at_full_speed) return total_time + 10 # +10 as a small tolerance to avoid instability def _assert_axis_position_reached(self, axis, position): timeout = self._get_timeout_for_moving_to_position(axis, position) self.ca.assert_that_pv_is_number(axis, position, tolerance=0.1, timeout=timeout)
class 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)
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)
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)
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))
class StabilityTests(unittest.TestCase): STOP_DATA_THREAD = threading.Event() def setUp(self): self.ca = ChannelAccess(20, device_prefix=DEVICE_PREFIX) self.STOP_DATA_THREAD.set() shared_setup(self.ca) @staticmethod def evaluate_current_instability(current_values): """ Evaluates the input current values against the stability criterion. Args: current_values: Array of input currents Returns: current_instability: Boolean array of len(current_values). True where element is unstable, else False """ current_instability = [ curr_measured >= (CURR_STEADY + CURR_LIMIT) for curr_measured in current_values ] return current_instability @staticmethod def evaluate_voltage_instability(voltage_values): """ Evaluates the input voltages against the stability criterion. Args: voltage_values: Array of input voltages Returns: voltage_instability: Boolean array of len(voltage_values). True where element is unstable, else False """ voltage_instability = [(VOLT_LOWERLIM >= volt_measured) or (volt_measured >= VOLT_UPPERLIM) for volt_measured in voltage_values] return voltage_instability def get_out_of_range_samples(self, current_values, voltage_values): """ Calculates the number of points which lie out of stability limits for a current and voltage dataset. Args: current_values: Array of input current values voltage_values: Array of input voltage values Returns: out_of_range_count: Integer, the number of samples in the dataset which are out of range """ current_instability = self.evaluate_current_instability(current_values) voltage_instability = self.evaluate_voltage_instability(voltage_values) overall_instability = [ curr or volt for curr, volt in zip(current_instability, voltage_instability) ] out_of_range_count = sum(overall_instability) return out_of_range_count def write_simulated_current(self, curr_data): """ Scales a current waveform to the DAQ voltage range and writes to current simulation PV Inputs: curr_data: list of floats containing the current data in engineering units """ scaled_data = [x / DAQ_CURR_READ_SCALE_FACTOR for x in curr_data] self.ca.set_pv_value("DAQ:CURR:WV:SIM", scaled_data, wait=True, sleep_after_set=0.0) def write_simulated_voltage(self, volt_data): """ Scales a voltage waveform to the DAQ voltage range and writes to voltage simulation PV Inputs: volt_data: list of floats containing the voltage data in engineering units """ scaled_data = [x * DAQ_VOLT_WRITE_SCALE_FACTOR for x in volt_data] self.ca.set_pv_value("DAQ:VOLT:WV:SIM", scaled_data, wait=True, sleep_after_set=0.0) @parameterized.expand([ ("steady_current_steady_voltage", [CURR_STEADY] * SAMPLE_LEN, [VOLT_STEADY] * SAMPLE_LEN), ("steady_current_unsteady_voltage", [CURR_STEADY] * SAMPLE_LEN, VOLTAGE_DATA), ("unsteady_current_steady_voltage", CURRENT_DATA, [VOLT_STEADY] * SAMPLE_LEN), ("unsteady_current_and_voltage", CURRENT_DATA, VOLTAGE_DATA) ]) def test_GIVEN_current_and_voltage_data_WHEN_limits_are_tested_THEN_number_of_samples_out_of_range_returned( self, _, curr_data, volt_data): self.write_simulated_current(curr_data) self.write_simulated_voltage(volt_data) averaged_volt_data = apply_average_filter(volt_data, stride=STRIDE_LENGTH) expected_out_of_range_samples = self.get_out_of_range_samples( curr_data, averaged_volt_data) self.ca.assert_that_pv_is_number("_STABILITYCHECK", expected_out_of_range_samples, tolerance=0.05 * expected_out_of_range_samples) def test_GIVEN_noisy_voltage_data_WHEN_moving_average_is_applied_THEN_averaged_data_has_fewer_out_of_range_points( self): curr_data = [CURR_STEADY] * SAMPLE_LEN self.write_simulated_current(curr_data) self.write_simulated_voltage(DAQ_DATA) out_of_range_samples_before_average = self.get_out_of_range_samples( curr_data, DAQ_DATA) out_of_range_samples_after_average = self.ca.get_pv_value( "_STABILITYCHECK") self.assertLess(out_of_range_samples_after_average, out_of_range_samples_before_average) @parameterized.expand([("unsteady_current", simulate_current_data(), [VOLT_STEADY] * SAMPLE_LEN), ("unsteady_voltage", [CURR_STEADY] * SAMPLE_LEN, simulate_voltage_data())]) def test_GIVEN_multiple_samples_in_one_second_WHEN_buffer_read_THEN_buffer_reads_all_out_of_range_samples( self, _, curr_data, volt_data): # Setting this to 3 as channel access takes ~0.25s. May need reducing for slow machines. writes_per_second = 3 # Change buffer length is 1 so UNSTABLETIME PV only reports 1 second of data length_of_buffer = 1 self.ca.set_pv_value("WINDOWSIZE", length_of_buffer) self.ca.set_pv_value("RESETWINDOW", 1) averaged_volt_data = apply_average_filter(volt_data, stride=STRIDE_LENGTH) expected_out_of_range_samples = self.get_out_of_range_samples( curr_data, averaged_volt_data) * writes_per_second self.STOP_DATA_THREAD.clear() scaled_curr_data = [x / DAQ_CURR_READ_SCALE_FACTOR for x in curr_data] scaled_volt_data = [x * DAQ_VOLT_WRITE_SCALE_FACTOR for x in volt_data] data_supply_thread = threading.Thread( target=stream_data, args=(self.ca, writes_per_second, scaled_curr_data, scaled_volt_data, self.STOP_DATA_THREAD)) # GIVEN data_supply_thread.start() self.assertGreater(writes_per_second, 1) # THEN self.ca.assert_that_pv_is_number("UNSTABLETIME", expected_out_of_range_samples * SAMPLETIME, timeout=60.0, tolerance=0.1 * SAMPLETIME) self.STOP_DATA_THREAD.set() def test_GIVEN_buffer_with_data_WHEN_resetwindow_PV_processed_THEN_buffer_is_cleared( self): number_of_writes = 50 averaged_volt_data = apply_average_filter(DAQ_DATA, stride=STRIDE_LENGTH) expected_out_of_range_samples = self.get_out_of_range_samples( CURRENT_DATA, averaged_volt_data) expected_out_of_range_samples *= number_of_writes * SAMPLETIME # GIVEN for i in range(number_of_writes): self.write_simulated_current(CURRENT_DATA) self.write_simulated_voltage(DAQ_DATA) self.ca.assert_that_pv_is_number("UNSTABLETIME", expected_out_of_range_samples) # WHEN self.ca.set_pv_value("RESETWINDOW", 1) # THEN self.ca.assert_that_pv_is_number("UNSTABLETIME", 0) def test_GIVEN_full_buffer_WHEN_more_data_added_to_buffer_THEN_oldest_values_overwritten( self): length_of_buffer = 10 testvalue = 50.0 self.assertNotEqual(testvalue, 1.0) self.ca.set_pv_value("WINDOWSIZE", length_of_buffer) self.ca.set_pv_value("RESETWINDOW", 1) self.ca.set_pv_value("_COUNTERTIMING.SCAN", "Passive") # GIVEN for i in range(length_of_buffer): self.ca.set_pv_value("_ADDCOUNTS", 1.0 / SAMPLETIME, wait=True, sleep_after_set=0.0) self.ca.set_pv_value("_COUNTERTIMING.PROC", 1, wait=True, sleep_after_set=0.0) self.ca.assert_that_pv_is_number("UNSTABLETIME", length_of_buffer) # WHEN self.ca.set_pv_value("_ADDCOUNTS", testvalue / SAMPLETIME, wait=True, sleep_after_set=0.0) self.ca.set_pv_value("_COUNTERTIMING.PROC", 1, wait=True, sleep_after_set=0.0) # THEN self.ca.assert_that_pv_is_number("UNSTABLETIME", (length_of_buffer - 1.) + testvalue) def test_GIVEN_input_data_over_several_seconds_WHEN_stability_PV_read_THEN_all_unstable_time_counted( self): # This number needs to be large enough to write over several seconds. Writing over multiple seconds is asserted. number_of_writes = 50 time1 = perf_counter() expected_out_of_range_samples = self.get_out_of_range_samples( CURRENT_DATA, VOLTAGE_DATA) * number_of_writes * SAMPLETIME # GIVEN for i in range(number_of_writes): self.write_simulated_current(CURRENT_DATA) self.write_simulated_voltage(VOLTAGE_DATA) processtime = perf_counter() - time1 self.assertGreater(processtime, 1.) # THEN self.ca.assert_that_pv_is_number("UNSTABLETIME", expected_out_of_range_samples, tolerance=0.025 * expected_out_of_range_samples) def test_GIVEN_power_supply_switched_on_WHEN_power_supply_out_of_stability_threshold_THEN_stability_PV_equals_zero_and_goes_into_alarm( self): # GIVEN self.ca.set_pv_value("VOLT:SP", 10) self.ca.assert_that_pv_is("POWER:STAT", "ON") # WHEN stability_threshold = self.ca.get_pv_value("THRESHOLD") testvalue = stability_threshold * 100 self.ca.set_pv_value("_ADDCOUNTS", testvalue / SAMPLETIME, wait=True, sleep_after_set=0.0) self.ca.assert_that_pv_is("STABILITY", "Unstable") # THEN self.ca.assert_that_pv_alarm_is("STABILITY", ChannelAccess.Alarms.MAJOR) def test_GIVEN_power_supply_switched_off_WHEN_power_supply_out_of_stability_threshold_THEN_no_alarms_raised( self): # GIVEN self.ca.set_pv_value("VOLT:SP", 0) self.ca.assert_that_pv_is("POWER:STAT", "OFF") # WHEN stability_threshold = self.ca.get_pv_value("THRESHOLD") testvalue = stability_threshold * 100 self.ca.set_pv_value("_ADDCOUNTS", testvalue / SAMPLETIME, wait=True, sleep_after_set=0.0) self.ca.assert_that_pv_is("STABILITY", "Unstable") # THEN self.ca.assert_that_pv_alarm_is("STABILITY", ChannelAccess.Alarms.NONE) def test_GIVEN_separator_stable_THEN_separator_stability_text_label_is_stable( self): # GIVEN # asserted in setup that separator is stable # THEN self.ca.assert_that_pv_is("STABILITY", "Stable")
class 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)
class JawsManagerBase(object): """ Base classes for all jaws manager tests. """ def setUp(self): self._ioc = IOCRegister.get_running("GALIL_01") self.ca = ChannelAccess() self.ca.assert_that_pv_exists("MOT:MTR0101", timeout=30) for jaw in range(1, self.get_num_of_jaws() + 1): self.ca.assert_that_pv_exists(UNDERLYING_GAP_SP.format(jaw, "V"), timeout=30) self.ca.assert_that_pv_exists(UNDERLYING_GAP_SP.format(jaw, "H"), timeout=30) self.ca.assert_that_pv_exists(self.get_sample_pv() + ":{}GAP:SP".format("V"), timeout=30) def get_sample_pv(self): return "JAWMAN:SAMPLE" @abc.abstractmethod def get_num_of_jaws(self): pass def _test_WHEN_centre_is_changed_THEN_centres_of_all_jaws_follow_and_gaps_unchanged( self, direction): expected_gaps = [ self.ca.get_pv_value(UNDERLYING_GAP_SP.format(jaw, direction)) for jaw in range(1, self.get_num_of_jaws() + 1) ] self.ca.set_pv_value( self.get_sample_pv() + ":{}CENT:SP".format(direction), 10) for jaw in range(1, self.get_num_of_jaws() + 1): self.ca.assert_that_pv_is_number( UNDERLYING_CENT_SP.format(jaw, direction), 10, 0.1) self.ca.assert_that_pv_is_number( UNDERLYING_GAP_SP.format(jaw, direction), expected_gaps[jaw - 1], 0.1) def _test_WHEN_sizes_at_moderator_and_sample_changed_THEN_centres_of_all_jaws_unchanged( self, direction): # Set up jaws initially to have "custom" centre. centre = 12.34 for jaw in range(1, self.get_num_of_jaws() + 1): # Set to centre * jaw so that each jaw is given a different centre self.ca.set_pv_value(UNDERLYING_CENT_SP.format(jaw, direction), centre * jaw) # Now change size at sample + moderator self.ca.set_pv_value( "{}:{}GAP:SP".format(self.get_sample_pv(), direction), 11.111) self.ca.set_pv_value(MOD_GAP.format(direction), 22.222) # Assert that centres are unchanged for jaw in range(1, self.get_num_of_jaws() + 1): self.ca.assert_that_pv_is_number( UNDERLYING_CENT_SP.format(jaw, direction), centre * jaw, 0.001) def _test_WHEN_sample_gap_set_THEN_other_jaws_as_expected( self, direction, sample_gap, expected): self.ca.set_pv_value( self.get_sample_pv() + ":{}GAP:SP".format(direction), sample_gap) for i, exp in enumerate(expected): self.ca.assert_that_pv_is_number(UNDERLYING_GAP_SP.format( i + 1, direction), exp, 0.1, timeout=1)
class 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)