Esempio n. 1
0
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"]
Esempio n. 2
0
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
Esempio n. 5
0
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,
            )