class TestProcess(unittest.TestCase): def setUp(self): self.o = Process("proc") self.o.start() def tearDown(self): self.o.stop(timeout=1) def test_init(self): assert self.o.name == "proc" def test_add_controller(self): controller = MagicMock(mri="mri") self.o.add_controller(controller) assert self.o.get_controller("mri") == controller def test_init_controller(self): class InitController(Controller): init = False def on_hook(self, hook): if isinstance(hook, ProcessStartHook): self.init = True c = InitController("mri") self.o.add_controller(c) assert c.init == True def test_publish_controller(self): class PublishController(Controller): published = [] def on_hook(self, hook): if isinstance(hook, ProcessPublishHook): hook(self.do_publish) @add_call_types def do_publish(self, published): # type: (APublished) -> None self.published = published class UnpublishableController(Controller): def on_hook(self, hook): if isinstance(hook, ProcessStartHook): hook(self.on_start) def on_start(self): return UnpublishedInfo(self.mri) c = PublishController("mri") self.o.add_controller(c) assert c.published == ["mri"] self.o.add_controller(Controller(mri="mri2")) assert c.published == ["mri", "mri2"] self.o.add_controller(UnpublishableController("mri3")) assert c.published == ["mri", "mri2"]
class TestProcess(unittest.TestCase): def setUp(self): self.o = Process("proc") self.o.start() def tearDown(self): self.o.stop(timeout=1) def test_init(self): assert self.o.name == "proc" def test_add_controller(self): controller = MagicMock(mri="mri") self.o.add_controller(controller) assert self.o.get_controller("mri") == controller def test_init_controller(self): class InitController(Controller): init = False def on_hook(self, hook): if isinstance(hook, ProcessStartHook): self.init = True c = InitController("mri") self.o.add_controller(c) assert c.init is True def test_publish_controller(self): c = PublishController("mri") self.o.add_controller(c) assert c.published == ["mri"] self.o.add_controller(Controller(mri="mri2")) assert c.published == ["mri", "mri2"] self.o.add_controller(UnpublishableController("mri3")) assert c.published == ["mri", "mri2"]
class TestKinematicsSavuPart(ChildTestCase): def setUp(self): self.process = Process("Process") self.context = Context(self.process) # Create a fake PandA self.panda = ManagerController("PANDA", "/tmp", use_git=False) self.busses = PositionsPart("busses") self.panda.add_part(self.busses) self.process.add_controller(self.panda) # TODO: Add axes to the positions table # And the PMAC pmac_block = make_block_creator( os.path.join(os.path.dirname(__file__), "..", "test_pmac", "blah"), "test_pmac_manager_block.yaml", ) self.config_dir = tmp_dir("config_dir") self.pmac = self.create_child_block( pmac_block, self.process, mri_prefix="PMAC", config_dir=self.config_dir.value, ) # These are the motors we are interested in self.child_x = self.process.get_controller("BL45P-ML-STAGE-01:X") self.child_y = self.process.get_controller("BL45P-ML-STAGE-01:Y") # Set up port and kinematics on CS1 self.child_cs1 = self.process.get_controller("PMAC:CS1") self.set_attributes(self.child_cs1, port="CS1") self.set_attributes(self.child_cs1, qVariables="Q22=12345 Q23=999") self.set_attributes(self.child_cs1, forwardKinematic="Q1=P1+10 Q5=Q22+4 Q7=8+4 RET ") # Set up variables on CS1 self.child_status = self.process.get_controller("PMAC:STATUS") self.set_attributes(self.child_status, iVariables="I12=3") self.set_attributes(self.child_status, pVariables="") self.set_attributes(self.child_status, mVariables="M45=1 M42=561") # Make the child block holding panda and pmac mri self.child = self.create_child_block( panda_kinematicssavu_block, self.process, mri="SCAN:KINSAV", panda="PANDA", pmac="PMAC", ) self.o = KinematicsSavuPart(name="kinsav", mri="SCAN:KINSAV", cs_port="CS1", cs_mri_suffix="CS1") self.context.set_notify_dispatch_request( self.o.notify_dispatch_request) self.process.start() self.set_attributes(self.pmac) self.completed_steps = 0 # goal for these is 3000, 2000, True cols, rows, alternate = 3000, 2000, False self.steps_to_do = cols * rows xs = LineGenerator("x", "mm", 0.0, 0.1, cols, alternate=alternate) ys = LineGenerator("y", "mm", 0.0, 0.1, rows) self.generator = CompoundGenerator([ys, xs], [], [], 0.1) self.generator.prepare() def tearDown(self): self.process.stop(timeout=1) shutil.rmtree(self.config_dir.value) def set_motor_attributes( self, x_pos=0.5, y_pos=0.0, units="mm", x_acceleration=2.5, y_acceleration=2.5, x_velocity=1.0, y_velocity=1.0, ): # create some parts to mock the motion controller and 2 axes in a CS self.set_attributes( self.child_x, cs="CS1,A", accelerationTime=x_velocity / x_acceleration, resolution=0.001, offset=0.0, maxVelocity=x_velocity, readback=x_pos, velocitySettle=0.0, units=units, axisNumber=1, ) self.set_attributes( self.child_y, cs="CS1,X", accelerationTime=y_velocity / y_acceleration, resolution=0.001, offset=0.0, maxVelocity=y_velocity, readback=y_pos, velocitySettle=0.0, units=units, axisNumber=23, ) def test_file_creation(self): tmp_dir = mkdtemp() + os.path.sep data_name = "p00-1234" file_template = data_name + "-%s.h5" self.set_motor_attributes(0.5, 0.0, "mm") xs = LineGenerator("x", "mm", 0.0, 0.4, 5, alternate=False) ys = LineGenerator("y", "mm", 1.2, 1.6, 5) generator = CompoundGenerator([ys, xs], [], [], 1.0) generator.prepare() part_info = dict(HDF=[ DatasetProducedInfo( "y.data", "kinematics_PANDABOX2.h5", DatasetType.POSITION_VALUE, 2, "/entry/NDAttributes/INENC1.VAL", "/p/uid", ), DatasetProducedInfo( "x.data", "kinematics_PANDABOX.h5", DatasetType.POSITION_VALUE, 0, "/entry/NDAttributes/INENC1.VAL", "/p/uid", ), DatasetProducedInfo( "y.max", "kinematics_PANDABOX2.h5", DatasetType.POSITION_MAX, 0, "/entry/NDAttributes/INENC1_MAX.VAL", "/p/uid", ), DatasetProducedInfo( "y.min", "kinematics_PANDABOX2.h5", DatasetType.POSITION_MIN, 0, "/entry/NDAttributes/INENC1_MIN.VAL", "/p/uid", ), DatasetProducedInfo( "x.max", "kinematics_PANDABOX.h5", DatasetType.POSITION_MAX, 0, "/entry/NDAttributes/INENC1_MAX.VAL", "/p/uid", ), DatasetProducedInfo( "x.min", "kinematics_PANDABOX.h5", DatasetType.POSITION_MIN, 0, "/entry/NDAttributes/INENC1_MIN.VAL", "/p/uid", ), DatasetProducedInfo("det.min", "fn1", DatasetType.SECONDARY, 0, "/p/s2", "/p/uid"), ]) kin_infos = [ DatasetProducedInfo( "y.mean", "p00-1234-kinematics-vds.nxs", DatasetType.POSITION_VALUE, 2, "/entry/y.mean", "", ), DatasetProducedInfo( "y.max", "p00-1234-kinematics-vds.nxs", DatasetType.POSITION_MAX, 2, "/entry/y.max", "", ), DatasetProducedInfo( "y.min", "p00-1234-kinematics-vds.nxs", DatasetType.POSITION_MIN, 2, "/entry/y.min", "", ), DatasetProducedInfo( "x.mean", "p00-1234-kinematics-vds.nxs", DatasetType.POSITION_VALUE, 2, "/entry/x.mean", "", ), DatasetProducedInfo( "x.max", "p00-1234-kinematics-vds.nxs", DatasetType.POSITION_MAX, 2, "/entry/x.max", "", ), DatasetProducedInfo( "x.min", "p00-1234-kinematics-vds.nxs", DatasetType.POSITION_MIN, 2, "/entry/x.min", "", ), ] infos = self.o.on_configure( self.context, fileDir=tmp_dir, generator=generator, axesToMove=AXES, fileTemplate=file_template, part_info=part_info, ) self.assertEqual(len(infos), len(kin_infos)) for info in kin_infos: assert str(info) in [str(x) for x in infos] self.o.on_post_configure(self.context, part_info) # Check Savu file has been created and contains the correct entries savu_path = os.path.join(tmp_dir, data_name + "-savu.nxs") savu_file = h5py.File(savu_path, "r") # Check the forward kinematics program has been written useminmax_dataset = savu_file["/entry/inputs/use_minmax"] self.assertEqual(useminmax_dataset[0], True) # Check the forward kinematics program has been written program_dataset = savu_file["/entry/inputs/program"] self.assertEqual(program_dataset.shape, (1, )) self.assertEqual(program_dataset[0], "Q1=P1+10 Q5=Q22+4 Q7=8+4 RET ") # Check the Q and I program variables have been written variables_dataset = savu_file["/entry/inputs/variables"] self.assertEqual(variables_dataset.shape, (5, )) found = False for dataset in variables_dataset: if dataset[0] == "Q22": found = True self.assertEqual(dataset[1], 12345) self.assertTrue(found) # Check the p1 datasets have been written # Create raw data file first that the file will link to raw_path = os.path.join(tmp_dir, "kinematics_PANDABOX.h5") raw = h5py.File(raw_path, "w", libver="latest") raw.require_group("/entry/NDAttributes/") fmnd_mean = np.zeros((5, 5, 1, 1)) fmnd_mean[0][0][0][0] = 61616 fmnd_mean[1][2][0][0] = 10101 raw.create_dataset("/entry/NDAttributes/INENC1.VAL", data=fmnd_mean) fmnd_max = np.zeros((5, 5, 1, 1)) fmnd_max[0][0][0][0] = 27 fmnd_max[3][4][0][0] = 18 raw.create_dataset("/entry/NDAttributes/INENC1_MAX.VAL", data=fmnd_max) fmnd_max = np.zeros((5, 5, 1, 1)) fmnd_max[0][0][0][0] = 54 fmnd_max[3][4][0][0] = 76 raw.create_dataset("/entry/NDAttributes/INENC1_MIN.VAL", data=fmnd_max) # Create raw data file first that the file will link to raw_path2 = os.path.join(tmp_dir, "kinematics_PANDABOX2.h5") raw2 = h5py.File(raw_path2, "w", libver="latest") raw2.require_group("/entry/NDAttributes/") fmnd_mean = np.zeros((5, 5, 1, 1)) fmnd_mean[0][0][0][0] = 12345 fmnd_mean[1][2][0][0] = 54321 raw2.create_dataset("/entry/NDAttributes/INENC1.VAL", data=fmnd_mean) fmnd_max = np.zeros((5, 5, 1, 1)) fmnd_max[0][0][0][0] = 99 fmnd_max[3][4][0][0] = 88 raw2.create_dataset("/entry/NDAttributes/INENC1_MAX.VAL", data=fmnd_max) fmnd_max = np.zeros((5, 5, 1, 1)) fmnd_max[0][0][0][0] = 76 fmnd_max[3][4][0][0] = 44 raw2.create_dataset("/entry/NDAttributes/INENC1_MIN.VAL", data=fmnd_max) raw.swmr_mode = True raw2.swmr_mode = True # Check p1 mean and max datasets are there p1mean_dataset = savu_file["/entry/inputs/p1mean"] self.assertEqual(p1mean_dataset.shape, (5, 5, 1, 1)) self.assertEqual(p1mean_dataset[0][0][0][0], 61616) self.assertEqual(p1mean_dataset[1][2][0][0], 10101) p1max_dataset = savu_file["/entry/inputs/p1max"] self.assertEqual(p1max_dataset.shape, (5, 5, 1, 1)) self.assertEqual(p1max_dataset[0][0][0][0], 27) self.assertEqual(p1max_dataset[3][4][0][0], 18) savu_file.close() # Check the final vds file has been created # First create a fake Savu output file that the vds will link to raw_savu_path = os.path.join(tmp_dir, data_name + "-savuproc") os.mkdir(raw_savu_path) raw_savu_path = os.path.join(raw_savu_path, data_name + "-savu_processed.nxs") raw_savu = h5py.File(raw_savu_path, "w") raw_savu.require_group("/entry/final_result_qmean/") savu_proc = np.ones((9, 5, 5)) savu_proc[0][0][0] = 555 savu_proc[0][2][1] = 666 raw_savu.create_dataset("/entry/final_result_qmean/data", data=savu_proc) raw_savu.close() # Check xmean is there vds_path = os.path.join(tmp_dir, data_name + "-kinematics-vds.nxs") vds_file = h5py.File(vds_path, "r") xmean = vds_file["/entry/x.mean"] self.assertEqual(xmean.shape, (5, 5)) self.assertEqual(xmean[0][0], 555) self.assertEqual(xmean[2][1], 666) vds_file.close() raw.close() raw2.close() rmtree(tmp_dir)
class TestPandaSeqTriggerPart(ChildTestCase): def setUp(self): self.process = Process("Process") self.context = Context(self.process) # Create a fake PandA self.panda = ManagerController("PANDA", "/tmp", use_git=False) self.busses = PositionsPart("busses") self.panda.add_part(self.busses) # Make 2 sequencers we can prod self.seq_parts = {} for i in (1, 2): controller = BasicController("PANDA:SEQ%d" % i) self.seq_parts[i] = SequencerPart("part") controller.add_part(self.seq_parts[i]) self.process.add_controller(controller) self.panda.add_part( ChildPart( "SEQ%d" % i, "PANDA:SEQ%d" % i, initial_visibility=True, stateful=False, )) self.child_seq1 = self.process.get_controller("PANDA:SEQ1") self.child_seq2 = self.process.get_controller("PANDA:SEQ2") # And an srgate controller = BasicController("PANDA:SRGATE1") self.gate_part = GatePart("part") controller.add_part(self.gate_part) self.process.add_controller(controller) self.panda.add_part( ChildPart("SRGATE1", "PANDA:SRGATE1", initial_visibility=True, stateful=False)) self.process.add_controller(self.panda) # And the PMAC pmac_block = make_block_creator( os.path.join(os.path.dirname(__file__), "..", "test_pmac", "blah"), "test_pmac_manager_block.yaml", ) self.config_dir = tmp_dir("config_dir") self.pmac = self.create_child_block( pmac_block, self.process, mri_prefix="PMAC", config_dir=self.config_dir.value, ) # These are the motors we are interested in self.child_x = self.process.get_controller("BL45P-ML-STAGE-01:X") self.child_y = self.process.get_controller("BL45P-ML-STAGE-01:Y") self.child_cs1 = self.process.get_controller("PMAC:CS1") # CS1 needs to have the right port otherwise we will error self.set_attributes(self.child_cs1, port="CS1") # Make the child block holding panda and pmac mri self.child = self.create_child_block( panda_seq_trigger_block, self.process, mri="SCAN:PCOMP", panda="PANDA", pmac="PMAC", ) # And our part under test self.o = PandASeqTriggerPart("pcomp", "SCAN:PCOMP") # Now start the process off and tell the panda which sequencer tables # to use self.process.start() exports = ExportTable.from_rows([ ("SEQ1.table", "seqTableA"), ("SEQ2.table", "seqTableB"), ("SRGATE1.forceSet", "seqSetEnable"), ("SRGATE1.forceReset", "seqReset"), ]) self.panda.set_exports(exports) def tearDown(self): self.process.stop(timeout=2) shutil.rmtree(self.config_dir.value) def set_motor_attributes( self, x_pos=0.5, y_pos=0.0, units="mm", x_acceleration=2.5, y_acceleration=2.5, x_velocity=1.0, y_velocity=1.0, ): # create some parts to mock the motion controller and 2 axes in a CS self.set_attributes( self.child_x, cs="CS1,A", accelerationTime=x_velocity / x_acceleration, resolution=0.001, offset=0.0, maxVelocity=x_velocity, readback=x_pos, velocitySettle=0.0, units=units, ) self.set_attributes( self.child_y, cs="CS1,B", accelerationTime=y_velocity / y_acceleration, resolution=0.001, offset=0.0, maxVelocity=y_velocity, readback=y_pos, velocitySettle=0.0, units=units, ) # Patch the super setup() method so we only get desired calls @patch("malcolm.modules.builtin.parts.ChildPart.setup") def test_setup(self, mocked_super_setup): mock_registrar = Mock(name="mock_registrar") call_list = [ call(ReportStatusHook, self.o.on_report_status), call((ConfigureHook, SeekHook), self.o.on_configure), call(RunHook, self.o.on_run), call(ResetHook, self.o.on_reset), call((AbortHook, PauseHook), self.o.on_abort), call(PostRunArmedHook, self.o.post_inner_scan), ] self.o.setup(mock_registrar) mocked_super_setup.assert_called_once_with(mock_registrar) mock_registrar.hook.assert_has_calls(call_list, any_order=True) @patch( "malcolm.modules.ADPandABlocks.parts.pandaseqtriggerpart.DoubleBuffer", autospec=True, ) def test_configure_and_run_prepare_components(self, buffer_class): buffer_instance = buffer_class.return_value buffer_instance.run.return_value = [] xs = LineGenerator("x", "mm", 0.0, 0.3, 4, alternate=True) ys = LineGenerator("y", "mm", 0.0, 0.1, 2) generator = CompoundGenerator([ys, xs], [], [], 1.0) generator.prepare() completed_steps = 0 steps_to_do = generator.size self.set_motor_attributes() axes_to_move = ["x", "y"] self.o.on_configure(self.context, completed_steps, steps_to_do, {}, generator, axes_to_move) assert self.o.generator is generator assert self.o.loaded_up_to == completed_steps assert self.o.scan_up_to == completed_steps + steps_to_do # Other unit tests check that the sequencer rows used here are correct buffer_instance.configure.assert_called_once() self.gate_part.enable_set.assert_not_called() buffer_instance.run.assert_not_called() # The SRGate should only be enabled by on_pre_run() here. self.o.on_pre_run(self.context) self.o.on_run(self.context) self.gate_part.enable_set.assert_called_once() buffer_instance.run.assert_called_once() @patch( "malcolm.modules.ADPandABlocks.parts.pandaseqtriggerpart.DoubleBuffer", autospec=True, ) def test_configure_and_run_prepare_no_axes(self, buffer_class): buffer_instance = buffer_class.return_value buffer_instance.run.return_value = [] generator = CompoundGenerator([StaticPointGenerator(size=1)], [], [], 1.0) generator.prepare() completed_steps = 0 steps_to_do = generator.size self.o.on_configure(self.context, completed_steps, steps_to_do, {}, generator, "") assert self.o.generator is generator assert self.o.loaded_up_to == completed_steps assert self.o.scan_up_to == completed_steps + steps_to_do buffer_instance.configure.assert_called_once() self.gate_part.enable_set.assert_not_called() buffer_instance.run.assert_not_called() # The SRGate should only be enabled by on_run() here. self.o.on_pre_run(self.context) self.o.on_run(self.context) self.gate_part.enable_set.assert_called_once() buffer_instance.run.assert_called_once() @patch( "malcolm.modules.ADPandABlocks.parts.pandaseqtriggerpart.PandASeqTriggerPart" ".on_abort", autospec=True, ) def test_reset_triggers_abort(self, abort_method): abort_method.assert_not_called() self.o.on_reset(self.context) abort_method.assert_called_once() @patch( "malcolm.modules.ADPandABlocks.parts.pandaseqtriggerpart.DoubleBuffer.clean_up", autospec=True, ) def test_abort_cleans_up_correctly(self, db_clean_up_method): xs = LineGenerator("x", "mm", 0.0, 0.3, 4, alternate=True) ys = LineGenerator("y", "mm", 0.0, 0.1, 2) generator = CompoundGenerator([ys, xs], [], [], 1.0) generator.prepare() completed_steps = 0 steps_to_do = generator.size self.set_motor_attributes() axes_to_move = ["x", "y"] db_clean_up_method.assert_not_called() self.o.on_configure(self.context, completed_steps, steps_to_do, {}, generator, axes_to_move) db_clean_up_method.assert_called_once() self.gate_part.seq_reset.assert_not_called() self.o.on_abort(self.context) self.gate_part.seq_reset.assert_called_once() assert 2 == db_clean_up_method.call_count def test_abort_does_not_throw_exception_if_no_seq_reset_exported(self): original_exports = self.panda.exports.value.rows() exports = ExportTable.from_rows( [row for row in original_exports if row[1] != "seqReset"]) self.panda.set_exports(exports) xs = LineGenerator("x", "mm", 0.0, 0.3, 4, alternate=True) ys = LineGenerator("y", "mm", 0.0, 0.1, 2) generator = CompoundGenerator([ys, xs], [], [], 1.0) generator.prepare() completed_steps = 0 steps_to_do = generator.size self.set_motor_attributes() axes_to_move = ["x", "y"] self.o.on_configure(self.context, completed_steps, steps_to_do, {}, generator, axes_to_move) self.o.on_abort(self.context) @patch( "malcolm.modules.ADPandABlocks.parts.pandaseqtriggerpart.DoubleBuffer", autospec=True, ) def get_sequencer_rows(self, generator, axes_to_move, buffer_class, steps=None): """Helper method for comparing table values.""" buffer_instance = buffer_class.return_value generator.prepare() completed_steps = 0 steps_to_do = steps if steps is not None else generator.size self.o.on_configure(self.context, completed_steps, steps_to_do, {}, generator, axes_to_move) rows_gen = buffer_instance.configure.call_args[0][0] rows = SequencerRows() for rs in rows_gen: rows.extend(rs) return rows def test_configure_continuous(self): xs = LineGenerator("x", "mm", 0.0, 0.3, 4, alternate=True) ys = LineGenerator("y", "mm", 0.0, 0.1, 2) generator = CompoundGenerator([ys, xs], [], [], 1.0) self.set_motor_attributes() axes_to_move = ["x", "y"] seq_rows = self.get_sequencer_rows(generator, axes_to_move) # Triggers GT = Trigger.POSA_GT IT = Trigger.IMMEDIATE LT = Trigger.POSA_LT # Half a frame hf = 62500000 # Half how long to be blind for hb = 22500000 expected = SequencerRows() expected.add_seq_entry(count=1, trigger=LT, position=50, half_duration=hf, live=1, dead=0) expected.add_seq_entry(3, IT, 0, hf, 1, 0) expected.add_seq_entry(1, IT, 0, hb, 0, 1) expected.add_seq_entry(1, GT, -350, hf, 1, 0) expected.add_seq_entry(3, IT, 0, hf, 1, 0) expected.add_seq_entry(1, IT, 0, MIN_PULSE, 0, 1) expected.add_seq_entry(0, IT, 0, MIN_PULSE, 0, 0) assert seq_rows.as_tuples() == expected.as_tuples() def test_configure_motion_controller_trigger(self): xs = LineGenerator("x", "mm", 0.0, 0.3, 4, alternate=True) ys = LineGenerator("y", "mm", 0.0, 0.1, 2) generator = CompoundGenerator([ys, xs], [], [], 1.0) self.set_motor_attributes() self.set_attributes(self.child, rowTrigger="Motion Controller") self.set_attributes(self.child_seq1, bita="TTLIN1.VAL") self.set_attributes(self.child_seq2, bita="TTLIN1.VAL") axes_to_move = ["x", "y"] seq_rows = self.get_sequencer_rows(generator, axes_to_move) # Triggers B0 = Trigger.BITA_0 B1 = Trigger.BITA_1 IT = Trigger.IMMEDIATE # Half a frame hf = 62500000 expected = SequencerRows() expected.add_seq_entry(count=1, trigger=B1, position=0, half_duration=hf, live=1, dead=0) expected.add_seq_entry(3, IT, 0, hf, 1, 0) expected.add_seq_entry(1, B0, 0, MIN_PULSE, 0, 1) expected.add_seq_entry(1, B1, 0, hf, 1, 0) expected.add_seq_entry(3, IT, 0, hf, 1, 0) expected.add_seq_entry(1, IT, 0, MIN_PULSE, 0, 1) expected.add_seq_entry(0, IT, 0, MIN_PULSE, 0, 0) assert seq_rows.as_tuples() == expected.as_tuples() # AssertionError is thrown as inputs are not are not set for SEQ bitA. def test_configure_assert_stepped(self): xs = LineGenerator("x", "mm", 0.0, 0.3, 4, alternate=True) ys = LineGenerator("y", "mm", 0.0, 0.1, 2) generator = CompoundGenerator([ys, xs], [], [], 1.0, continuous=False) generator.prepare() completed_steps = 0 steps_to_do = generator.size self.set_motor_attributes() self.set_attributes(self.child, rowTrigger="Motion Controller") axes_to_move = ["x", "y"] with self.assertRaises(AssertionError): self.o.on_configure(self.context, completed_steps, steps_to_do, {}, generator, axes_to_move) def test_configure_stepped(self): xs = LineGenerator("x", "mm", 0.0, 0.3, 4) ys = LineGenerator("y", "mm", 0.0, 0.2, 3) generator = CompoundGenerator([ys, xs], [], [], 1.0, continuous=False) generator.prepare() self.set_motor_attributes() self.set_attributes(self.child, rowTrigger="Motion Controller") self.set_attributes(self.child_seq1, bita="TTLIN1.VAL") self.set_attributes(self.child_seq2, bita="TTLIN1.VAL") axes_to_move = ["x", "y"] seq_rows = self.get_sequencer_rows(generator, axes_to_move) # Triggers B0 = Trigger.BITA_0 B1 = Trigger.BITA_1 IT = Trigger.IMMEDIATE # Half a frame hf = 62500000 expected = SequencerRows() for i in range(11): expected.add_seq_entry(1, B1, 0, hf, 1, 0) expected.add_seq_entry(1, B0, 0, MIN_PULSE, 0, 1) expected.add_seq_entry(1, B1, 0, hf, 1, 0) expected.add_seq_entry(1, IT, 0, MIN_PULSE, 0, 1) expected.add_seq_entry(0, IT, 0, MIN_PULSE, 0, 0) assert seq_rows.as_tuples() == expected.as_tuples() def test_acquire_scan(self): generator = CompoundGenerator([StaticPointGenerator(size=5)], [], [], 1.0) generator.prepare() seq_rows = self.get_sequencer_rows(generator, []) # Triggers IT = Trigger.IMMEDIATE # Half a frame hf = 62500000 expected = SequencerRows() expected.add_seq_entry(count=5, trigger=IT, position=0, half_duration=hf, live=1, dead=0) expected.add_seq_entry(1, IT, 0, MIN_PULSE, 0, 1) expected.add_seq_entry(0, IT, 0, MIN_PULSE, 0, 0) assert seq_rows.as_tuples() == expected.as_tuples() def test_configure_pcomp_row_trigger_with_single_point_rows(self): x_steps, y_steps = 1, 5 xs = LineGenerator("x", "mm", 0.0, 0.5, x_steps, alternate=True) ys = LineGenerator("y", "mm", 0.0, 4, y_steps) generator = CompoundGenerator([ys, xs], [], [], 1.0) self.set_motor_attributes() axes_to_move = ["x", "y"] seq_rows = self.get_sequencer_rows(generator, axes_to_move) # Triggers GT = Trigger.POSA_GT LT = Trigger.POSA_LT IT = Trigger.IMMEDIATE # Half a frame hf = 62500000 # Half blind hb = 75000000 expected = SequencerRows() expected.add_seq_entry(count=1, trigger=LT, position=0, half_duration=hf, live=1, dead=0) expected.add_seq_entry(1, IT, 0, hb, 0, 1) expected.add_seq_entry(1, GT, -500, hf, 1, 0) expected.add_seq_entry(1, IT, 0, hb, 0, 1) expected.add_seq_entry(1, LT, 0, hf, 1, 0) expected.add_seq_entry(1, IT, 0, hb, 0, 1) expected.add_seq_entry(1, GT, -500, hf, 1, 0) expected.add_seq_entry(1, IT, 0, hb, 0, 1) expected.add_seq_entry(1, LT, 0, hf, 1, 0) expected.add_seq_entry(1, IT, 0, MIN_PULSE, 0, 1) expected.add_seq_entry(0, IT, 0, MIN_PULSE, 0, 0) assert seq_rows.as_tuples() == expected.as_tuples() def test_configure_with_delay_after(self): # a test to show that delay_after inserts a "loop_back" turnaround delay = 1.0 x_steps, y_steps = 3, 2 xs = LineGenerator("x", "mm", 0.0, 0.5, x_steps, alternate=True) ys = LineGenerator("y", "mm", 0.0, 0.1, y_steps) generator = CompoundGenerator([ys, xs], [], [], 1.0, delay_after=delay) self.set_motor_attributes() axes_to_move = ["x", "y"] seq_rows = self.get_sequencer_rows(generator, axes_to_move) # Triggers GT = Trigger.POSA_GT IT = Trigger.IMMEDIATE LT = Trigger.POSA_LT # Half a frame hf = 62500000 # Half how long to be blind for a single point hfb = 55625000 # Half how long to be blind for end of row hrb = 56500000 expected = SequencerRows() expected.add_seq_entry(count=1, trigger=LT, position=125, half_duration=hf, live=1, dead=0) expected.add_seq_entry(1, IT, 0, hfb, 0, 1) expected.add_seq_entry(1, LT, -125, hf, 1, 0) expected.add_seq_entry(1, IT, 0, hfb, 0, 1) expected.add_seq_entry(1, LT, -375, hf, 1, 0) expected.add_seq_entry(1, IT, 0, hrb, 0, 1) expected.add_seq_entry(1, GT, -625, hf, 1, 0) expected.add_seq_entry(1, IT, 0, hfb, 0, 1) expected.add_seq_entry(1, GT, -375, hf, 1, 0) expected.add_seq_entry(1, IT, 0, hfb, 0, 1) expected.add_seq_entry(1, GT, -125, hf, 1, 0) expected.add_seq_entry(1, IT, 0, MIN_PULSE, 0, 1) expected.add_seq_entry(0, IT, 0, MIN_PULSE, 0, 0) assert seq_rows.as_tuples() == expected.as_tuples() def test_configure_with_zero_points(self): xs = LineGenerator("x", "mm", 0.0, 0.3, 4, alternate=True) ys = LineGenerator("y", "mm", 0.0, 0.1, 2) generator = CompoundGenerator([ys, xs], [], [], 1.0) self.set_motor_attributes() axes_to_move = ["x", "y"] seq_rows = self.get_sequencer_rows(generator, axes_to_move, steps=0) # Triggers IT = Trigger.IMMEDIATE expected = SequencerRows() expected.add_seq_entry(1, IT, 0, MIN_PULSE, 0, 1) expected.add_seq_entry(0, IT, 0, MIN_PULSE, 0, 0) assert seq_rows.as_tuples() == expected.as_tuples() def test_configure_with_one_point(self): xs = LineGenerator("x", "mm", 0.0, 0.3, 4, alternate=True) ys = LineGenerator("y", "mm", 0.0, 0.1, 2) generator = CompoundGenerator([ys, xs], [], [], 1.0) self.set_motor_attributes() axes_to_move = ["x", "y"] seq_rows = self.get_sequencer_rows(generator, axes_to_move, steps=1) # Triggers IT = Trigger.IMMEDIATE LT = Trigger.POSA_LT # Half a frame hf = 62500000 expected = SequencerRows() expected.add_seq_entry(count=1, trigger=LT, position=50, half_duration=hf, live=1, dead=0) expected.add_seq_entry(1, IT, 0, MIN_PULSE, 0, 1) expected.add_seq_entry(0, IT, 0, MIN_PULSE, 0, 0) assert seq_rows.as_tuples() == expected.as_tuples() def test_configure_long_pcomp_row_trigger(self): # Skip on GitHub Actions and GitLab CI if "CI" in os.environ: pytest.skip("performance test only") self.set_motor_attributes( 0, 0, "mm", x_velocity=300, y_velocity=300, x_acceleration=30, y_acceleration=30, ) x_steps, y_steps = 4000, 1000 xs = LineGenerator("x", "mm", 0.0, 10, x_steps, alternate=True) ys = LineGenerator("y", "mm", 0.0, 8, y_steps) generator = CompoundGenerator([ys, xs], [], [], 0.005) generator.prepare() completed_steps = 0 steps_to_do = x_steps * y_steps self.set_motor_attributes() axes_to_move = ["x", "y"] start = datetime.now() self.o.on_configure(self.context, completed_steps, steps_to_do, {}, generator, axes_to_move) elapsed = datetime.now() - start assert elapsed.total_seconds() < 3.0 def test_on_report_status_doing_pcomp(self): mock_context = MagicMock(name="context_mock") mock_child = MagicMock(name="child_mock") mock_child.rowTrigger.value = "Position Compare" mock_context.block_view.return_value = mock_child info = self.o.on_report_status(mock_context) assert info.trigger == MotionTrigger.NONE def test_on_report_status_not_doing_pcomp_is_row_gate(self): mock_context = MagicMock(name="context_mock") mock_child = MagicMock(name="child_mock") mock_child.rowTrigger.value = "Motion Controller" mock_context.block_view.return_value = mock_child info = self.o.on_report_status(mock_context) assert info.trigger == MotionTrigger.ROW_GATE
class TestPMACChildPart(ChildTestCase): def setUp(self): self.process = Process("Process") self.context = Context(self.process) pmac_block = make_block_creator(__file__, "test_pmac_manager_block.yaml") self.child = self.create_child_block(pmac_block, self.process, mri_prefix="PMAC", config_dir="/tmp") # These are the child blocks we are interested in self.child_x = self.process.get_controller("BL45P-ML-STAGE-01:X") self.child_y = self.process.get_controller("BL45P-ML-STAGE-01:Y") self.child_cs1 = self.process.get_controller("PMAC:CS1") self.child_traj = self.process.get_controller("PMAC:TRAJ") self.child_status = self.process.get_controller("PMAC:STATUS") # CS1 needs to have the right port otherwise we will error self.set_attributes(self.child_cs1, port="CS1") self.o = PmacChildPart(name="pmac", mri="PMAC") self.context.set_notify_dispatch_request( self.o.notify_dispatch_request) self.process.start() def tearDown(self): del self.context self.process.stop(timeout=1) # TODO: restore this tests when GDA does units right def test_bad_units(self): pytest.skip("awaiting GDA units fix") with self.assertRaises(AssertionError) as cm: self.do_configure(["x", "y"], units="m") assert str(cm.exception) == "x: Expected scan units of 'm', got 'mm'" def resolutions_and_use_call(self, useB=True): return [ call.put("useA", True), call.put("useB", useB), call.put("useC", False), call.put("useU", False), call.put("useV", False), call.put("useW", False), call.put("useX", False), call.put("useY", False), call.put("useZ", False), ] def set_motor_attributes( self, x_pos=0.5, y_pos=0.0, units="mm", x_acceleration=2.5, y_acceleration=2.5, x_velocity=1.0, y_velocity=1.0, settle=0.0, ): # create some parts to mock the motion controller and 2 axes in a CS self.set_attributes( self.child_x, cs="CS1,A", accelerationTime=x_velocity / x_acceleration, resolution=0.001, offset=0.0, maxVelocity=x_velocity, readback=x_pos, velocitySettle=0.0, units=units, ) self.set_attributes( self.child_y, cs="CS1,B", accelerationTime=y_velocity / y_acceleration, resolution=0.001, offset=0.0, maxVelocity=y_velocity, readback=y_pos, velocitySettle=settle, units=units, ) def do_configure( self, axes_to_scan, completed_steps=0, x_pos=0.5, y_pos=0.0, duration=1.0, units="mm", infos=None, settle=0.0, continuous=True, ): self.set_motor_attributes(x_pos, y_pos, units, settle=settle) xs = LineGenerator("x", "mm", 0.0, 0.5, 3, alternate=True) ys = LineGenerator("y", "mm", 0.0, 0.1, 2) if len(axes_to_scan) == 1: # when only scanning one axis, we only do xs once steps_to_do = xs.size else: # otherwise we do xs for each step in ys steps_to_do = xs.size * ys.size generator = CompoundGenerator([ys, xs], [], [], duration, continuous=continuous) generator.prepare() self.o.on_configure( self.context, completed_steps, steps_to_do, {"part": infos}, generator, axes_to_scan, ) def test_validate(self): generator = CompoundGenerator([], [], [], 0.0102) axesToMove = ["x"] # servoFrequency() return value self.child.handled_requests.post.return_value = 4919.300698316487 ret = self.o.on_validate(self.context, generator, axesToMove, {}) expected = 0.010166 assert ret.value.duration == expected def do_check_output_quantized(self): assert self.child.handled_requests.mock_calls[:4] == [ call.post("writeProfile", csPort="CS1", timeArray=[0.002], userPrograms=[8]), call.post("executeProfile"), call.post("moveCS1", a=-0.1374875093687539, b=0.0, moveTime=1.0374875094), call.post( "writeProfile", csPort="CS1", timeArray=pytest.approx([ 99950, 500250, 500250, 500250, 500250, 500250, 500250, 100000, 101000, 101000, 100000, 500250, 500250, 500250, 500250, 500250, 500250, 99950, ]), velocityMode=pytest.approx( [1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 3]), userPrograms=pytest.approx( [1, 4, 1, 4, 1, 4, 2, 8, 8, 8, 1, 4, 1, 4, 1, 4, 2, 8]), a=pytest.approx([ -0.125, 0.0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.63749375, 0.63749375, 0.63749375, 0.625, 0.5, 0.375, 0.25, 0.125, 0.0, -0.125, -0.13748751, ]), b=pytest.approx([ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.01237593, 0.05, 0.08762407, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, ]), ), ] assert self.o.completed_steps_lookup == [ 0, 0, 1, 1, 2, 2, 3, 3, 3, 3, 3, 3, 4, 4, 5, 5, 6, 6, ] a_positions = [ -0.125, 0.0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.6375, 0.6375, 0.6375, 0.625, 0.5, 0.375, 0.25, 0.125, 0.0, -0.125, -0.1375, ] b_positions = [ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0125, 0.05, 0.0875, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, ] user_programs = [1, 4, 1, 4, 1, 4, 2, 8, 8, 8, 1, 4, 1, 4, 1, 4, 2, 8] velocity_mode = [1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 3] time_array = [ 100000, 500000, 500000, 500000, 500000, 500000, 500000, 100000, 100000, 100000, 100000, 500000, 500000, 500000, 500000, 500000, 500000, 100000, ] def do_check_output(self, user_programs=None): if user_programs is None: user_programs = self.user_programs # use a slice here because I'm getting calls to __str__ in debugger assert self.child.handled_requests.mock_calls[:4] == [ call.post("writeProfile", csPort="CS1", timeArray=[0.002], userPrograms=[8]), call.post("executeProfile"), call.post("moveCS1", a=-0.1375, b=0.0, moveTime=1.0375), # pytest.approx to allow sensible compare with numpy arrays call.post( "writeProfile", a=pytest.approx(self.a_positions), b=pytest.approx(self.b_positions), csPort="CS1", timeArray=pytest.approx(self.time_array), userPrograms=pytest.approx(user_programs), velocityMode=pytest.approx(self.velocity_mode), ), ] assert self.o.completed_steps_lookup == [ 0, 0, 1, 1, 2, 2, 3, 3, 3, 3, 3, 3, 4, 4, 5, 5, 6, 6, ] def do_check_output_slower(self): # use a slice here because I'm getting calls to __str__ in debugger assert self.child.handled_requests.mock_calls[:4] == [ call.post("writeProfile", csPort="CS1", timeArray=[0.002], userPrograms=[8]), call.post("executeProfile"), call.post("moveCS1", a=-0.1375, b=0.0, moveTime=1.0375), call.post( "writeProfile", a=pytest.approx([ -0.125, 0.0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.6375, 0.6375, 0.6375, 0.6375, 0.625, 0.5, 0.375, 0.25, 0.125, 0.0, -0.125, -0.1375, ]), b=pytest.approx([ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.00125, 0.02, 0.08, 0.09875, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, ]), csPort="CS1", timeArray=pytest.approx([ 100000, 500000, 500000, 500000, 500000, 500000, 500000, 100000, 300000, 600000, 300000, 100000, 500000, 500000, 500000, 500000, 500000, 500000, 100000, ]), userPrograms=pytest.approx( [1, 4, 1, 4, 1, 4, 2, 8, 8, 8, 8, 1, 4, 1, 4, 1, 4, 2, 8]), velocityMode=pytest.approx( [1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 3]), ), ] assert self.o.completed_steps_lookup == [ 0, 0, 1, 1, 2, 2, 3, 3, 3, 3, 3, 3, 3, 4, 4, 5, 5, 6, 6, ] def do_check_sparse_output(self, user_programs=None): if user_programs is None: user_programs = [1, 8, 0, 0, 0, 1, 8, 0] assert self.child.handled_requests.mock_calls == [ call.post("writeProfile", csPort="CS1", timeArray=[0.002], userPrograms=[8]), call.post("executeProfile"), call.post("moveCS1", a=-0.1375, b=0.0, moveTime=1.0375), # pytest.approx to allow sensible compare with numpy arrays call.post( "writeProfile", a=pytest.approx([ -0.125, 0.625, 0.6375, 0.6375, 0.6375, 0.625, -0.125, -0.1375 ]), b=pytest.approx( [0.0, 0.0, 0.0125, 0.05, 0.0875, 0.1, 0.1, 0.1]), csPort="CS1", timeArray=pytest.approx([ 100000, 3000000, 100000, 100000, 100000, 100000, 3000000, 100000 ]), userPrograms=pytest.approx(user_programs), # note the use of mode 2 AVERAGE_PREV_CURRENT at the end of each # sparse linear row velocityMode=pytest.approx([1, 2, 1, 1, 1, 1, 2, 3]), ), ] assert self.o.completed_steps_lookup == [0, 3, 3, 3, 3, 3, 6, 6] def test_configure(self): self.do_configure(axes_to_scan=["x", "y"]) self.do_check_output() def test_configure_quantize(self): m = [MinTurnaroundInfo(0.002, 0.001)] self.do_configure(axes_to_scan=["x", "y"], duration=1.0005, infos=m) self.do_check_output_quantized() def test_configure_slower_vmax(self): self.set_attributes(self.child_y, maxVelocityPercent=10) self.do_configure(axes_to_scan=["x", "y"]) self.do_check_output_slower() def test_configure_no_pulses(self): self.do_configure(axes_to_scan=["x", "y"], infos=[MotionTriggerInfo(MotionTrigger.NONE)]) self.do_check_sparse_output(user_programs=[0] * 8) def test_configure_start_of_row_pulses(self): self.do_configure(axes_to_scan=["x", "y"], infos=[MotionTriggerInfo(MotionTrigger.ROW_GATE)]) self.do_check_sparse_output() def test_configure_no_axes(self): self.set_motor_attributes() generator = CompoundGenerator([StaticPointGenerator(6)], [], [], duration=0.1) generator.prepare() self.o.on_configure(self.context, 0, 6, {}, generator, []) assert self.child.handled_requests.mock_calls == [ call.post("writeProfile", csPort="CS1", timeArray=[0.002], userPrograms=[8]), call.post("executeProfile"), # pytest.approx to allow sensible compare with numpy arrays call.post( "writeProfile", csPort="CS1", timeArray=pytest.approx([ 2000, 50000, 50000, 50000, 50000, 50000, 50000, 50000, 50000, 50000, 50000, 50000, 50000, 2000, ]), userPrograms=pytest.approx( [1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 2, 8]), velocityMode=pytest.approx( [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 3]), ), ] assert self.o.completed_steps_lookup == [ 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, ] @patch("malcolm.modules.pmac.parts.pmacchildpart.PROFILE_POINTS", 4) def test_update_step(self): self.do_configure(axes_to_scan=["x", "y"], x_pos=0.0, y_pos=0.2) assert len(self.child.handled_requests.mock_calls) == 4 # Check that the first trajectory moves to the first place positionsA = self.child.handled_requests.post.call_args_list[-1][1][ "a"] assert len(positionsA) == 4 assert positionsA[-1] == 0.25 assert self.o.end_index == 2 assert len(self.o.completed_steps_lookup) == 5 assert len(self.o.profile["timeArray"]) == 1 self.o.registrar = Mock() self.child.handled_requests.reset_mock() self.o.update_step(3, self.context.block_view("PMAC")) self.o.registrar.report.assert_called_once() assert self.o.registrar.report.call_args[0][0].steps == 1 assert not self.o.loading assert self.child.handled_requests.mock_calls == [ # pytest.approx to allow sensible compare with numpy arrays call.post( "writeProfile", a=pytest.approx([0.375, 0.5, 0.625, 0.6375]), b=pytest.approx([0.0, 0.0, 0.0, 0.0125]), timeArray=pytest.approx([500000, 500000, 500000, 100000]), userPrograms=pytest.approx([1, 4, 2, 8]), velocityMode=pytest.approx([0, 0, 1, 1]), ) ] assert self.o.end_index == 3 assert len(self.o.completed_steps_lookup) == 11 assert len(self.o.profile["timeArray"]) == 3 def test_run(self): self.o.generator = ANY self.o.on_run(self.context) assert self.child.handled_requests.mock_calls == [ call.post("executeProfile") ] def test_reset(self): self.o.generator = ANY self.o.on_reset(self.context) assert self.child.handled_requests.mock_calls == [ call.post("abortProfile") ] def test_multi_run(self): self.do_configure(axes_to_scan=["x"]) assert self.o.completed_steps_lookup == ([0, 0, 1, 1, 2, 2, 3, 3]) self.child.handled_requests.reset_mock() self.do_configure(axes_to_scan=["x"], completed_steps=3, x_pos=0.6375) assert self.child.handled_requests.mock_calls == [ call.post("writeProfile", csPort="CS1", timeArray=[0.002], userPrograms=[8]), call.post("executeProfile"), call.post("moveCS1", a=0.6375, moveTime=0.0), call.post( "writeProfile", csPort="CS1", a=pytest.approx( [0.625, 0.5, 0.375, 0.25, 0.125, 0.0, -0.125, -0.1375]), timeArray=pytest.approx([ 100000, 500000, 500000, 500000, 500000, 500000, 500000, 100000 ]), userPrograms=pytest.approx([1, 4, 1, 4, 1, 4, 2, 8]), velocityMode=pytest.approx([1, 0, 0, 0, 0, 0, 1, 3]), ), ] def test_long_steps_lookup(self): self.do_configure(axes_to_scan=["x"], completed_steps=3, x_pos=0.62506, duration=14.0) # Ignore the trigger reset and move to start, just look at the last call # which is the profile write assert self.child.handled_requests.mock_calls[-1] == call.post( "writeProfile", csPort="CS1", a=pytest.approx([ 0.625, 0.5625, 0.5, 0.4375, 0.375, 0.3125, 0.25, 0.1875, 0.125, 0.0625, 0.0, -0.0625, -0.125, -0.12506377551020409, ]), timeArray=pytest.approx([ 7143, 3500000, 3500000, 3500000, 3500000, 3500000, 3500000, 3500000, 3500000, 3500000, 3500000, 3500000, 3500000, 7143, ]), userPrograms=pytest.approx( [1, 0, 4, 0, 1, 0, 4, 0, 1, 0, 4, 0, 2, 8]), velocityMode=pytest.approx( [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 3]), ) assert self.o.completed_steps_lookup == ([ 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6 ]) @patch("malcolm.modules.pmac.parts.pmacchildpart.PROFILE_POINTS", 9) def test_split_in_a_long_step_lookup(self): self.do_configure(axes_to_scan=["x"], completed_steps=3, x_pos=0.62506, duration=14.0) # Ignore the trigger reset and move to start, just look at the last call # which is the profile write assert self.child.handled_requests.mock_calls[-1] == call.post( "writeProfile", csPort="CS1", a=pytest.approx([ 0.625, 0.5625, 0.5, 0.4375, 0.375, 0.3125, 0.25, 0.1875, 0.125 ]), timeArray=pytest.approx([ 7143, 3500000, 3500000, 3500000, 3500000, 3500000, 3500000, 3500000, 3500000, ]), userPrograms=pytest.approx([1, 0, 4, 0, 1, 0, 4, 0, 1]), velocityMode=pytest.approx([1, 0, 0, 0, 0, 0, 0, 0, 0]), ) # The completed steps works on complete (not split) steps, so we expect # the last value to be the end of step 6, even though it doesn't # actually appear in the velocity arrays assert self.o.completed_steps_lookup == ([ 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 6 ]) # Mock out the registrar that would have been registered when we # attached to a controller self.o.registrar = Mock() # Now call update step and get it to generate the next lot of points # scanned can be any index into completed_steps_lookup so that there # are less than PROFILE_POINTS left to go in it self.o.update_step(scanned=2, child=self.process.block_view("PMAC")) # Expect the rest of the points assert self.child.handled_requests.mock_calls[-1] == call.post( "writeProfile", a=pytest.approx( [0.0625, 0.0, -0.0625, -0.125, -0.12506377551020409]), timeArray=pytest.approx([3500000, 3500000, 3500000, 3500000, 7143]), userPrograms=pytest.approx([0, 4, 0, 2, 8]), velocityMode=pytest.approx([0, 0, 0, 1, 3]), ) assert self.o.registrar.report.call_count == 1 assert self.o.registrar.report.call_args[0][0].steps == 3 # And for the rest of the lookup table to be added assert self.o.completed_steps_lookup == ([ 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6 ]) def do_2d_trajectory_with_plot(self, gen, xv, yv, xa, ya, title): gen.prepare() self.set_motor_attributes(x_acceleration=xv / xa, y_acceleration=yv / ya, x_velocity=xv, y_velocity=yv) infos = [MotionTriggerInfo(MotionTrigger.ROW_GATE)] # infos = [MotionTriggerInfo(MotionTrigger.EVERY_POINT)] self.o.on_configure(self.context, 0, gen.size, {"part": infos}, gen, ["x", "y"]) name, args, kwargs = self.child.handled_requests.mock_calls[2] assert name == "post" assert args[0] == "moveCS1" # add in the start point to the position and time arrays xp = np.array([kwargs["a"]]) yp = np.array([kwargs["b"]]) tp = np.array([0]) # And the profile write name, args, kwargs = self.child.handled_requests.mock_calls[-1] assert name == "post" assert args[0] == "writeProfile" xp = np.append(xp, kwargs["a"]) yp = np.append(yp, kwargs["b"]) tp = np.append(tp, kwargs["timeArray"]) # if this test is run in pycharm then it plots some results # to help diagnose issues if environ.get("PLOTS") == "1": import matplotlib.pyplot as plt times = np.cumsum(tp / 1000) # show in millisecs plt.figure(figsize=(8, 6), dpi=300) plt.title("{} x/time {} points".format(title, xp.size)) plt.plot(xp, times, "+", ms=2.5) plt.figure(figsize=(8, 6), dpi=300) plt.title("{} x/y".format(title)) plt.plot(xp, yp, "+", ms=2.5) plt.show() return xp, yp def check_bounds(self, a, name): # tiny amounts of overshoot are acceptable npa = np.array(a) less_start = np.argmax((npa[0] - npa) > 0.000001) greater_end = np.argmax((npa - npa[-1]) > 0.000001) self.assertEqual( less_start, 0, "Position {} < start for {}\n{}".format(less_start, name, a)) self.assertEqual( greater_end, 0, "Position {} > end for {}\n{}".format(greater_end, name, a)) def test_turnaround_overshoot(self): """check for a previous bug in a sawtooth X,Y scan The issue was that the first point at the start of each rising edge overshot in Y. The parameters for each rising edge are below. Line Y, start=-2.5, stop= -2.5 +0.025, points=30 Line X, start=-0.95, stop= -0.95 +0.025, points=30 duration=0.15 X motor: VMAX=17, ACCL=0.1 (time to VMAX) Y motor: VMAX=1, ACCL=0.2 """ xs = LineGenerator("x", "mm", -2.5, -2.475, 30) ys = LineGenerator("y", "mm", -0.95, -0.925, 2) generator = CompoundGenerator([ys, xs], [], [], 0.15) x1, y1 = self.do_2d_trajectory_with_plot( generator, xv=17, yv=1, xa=0.1, ya=0.2, title="test_turnaround_overshoot 10 fast", ) self.child.handled_requests.reset_mock() x2, y2 = self.do_2d_trajectory_with_plot( generator, xv=17, yv=34, xa=10, ya=0.1, title="test_turnaround_overshoot 10 slower", ) self.child.handled_requests.reset_mock() # check all the points in the arrays are within their start and stop self.check_bounds(x1, "x1") self.check_bounds(x2, "x2") self.check_bounds(y1, "y1") self.check_bounds(y2, "y2") def test_step_scan(self): axes_to_scan = ["x", "y"] duration = 1.0005 self.set_motor_attributes(0.0, 0.0, "mm") steps_to_do = 3 * len(axes_to_scan) xs = LineGenerator("x", "mm", 0.0, 5, 3, alternate=True) ys = LineGenerator("y", "mm", 0.0, 10, 2) generator = CompoundGenerator([ys, xs], [], [], duration, continuous=False) generator.prepare() self.o.on_configure(self.context, 0, steps_to_do, {"part": None}, generator, axes_to_scan) action, func, args = self.child.handled_requests.mock_calls[-1] assert args["a"] == pytest.approx([ 0.0, 0.0, 0.0, 0.2, 2.3, 2.5, 2.5, 2.5, 2.7, 4.8, 5.0, 5.0, 5.0, 5.0, 5.0, 5.0, 5.0, 5.0, 5.0, 5.0, 4.8, 2.7, 2.5, 2.5, 2.5, 2.3, 0.2, 0, 0.0, 0.0, 0.0, ]) assert args["b"] == pytest.approx([ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.2, 3.4, 6.6, 9.8, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0, ]) assert args["timeArray"] == pytest.approx([ 2000, 500250, 500250, 400000, 2100000, 400000, 500250, 500250, 400000, 2100000, 400000, 500250, 500250, 400000, 3200000, 3200000, 3200000, 400000, 500250, 500250, 400000, 2100000, 400000, 500250, 500250, 400000, 2100000, 400000, 500250, 500250, 2000, ]) def test_minimum_profile(self): # tests that a turnaround that is >= minturnaround # is reduced to a start and end point only # this supports keeping the turnaround really short where # the motors are fast e.g. j15 axes_to_scan = ["x", "y"] duration = 0.005 self.set_motor_attributes( 0.0, 0.0, "mm", x_acceleration=100.0, x_velocity=2.0, y_acceleration=100.0, y_velocity=2.0, ) steps_to_do = 1 * len(axes_to_scan) xs = LineGenerator("x", "mm", 0, 0.0001, 2) ys = LineGenerator("y", "mm", 0, 0, 2) generator = CompoundGenerator([ys, xs], [], [], duration, continuous=False) generator.prepare() m = [MinTurnaroundInfo(0.002, 0.002)] self.o.on_configure(self.context, 0, steps_to_do, {"part": m}, generator, axes_to_scan) action, func, args = self.child.handled_requests.mock_calls[-1] assert args["a"] == pytest.approx( [0, 0, 0, 0.0001, 0.0001, 0.0001, 0.0001]) assert args["b"] == pytest.approx([0, 0, 0, 0, 0, 0, 0]) assert args["timeArray"] == pytest.approx( [2000, 2500, 2500, 2000, 2500, 2500, 2000]) # now make the acceleration slower so that the turnaround takes # longer than the minimum interval self.set_motor_attributes( 0.0, 0.0, "mm", x_acceleration=10.0, x_velocity=2.0, y_acceleration=100.0, y_velocity=2.0, ) self.o.on_configure(self.context, 0, steps_to_do, {"part": m}, generator, axes_to_scan) # this generates one mid point between the 1st upper and 2nd lower # bounds. Note that the point is not exactly central due to time # quantization action, func, args = self.child.handled_requests.mock_calls[-1] assert args["a"] == pytest.approx( [0, 0, 0, 5.0e-05, 0.0001, 0.0001, 0.0001, 0.0001]) assert args["b"] == pytest.approx([0, 0, 0, 0, 0, 0, 0, 0]) assert args["timeArray"] == pytest.approx( [2000, 2500, 2500, 6000, 6000, 2500, 2500, 2000]) def test_settle_time(self): """ Test the standard do_configure but with motor settle time """ self.do_configure(axes_to_scan=["x", "y"], settle=0.01) self.do_check_output_settle() def do_check_output_settle(self): wp = self.child.handled_requests.mock_calls[3] # this is the same as do_check_output but with an extra time slot in the # turnaround: I jump through some hoops here to make it clear what the # difference is (rather than writing out the writeProfile parameters in full) a_pos = self.a_positions a_pos = a_pos[:10] + [0.627375] + a_pos[10:] b_pos = self.b_positions b_pos = b_pos[:9] + [0.089875, 0.1] + b_pos[10:] up = self.user_programs up = up[:10] + [8] + up[10:] ta = self.time_array ta = ta[:9] + [110000, 90000, 10000] + ta[11:] vm = self.velocity_mode vm = vm[:10] + [1] + vm[10:] assert wp == call.post( "writeProfile", a=pytest.approx(a_pos), b=pytest.approx(b_pos), csPort="CS1", timeArray=pytest.approx(ta), userPrograms=pytest.approx(up), velocityMode=pytest.approx(vm), ) def long_configure(self, row_gate=False): # test 4,000,000 points configure - used to check performance if row_gate: infos = [ MotionTriggerInfo(MotionTrigger.ROW_GATE), ] else: infos = None self.set_motor_attributes( 0, 0, "mm", x_velocity=300, y_velocity=300, x_acceleration=30, y_acceleration=30, ) axes_to_scan = ["x", "y"] x_steps, y_steps = 4000, 1000 steps_to_do = x_steps * y_steps xs = LineGenerator("x", "mm", 0.0, 10, x_steps, alternate=True) ys = LineGenerator("y", "mm", 0.0, 8, y_steps) generator = CompoundGenerator([ys, xs], [], [], 0.0001) generator.prepare() start = datetime.now() self.o.on_configure(self.context, 0, steps_to_do, {"part": infos}, generator, axes_to_scan) elapsed = datetime.now() - start # todo goal was sub 1 second but we achieved sub 3 secs assert elapsed.total_seconds() < 3.5 def test_configure_long_trajectory(self): # Skip on GitHub Actions and GitLab CI if "CI" in environ: pytest.skip("performance test only") # brick triggered self.long_configure(False) # 'sparse' trajectory linear point removal self.long_configure(True) def test_long_turnaround(self): """ Verify that if the turnaround time exceeds maximum time between PVT points then addtional points are added """ duration = 2 axes_to_scan = ["x"] # make the motors slow so that it takes 10 secs to do the turnaround self.set_motor_attributes(x_pos=0, y_pos=0, units="mm", x_velocity=0.1, y_velocity=0.1) # very simple trajectory with two points and a turnaround between them xs = LineGenerator("x", "mm", 0.0, 1.0, 2) generator = CompoundGenerator([xs], [], [], duration, continuous=False) generator.prepare() self.o.on_configure(self.context, 0, 2, {"part": None}, generator, axes_to_scan) assert self.child.handled_requests.mock_calls[-1] == call.post( "writeProfile", csPort="CS1", a=pytest.approx([ 0.0, 0.0, 0.0, 0.002, 0.334, 0.666, 0.998, 1.0, 1.0, 1.0, 1.0 ]), timeArray=pytest.approx([ 2000, 1000000, 1000000, 40000, 3320000, 3320000, 3320000, 40000, 1000000, 1000000, 2000, ]), userPrograms=pytest.approx([1, 4, 2, 8, 0, 0, 8, 1, 4, 2, 8]), velocityMode=pytest.approx([1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 3]), ) def test_configure_delay_after(self): # a test to show that delay_after inserts a "loop_back" turnaround delay = 1.0 axes_to_scan = ["x", "y"] self.set_motor_attributes(0.5, 0.0, "mm") steps_to_do = 3 xs = LineGenerator("x", "mm", 0.0, 0.5, 3, alternate=True) ys = LineGenerator("y", "mm", 0.0, 0.1, 2) generator = CompoundGenerator([ys, xs], [], [], 1.0, delay_after=delay) generator.prepare() self.o.on_configure(self.context, 0, steps_to_do, {"part": None}, generator, axes_to_scan) action, func, args = self.child.handled_requests.mock_calls[-1] assert args["a"] == pytest.approx([ -0.125, 0.0, 0.125, 0.13742472, 0.11257528, 0.125, 0.25, 0.375, 0.38742472, 0.36257528, 0.375, 0.5, 0.625, 0.6375, ]) assert args["b"] == pytest.approx([ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 ]) assert args["timeArray"] == pytest.approx([ 100000, 500000, 500000, 114000, 776000, 114000, 500000, 500000, 114000, 776000, 114000, 500000, 500000, 100000, ]) # check the delay times are correct (in microsecs) t = args["timeArray"] assert t[3] + t[4] + t[5] >= delay * 1000000 assert t[8] + t[9] + t[10] >= delay * 1000000 assert args["userPrograms"] == pytest.approx( [1, 4, 2, 8, 8, 1, 4, 2, 8, 8, 1, 4, 2, 8]) assert args["velocityMode"] == pytest.approx( [1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 3])
class TestBeamSelectorPart(ChildTestCase): def setUp(self): self.process = Process("Process") self.context = Context(self.process) pmac_block = make_block_creator(__file__, "test_pmac_manager_block.yaml") self.child = self.create_child_block(pmac_block, self.process, mri_prefix="PMAC", config_dir="/tmp") # These are the child blocks we are interested in self.child_x = self.process.get_controller("BL45P-ML-STAGE-01:X") # self.child_y = self.process.get_controller( # "BL45P-ML-STAGE-01:Y") self.child_cs1 = self.process.get_controller("PMAC:CS1") self.child_traj = self.process.get_controller("PMAC:TRAJ") self.child_status = self.process.get_controller("PMAC:STATUS") # CS1 needs to have the right port otherwise we will error self.set_attributes(self.child_cs1, port="CS1") self.o = BeamSelectorPart( name="beamSelector", mri="PMAC", selectorAxis="x", tomoAngle=0, diffAngle=0.5, moveTime=0.5, ) self.context.set_notify_dispatch_request( self.o.notify_dispatch_request) self.process.start() pass def tearDown(self): del self.context self.process.stop(timeout=1) pass def set_motor_attributes(self, x_pos=0.5, units="deg", x_acceleration=4.0, x_velocity=10.0): # create some parts to mock # the motion controller and an axis in a CS self.set_attributes( self.child_x, cs="CS1,A", accelerationTime=x_velocity / x_acceleration, resolution=0.001, offset=0.0, maxVelocity=x_velocity, readback=x_pos, velocitySettle=0.0, units=units, ) def test_configure_cycle(self): self.set_motor_attributes() nCycles = 1 generator = CompoundGenerator([StaticPointGenerator(nCycles)], [], [], duration=4.0) generator.prepare() self.o.on_configure(self.context, 0, nCycles, {}, generator, []) assert generator.duration == 1.5 assert self.child.handled_requests.mock_calls == [ call.post("writeProfile", csPort="CS1", timeArray=[0.002], userPrograms=[8]), call.post("executeProfile"), call.post("moveCS1", a=-0.125, moveTime=pytest.approx(0.790, abs=1e-3)), # pytest.approx to allow sensible compare with numpy arrays call.post( "writeProfile", a=pytest.approx([ 0.0, 0.25, 0.5, 0.625, 0.625, 0.5, 0.25, 0.0, -0.125, -0.125, 0.0 ]), csPort="CS1", timeArray=pytest.approx([ 250000, 250000, 250000, 250000, 1000000, 250000, 250000, 250000, 250000, 1000000, 250000, ]), userPrograms=pytest.approx([1, 4, 2, 8, 8, 1, 4, 2, 8, 8, 1]), velocityMode=pytest.approx([1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 3]), ), ] assert self.o.completed_steps_lookup == [ 0, 0, 1, 1, 1, 1, 1, 2, 3, 3, 3 ] def test_validate(self): generator = CompoundGenerator([StaticPointGenerator(2)], [], [], 0.0102) axesToMove = ["x"] # servoFrequency() return value self.child.handled_requests.post.return_value = 4919.300698316487 ret = self.o.on_validate(self.context, generator, axesToMove, {}) expected = 0.010166 assert ret.value.duration == expected def test_critical_exposure(self): self.set_motor_attributes() nCycles = 1 generator = CompoundGenerator([StaticPointGenerator(nCycles)], [], [], duration=0.5) generator.prepare() self.o.on_configure(self.context, 0, nCycles, {}, generator, []) assert generator.duration == MIN_TIME assert self.child.handled_requests.mock_calls == [ call.post("writeProfile", csPort="CS1", timeArray=[0.002], userPrograms=[8]), call.post("executeProfile"), call.post("moveCS1", a=-0.125, moveTime=pytest.approx(0.790, abs=1e-3)), # pytest.approx to allow sensible compare with numpy arrays call.post( "writeProfile", a=pytest.approx( [0.0, 0.25, 0.5, 0.625, 0.5, 0.25, 0.0, -0.125, 0.0]), csPort="CS1", timeArray=pytest.approx([ 250000, 250000, 250000, 250000, 250000, 250000, 250000, 250000, 250000, ]), userPrograms=pytest.approx([1, 4, 2, 8, 1, 4, 2, 8, 1]), velocityMode=pytest.approx([1, 0, 1, 1, 1, 0, 1, 1, 3]), ), ] def test_invalid_parameters(self): self.part_under_test = BeamSelectorPart( name="beamSelector2", mri="PMAC", selectorAxis="x", tomoAngle="invalid", diffAngle=0.5, moveTime=0.5, ) self.context.set_notify_dispatch_request( self.part_under_test.notify_dispatch_request) self.set_motor_attributes() nCycles = 1 generator = CompoundGenerator([StaticPointGenerator(nCycles)], [], [], duration=0.5) generator.prepare() self.part_under_test.on_configure(self.context, 0, nCycles, {}, generator, []) assert self.part_under_test.tomoAngle == 0.0 assert self.part_under_test.diffAngle == 0.0 assert self.part_under_test.move_time == 0.5
class TestPandaSeqTriggerPart(ChildTestCase): def setUp(self): self.process = Process("Process") self.context = Context(self.process) # Create a fake PandA self.panda = ManagerController("PANDA", "/tmp") self.busses = PositionsPart("busses") self.panda.add_part(self.busses) # Make 2 sequencers we can prod self.seq_parts = {} for i in (1, 2): controller = BasicController("PANDA:SEQ%d" % i) self.seq_parts[i] = SequencerPart("part") controller.add_part(self.seq_parts[i]) self.process.add_controller(controller) self.panda.add_part( ChildPart( "SEQ%d" % i, "PANDA:SEQ%d" % i, initial_visibility=True, stateful=False, )) self.child_seq1 = self.process.get_controller("PANDA:SEQ1") self.child_seq2 = self.process.get_controller("PANDA:SEQ2") # And an srgate controller = BasicController("PANDA:SRGATE1") self.gate_part = GatePart("part") controller.add_part(self.gate_part) self.process.add_controller(controller) self.panda.add_part( ChildPart("SRGATE1", "PANDA:SRGATE1", initial_visibility=True, stateful=False)) self.process.add_controller(self.panda) # And the PMAC pmac_block = make_block_creator( os.path.join(os.path.dirname(__file__), "..", "test_pmac", "blah"), "test_pmac_manager_block.yaml", ) self.pmac = self.create_child_block(pmac_block, self.process, mri_prefix="PMAC", config_dir="/tmp") # These are the motors we are interested in self.child_x = self.process.get_controller("BL45P-ML-STAGE-01:X") self.child_y = self.process.get_controller("BL45P-ML-STAGE-01:Y") self.child_cs1 = self.process.get_controller("PMAC:CS1") # CS1 needs to have the right port otherwise we will error self.set_attributes(self.child_cs1, port="CS1") # Make the child block holding panda and pmac mri self.child = self.create_child_block( panda_seq_trigger_block, self.process, mri="SCAN:PCOMP", panda="PANDA", pmac="PMAC", ) # And our part under test self.o = PandASeqTriggerPart("pcomp", "SCAN:PCOMP") # Now start the process off and tell the panda which sequencer tables # to use self.process.start() exports = ExportTable.from_rows([ ("SEQ1.table", "seqTableA"), ("SEQ2.table", "seqTableB"), ("SRGATE1.forceSet", "seqSetEnable"), ]) self.panda.set_exports(exports) def tearDown(self): self.process.stop(timeout=2) def set_motor_attributes( self, x_pos=0.5, y_pos=0.0, units="mm", x_acceleration=2.5, y_acceleration=2.5, x_velocity=1.0, y_velocity=1.0, ): # create some parts to mock the motion controller and 2 axes in a CS self.set_attributes( self.child_x, cs="CS1,A", accelerationTime=x_velocity / x_acceleration, resolution=0.001, offset=0.0, maxVelocity=x_velocity, readback=x_pos, velocitySettle=0.0, units=units, ) self.set_attributes( self.child_y, cs="CS1,B", accelerationTime=y_velocity / y_acceleration, resolution=0.001, offset=0.0, maxVelocity=y_velocity, readback=y_pos, velocitySettle=0.0, units=units, ) def test_configure_continuous(self): xs = LineGenerator("x", "mm", 0.0, 0.3, 4, alternate=True) ys = LineGenerator("y", "mm", 0.0, 0.1, 2) generator = CompoundGenerator([ys, xs], [], [], 1.0) generator.prepare() completed_steps = 0 steps_to_do = 8 self.set_motor_attributes() axes_to_move = ["x", "y"] self.o.on_configure(self.context, completed_steps, steps_to_do, {}, generator, axes_to_move) assert self.o.generator is generator assert self.o.loaded_up_to == completed_steps assert self.o.scan_up_to == completed_steps + steps_to_do # Triggers GT = Trigger.POSA_GT IT = Trigger.IMMEDIATE LT = Trigger.POSA_LT # Half a frame hf = 62500000 # Half how long to be blind for hb = 22500000 self.seq_parts[1].table_set.assert_called_once() table = self.seq_parts[1].table_set.call_args[0][0] assert table.repeats == [1, 3, 1, 1, 3, 1] assert table.trigger == [LT, IT, IT, GT, IT, IT] assert table.position == [50, 0, 0, -350, 0, 0] assert table.time1 == [hf, hf, hb, hf, hf, 125000000] assert table.outa1 == [1, 1, 0, 1, 1, 0] # Live assert table.outb1 == [0, 0, 1, 0, 0, 1] # Dead assert (table.outc1 == table.outd1 == table.oute1 == table.outf1 == [0, 0, 0, 0, 0, 0]) assert table.time2 == [hf, hf, hb, hf, hf, 125000000] assert (table.outa2 == table.outb2 == table.outc2 == table.outd2 == table.oute2 == table.outf2 == [0, 0, 0, 0, 0, 0]) # Check we didn't press the gate part self.gate_part.enable_set.assert_not_called() self.o.on_run(self.context) # Check we pressed the gate part self.gate_part.enable_set.assert_called_once() def test_configure_motion_controller_trigger(self): xs = LineGenerator("x", "mm", 0.0, 0.3, 4, alternate=True) ys = LineGenerator("y", "mm", 0.0, 0.1, 2) generator = CompoundGenerator([ys, xs], [], [], 1.0) generator.prepare() completed_steps = 0 steps_to_do = 8 self.set_motor_attributes() self.set_attributes(self.child, rowTrigger="Motion Controller") self.set_attributes(self.child_seq1, bita="TTLIN1.VAL") self.set_attributes(self.child_seq2, bita="TTLIN1.VAL") axes_to_move = ["x", "y"] self.o.on_configure(self.context, completed_steps, steps_to_do, {}, generator, axes_to_move) assert self.o.generator is generator assert self.o.loaded_up_to == completed_steps assert self.o.scan_up_to == completed_steps + steps_to_do # Triggers B0 = Trigger.BITA_0 B1 = Trigger.BITA_1 IT = Trigger.IMMEDIATE # Half a frame hf = 62500000 self.seq_parts[1].table_set.assert_called_once() table = self.seq_parts[1].table_set.call_args[0][0] assert table.repeats == [1, 3, 1, 1, 3, 1] assert table.trigger == [B1, IT, B0, B1, IT, IT] assert table.time1 == [hf, hf, 1250, hf, hf, 125000000] assert table.position == [0, 0, 0, 0, 0, 0] assert table.outa1 == [1, 1, 0, 1, 1, 0] # Live assert table.outb1 == [0, 0, 1, 0, 0, 1] # Dead assert (table.outc1 == table.outd1 == table.oute1 == table.outf1 == [0, 0, 0, 0, 0, 0]) assert table.time2 == [hf, hf, 1250, hf, hf, 125000000] assert (table.outa2 == table.outb2 == table.outc2 == table.outd2 == table.oute2 == table.outf2 == [0, 0, 0, 0, 0, 0]) # Check we didn't press the gate part self.gate_part.enable_set.assert_not_called() self.o.on_run(self.context) # Check we pressed the gate part self.gate_part.enable_set.assert_called_once() def test_configure_stepped(self): xs = LineGenerator("x", "mm", 0.0, 0.3, 4, alternate=True) ys = LineGenerator("y", "mm", 0.0, 0.1, 2) generator = CompoundGenerator([ys, xs], [], [], 1.0, continuous=False) generator.prepare() completed_steps = 0 steps_to_do = 8 self.set_motor_attributes() axes_to_move = ["x", "y"] with self.assertRaises(AssertionError): self.o.on_configure(self.context, completed_steps, steps_to_do, {}, generator, axes_to_move) def test_acquire_scan(self): generator = CompoundGenerator([StaticPointGenerator(size=5)], [], [], 1.0) generator.prepare() completed_steps = 0 steps_to_do = 5 self.o.on_configure(self.context, completed_steps, steps_to_do, {}, generator, []) assert self.o.generator is generator assert self.o.loaded_up_to == completed_steps assert self.o.scan_up_to == completed_steps + steps_to_do # Triggers IT = Trigger.IMMEDIATE # Half a frame hf = 62500000 self.seq_parts[1].table_set.assert_called_once() table = self.seq_parts[1].table_set.call_args[0][0] assert table.repeats == [5, 1] assert table.trigger == [IT, IT] assert table.position == [0, 0] assert table.time1 == [hf, 125000000] assert table.outa1 == [1, 0] # Live assert table.outb1 == [0, 1] # Dead assert table.outc1 == table.outd1 == table.oute1 == table.outf1 == [ 0, 0 ] assert table.time2 == [hf, 125000000] assert (table.outa2 == table.outb2 == table.outc2 == table.outd2 == table.oute2 == table.outf2 == [0, 0]) # Check we didn't press the gate part self.gate_part.enable_set.assert_not_called() def test_configure_single_point_multi_frames(self): # This test uses PCAP to generate a static point test. # The test moves the motors to a new position and then generates # 5 triggers at that position xs = LineGenerator("x", "mm", 0.0, 0.0, 5, alternate=True) ys = LineGenerator("y", "mm", 1.0, 1.0, 1) generator = CompoundGenerator([ys, xs], [], [], 1.0) generator.prepare() steps_to_do = 5 self.assertEqual(steps_to_do, generator.size) completed_steps = 0 self.set_motor_attributes() axes_to_move = ["x", "y"] self.o.on_configure(self.context, completed_steps, steps_to_do, {}, generator, axes_to_move) def test_configure_pcomp_row_trigger_with_single_point_rows(self): x_steps, y_steps = 1, 5 xs = LineGenerator("x", "mm", 0.0, 0.5, x_steps, alternate=True) ys = LineGenerator("y", "mm", 0.0, 4, y_steps) generator = CompoundGenerator([ys, xs], [], [], 1.0) generator.prepare() completed_steps = 0 steps_to_do = x_steps * y_steps self.set_motor_attributes() axes_to_move = ["x", "y"] self.o.on_configure(self.context, completed_steps, steps_to_do, {}, generator, axes_to_move) # Triggers GT = Trigger.POSA_GT LT = Trigger.POSA_LT IT = Trigger.IMMEDIATE # Half a frame hf = 62500000 # Half blind hb = 75000000 self.seq_parts[1].table_set.assert_called_once() table = self.seq_parts[1].table_set.call_args[0][0] assert table.repeats == [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] assert table.trigger == [LT, IT, GT, IT, LT, IT, GT, IT, LT, IT] assert table.time1 == [hf, hb, hf, hb, hf, hb, hf, hb, hf, 125000000] assert table.position == [0, 0, -500, 0, 0, 0, -500, 0, 0, 0] assert table.outa1 == [1, 0, 1, 0, 1, 0, 1, 0, 1, 0] # Live assert table.outb1 == [0, 1, 0, 1, 0, 1, 0, 1, 0, 1] # Dead assert (table.outc1 == table.outd1 == table.oute1 == table.outf1 == [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) assert table.time2 == [hf, hb, hf, hb, hf, hb, hf, hb, hf, 125000000] assert (table.outa2 == table.outb2 == table.outc2 == table.outd2 == table.oute2 == table.outf2 == [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) # Check we didn't press the gate part self.gate_part.enable_set.assert_not_called() self.o.on_run(self.context) # Check we pressed the gate part self.gate_part.enable_set.assert_called_once() def test_configure_with_delay_after(self): # a test to show that delay_after inserts a "loop_back" turnaround delay = 1.0 x_steps, y_steps = 3, 2 xs = LineGenerator("x", "mm", 0.0, 0.5, x_steps, alternate=True) ys = LineGenerator("y", "mm", 0.0, 0.1, y_steps) generator = CompoundGenerator([ys, xs], [], [], 1.0, delay_after=delay) generator.prepare() completed_steps = 0 steps_to_do = x_steps * y_steps self.set_motor_attributes() axes_to_move = ["x", "y"] self.o.on_configure(self.context, completed_steps, steps_to_do, {}, generator, axes_to_move) assert self.o.generator is generator assert self.o.loaded_up_to == completed_steps assert self.o.scan_up_to == completed_steps + steps_to_do # Triggers GT = Trigger.POSA_GT IT = Trigger.IMMEDIATE LT = Trigger.POSA_LT # Half a frame hf = 62500000 # Half how long to be blind for a single point hfb = 55625000 # Half how long to be blind for end of row hrb = 56500000 self.seq_parts[1].table_set.assert_called_once() table = self.seq_parts[1].table_set.call_args[0][0] assert table.repeats == [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] assert table.trigger == [ LT, IT, LT, IT, LT, IT, GT, IT, GT, IT, GT, IT ] assert table.position == [ 125, 0, -125, 0, -375, 0, -625, 0, -375, 0, -125, 0 ] assert table.time1 == [ hf, hfb, hf, hfb, hf, hrb, hf, hfb, hf, hfb, hf, 125000000, ] assert table.outa1 == [1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0] # Live assert table.outb1 == [0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1] # Dead assert (table.outc1 == table.outd1 == table.oute1 == table.outf1 == [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) assert table.time2 == [ hf, hfb, hf, hfb, hf, hrb, hf, hfb, hf, hfb, hf, 125000000, ] assert (table.outa2 == table.outb2 == table.outc2 == table.outd2 == table.oute2 == table.outf2 == [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) # Check we didn't press the gate part self.gate_part.enable_set.assert_not_called() self.o.on_run(self.context) # Check we pressed the gate part self.gate_part.enable_set.assert_called_once() def test_configure_long_pcomp_row_trigger(self): # Skip on GitHub Actions and GitLab CI if "CI" in os.environ: pytest.skip("performance test only") self.set_motor_attributes( 0, 0, "mm", x_velocity=300, y_velocity=300, x_acceleration=30, y_acceleration=30, ) x_steps, y_steps = 4000, 1000 xs = LineGenerator("x", "mm", 0.0, 10, x_steps, alternate=True) ys = LineGenerator("y", "mm", 0.0, 8, y_steps) generator = CompoundGenerator([ys, xs], [], [], 0.005) generator.prepare() completed_steps = 0 steps_to_do = x_steps * y_steps self.set_motor_attributes() axes_to_move = ["x", "y"] start = datetime.now() self.o.on_configure(self.context, completed_steps, steps_to_do, {}, generator, axes_to_move) elapsed = datetime.now() - start assert elapsed.total_seconds() < 3.0
class TestBeamSelectorPart(ChildTestCase): def setUp(self): self.process = Process("Process") self.context = Context(self.process) self.config_dir = tmp_dir("config_dir") pmac_block = make_block_creator(__file__, "test_pmac_manager_block.yaml") self.child = self.create_child_block( pmac_block, self.process, mri_prefix="PMAC", config_dir=self.config_dir.value, ) # These are the child blocks we are interested in self.child_x = self.process.get_controller("BL45P-ML-STAGE-01:X") # self.child_y = self.process.get_controller( # "BL45P-ML-STAGE-01:Y") self.child_cs1 = self.process.get_controller("PMAC:CS1") self.child_traj = self.process.get_controller("PMAC:TRAJ") self.child_status = self.process.get_controller("PMAC:STATUS") # CS1 needs to have the right port otherwise we will error self.set_attributes(self.child_cs1, port="CS1") self.move_time = 0.5 self.o = BeamSelectorPart( name="beamSelector", mri="PMAC", selector_axis="x", imaging_angle=0, diffraction_angle=0.5, imaging_detector="imagingDetector", diffraction_detector="diffDetector", move_time=self.move_time, ) self.context.set_notify_dispatch_request( self.o.notify_dispatch_request) self.process.start() pass def tearDown(self): del self.context self.process.stop(timeout=1) shutil.rmtree(self.config_dir.value) def set_motor_attributes(self, x_pos=0.5, units="deg", x_acceleration=4.0, x_velocity=10.0): # create some parts to mock # the motion controller and an axis in a CS self.set_attributes( self.child_x, cs="CS1,A", accelerationTime=x_velocity / x_acceleration, resolution=0.001, offset=0.0, maxVelocity=x_velocity, readback=x_pos, velocitySettle=0.0, units=units, ) def _get_detector_table(self, imaging_exposure_time, diffraction_exposure_time): return DetectorTable( [True, True, True], ["imagingDetector", "diffDetector", "PandA"], ["ML-IMAGING-01", "ML-DIFF-01", "ML-PANDA-01"], [imaging_exposure_time, diffraction_exposure_time, 0.0], [1, 1, 2], ) def test_validate_returns_tweaked_generator_duration(self): nCycles = 1 generator = CompoundGenerator([StaticPointGenerator(nCycles)], [], [], duration=0.0) imaging_exposure_time = 0.1 diffraction_exposure_time = 0.3 detectors = self._get_detector_table(imaging_exposure_time, diffraction_exposure_time) # First pass we should tweak infos = self.o.on_validate(generator, {}, detectors) self.assertEqual(infos.parameter, "generator") assert infos.value.duration == pytest.approx(self.move_time * 2 + imaging_exposure_time + diffraction_exposure_time) # Now re-run with our tweaked generator infos = self.o.on_validate(infos.value, {}, detectors) assert infos is None, "We shouldn't need to tweak again" def test_validate_raises_AssertionError_for_bad_generator_type(self): line_generator = LineGenerator("x", "mm", 0.0, 5.0, 10) generator = CompoundGenerator([line_generator], [], [], duration=0.0) imaging_exposure_time = 0.1 diffraction_exposure_time = 0.3 detectors = self._get_detector_table(imaging_exposure_time, diffraction_exposure_time) self.assertRaises(AssertionError, self.o.on_validate, generator, {}, detectors) def test_validate_raises_ValueError_for_detector_with_invalid_frames_per_step( self): nCycles = 1 generator = CompoundGenerator([StaticPointGenerator(nCycles)], [], [], duration=0.0) imaging_exposure_time = 0.1 diffraction_exposure_time = 0.3 bad_imaging_frames_per_step = DetectorTable( [True, True, True], ["imagingDetector", "diffDetector", "PandA"], ["ML-IMAGING-01", "ML-DIFF-01", "ML-PANDA-01"], [imaging_exposure_time, diffraction_exposure_time, 0.0], [3, 1, 2], ) bad_diffraction_frames_per_step = DetectorTable( [True, True, True], ["imagingDetector", "diffDetector", "PandA"], ["ML-IMAGING-01", "ML-DIFF-01", "ML-PANDA-01"], [imaging_exposure_time, diffraction_exposure_time, 0.0], [1, 10, 2], ) self.assertRaises(ValueError, self.o.on_validate, generator, {}, bad_imaging_frames_per_step) self.assertRaises( ValueError, self.o.on_validate, generator, {}, bad_diffraction_frames_per_step, ) def test_validate_raises_ValueError_when_detector_not_enabled(self): nCycles = 1 generator = CompoundGenerator([StaticPointGenerator(nCycles)], [], [], duration=0.0) imaging_exposure_time = 0.1 diffraction_exposure_time = 0.3 detectors_with_imaging_disabled = DetectorTable( [False, True, True], ["imagingDetector", "diffDetector", "PandA"], ["ML-IMAGING-01", "ML-DIFF-01", "ML-PANDA-01"], [imaging_exposure_time, diffraction_exposure_time, 0.0], [1, 1, 2], ) detectors_with_diffraction_disabled = DetectorTable( [True, False, True], ["imagingDetector", "diffDetector", "PandA"], ["ML-IMAGING-01", "ML-DIFF-01", "ML-PANDA-01"], [imaging_exposure_time, diffraction_exposure_time, 0.0], [1, 1, 2], ) self.assertRaises( ValueError, self.o.on_validate, generator, {}, detectors_with_imaging_disabled, ) self.assertRaises( ValueError, self.o.on_validate, generator, {}, detectors_with_diffraction_disabled, ) def test_validate_raises_ValueError_for_detector_with_zero_exposure(self): nCycles = 1 generator = CompoundGenerator([StaticPointGenerator(nCycles)], [], [], duration=0.0) imaging_exposure_time = 0.1 diffraction_exposure_time = 0.3 detectors_with_zero_exposure_for_imaging = self._get_detector_table( 0.0, diffraction_exposure_time) detectors_with_zero_exposure_for_diffraction = self._get_detector_table( imaging_exposure_time, 0.0) self.assertRaises( ValueError, self.o.on_validate, generator, {}, detectors_with_zero_exposure_for_imaging, ) self.assertRaises( ValueError, self.o.on_validate, generator, {}, detectors_with_zero_exposure_for_diffraction, ) def test_validate_raises_ValueError_for_missing_detector(self): nCycles = 1 generator = CompoundGenerator([StaticPointGenerator(nCycles)], [], [], duration=0.0) imaging_exposure_time = 0.1 diffraction_exposure_time = 0.3 table_without_imaging_detector = DetectorTable( [True, True], ["diffDetector", "PandA"], ["ML-DIFF-01", "ML-PANDA-01"], [diffraction_exposure_time, 0.0], [1, 2], ) table_without_diffraction_detector = DetectorTable( [True, True], ["imagingDetector", "PandA"], ["ML-IMAGING-01", "ML-PANDA-01"], [imaging_exposure_time, 0.0], [1, 2], ) self.assertRaises( ValueError, self.o.on_validate, generator, {}, table_without_imaging_detector, ) self.assertRaises( ValueError, self.o.on_validate, generator, {}, table_without_diffraction_detector, ) def test_configure_with_one_cycle(self): self.o.imaging_angle = 50.0 self.o.diffraction_angle = 90.0 self.set_motor_attributes(x_pos=50.0, x_velocity=800.0, x_acceleration=100000.0) nCycles = 1 generator = CompoundGenerator([StaticPointGenerator(nCycles)], [], [], duration=0.0) imaging_exposure_time = 0.1 diffraction_exposure_time = 0.3 detectors = self._get_detector_table(imaging_exposure_time, diffraction_exposure_time) # Update generator duration based on validate method infos = self.o.on_validate(generator, {}, detectors) generator.duration = infos.value.duration generator.prepare() # Run configure self.o.on_configure(self.context, 0, nCycles, {}, generator, detectors, []) # Expected generator duration is sum of exposure times + 2*move_time assert generator.duration == pytest.approx(self.move_time * 2 + imaging_exposure_time + diffraction_exposure_time) # Build up our expected values diffraction_detector_time_row = [2000, 250000, 250000, 2000, 300000] imaging_detector_time_row = [2000, 250000, 250000, 2000, 100000] times = nCycles * (diffraction_detector_time_row + imaging_detector_time_row) + [2000] diffraction_velocity_row = [1, 0, 1, 1, 1] imaging_velocity_row = [1, 0, 1, 1, 1] velocity_modes = nCycles * (diffraction_velocity_row + imaging_velocity_row) + [3] diffraction_detector_program_row = [1, 4, 2, 8, 8] imaging_detector_program_row = [1, 4, 2, 8, 8] user_programs = nCycles * (diffraction_detector_program_row + imaging_detector_program_row) + [1] diffraction_detector_pos_row = [50.0, 70.0, 90.0, 90.08, 90.08] imaging_detector_pos_row = [90.0, 70.0, 50.0, 49.92, 49.92] positions = nCycles * (diffraction_detector_pos_row + imaging_detector_pos_row) + [50.0] completed_steps = [0, 0, 1, 1, 1, 1, 1, 2, 3, 3, 3] assert self.child.handled_requests.mock_calls == [ call.post("writeProfile", csPort="CS1", timeArray=[0.002], userPrograms=[8]), call.post("executeProfile"), call.post("moveCS1", moveTime=0.0017888544, a=49.92), # pytest.approx to allow sensible compare with numpy arrays call.post( "writeProfile", csPort="CS1", timeArray=pytest.approx(times), velocityMode=pytest.approx(velocity_modes), userPrograms=pytest.approx(user_programs), a=pytest.approx(positions), ), ] assert self.o.completed_steps_lookup == completed_steps def test_configure_with_three_cycles(self): self.o.imaging_angle = 50.0 self.o.diffraction_angle = 90.0 self.set_motor_attributes(x_pos=50.0, x_velocity=800.0, x_acceleration=100000.0) nCycles = 3 generator = CompoundGenerator([StaticPointGenerator(nCycles)], [], [], duration=0.0) imaging_exposure_time = 0.1 diffraction_exposure_time = 0.3 detectors = self._get_detector_table(imaging_exposure_time, diffraction_exposure_time) # Update generator duration based on validate method infos = self.o.on_validate(generator, {}, detectors) generator.duration = infos.value.duration generator.prepare() # Run configure self.o.on_configure(self.context, 0, nCycles, {}, generator, detectors, []) # Expected generator duration is sum of exposure times + 2*move_time assert generator.duration == pytest.approx(self.move_time * 2 + imaging_exposure_time + diffraction_exposure_time) # Build up our expected values diffraction_detector_time_row = [2000, 250000, 250000, 2000, 300000] imaging_detector_time_row = [2000, 250000, 250000, 2000, 100000] times = nCycles * (diffraction_detector_time_row + imaging_detector_time_row) + [2000] diffraction_velocity_row = [1, 0, 1, 1, 1] imaging_velocity_row = [1, 0, 1, 1, 1] velocity_modes = nCycles * (diffraction_velocity_row + imaging_velocity_row) + [3] diffraction_detector_program_row = [1, 4, 2, 8, 8] imaging_detector_program_row = [1, 4, 2, 8, 8] user_programs = nCycles * (diffraction_detector_program_row + imaging_detector_program_row) + [1] diffraction_detector_pos_row = [50.0, 70.0, 90.0, 90.08, 90.08] imaging_detector_pos_row = [90.0, 70.0, 50.0, 49.92, 49.92] positions = nCycles * (diffraction_detector_pos_row + imaging_detector_pos_row) + [50.0] completed_steps = [ 0, 0, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 6, 7, 7, 7, ] assert self.child.handled_requests.mock_calls == [ call.post("writeProfile", csPort="CS1", timeArray=[0.002], userPrograms=[8]), call.post("executeProfile"), call.post("moveCS1", moveTime=0.0017888544, a=49.92), # pytest.approx to allow sensible compare with numpy arrays call.post( "writeProfile", csPort="CS1", timeArray=pytest.approx(times), velocityMode=pytest.approx(velocity_modes), userPrograms=pytest.approx(user_programs), a=pytest.approx(positions), ), ] assert self.o.completed_steps_lookup == completed_steps def test_configure_with_one_cycle_with_long_exposure(self): self.o.imaging_angle = 35.0 self.o.diffraction_angle = 125.0 self.set_motor_attributes(x_pos=35.0, x_velocity=800.0, x_acceleration=100000.0) nCycles = 1 generator = CompoundGenerator([StaticPointGenerator(nCycles)], [], [], duration=0.0) imaging_exposure_time = 4.0 diffraction_exposure_time = 10.0 detectors = self._get_detector_table(imaging_exposure_time, diffraction_exposure_time) # Update generator duration based on validate method infos = self.o.on_validate(generator, {}, detectors) generator.duration = infos.value.duration generator.prepare() # Run configure self.o.on_configure(self.context, 0, nCycles, {}, generator, detectors, []) # Expected generator duration is sum of exposure times + 2*move_time assert (generator.duration == self.move_time * 2 + imaging_exposure_time + diffraction_exposure_time) # Build up our expected values diffraction_detector_time_row = [ 2000, 250000, 250000, 2000, 3333333, 3333334, 3333333, ] imaging_detector_time_row = [2000, 250000, 250000, 2000, 4000000] times = nCycles * (diffraction_detector_time_row + imaging_detector_time_row) + [2000] diffraction_velocity_row = [1, 0, 1, 1, 0, 0, 1] imaging_velocity_row = [1, 0, 1, 1, 1] velocity_modes = nCycles * (diffraction_velocity_row + imaging_velocity_row) + [3] diffraction_detector_program_row = [1, 4, 2, 8, 0, 0, 8] imaging_detector_program_row = [1, 4, 2, 8, 8] user_programs = nCycles * (diffraction_detector_program_row + imaging_detector_program_row) + [1] diffraction_detector_pos_row = [ 35.0, 80.0, 125.0, 125.18, 125.18, 125.18, 125.18, ] imaging_detector_pos_row = [125.0, 80.0, 35.0, 34.82, 34.82] positions = nCycles * (diffraction_detector_pos_row + imaging_detector_pos_row) + [35.0] completed_steps = [0, 0, 1, 1, 1, 1, 1, 1, 1, 2, 3, 3, 3] assert self.child.handled_requests.mock_calls == [ call.post("writeProfile", csPort="CS1", timeArray=[0.002], userPrograms=[8]), call.post("executeProfile"), call.post("moveCS1", moveTime=0.0026832816, a=34.82), # pytest.approx to allow sensible compare with numpy arrays call.post( "writeProfile", csPort="CS1", timeArray=pytest.approx(times), velocityMode=pytest.approx(velocity_modes), userPrograms=pytest.approx(user_programs), a=pytest.approx(positions), ), ] assert self.o.completed_steps_lookup == completed_steps def test_configure_with_three_cycles_with_long_exposure(self): self.o.imaging_angle = 35.0 self.o.diffraction_angle = 125.0 self.set_motor_attributes(x_pos=35.0, x_velocity=800.0, x_acceleration=100000.0) nCycles = 3 generator = CompoundGenerator([StaticPointGenerator(nCycles)], [], [], duration=0.0) imaging_exposure_time = 4.0 diffraction_exposure_time = 10.0 detectors = self._get_detector_table(imaging_exposure_time, diffraction_exposure_time) # Update generator duration based on validate method infos = self.o.on_validate(generator, {}, detectors) generator.duration = infos.value.duration generator.prepare() # Run configure self.o.on_configure(self.context, 0, nCycles, {}, generator, detectors, []) # Expected generator duration is sum of exposure times + 2*move_time assert (generator.duration == self.move_time * 2 + imaging_exposure_time + diffraction_exposure_time) # Build up our expected values diffraction_detector_time_row = [ 2000, 250000, 250000, 2000, 3333333, 3333334, 3333333, ] imaging_detector_time_row = [2000, 250000, 250000, 2000, 4000000] times = nCycles * (diffraction_detector_time_row + imaging_detector_time_row) + [2000] diffraction_velocity_row = [1, 0, 1, 1, 0, 0, 1] imaging_velocity_row = [1, 0, 1, 1, 1] velocity_modes = nCycles * (diffraction_velocity_row + imaging_velocity_row) + [3] diffraction_detector_program_row = [1, 4, 2, 8, 0, 0, 8] imaging_detector_program_row = [1, 4, 2, 8, 8] user_programs = nCycles * (diffraction_detector_program_row + imaging_detector_program_row) + [1] diffraction_detector_pos_row = [ 35.0, 80.0, 125.0, 125.18, 125.18, 125.18, 125.18, ] imaging_detector_pos_row = [125.0, 80.0, 35.0, 34.82, 34.82] positions = nCycles * (diffraction_detector_pos_row + imaging_detector_pos_row) + [35.0] completed_steps = [ 0, 0, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 6, 7, 7, 7, ] assert self.child.handled_requests.mock_calls == [ call.post("writeProfile", csPort="CS1", timeArray=[0.002], userPrograms=[8]), call.post("executeProfile"), call.post("moveCS1", moveTime=0.0026832816, a=34.82), # pytest.approx to allow sensible compare with numpy arrays call.post( "writeProfile", csPort="CS1", timeArray=pytest.approx(times), velocityMode=pytest.approx(velocity_modes), userPrograms=pytest.approx(user_programs), a=pytest.approx(positions), ), ] assert self.o.completed_steps_lookup == completed_steps def test_configure_with_exposure_time_less_than_min_turnaround(self): self.o.imaging_angle = 50.0 self.o.diffraction_angle = 90.0 self.set_motor_attributes(x_pos=50.0, x_velocity=800.0, x_acceleration=100000.0) nCycles = 1 generator = CompoundGenerator([StaticPointGenerator(nCycles)], [], [], duration=0.0) imaging_exposure_time = 0.0001 diffraction_exposure_time = 0.3 detectors = self._get_detector_table(imaging_exposure_time, diffraction_exposure_time) # Update generator duration based on validate method infos = self.o.on_validate(generator, {}, detectors) generator.duration = infos.value.duration generator.prepare() # Run configure self.o.on_configure(self.context, 0, nCycles, {}, generator, detectors, []) # Expected generator duration is affected by min turnaround time assert generator.duration == pytest.approx(self.move_time * 2 + MIN_TIME + diffraction_exposure_time) # Build up our expected values diffraction_detector_time_row = [2000, 250000, 250000, 2000, 300000] imaging_detector_time_row = [2000, 250000, 250000] times = nCycles * (diffraction_detector_time_row + imaging_detector_time_row) + [2000] diffraction_velocity_row = [1, 0, 1, 1, 1] imaging_velocity_row = [1, 0, 1] velocity_modes = nCycles * (diffraction_velocity_row + imaging_velocity_row) + [3] diffraction_detector_program_row = [1, 4, 2, 8, 8] imaging_detector_program_row = [1, 4, 2] user_programs = nCycles * (diffraction_detector_program_row + imaging_detector_program_row) + [1] diffraction_detector_pos_row = [50.0, 70.0, 90.0, 90.08, 90.08] imaging_detector_pos_row = [90.0, 70.0, 50.0] positions = nCycles * (diffraction_detector_pos_row + imaging_detector_pos_row) + [50.0] completed_steps = [0, 0, 1, 1, 1, 1, 1, 2, 3] assert self.child.handled_requests.mock_calls == [ call.post("writeProfile", csPort="CS1", timeArray=[0.002], userPrograms=[8]), call.post("executeProfile"), call.post("moveCS1", moveTime=0.0017888544, a=49.92), # pytest.approx to allow sensible compare with numpy arrays call.post( "writeProfile", csPort="CS1", timeArray=pytest.approx(times), velocityMode=pytest.approx(velocity_modes), userPrograms=pytest.approx(user_programs), a=pytest.approx(positions), ), ] assert self.o.completed_steps_lookup == completed_steps def test_configure_raises_ValueError_with_invalid_frames_per_step(self): self.set_motor_attributes() nCycles = 1 generator = CompoundGenerator([StaticPointGenerator(nCycles)], [], [], duration=0.0) generator.prepare() imaging_exposure_time = 0.01 diffraction_exposure_time = 1.0 detectors_with_bad_imaging_frames_per_step = DetectorTable( [True, True, True], ["imagingDetector", "diffDetector", "PandA"], ["ML-IMAGING-01", "ML-DIFF-01", "ML-PANDA-01"], [imaging_exposure_time, diffraction_exposure_time, 0.0], [3, 1, 2], ) detectors_with_bad_diffraction_frames_per_step = DetectorTable( [True, True, True], ["imagingDetector", "diffDetector", "PandA"], ["ML-IMAGING-01", "ML-DIFF-01", "ML-PANDA-01"], [imaging_exposure_time, diffraction_exposure_time, 0.0], [1, 10, 2], ) self.assertRaises( ValueError, self.o.on_configure, self.context, 0, nCycles, {}, generator, detectors_with_bad_imaging_frames_per_step, [], ) self.assertRaises( ValueError, self.o.on_configure, self.context, 0, nCycles, {}, generator, detectors_with_bad_diffraction_frames_per_step, [], ) def test_configure_raises_ValueError_when_detector_not_enabled(self): nCycles = 1 generator = CompoundGenerator([StaticPointGenerator(nCycles)], [], [], duration=0.0) imaging_exposure_time = 0.1 diffraction_exposure_time = 0.3 detectors_with_imaging_disabled = DetectorTable( [False, True, True], ["imagingDetector", "diffDetector", "PandA"], ["ML-IMAGING-01", "ML-DIFF-01", "ML-PANDA-01"], [imaging_exposure_time, diffraction_exposure_time, 0.0], [1, 1, 2], ) detectors_with_diffraction_disabled = DetectorTable( [True, False, True], ["imagingDetector", "diffDetector", "PandA"], ["ML-IMAGING-01", "ML-DIFF-01", "ML-PANDA-01"], [imaging_exposure_time, diffraction_exposure_time, 0.0], [1, 1, 2], ) self.assertRaises( ValueError, self.o.on_configure, self.context, 0, nCycles, {}, generator, detectors_with_imaging_disabled, [], ) self.assertRaises( ValueError, self.o.on_configure, self.context, 0, nCycles, {}, generator, detectors_with_diffraction_disabled, [], ) def test_configure_raises_ValueError_when_exposure_is_zero(self): nCycles = 1 generator = CompoundGenerator([StaticPointGenerator(nCycles)], [], [], duration=0.0) imaging_exposure_time = 0.1 diffraction_exposure_time = 0.3 detectors_with_imaging_zero_exposure = DetectorTable( [False, True, True], ["imagingDetector", "diffDetector", "PandA"], ["ML-IMAGING-01", "ML-DIFF-01", "ML-PANDA-01"], [imaging_exposure_time, 0.0, 0.0], [1, 1, 2], ) detectors_with_diffraction_zero_exposure = DetectorTable( [True, False, True], ["imagingDetector", "diffDetector", "PandA"], ["ML-IMAGING-01", "ML-DIFF-01", "ML-PANDA-01"], [0.0, diffraction_exposure_time, 0.0], [1, 1, 2], ) self.assertRaises( ValueError, self.o.on_configure, self.context, 0, nCycles, {}, generator, detectors_with_imaging_zero_exposure, [], ) self.assertRaises( ValueError, self.o.on_configure, self.context, 0, nCycles, {}, generator, detectors_with_diffraction_zero_exposure, [], ) def test_configure_raises_ValueError_with_missing_detector(self): self.set_motor_attributes() nCycles = 1 generator = CompoundGenerator([StaticPointGenerator(nCycles)], [], [], duration=0.0) generator.prepare() exposure_time = 0.01 detectors_without_diffraction = DetectorTable( [True, True], ["imagingDetector", "PandA"], ["ML-IMAGING-01", "ML-PANDA-01"], [exposure_time, 0.0], [1, 2], ) detectors_without_imaging = DetectorTable( [True, True], ["diffDetector", "PandA"], ["ML-DIFF-01", "ML-PANDA-01"], [exposure_time, 0.0], [1, 2], ) self.assertRaises( ValueError, self.o.on_configure, self.context, 0, nCycles, {}, generator, detectors_without_diffraction, [], ) self.assertRaises( ValueError, self.o.on_configure, self.context, 0, nCycles, {}, generator, detectors_without_imaging, [], ) def test_invalid_parameters_raise_ValueError(self): # Some valid parameters name = "beamSelectorPart" mri = "PMAC" selector_axis = "x" imaging_angle = 30.0 diffraction_angle = 65.0 imaging_detector = "imagingDetector" diffraction_detector = "diffDetector" move_time = 0.25 # Check the valid parameters BeamSelectorPart( name, mri, selector_axis, imaging_angle, diffraction_angle, imaging_detector, diffraction_detector, move_time, ) # Mix with one of these invalid parameters invalid_selector_axes = [0.0, 1] invalid_angles = ["not_an_angle"] invalid_detector_names = [10, 53.3] invalid_move_times = ["this is not a number", -1.0, 0.0, "-0.45"] # Now we check they raise errors for invalid_axis in invalid_selector_axes: self.assertRaises( ValueError, BeamSelectorPart, name, mri, invalid_axis, imaging_angle, diffraction_angle, imaging_detector, diffraction_detector, move_time, ) for invalid_angle in invalid_angles: self.assertRaises( ValueError, BeamSelectorPart, name, mri, selector_axis, invalid_angle, diffraction_angle, imaging_detector, diffraction_detector, move_time, ) self.assertRaises( ValueError, BeamSelectorPart, name, mri, selector_axis, imaging_angle, invalid_angle, imaging_detector, diffraction_detector, move_time, ) for invalid_detector_name in invalid_detector_names: self.assertRaises( ValueError, BeamSelectorPart, name, mri, selector_axis, imaging_angle, diffraction_angle, invalid_detector_name, diffraction_detector, move_time, ) self.assertRaises( ValueError, BeamSelectorPart, name, mri, selector_axis, imaging_angle, diffraction_angle, imaging_detector, invalid_detector_name, move_time, ) for invalid_move_time in invalid_move_times: self.assertRaises( ValueError, BeamSelectorPart, name, mri, selector_axis, imaging_angle, diffraction_angle, imaging_detector, diffraction_detector, invalid_move_time, )