Beispiel #1
0
def _get_blocks(context: Context, panda_mri: str) -> List[Block]:
    """Get panda, seqA and seqB Blocks using the given context"""
    # {part_name: export_name}
    panda = context.block_view(panda_mri)
    seq_part_names = {}
    for source, export in panda.exports.value.rows():
        if export in SEQ_TABLES:
            assert source.endswith(
                ".table"
            ), "Expected export %s to come from SEQx.table, got %s" % (export,
                                                                       source)
            seq_part_names[source[:-len(".table")]] = export
    assert (tuple(sorted(seq_part_names.values())) == SEQ_TABLES
            ), "Expected exported attributes %s, got %s" % (
                SEQ_TABLES,
                panda.exports.value.export,
            )
    # {export_name: mri}
    seq_mris = {}
    for name, mri, _, _, _ in panda.layout.value.rows():
        if name in seq_part_names:
            export = seq_part_names[name]
            seq_mris[export] = mri
    assert sorted(seq_mris) == sorted(seq_part_names.values(
    )), "Couldn't find MRI for some of %s" % (seq_part_names.values(), )
    blocks = [panda]
    blocks += [context.block_view(seq_mris[x]) for x in SEQ_TABLES]
    return blocks
class TestRunnableControllerCollectsAllParams(unittest.TestCase):
    def setUp(self):
        self.p = Process('process1')
        self.context = Context(self.p)

    def tearDown(self):
        self.p.stop(timeout=1)

    def test_no_hook_passes(self):
        # create a root block for the RunnableController block to reside in
        self.c = call_with_params(
            RunnableController,
            self.p, [PartTester1("1"), PartTester2("2")],
            mri='mainBlock',
            configDir="/tmp",
            axesToMove=["x"])
        self.p.add_controller('mainBlock', self.c)
        self.b = self.context.block_view("mainBlock")

        # start the process off
        self.p.start()

        takes = list(self.b.configure.takes.elements)
        self.assertEqual(takes, ["size", "generator", "axesToMove"])

    def test_hook_fails(self):
        # create a root block for the RunnableController block to reside in
        self.c = call_with_params(
            RunnableController,
            self.p, [PartTester1("1"), PartTester3("2")],
            mri='mainBlock',
            configDir="/tmp",
            axesToMove=["x"])
        self.p.add_controller('mainBlock', self.c)
        self.b = self.context.block_view("mainBlock")

        # start the process off
        self.p.start()

        takes = list(self.b.configure.takes.elements)
        self.assertEqual(takes, ["size", "generator", "axesToMove"])

    def test_hook_plus_method_takes_nothing_passes(self):
        # create a root block for the RunnableController block to reside in
        self.c = call_with_params(
            RunnableController,
            self.p, [PartTester1("1"), PartTester4("2")],
            mri='mainBlock',
            configDir="/tmp",
            axesToMove=["x"])
        self.p.add_controller('mainBlock', self.c)
        self.b = self.context.block_view("mainBlock")

        # start the process off
        self.p.start()

        takes = list(self.b.configure.takes.elements)
        self.assertEqual(takes, ["size", "generator", "axesToMove"])
 def test_set_export_parts(self):
     context = Context(self.p)
     b = context.block_view("mainBlock")
     assert list(b) == [
         'meta', 'health', 'state', 'layout', 'design', 'exports',
         'modified', 'disable', 'reset', 'save', 'attr'
     ]
     new_exports = Table(self.c.exports.meta)
     new_exports.append(('part2.attr', 'childAttr'))
     new_exports.append(('part2.reset', 'childReset'))
     self.c.set_exports(new_exports)
     assert self.c.modified.value == True
     assert self.c.modified.alarm.message == "exports changed"
     call_with_params(self.c.save, design='testSaveLayout')
     assert self.c.modified.value == False
     # block has changed, get a new view
     b = context.block_view("mainBlock")
     assert list(b) == [
         'meta', 'health', 'state', 'layout', 'design', 'exports',
         'modified', 'disable', 'reset', 'save', 'attr', 'childAttr',
         'childReset'
     ]
     assert self.c.state.value == "Ready"
     assert b.childAttr.value == "defaultv"
     assert self.c.modified.value == False
     m = MagicMock()
     f = b.childAttr.subscribe_value(m)
     # allow a subscription to come through
     context.sleep(0.1)
     m.assert_called_once_with("defaultv")
     m.reset_mock()
     self.c_part.attr.set_value("newv")
     assert b.childAttr.value == "newv"
     assert self.c_part.attr.value == "newv"
     assert self.c.modified.value == True
     assert self.c.modified.alarm.message == \
            "part2.attr.value = 'newv' not 'defaultv'"
     # allow a subscription to come through
     context.sleep(0.1)
     m.assert_called_once_with("newv")
     b.childAttr.put_value("again")
     assert b.childAttr.value == "again"
     assert self.c_part.attr.value == "again"
     assert self.c.modified.value == True
     assert self.c.modified.alarm.message == \
            "part2.attr.value = 'again' not 'defaultv'"
     # remove the field
     new_exports = Table(self.c.exports.meta)
     self.c.set_exports(new_exports)
     assert self.c.modified.value == True
     call_with_params(self.c.save)
     assert self.c.modified.value == False
     # block has changed, get a new view
     b = context.block_view("mainBlock")
     assert "childAttr" not in b
class TestIocIconPart(unittest.TestCase):
    def add_part_and_start(self):
        self.icon = IocIconPart(
            "ICON",
            os.path.split(__file__)[0] +
            "/../../../malcolm/modules/system/icons/epics-logo.svg",
        )
        self.c1.add_part(self.icon)
        self.p.add_controller(self.c1)
        self.p.start()

    def setUp(self):
        self.p = Process("process1")
        self.context = Context(self.p)
        self.c1 = RunnableController(mri="SYS",
                                     config_dir="/tmp",
                                     use_git=False)

    def tearDown(self):
        self.p.stop(timeout=1)

    @patch("malcolm.modules.ca.util.CAAttribute")
    def test_has_pv(self, CAAttribute):
        self.add_part_and_start()
        CAAttribute.assert_called_once_with(
            ANY,
            catools.DBR_STRING,
            "",
            "ICON:KERNEL_VERS",
            throw=False,
            callback=self.icon.update_icon,
        )
        assert isinstance(CAAttribute.call_args[0][0], StringMeta)
        meta = CAAttribute.call_args[0][0]
        assert meta.description == "Host Architecture"
        assert not meta.writeable
        assert len(meta.tags) == 0

    def test_adds_correct_icons(self):
        self.add_part_and_start()
        assert self.context.block_view("SYS").icon.value == defaultIcon

        arch = MockPv("windows")
        self.icon.update_icon(arch)
        assert self.context.block_view("SYS").icon.value == winIcon

        arch = MockPv("WIND")
        self.icon.update_icon(arch)
        assert self.context.block_view("SYS").icon.value == vxIcon

        arch = MockPv("linux")
        self.icon.update_icon(arch)
        assert self.context.block_view("SYS").icon.value == linuxIcon
Beispiel #5
0
class TestMotorPreMovePart(ChildTestCase):
    def setUp(self):
        self.process = Process("test_process")
        self.context = Context(self.process)

        # Create a raw motor mock to handle axis request
        self.child = self.create_child_block(raw_motor_block,
                                             self.process,
                                             mri="BS",
                                             pv_prefix="PV:PRE")
        # Add Beam Selector object
        self.o = MotorPreMovePart(name="MotorPreMovePart", mri="BS", demand=50)

        controller = RunnableController("SCAN", "/tmp")
        controller.add_part(self.o)

        self.process.add_controller(controller)
        self.process.start()

    def tearDown(self):
        del self.context
        self.process.stop(timeout=1)

    def test_bs(self):
        b = self.context.block_view("SCAN")
        generator = CompoundGenerator([LineGenerator("x", "mm", 0, 1, 10)], [],
                                      [], 0.1)
        b.configure(generator)

        self.o.on_configure(self.context)

        assert self.child.handled_requests.mock_calls == [
            call.put("demand", 50)
        ]
Beispiel #6
0
def cs_axis_numbers(
    context: Context,
    layout_table: builtin.util.LayoutTable,
    cs_port: str,
) -> Dict[str, int]:
    """Given the layout table of a PMAC, get the axis number for each name
    in the table which is in the specified CS"""
    axis_numbers = {}
    for name, mri in zip(layout_table.name, layout_table.mri):
        child = context.block_view(mri)
        try:
            axis_number = child.axisNumber.value
        except (AttributeError, KeyError):
            axis_number = None
        try:
            cs = child.cs.value
        except (AttributeError, KeyError):
            cs = None

        if cs and axis_number:
            child_cs_port = child.cs.value.split(",", 1)[0]
            if cs_port == child_cs_port:
                if name in axis_numbers:
                    axis_numbers[name] = axis_number
                else:
                    axis_numbers[name] = axis_number
    return axis_numbers
Beispiel #7
0
 def test_save(self):
     assert self.c.design.value == ""
     assert self.c.design.meta.choices == [""]
     c = Context(self.p)
     li = []
     c.subscribe(["mainBlock", "design", "meta"], li.append)
     # Wait for long enough for the other process to get a look in
     c.sleep(0.1)
     assert len(li) == 1
     assert li.pop()["choices"] == [""]
     b = c.block_view("mainBlock")
     design_name = "testSaveLayout"
     b.save(designName=design_name)
     assert len(li) == 3
     assert li[0]["writeable"] is False
     assert li[1]["choices"] == ["", design_name]
     assert li[2]["writeable"] is True
     assert self.c.design.meta.choices == ["", design_name]
     self.check_expected_save(design_name)
     assert self.c.state.value == "Ready"
     assert self.c.design.value == design_name
     assert self.c.modified.value is False
     os.remove(self._get_design_filename(self.main_block_name, design_name))
     self.c_part.attr.set_value("newv")
     assert self.c.modified.value is True
     assert (self.c.modified.alarm.message ==
             "part2.attr.value = 'newv' not 'defaultv'")
     self.c.save(designName="")
     self.check_expected_save(design_name, attr="newv")
     assert self.c.design.value == "testSaveLayout"
Beispiel #8
0
 def abort_after_1s(self):
     # Need a new context as in a different cothread
     c = Context(self.p)
     b = c.block_view("mainBlock")
     c.sleep(1.0)
     self.checkState(self.ss.RUNNING)
     b.abort()
     self.checkState(self.ss.ABORTED)
Beispiel #9
0
 def abort_detector(self, context: Context) -> None:
     child = context.block_view(self.mri)
     child.stop()
     # Stop is a put to a busy record which returns immediately
     # The detector might take a while to actually stop so use the
     # acquiring pv (which is the same asyn parameter as the busy record
     # that stop() pokes) to check that it has finished
     child.when_value_matches("acquiring", False, timeout=DEFAULT_TIMEOUT)
    def setup_detector(
        self,
        context: Context,
        completed_steps: scanning.hooks.ACompletedSteps,
        steps_to_do: scanning.hooks.AStepsToDo,
        num_images: int,
        duration: float,
        part_info: scanning.hooks.APartInfo,
        initial_configure: bool = True,
        **kwargs: Any,
    ) -> None:
        if initial_configure:
            # This is an initial configure, so reset arrayCounter to 0
            array_counter = 0
            self.done_when_reaches = steps_to_do
        else:
            # This is rewinding or setting up for another batch,
            # skip to a uniqueID that has not been produced yet
            array_counter = self.done_when_reaches
            self.done_when_reaches += steps_to_do
        self.uniqueid_offset = completed_steps - array_counter

        child = context.block_view(self.mri)
        for k, v in dict(
                arrayCounter=array_counter,
                imageMode=self.multiple_image_mode,
                numImages=num_images,
                arrayCallbacks=True,
        ).items():
            if k not in kwargs and k in child:
                kwargs[k] = v

        # Ignore exposure time attribute
        kwargs.pop("exposure", None)

        child.put_attribute_values(kwargs)

        # Calculate number of samples
        post_trigger_samples = self._number_of_adc_samples(duration)
        if self.is_hardware_triggered:
            if self.gated_trigger:
                # Gated signal is responsible for number of samples
                post_trigger_samples = 0
                # We also need averaging to ensure we get consistent frame dimensions
                child.averageSamples.put_value("Yes")
            else:
                # For getting just start triggers, ensure we do not miss one
                post_trigger_samples -= 1
                assert (
                    post_trigger_samples > 0
                ), f"Generator duration {duration} too short for start triggers"
        else:
            # Need triggerOffCondition to be Always On for Software triggers
            assert (
                child.triggerOffCondition.value == "Always On"
            ), "Software triggering requires off condition to be 'Always On'"
        child.postCount.put_value(post_trigger_samples)
Beispiel #11
0
def cs_axis_mapping(
    context: Context,
    layout_table: builtin.util.LayoutTable,
    axes_to_move: Sequence[str],
) -> Dict[str, MotorInfo]:
    """Given the layout table of a PMAC, create a MotorInfo for every axis in
    axes_to_move that isn't generated by a StaticPointGenerator. Check that they
    are all in the same CS"""
    cs_ports: Set[str] = set()
    axis_mapping: Dict[str, MotorInfo] = {}
    for name, mri in zip(layout_table.name, layout_table.mri):
        if name in axes_to_move:
            child = context.block_view(mri)
            max_velocity = child.maxVelocity.value * (
                child.maxVelocityPercent.value / 100.0)
            acceleration = float(max_velocity) / child.accelerationTime.value
            cs = child.cs.value
            if cs:
                cs_port, cs_axis = child.cs.value.split(",", 1)
            else:
                cs_port, cs_axis = "", ""
            assert cs_axis in CS_AXIS_NAMES, "Can only scan 1-1 mappings, %r is %r" % (
                name,
                cs_axis,
            )
            cs_ports.add(cs_port)
            axis_mapping[name] = MotorInfo(
                cs_axis=cs_axis,
                cs_port=cs_port,
                acceleration=acceleration,
                resolution=child.resolution.value,
                offset=child.offset.value,
                max_velocity=max_velocity,
                current_position=child.readback.value,
                scannable=name,
                velocity_settle=child.velocitySettle.value,
                units=child.units.value,
                user_high_limit=child.userHighLimit.value,
                user_low_limit=child.userLowLimit.value,
                dial_high_limit=child.dialHighLimit.value,
                dial_low_limit=child.dialLowLimit.value,
            )
    missing = list(set(axes_to_move) - set(axis_mapping))
    assert not missing, "Some scannables %s are not in the CS mapping %s" % (
        missing,
        axis_mapping,
    )
    assert len(
        cs_ports) <= 1, "Requested axes %s are in multiple CS numbers %s" % (
            axes_to_move,
            list(cs_ports),
        )
    cs_axis_counts = Counter([x.cs_axis for x in axis_mapping.values()])
    # Any cs_axis defs that are used for more that one raw motor
    overlap = [k for k, v in cs_axis_counts.items() if v > 1]
    assert not overlap, f"CS axis defs {overlap} have more that one raw motor attached"
    return axis_mapping
Beispiel #12
0
def cs_port_with_motors_in(
    context: Context,
    layout_table: builtin.util.LayoutTable,
) -> str:
    for mri in layout_table.mri:
        child = context.block_view(mri)
        cs = child.cs.value
        if cs:
            cs_port, cs_axis = child.cs.value.split(",", 1)
            if cs_axis in CS_AXIS_NAMES:
                return cs_port
    raise ValueError(f"Can't find a cs port to use in {layout_table.name}")
 def test_save(self):
     self.c._run_git_cmd = MagicMock()
     assert self.c.design.value == ""
     assert self.c.design.meta.choices == [""]
     c = Context(self.p)
     li = []
     c.subscribe(["mainBlock", "design", "meta"], li.append)
     # Wait for long enough for the other process to get a look in
     c.sleep(0.1)
     assert len(li) == 1
     assert li.pop()["choices"] == [""]
     b = c.block_view("mainBlock")
     b.save(designName="testSaveLayout")
     assert len(li) == 3
     assert li[0]["writeable"] is False
     assert li[1]["choices"] == ["", "testSaveLayout"]
     assert li[2]["writeable"] is True
     assert self.c.design.meta.choices == ["", "testSaveLayout"]
     self.check_expected_save()
     assert self.c.state.value == "Ready"
     assert self.c.design.value == "testSaveLayout"
     assert self.c.modified.value is False
     os.remove("/tmp/mainBlock/testSaveLayout.json")
     self.c_part.attr.set_value("newv")
     assert self.c.modified.value is True
     assert (
         self.c.modified.alarm.message == "part2.attr.value = 'newv' not 'defaultv'"
     )
     self.c.save(designName="")
     self.check_expected_save(attr="newv")
     assert self.c.design.value == "testSaveLayout"
     assert self.c._run_git_cmd.call_args_list == [
         call("add", "/tmp/mainBlock/testSaveLayout.json"),
         call(
             "commit",
             "--allow-empty",
             "-m",
             "Saved mainBlock testSaveLayout",
             "/tmp/mainBlock/testSaveLayout.json",
         ),
         call("add", "/tmp/mainBlock/testSaveLayout.json"),
         call(
             "commit",
             "--allow-empty",
             "-m",
             "Saved mainBlock testSaveLayout",
             "/tmp/mainBlock/testSaveLayout.json",
         ),
     ]
    def wait_for_detector(
        self,
        context: Context,
        registrar: PartRegistrar,
        event_timeout: Optional[float] = None,
    ) -> None:
        child = context.block_view(self.mri)
        child.arrayCounterReadback.subscribe_value(self.update_completed_steps,
                                                   registrar)

        # Wait for the array counter to reach the desired value. If any one frame takes
        # more than event_timeout to appear, consider scan dead
        child.when_value_matches(
            "arrayCounterReadback",
            self.done_when_reaches,
            event_timeout=event_timeout,
        )
Beispiel #15
0
    def setup_detector(
        self,
        context: Context,
        completed_steps: scanning.hooks.ACompletedSteps,
        steps_to_do: scanning.hooks.AStepsToDo,
        duration: float,
        part_info: scanning.hooks.APartInfo,
        **kwargs: Any,
    ):
        # Calculate the readout time
        child = context.block_view(self.mri)
        if child.andorFrameTransferMode.value:
            # Set exposure to zero and use accumulation period for readout time
            exposure = 0.0
            child.exposure.put_value(exposure)
            child.acquirePeriod.put_value(exposure)
            readout_time = child.andorAccumulatePeriod.value
            # With frame transfer mode enabled, the exposure is the time between
            # triggers. The epics 'acquireTime' (exposure) actually becomes the
            # User Defined delay before acquisition start after the trigger. The
            # duration floor becomes the readout time
            assert duration > readout_time, (
                "The duration: %s has to be longer than the Andor 2 readout "
                "time: %s." % (duration, readout_time))
            period = readout_time
        else:
            # Behaves like a "normal" detector
            child.exposure.put_value(duration)
            child.acquirePeriod.put_value(duration)
            # Readout time can be approximated from difference in exposure time
            # and acquire period
            readout_time = child.acquirePeriod.value - child.exposure.value
            # Calculate the adjusted exposure time
            (exposure,
             period) = self.get_adjusted_exposure_time_and_acquire_period(
                 duration, readout_time, kwargs.get("exposure", 0))

        # The real exposure
        self.exposure.set_value(exposure)
        kwargs["exposure"] = exposure

        super().setup_detector(context, completed_steps, steps_to_do, duration,
                               part_info, **kwargs)

        child.acquirePeriod.put_value(period)
Beispiel #16
0
 def wait_for_detector(
     self,
     context: Context,
     registrar: PartRegistrar,
     event_timeout: Optional[float] = None,
 ) -> None:
     child = context.block_view(self.mri)
     child.arrayCounterReadback.subscribe_value(
         self.update_completed_steps, registrar
     )
     # If no new frames produced in event_timeout seconds, consider scan dead
     context.wait_all_futures(self.start_future, event_timeout=event_timeout)
     # Now wait to make sure any update_completed_steps come in. Give
     # it 5 seconds to timeout just in case there are any stray frames that
     # haven't made it through yet
     child.when_value_matches(
         "arrayCounterReadback", self.done_when_reaches, timeout=DEFAULT_TIMEOUT
     )
 def setup_detector(
     self,
     context: Context,
     completed_steps: scanning.hooks.ACompletedSteps,
     steps_to_do: scanning.hooks.AStepsToDo,
     num_images: int,
     duration: float,
     part_info: scanning.hooks.APartInfo,
     initial_configure: bool = True,
     **kwargs: Any,
 ) -> None:
     child = context.block_view(self.mri)
     if initial_configure:
         # This is an initial configure, so reset arrayCounter to 0
         array_counter = 0
         self.done_when_reaches = steps_to_do
     else:
         # This is rewinding or setting up for another batch,
         # skip to a uniqueID that has not been produced yet
         array_counter = self.done_when_reaches
         self.done_when_reaches += steps_to_do
     self.uniqueid_offset = completed_steps - array_counter
     for k, v in dict(
             arrayCounter=array_counter,
             imageMode=self.multiple_image_mode,
             numImages=num_images,
             arrayCallbacks=True,
     ).items():
         if k not in kwargs and k in child:
             kwargs[k] = v
     child.put_attribute_values(kwargs)
     # Might need to reset acquirePeriod as it's sometimes wrong
     # in some detectors
     try:
         info: ExposureDeadtimeInfo = ExposureDeadtimeInfo.filter_single_value(
             part_info)
     except BadValueError:
         # This is ok, no exposure info
         pass
     else:
         exposure = kwargs.get("exposure",
                               info.calculate_exposure(duration))
         child.acquirePeriod.put_value(exposure + info.readout_time)
class TestRunnableController(unittest.TestCase):
    def setUp(self):
        self.p = Process('process1')
        self.context = Context(self.p)

        # Make a ticker_block block to act as our child
        for c in ticker_block(mri="childBlock", config_dir="/tmp"):
            self.p.add_controller(c)
        self.b_child = self.context.block_view("childBlock")

        # Make an empty part for our parent
        part1 = Part("part1")

        # Make a RunnableChildPart to control the ticker_block
        part2 = RunnableChildPart(mri='childBlock',
                                  name='part2',
                                  initial_visibility=True)

        # create a root block for the RunnableController block to reside in
        self.c = RunnableController(mri='mainBlock', config_dir="/tmp")
        self.c.add_part(part1)
        self.c.add_part(part2)
        self.p.add_controller(self.c)
        self.b = self.context.block_view("mainBlock")
        self.ss = self.c.state_set

        # start the process off
        self.checkState(self.ss.DISABLED)
        self.p.start()
        self.checkState(self.ss.READY)

    def tearDown(self):
        self.p.stop(timeout=1)

    def checkState(self, state, child=True, parent=True):
        if child:
            assert self.b_child.state.value == state
        if parent:
            assert self.c.state.value == state

    def checkSteps(self, configured, completed, total):
        assert self.b.configuredSteps.value == configured
        assert self.b.completedSteps.value == completed
        assert self.b.totalSteps.value == total
        assert self.b_child.configuredSteps.value == configured
        assert self.b_child.completedSteps.value == completed
        assert self.b_child.totalSteps.value == total

    def test_init(self):
        assert self.c.completed_steps.value == 0
        assert self.c.configured_steps.value == 0
        assert self.c.total_steps.value == 0
        assert list(self.b.configure.takes.elements) == \
               ["generator", "axesToMove", "exceptionStep"]

    def test_reset(self):
        self.c.disable()
        self.checkState(self.ss.DISABLED)
        self.c.reset()
        self.checkState(self.ss.READY)

    def test_modify_child(self):
        # Save an initial setting for the child
        self.b_child.save("init_child")
        assert self.b_child.modified.value is False
        x = self.context.block_view("COUNTERX")
        x.counter.put_value(31)
        # x counter now at 31, child should be modified
        assert x.counter.value == 31
        assert self.b_child.modified.value is True
        assert self.b_child.modified.alarm.severity == AlarmSeverity.MINOR_ALARM
        assert self.b_child.modified.alarm.status == AlarmStatus.CONF_STATUS
        assert self.b_child.modified.alarm.message == \
            "x.counter.value = 31.0 not 0.0"
        self.prepare_half_run()
        self.b.run()
        # x counter now at 2, child should be modified by us
        assert self.b_child.modified.value is True
        assert self.b_child.modified.alarm.severity == AlarmSeverity.NO_ALARM
        assert self.b_child.modified.alarm.status == AlarmStatus.CONF_STATUS
        assert self.b_child.modified.alarm.message == \
            "(We modified) x.counter.value = 2.0 not 0.0"
        assert x.counter.value == 2.0
        x.counter.put_value(0.0)
        # x counter now at 0, child should be unmodified
        assert x.counter.value == 0
        assert self.b_child.modified.alarm.message == ""
        assert self.b_child.modified.value is False

    def test_modify_parent(self):
        # Save an initial setting for child and parent
        self.b_child.save("init_child")
        self.b.save("init_parent")
        # Change a value and save as a new child setting
        x = self.context.block_view("COUNTERX")
        x.counter.put_value(31)
        self.b_child.save("new_child")
        assert self.b_child.modified.value is False
        assert self.b.modified.value is True
        assert self.b.modified.alarm.severity == AlarmSeverity.MINOR_ALARM
        assert self.b.modified.alarm.status == AlarmStatus.CONF_STATUS
        assert self.b.modified.alarm.message == \
            "part2.design.value = 'new_child' not 'init_child'"
        # Load the child again
        self.b_child.design.put_value("new_child")
        assert self.b.modified.value is True
        # And check that loading parent resets it
        self.b.design.put_value("init_parent")
        assert self.b.modified.value is False
        assert self.b_child.design.value == "init_child"
        # Put back
        self.b_child.design.put_value("new_child")
        assert self.b.modified.value is True
        # Do a configure, and check we get set back
        self.prepare_half_run()
        assert self.b_child.design.value == "init_child"
        assert self.b_child.modified.value is False
        assert self.b.modified.value is False

    def test_abort(self):
        self.b.abort()
        self.checkState(self.ss.ABORTED)

    def test_validate(self):
        line1 = LineGenerator('y', 'mm', 0, 2, 3)
        line2 = LineGenerator('x', 'mm', 0, 2, 2)
        compound = CompoundGenerator([line1, line2], [], [])
        actual = self.b.validate(generator=compound, axesToMove=['x'])
        assert actual["generator"].to_dict() == compound.to_dict()
        assert actual["axesToMove"] == ['x']

    def prepare_half_run(self, duration=0.01, exception=0):
        line1 = LineGenerator('y', 'mm', 0, 2, 3)
        line2 = LineGenerator('x', 'mm', 0, 2, 2)
        compound = CompoundGenerator([line1, line2], [], [], duration)
        self.b.configure(generator=compound,
                         axesToMove=['x'],
                         exceptionStep=exception)

    def test_configure_run(self):
        assert self.b.configure.writeable is True
        assert self.b.configure.takes.elements["generator"].writeable is True
        assert self.b.validate.takes.elements["generator"].writeable is True
        assert self.b.validate.returns.elements["generator"].writeable is False
        self.prepare_half_run()
        self.checkSteps(2, 0, 6)
        self.checkState(self.ss.ARMED)
        assert self.b.configure.writeable is False
        assert self.b.configure.takes.elements["generator"].writeable is True
        assert self.b.validate.takes.elements["generator"].writeable is True
        assert self.b.validate.returns.elements["generator"].writeable is False

        self.b.run()
        self.checkState(self.ss.ARMED)
        self.checkSteps(4, 2, 6)

        self.b.run()
        self.checkState(self.ss.ARMED)
        self.checkSteps(6, 4, 6)

        self.b.run()
        self.checkState(self.ss.READY)

    def test_abort(self):
        self.prepare_half_run()
        self.b.run()
        self.b.abort()
        self.checkState(self.ss.ABORTED)

    def test_pause_seek_resume(self):
        self.prepare_half_run()
        self.checkSteps(configured=2, completed=0, total=6)
        self.b.run()
        self.checkState(self.ss.ARMED)
        self.checkSteps(4, 2, 6)
        self.b.pause(completedSteps=1)
        self.checkState(self.ss.ARMED)
        self.checkSteps(2, 1, 6)
        self.b.run()
        self.checkSteps(4, 2, 6)
        self.b.completedSteps.put_value(5)
        self.checkSteps(6, 5, 6)
        self.b.run()
        self.checkState(self.ss.READY)

    def test_resume_in_run(self):
        self.prepare_half_run(duration=0.5)
        f = self.b.run_async()
        self.context.sleep(0.95)
        self.b.pause()
        self.checkState(self.ss.PAUSED)
        self.checkSteps(2, 1, 6)
        self.b.resume()
        # Parent should be running, child won't have got request yet
        then = time.time()
        self.checkState(self.ss.RUNNING, child=False)
        self.context.wait_all_futures(f, timeout=2)
        now = time.time()
        self.checkState(self.ss.ARMED)
        self.checkSteps(4, 2, 6)
        # This test fails on Travis sometimes, looks like the docker container
        # just gets starved
        # self.assertAlmostEqual(now - then, 0.5, delta=0.1)

    def test_run_exception(self):
        self.prepare_half_run(exception=1)
        with self.assertRaises(AssertionError):
            self.b.run()
        self.checkState(self.ss.FAULT)

    def test_run_stop(self):
        self.prepare_half_run(duration=0.1)
        f = self.b.run_async()
        self.context.sleep(0.1)
        self.b.abort()
        with self.assertRaises(AbortedError):
            f.result()
        self.checkState(self.ss.ABORTED)
class TestDoubleBuffer(ChildTestCase):
    def setUp(self):
        self.process = Process("Process")
        self.context = Context(self.process)

        # Make 2 sequencers we can prod
        self.seq_parts = {}
        for i in (1, 2):
            controller = BasicController("TEST:SEQ%d" % i)
            self.seq_parts[i] = SequencerPart("part")
            controller.add_part(self.seq_parts[i])
            self.process.add_controller(controller)

        # Now start the process off
        self.process.start()

        self.seq1_block = self.context.block_view("TEST:SEQ1")
        self.seq2_block = self.context.block_view("TEST:SEQ2")
        self.db = DoubleBuffer(self.context, self.seq1_block, self.seq2_block)

    def tearDown(self):
        self.process.stop(timeout=2)

    @staticmethod
    def assert_rows_equal_table(rows, table):
        """Compare sequencer table output to SequencerRows object.

        This converts a sequencer table to the same format as produced by the
        SequencerRows.as_tuples() method.
        """
        t = table
        table_params = [
            t.repeats,
            t.trigger,
            t.position,
            t.time1,
            t.outa1,
            t.outb1,
            t.outc1,
            t.outd1,
            t.oute1,
            t.outf1,
            t.time2,
            t.outa2,
            t.outb2,
            t.outc2,
            t.outd2,
            t.oute2,
            t.outf2,
        ]

        # Transpose and convert to tuples
        table_tuple = tuple(zip(*table_params))
        assert table_tuple == rows.as_tuples()

    @staticmethod
    def rows_generator(rows_arr):
        for rows in rows_arr:
            yield rows

    def test_tables_are_set_correctly_on_configure(self):
        min_ticks = int(MIN_TABLE_DURATION / TICK)

        rows1 = SequencerRows()  # Just over half min duration
        rows1.add_seq_entry(count=2, half_duration=min_ticks // 8 + 1000)

        rows2 = SequencerRows()  # Just over minimum duration
        rows2.add_seq_entry(count=1, half_duration=min_ticks // 8 + 1000)
        rows2.add_seq_entry(count=3, half_duration=min_ticks // 8 + 1000)

        extra = SequencerRows()  # Extra tables are ignored for configure()
        extra.add_seq_entry(count=2, half_duration=min_ticks // 8 + 1000)
        extra.add_seq_entry(count=2, half_duration=min_ticks // 8 + 1000)
        self.db.configure(self.rows_generator([rows1, rows1, rows2, extra]))

        # Check to ensure repeats is set correctly
        for table in self.db._table_map.values():
            assert table.repeats.value == 1

        self.seq_parts[1].table_set.assert_called_once()
        table1 = self.seq_parts[1].table_set.call_args[0][0]
        expected1 = SequencerRows()
        expected1.add_seq_entry(count=2, half_duration=min_ticks // 8 + 1000)
        expected1.add_seq_entry(count=1, half_duration=min_ticks // 8 + 1000)
        expected1.add_seq_entry(count=1,
                                half_duration=min_ticks // 8 + 1000,
                                trim=SEQ_TABLE_SWITCH_DELAY)
        self.assert_rows_equal_table(expected1, table1)

        self.seq_parts[2].table_set.assert_called_once()
        table2 = self.seq_parts[2].table_set.call_args[0][0]
        expected2 = SequencerRows()
        expected2.add_seq_entry(count=1, half_duration=min_ticks // 8 + 1000)
        expected2.add_seq_entry(count=2, half_duration=min_ticks // 8 + 1000)
        expected2.add_seq_entry(count=1,
                                half_duration=min_ticks // 8 + 1000,
                                trim=SEQ_TABLE_SWITCH_DELAY)
        self.assert_rows_equal_table(expected2, table2)

    @staticmethod
    def get_sequencer_rows(position=0):
        min_ticks = int(MIN_TABLE_DURATION / TICK)

        rows = SequencerRows()
        rows.add_seq_entry(count=2,
                           half_duration=min_ticks // 4 + 100,
                           position=position)

        expected = SequencerRows()
        expected.add_seq_entry(count=1,
                               half_duration=min_ticks // 4 + 100,
                               position=position)
        expected.add_seq_entry(
            count=1,
            half_duration=min_ticks // 4 + 100,
            position=position,
            trim=SEQ_TABLE_SWITCH_DELAY,
        )
        return rows, expected

    def test_tables_update_correctly_on_active_status(self):
        rows_list = []
        exp_list = []
        for i in range(5):  # Generate list of identifiable tables
            generated, expected = self.get_sequencer_rows(i)
            rows_list.append(generated)
            exp_list.append(expected)

        self.db.configure(self.rows_generator(rows_list))

        self.seq_parts[1].table_set.assert_called_once()
        table = self.seq_parts[1].table_set.call_args[0][0]
        self.assert_rows_equal_table(exp_list[0], table)

        self.seq_parts[2].table_set.assert_called_once()
        table2 = self.seq_parts[2].table_set.call_args[0][0]
        self.assert_rows_equal_table(exp_list[1], table2)

        self.seq_parts[1].table_set.reset_mock()
        self.seq_parts[2].table_set.reset_mock()

        futures = self.db.run()

        self.seq_parts[1].table_set.assert_not_called()
        self.seq_parts[2].table_set.assert_not_called()

        self.seq1_block.active.put_value_async(True)
        self.context.sleep(0)
        self.seq_parts[1].table_set.assert_not_called()
        self.seq_parts[2].table_set.assert_not_called()

        self.seq2_block.active.put_value_async(True)
        self.seq1_block.active.put_value_async(False)
        self.context.sleep(0)
        self.seq_parts[1].table_set.assert_called_once()
        table = self.seq_parts[1].table_set.call_args[0][0]
        self.assert_rows_equal_table(exp_list[2], table)
        self.seq_parts[2].table_set.assert_not_called()
        self.seq_parts[1].table_set.reset_mock()

        self.seq2_block.active.put_value_async(False)
        self.seq1_block.active.put_value_async(True)
        self.context.sleep(0)
        self.seq_parts[1].table_set.assert_not_called()
        self.seq_parts[2].table_set.assert_called_once()
        table2 = self.seq_parts[2].table_set.call_args[0][0]
        self.assert_rows_equal_table(exp_list[3], table2)
        self.seq_parts[2].table_set.reset_mock()

        self.seq2_block.active.put_value_async(True)
        self.seq1_block.active.put_value_async(False)
        self.context.sleep(0)
        self.seq_parts[1].table_set.assert_called_once()
        table = self.seq_parts[1].table_set.call_args[0][0]
        self.assert_rows_equal_table(exp_list[4], table)
        self.seq_parts[2].table_set.assert_not_called()
        self.seq_parts[1].table_set.reset_mock()

        self.seq2_block.active.put_value_async(False)
        self.seq1_block.active.put_value_async(True)
        self.context.sleep(0)
        self.seq_parts[1].table_set.assert_not_called()
        self.seq_parts[2].table_set.assert_not_called()

        with pytest.raises(Exception):
            for future in futures:
                self.context.unsubscribe(future)
 def test_set_export_parts(self):
     context = Context(self.p)
     b = context.block_view("mainBlock")
     assert list(b) == [
         "meta",
         "health",
         "state",
         "disable",
         "reset",
         "mri",
         "layout",
         "design",
         "exports",
         "modified",
         "save",
         "attr",
     ]
     assert b.attr.meta.tags == ["widget:textinput"]
     new_exports = ExportTable.from_rows([("part2.attr", "childAttr"),
                                          ("part2.reset", "childReset")])
     self.c.set_exports(new_exports)
     assert self.c.modified.value is True
     assert self.c.modified.alarm.message == "exports changed"
     self.c.save(designName="testSaveLayout")
     assert self.c.modified.value is False
     # block has changed, get a new view
     b = context.block_view("mainBlock")
     assert list(b) == [
         "meta",
         "health",
         "state",
         "disable",
         "reset",
         "mri",
         "layout",
         "design",
         "exports",
         "modified",
         "save",
         "attr",
         "childAttr",
         "childReset",
     ]
     assert self.c.state.value == "Ready"
     assert b.childAttr.value == "defaultv"
     assert self.c.modified.value is False
     m = MagicMock()
     b.childAttr.subscribe_value(m)
     # allow a subscription to come through
     context.sleep(0.1)
     m.assert_called_once_with("defaultv")
     m.reset_mock()
     self.c_part.attr.set_value("newv")
     assert b.childAttr.value == "newv"
     assert self.c_part.attr.value == "newv"
     assert self.c.modified.value is True
     assert (self.c.modified.alarm.message ==
             "part2.attr.value = 'newv' not 'defaultv'")
     # allow a subscription to come through
     context.sleep(0.1)
     m.assert_called_once_with("newv")
     b.childAttr.put_value("again")
     assert b.childAttr.value == "again"
     assert self.c_part.attr.value == "again"
     assert self.c.modified.value is True
     assert (self.c.modified.alarm.message ==
             "part2.attr.value = 'again' not 'defaultv'")
     # remove the field
     new_exports = ExportTable([], [])
     self.c.set_exports(new_exports)
     assert self.c.modified.value is True
     self.c.save()
     assert self.c.modified.value is False
     # block has changed, get a new view
     b = context.block_view("mainBlock")
     assert "childAttr" not in b
Beispiel #21
0
 def arm_detector(self, context: Context) -> None:
     child = context.block_view(self.mri)
     self.start_future = child.start_async()
     child.when_value_matches("acquiring", True, timeout=DEFAULT_TIMEOUT)
Beispiel #22
0
class TestDetectorChildPart(unittest.TestCase):
    def setUp(self):
        self.p = Process("process1")
        self.context = Context(self.p)

        # Make a fast child, this will load the wait of 0.01 from saved file
        c1 = RunnableController(
            mri="fast", config_dir=DESIGN_PATH, use_git=False, initial_design="fast"
        )
        c1.add_part(WaitingPart("wait"))
        c1.add_part(ExposureDeadtimePart("dt", 0.001))
        c1.add_part(DatasetTablePart("dset"))
        self.p.add_controller(c1)

        # And a slow one, this has the same saved files as fast, but doesn't
        # load at startup
        c2 = RunnableController(mri="slow", config_dir=DESIGN_PATH, use_git=False)
        c2.add_part(WaitingPart("wait", 0.123))
        c2.add_part(DatasetTablePart("dset"))
        self.p.add_controller(c2)

        # And a faulty one, this is hidden at startup by default
        c3 = RunnableController(mri="faulty", config_dir=DESIGN_PATH, use_git=False)
        c3.add_part(FaultyPart("bad"))
        c3.add_part(DatasetTablePart("dset"))
        self.p.add_controller(c3)

        # And a top level one, this loads slow and fast designs for the
        # children on every configure (or load), but not at init
        self.ct = RunnableController(
            mri="top", config_dir=DESIGN_PATH, use_git=False, initial_design="default"
        )
        self.ct.add_part(
            DetectorChildPart(name="FAST", mri="fast", initial_visibility=True)
        )
        self.ct.add_part(
            DetectorChildPart(name="SLOW", mri="slow", initial_visibility=True)
        )
        self.ct.add_part(
            DetectorChildPart(name="BAD", mri="faulty", initial_visibility=False)
        )
        self.ct.add_part(
            DetectorChildPart(
                name="BAD2",
                mri="faulty",
                initial_visibility=False,
                initial_frames_per_step=0,
            )
        )
        self.fast_multi = MaybeMultiPart("fast")
        self.slow_multi = MaybeMultiPart("slow")
        self.ct.add_part(self.fast_multi)
        self.ct.add_part(self.slow_multi)
        self.p.add_controller(self.ct)

        # Some blocks to interface to them
        self.b = self.context.block_view("top")
        self.bf = self.context.block_view("fast")
        self.bs = self.context.block_view("slow")

        # start the process off
        self.p.start()
        self.tmpdir = tempfile.mkdtemp()

    def tearDown(self):
        self.p.stop(timeout=1)
        shutil.rmtree(self.tmpdir)

    def make_generator(self):
        line1 = LineGenerator("y", "mm", 0, 2, 3)
        line2 = LineGenerator("x", "mm", 0, 2, 2)
        compound = CompoundGenerator([line1, line2], [], [], duration=1)
        return compound

    def test_init(self):
        assert list(self.b.configure.meta.defaults["detectors"].rows()) == [
            [True, "FAST", "fast", 0.0, 1],
            [True, "SLOW", "slow", 0.0, 1],
        ]

    def test_validate_returns_exposures(self):
        ret = self.b.validate(
            self.make_generator(),
            self.tmpdir,
            detectors=DetectorTable.from_rows(
                [(True, "SLOW", "slow", 0.0, 1), (True, "FAST", "fast", 0.0, 1)]
            ),
        )
        assert list(ret.detectors.rows()) == [
            [True, "SLOW", "slow", 0.0, 1],
            [True, "FAST", "fast", 0.99895, 1],
        ]

    def test_guessing_frames_1(self):
        ret = self.b.validate(
            self.make_generator(),
            self.tmpdir,
            detectors=DetectorTable.from_rows(
                [(True, "FAST", "fast", 0.5, 0), (True, "SLOW", "slow", 0.0, 1)]
            ),
        )
        assert list(ret.detectors.rows()) == [
            [True, "FAST", "fast", 0.5, 1],
            [True, "SLOW", "slow", 0.0, 1],
        ]

    def test_setting_exposure_on_no_exposure_det_fails(self):
        with self.assertRaises(BadValueError) as cm:
            self.b.validate(
                self.make_generator(),
                self.tmpdir,
                detectors=DetectorTable.from_rows(
                    [(True, "FAST", "fast", 0.0, 1), (True, "SLOW", "slow", 0.5, 1)]
                ),
            )
        assert str(cm.exception) == "Detector SLOW doesn't take exposure"

    def test_guessing_frames_and_exposure(self):
        self.slow_multi.active = True
        ret = self.b.validate(
            self.make_generator(),
            self.tmpdir,
            detectors=DetectorTable.from_rows([(True, "FAST", "fast", 0.0, 0)]),
        )
        assert list(ret.detectors.rows()) == [
            [True, "FAST", "fast", 0.99895, 1],
            [False, "SLOW", "slow", 0, 0],
        ]

    def test_guessing_frames_5(self):
        self.fast_multi.active = True
        ret = self.b.validate(
            self.make_generator(),
            self.tmpdir,
            detectors=DetectorTable.from_rows(
                [(True, "FAST", "fast", 0.198, 0), (True, "SLOW", "slow", 0.0, 1)]
            ),
        )
        assert list(ret.detectors.rows()) == [
            [True, "FAST", "fast", 0.198, 5],
            [True, "SLOW", "slow", 0.0, 1],
        ]

    def test_adding_faulty_fails(self):
        t = LayoutTable.from_rows([["BAD", "faulty", 0, 0, True]])
        self.b.layout.put_value(t)
        assert list(self.b.configure.meta.defaults["detectors"].rows()) == [
            [True, "FAST", "fast", 0.0, 1],
            [True, "SLOW", "slow", 0.0, 1],
            [True, "BAD", "faulty", 0.0, 1],
        ]
        with self.assertRaises(BadValueError) as cm:
            self.b.configure(self.make_generator(), self.tmpdir)
        assert str(cm.exception) == (
            "Detector BAD was faulty at init and is unusable. "
            "If the detector is now working please restart Malcolm"
        )
        self.b.configure(
            self.make_generator(),
            self.tmpdir,
            detectors=DetectorTable.from_rows([(False, "BAD", "faulty", 0.0, 1)]),
        )
        self.b.reset()
        t = LayoutTable.from_rows([["BAD", "faulty", 0, 0, False]])
        self.b.layout.put_value(t)
        self.test_init()
        self.b.configure(self.make_generator(), self.tmpdir)

    def test_adding_faulty_non_default_works(self):
        t = LayoutTable.from_rows([["BAD2", "faulty", 0, 0, True]])
        self.b.layout.put_value(t)
        assert list(self.b.configure.meta.defaults["detectors"].rows()) == [
            [True, "FAST", "fast", 0.0, 1],
            [True, "SLOW", "slow", 0.0, 1],
            [False, "BAD2", "faulty", 0.0, 1],
        ]
        self.b.configure(self.make_generator(), self.tmpdir)

    def test_only_one_det(self):
        # Disable one detector
        self.b.configure(
            self.make_generator(),
            self.tmpdir,
            detectors=DetectorTable.from_rows(
                [(False, "SLOW", "slow", 0.0, 0), [True, "FAST", "fast", 0.0, 1]]
            ),
        )
        assert self.b.state.value == "Armed"
        assert self.bs.state.value == "Ready"
        assert self.bf.state.value == "Armed"
        self.b.completedSteps.put_value(2)
        assert self.b.state.value == "Armed"
        assert self.bs.state.value == "Ready"
        assert self.bf.state.value == "Armed"
        self.b.run()
        assert self.b.state.value == "Finished"
        assert self.bs.state.value == "Ready"
        assert self.bf.state.value == "Finished"
        self.b.reset()
        assert self.b.state.value == "Ready"
        assert self.bs.state.value == "Ready"
        assert self.bf.state.value == "Ready"
        self.b.abort()
        assert self.b.state.value == "Aborted"
        assert self.bs.state.value == "Aborted"
        assert self.bf.state.value == "Aborted"

    def test_multi_frame_no_infos_fails(self):
        with self.assertRaises(BadValueError) as cm:
            self.b.configure(
                self.make_generator(),
                self.tmpdir,
                detectors=DetectorTable.from_rows(
                    [(True, "SLOW", "slow", 0.0, 1), (True, "FAST", "fast", 0.0, 5)]
                ),
            )
        assert str(cm.exception) == (
            "There are no trigger multipliers setup for Detector 'FAST' "
            "so framesPerStep can only be 0 or 1 for this row in the detectors "
            "table"
        )

    def test_multi_frame_fast_det(self):
        self.fast_multi.active = True
        self.b.configure(
            self.make_generator(),
            self.tmpdir,
            detectors=DetectorTable.from_rows(
                [(True, "SLOW", "slow", 0.0, 1), (True, "FAST", "fast", 0.0, 5)]
            ),
        )
        assert self.b.completedSteps.value == 0
        assert self.b.totalSteps.value == 6
        assert self.b.configuredSteps.value == 6
        assert self.bs.completedSteps.value == 0
        assert self.bs.totalSteps.value == 6
        assert self.bs.configuredSteps.value == 6
        assert self.bf.completedSteps.value == 0
        assert self.bf.totalSteps.value == 30
        assert self.bf.configuredSteps.value == 30

    def test_bad_det_mri(self):
        # Send mismatching rows
        with self.assertRaises(AssertionError) as cm:
            self.b.configure(
                self.make_generator(),
                self.tmpdir,
                axesToMove=(),
                detectors=DetectorTable.from_rows([(True, "SLOW", "fast", 0.0, 0)]),
            )
        assert str(cm.exception) == "SLOW has mri slow, passed fast"

    def test_not_paused_when_resume(self):
        # Set it up to do 6 steps
        self.b.configure(
            self.make_generator(),
            self.tmpdir,
            axesToMove=(),
            detectors=DetectorTable.from_rows(
                [(True, "FAST", "fast", 0, 1), (True, "SLOW", "slow", 0, 1)]
            ),
        )
        assert self.b.completedSteps.value == 0
        assert self.b.totalSteps.value == 6
        assert self.b.configuredSteps.value == 1
        # Do one step
        self.b.run()
        assert self.b.completedSteps.value == 1
        assert self.b.totalSteps.value == 6
        assert self.b.configuredSteps.value == 2
        assert self.b.state.value == "Armed"
        assert self.bs.state.value == "Armed"
        assert self.bf.state.value == "Armed"
        # Now do a second step but pause before the second one is done
        f = self.b.run_async()
        self.context.sleep(0.2)
        assert self.b.state.value == "Running"
        assert self.bf.state.value == "Armed"
        assert self.bs.state.value == "Running"
        self.b.pause()
        assert self.b.state.value == "Paused"
        assert self.bf.state.value == "Armed"
        assert self.bs.state.value == "Paused"
        assert self.b.completedSteps.value == 1
        assert self.b.totalSteps.value == 6
        assert self.b.configuredSteps.value == 2
        self.b.resume()
        self.context.wait_all_futures(f)
        assert self.b.completedSteps.value == 2
        assert self.b.totalSteps.value == 6
        assert self.b.configuredSteps.value == 3

    def test_parent_with_initial_config_does_not_set_child(self):
        assert self.bs.wait.value == 0.123
        assert self.bs.design.value == ""
        assert self.bf.wait.value == 0.01
        assert self.bf.design.value == "fast"
        assert self.b.design.value == "default"
        assert self.b.modified.value is True
        assert self.b.modified.alarm.message == "SLOW.design.value = '' not 'slow'"
        self.b.configure(self.make_generator(), self.tmpdir, axesToMove=())
        assert self.bs.wait.value == 1.0
        assert self.bs.design.value == "slow"
        assert self.bf.wait.value == 0.01
        assert self.bf.design.value == "fast"
        assert self.b.design.value == "default"
        assert self.b.modified.value is False

    def make_generator_breakpoints(self):
        line1 = LineGenerator("x", "mm", -10, -10, 5)
        line2 = LineGenerator("x", "mm", 0, 180, 10)
        line3 = LineGenerator("x", "mm", 190, 190, 2)
        duration = 0.01
        concat = ConcatGenerator([line1, line2, line3])

        return CompoundGenerator([concat], [], [], duration)

    def checkSteps(self, block, configured, completed, total):
        assert block.configuredSteps.value == configured
        assert block.completedSteps.value == completed
        assert block.totalSteps.value == total

    def checkState(self, block, state):
        assert block.state.value == state

    def test_breakpoints_tomo(self):
        breakpoints = [2, 3, 10, 2]
        # Configure RunnableController(mri='top')
        self.b.configure(
            generator=self.make_generator_breakpoints(),
            fileDir=self.tmpdir,
            detectors=DetectorTable.from_rows(
                [[False, "SLOW", "slow", 0.0, 1], [True, "FAST", "fast", 0.0, 1]]
            ),
            axesToMove=["x"],
            breakpoints=breakpoints,
        )

        assert self.ct.configure_params.generator.size == 17
        self.checkSteps(self.b, 2, 0, 17)
        self.checkSteps(self.bf, 2, 0, 17)
        assert self.b.state.value == "Armed"
        assert self.bs.state.value == "Ready"
        assert self.bf.state.value == "Armed"

        self.b.run()
        self.checkSteps(self.b, 5, 2, 17)
        self.checkSteps(self.bf, 5, 2, 17)
        assert self.b.state.value == "Armed"
        assert self.bs.state.value == "Ready"
        assert self.bf.state.value == "Armed"

        self.b.run()
        self.checkSteps(self.b, 15, 5, 17)
        assert self.b.state.value == "Armed"
        assert self.bs.state.value == "Ready"
        assert self.bf.state.value == "Armed"

        self.b.run()
        self.checkSteps(self.b, 17, 15, 17)
        assert self.b.state.value == "Armed"
        assert self.bs.state.value == "Ready"
        assert self.bf.state.value == "Armed"

        self.b.run()
        self.checkSteps(self.b, 17, 17, 17)
        self.checkSteps(self.bf, 17, 17, 17)
        assert self.b.state.value == "Finished"
        assert self.bs.state.value == "Ready"
        assert self.bf.state.value == "Finished"

    def test_breakpoints_with_pause(self):
        breakpoints = [2, 3, 10, 2]
        self.b.configure(
            generator=self.make_generator_breakpoints(),
            fileDir=self.tmpdir,
            detectors=DetectorTable.from_rows(
                [[False, "SLOW", "slow", 0.0, 1], [True, "FAST", "fast", 0.0, 1]]
            ),
            axesToMove=["x"],
            breakpoints=breakpoints,
        )

        assert self.ct.configure_params.generator.size == 17

        self.checkSteps(self.b, 2, 0, 17)
        self.checkSteps(self.bf, 2, 0, 17)
        self.checkState(self.b, RunnableStates.ARMED)

        self.b.run()
        self.checkSteps(self.b, 5, 2, 17)
        self.checkSteps(self.bf, 5, 2, 17)
        self.checkState(self.b, RunnableStates.ARMED)

        # rewind
        self.b.pause(lastGoodStep=1)
        self.checkSteps(self.b, 2, 1, 17)
        self.checkSteps(self.bf, 2, 1, 17)
        self.checkState(self.b, RunnableStates.ARMED)
        self.b.run()
        self.checkSteps(self.b, 5, 2, 17)
        self.checkSteps(self.bf, 5, 2, 17)
        self.checkState(self.b, RunnableStates.ARMED)

        self.b.run()
        self.checkSteps(self.b, 15, 5, 17)
        self.checkSteps(self.bf, 15, 5, 17)
        self.checkState(self.b, RunnableStates.ARMED)

        self.b.run()
        self.checkSteps(self.b, 17, 15, 17)
        self.checkSteps(self.bf, 17, 15, 17)
        self.checkState(self.b, RunnableStates.ARMED)

        # rewind
        self.b.pause(lastGoodStep=11)
        self.checkSteps(self.b, 15, 11, 17)
        self.checkSteps(self.bf, 15, 11, 17)
        self.checkState(self.b, RunnableStates.ARMED)
        self.b.run()
        self.checkSteps(self.b, 17, 15, 17)
        self.checkSteps(self.bf, 17, 15, 17)
        self.checkState(self.b, RunnableStates.ARMED)

        self.b.run()
        self.checkSteps(self.b, 17, 17, 17)
        self.checkSteps(self.bf, 17, 17, 17)
        self.checkState(self.b, RunnableStates.FINISHED)
class TestRunnableChildPart(unittest.TestCase):
    def setUp(self):
        self.p = Process('process1')
        self.context = Context(self.p)

        # Make a fast child, this will load the wait of 0.01 from saved file
        c1 = RunnableController(mri="fast",
                                config_dir=DESIGN_PATH,
                                use_git=False,
                                initial_design="fast")
        c1.add_part(WaitingPart("wait"))
        self.p.add_controller(c1)

        # And a slow one, this has the same saved files as fast, but doesn't
        # load at startup
        c2 = RunnableController(mri="slow",
                                config_dir=DESIGN_PATH,
                                use_git=False)
        c2.add_part(WaitingPart("wait", 0.123))
        self.p.add_controller(c2)

        # And a top level one, this loads slow and fast designs for the
        # children on every configure (or load), but not at init
        c3 = RunnableController(mri="top",
                                config_dir=DESIGN_PATH,
                                use_git=False,
                                initial_design="default")
        c3.add_part(
            RunnableChildPart(name="FAST", mri="fast",
                              initial_visibility=True))
        c3.add_part(
            RunnableChildPart(name="SLOW", mri="slow",
                              initial_visibility=True))
        self.p.add_controller(c3)

        # Some blocks to interface to them
        self.b = self.context.block_view("top")
        self.bf = self.context.block_view("fast")
        self.bs = self.context.block_view("slow")

        # start the process off
        self.p.start()

    def tearDown(self):
        self.p.stop(timeout=1)

    def make_generator(self):
        line1 = LineGenerator('y', 'mm', 0, 2, 3)
        line2 = LineGenerator('x', 'mm', 0, 2, 2)
        compound = CompoundGenerator([line1, line2], [], [])
        return compound

    def test_not_paused_when_resume(self):
        # Set it up to do 6 steps
        self.b.configure(generator=self.make_generator(), axesToMove=())
        assert self.b.completedSteps.value == 0
        assert self.b.totalSteps.value == 6
        assert self.b.configuredSteps.value == 1
        # Do one step
        self.b.run()
        assert self.b.completedSteps.value == 1
        assert self.b.totalSteps.value == 6
        assert self.b.configuredSteps.value == 2
        # Now do a second step but pause before the second one is done
        f = self.b.run_async()
        self.context.sleep(0.2)
        assert self.b.state.value == "Running"
        assert self.bf.state.value == "Armed"
        assert self.bs.state.value == "Running"
        self.b.pause()
        assert self.b.state.value == "Paused"
        assert self.bf.state.value == "Armed"
        assert self.bs.state.value == "Paused"
        assert self.b.completedSteps.value == 1
        assert self.b.totalSteps.value == 6
        assert self.b.configuredSteps.value == 2
        self.b.resume()
        self.context.wait_all_futures(f)
        assert self.b.completedSteps.value == 2
        assert self.b.totalSteps.value == 6
        assert self.b.configuredSteps.value == 3

    def test_parent_with_initial_config_does_not_set_child(self):
        assert self.bs.wait.value == 0.123
        assert self.bs.design.value == ""
        assert self.bf.wait.value == 0.01
        assert self.bf.design.value == "fast"
        assert self.b.design.value == "default"
        assert self.b.modified.value is True
        assert self.b.modified.alarm.message == \
            "SLOW.design.value = '' not 'slow'"
        self.b.configure(generator=self.make_generator(), axesToMove=())
        assert self.bs.wait.value == 1.0
        assert self.bs.design.value == "slow"
        assert self.bf.wait.value == 0.01
        assert self.bf.design.value == "fast"
        assert self.b.design.value == "default"
        assert self.b.modified.value is False
Beispiel #24
0
class TestRunnableChildPart(unittest.TestCase):

    def setUp(self):
        self.p = Process('process1')
        self.context = Context(self.p)

        # Make a fast child
        c1 = call_with_params(RunnableController, self.p,
                              [WaitingPart("p", 0.01)],
                              mri="fast", configDir="/tmp")
        self.p.add_controller("fast", c1)

        # And a slow one
        c2 = call_with_params(RunnableController,  self.p,
                              [WaitingPart("p", 1.0)],
                              mri="slow", configDir="/tmp")
        self.p.add_controller("slow", c2)

        # And a top level one
        p1 = call_with_params(RunnableChildPart, name="FAST", mri="fast")
        p2 = call_with_params(RunnableChildPart, name="SLOW", mri="slow")
        c3 = call_with_params(RunnableController, self.p, [p1, p2],
                              mri="top", configDir="/tmp")
        self.p.add_controller("top", c3)
        self.b = self.context.block_view("top")
        self.bf = self.context.block_view("fast")
        self.bs = self.context.block_view("slow")

        # start the process off
        self.p.start()

    def tearDown(self):
        self.p.stop(timeout=1)

    def make_generator(self):
        line1 = LineGenerator('y', 'mm', 0, 2, 3)
        line2 = LineGenerator('x', 'mm', 0, 2, 2)
        compound = CompoundGenerator([line1, line2], [], [])
        return compound

    def test_not_paused_when_resume(self):
        # Set it up to do 6 steps
        self.b.configure(generator=self.make_generator())
        assert self.b.completedSteps.value == 0
        assert self.b.totalSteps.value == 6
        assert self.b.configuredSteps.value == 1
        # Do one step
        self.b.run()
        assert self.b.completedSteps.value == 1
        assert self.b.totalSteps.value == 6
        assert self.b.configuredSteps.value == 2
        # Now do a second step but pause before the second one is done
        f = self.b.run_async()
        self.context.sleep(0.2)
        assert self.b.state.value == "Running"
        assert self.bf.state.value == "Armed"
        assert self.bs.state.value == "Running"
        self.b.pause()
        assert self.b.state.value == "Paused"
        assert self.bf.state.value == "Armed"
        assert self.bs.state.value == "Paused"
        assert self.b.completedSteps.value == 1
        assert self.b.totalSteps.value == 6
        assert self.b.configuredSteps.value == 2
        self.b.resume()
        self.context.wait_all_futures(f)
        assert self.b.completedSteps.value == 2
        assert self.b.totalSteps.value == 6
        assert self.b.configuredSteps.value == 3
Beispiel #25
0
class TestPMACTrajectoryPart(ChildTestCase):
    def setUp(self):
        self.process = Process("Process")
        self.context = Context(self.process)
        self.child = self.create_child_block(pmac_trajectory_block,
                                             self.process,
                                             mri="PMAC:TRAJ",
                                             prefix="PV:PRE",
                                             statPrefix="PV:STAT")
        self.child.parts["i10"].attr.set_value(1705244)
        self.o = call_with_params(PmacTrajectoryPart,
                                  name="pmac",
                                  mri="PMAC:TRAJ")
        list(self.o.create_attribute_models())
        self.process.start()

    #def tearDown(self):
    #    del self.context
    #    self.process.stop(timeout=1)

    def resolutions_and_use_call(self, useB=True):
        offset = [call.put('offsetA', 0.0)]
        resolution = [call.put('resolutionA', 0.001)]
        if useB:
            offset.append(call.put('offsetB', 0.0))
            resolution.append(call.put('resolutionB', 0.001))
        call.puts = offset + resolution + [
            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)
        ]
        return call.puts

    def make_part_info(self, x_pos=0.5, y_pos=0.0):
        part_info = dict(xpart=[
            MotorInfo(
                cs_axis="A",
                cs_port="CS1",
                acceleration=2.5,
                resolution=0.001,
                offset=0.0,
                max_velocity=1.0,
                current_position=x_pos,
                scannable="x",
                velocity_settle=0.0,
            )
        ],
                         ypart=[
                             MotorInfo(
                                 cs_axis="B",
                                 cs_port="CS1",
                                 acceleration=2.5,
                                 resolution=0.001,
                                 offset=0.0,
                                 max_velocity=1.0,
                                 current_position=y_pos,
                                 scannable="y",
                                 velocity_settle=0.0,
                             )
                         ])
        return part_info

    def do_configure(self,
                     axes_to_scan,
                     completed_steps=0,
                     x_pos=0.5,
                     y_pos=0.0,
                     duration=1.0):
        part_info = self.make_part_info(x_pos, y_pos)
        steps_to_do = 3 * len(axes_to_scan)
        params = Mock()
        xs = LineGenerator("x", "mm", 0.0, 0.5, 3, alternate=True)
        ys = LineGenerator("y", "mm", 0.0, 0.1, 2)
        params.generator = CompoundGenerator([ys, xs], [], [], duration)
        params.generator.prepare()
        params.axesToMove = axes_to_scan
        self.o.configure(self.context, completed_steps, steps_to_do, part_info,
                         params)

    def test_validate(self):
        params = Mock()
        params.generator = CompoundGenerator([], [], [], 0.0102)
        params.axesToMove = ["x"]
        part_info = self.make_part_info()
        ret = self.o.validate(self.context, part_info, params)
        expected = 0.010166
        assert ret[0].value.duration == expected

    @patch(
        "malcolm.modules.pmac.parts.pmactrajectorypart.INTERPOLATE_INTERVAL",
        0.2)
    def test_configure(self):
        self.do_configure(axes_to_scan=["x", "y"])
        assert self.child.handled_requests.mock_calls == [
            call.put('numPoints', 4000000),
            call.put('cs', 'CS1'),
        ] + self.resolutions_and_use_call() + [
            call.put('pointsToBuild', 5),
            call.put(
                'positionsA',
                pytest.approx(
                    [0.4461796875, 0.285, 0.0775, -0.0836796875, -0.1375])),
            call.put('positionsB', [0.0, 0.0, 0.0, 0.0, 0.0]),
            call.put('timeArray', [207500, 207500, 207500, 207500, 207500]),
            call.put('userPrograms', [8, 8, 8, 8, 8]),
            call.put('velocityMode', [0, 0, 0, 0, 2]),
            call.post('buildProfile'),
            call.post('executeProfile'),
        ] + self.resolutions_and_use_call() + [
            call.put('pointsToBuild', 16),
            call.put(
                'positionsA',
                pytest.approx([
                    -0.125, 0.0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.6375, 0.625,
                    0.5, 0.375, 0.25, 0.125, 0.0, -0.125, -0.1375
                ])),
            call.put(
                'positionsB',
                pytest.approx([
                    0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.05, 0.1, 0.1, 0.1,
                    0.1, 0.1, 0.1, 0.1, 0.1
                ])),
            call.put('timeArray', [
                100000, 500000, 500000, 500000, 500000, 500000, 500000, 200000,
                200000, 500000, 500000, 500000, 500000, 500000, 500000, 100000
            ]),
            call.put('userPrograms',
                     [3, 4, 3, 4, 3, 4, 2, 8, 3, 4, 3, 4, 3, 4, 2, 8]),
            call.put('velocityMode',
                     [2, 0, 0, 0, 0, 0, 1, 0, 2, 0, 0, 0, 0, 0, 1, 3]),
            call.post('buildProfile')
        ]
        assert self.o.completed_steps_lookup == [
            0, 0, 1, 1, 2, 2, 3, 3, 3, 3, 4, 4, 5, 5, 6, 6
        ]

    @patch(
        "malcolm.modules.pmac.parts.pmactrajectorypart.INTERPOLATE_INTERVAL",
        0.2)
    def test_2_axis_move_to_start(self):
        self.do_configure(axes_to_scan=["x", "y"], x_pos=0.0, y_pos=0.2)
        assert self.child.handled_requests.mock_calls == [
            call.put('numPoints', 4000000),
            call.put('cs', 'CS1'),
        ] + self.resolutions_and_use_call() + [
            call.put('pointsToBuild', 2),
            call.put('positionsA', pytest.approx([-0.06875, -0.1375])),
            call.put('positionsB', pytest.approx([0.1, 0.0])),
            call.put('timeArray', [282843, 282842]),
            call.put('userPrograms', [8, 8]),
            call.put('velocityMode', [0, 2]),
            call.post('buildProfile'),
            call.post('executeProfile'),
        ] + self.resolutions_and_use_call() + [
            call.put('pointsToBuild', ANY),
            call.put('positionsA', ANY),
            call.put('positionsB', ANY),
            call.put('timeArray', ANY),
            call.put('userPrograms', ANY),
            call.put('velocityMode', ANY),
            call.post('buildProfile')
        ]

    @patch("malcolm.modules.pmac.parts.pmactrajectorypart.PROFILE_POINTS", 4)
    @patch(
        "malcolm.modules.pmac.parts.pmactrajectorypart.INTERPOLATE_INTERVAL",
        0.2)
    def test_update_step(self):
        self.do_configure(axes_to_scan=["x", "y"], x_pos=0.0, y_pos=0.2)
        positionsA = self.child.handled_requests.put.call_args_list[-5][0][1]
        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["time_array"]) == 1
        update_completed_steps = Mock()
        self.child.handled_requests.reset_mock()
        self.o.update_step(3, update_completed_steps,
                           self.context.block_view("PMAC:TRAJ"))
        update_completed_steps.assert_called_once_with(1, self.o)
        assert not self.o.loading
        assert self.child.handled_requests.mock_calls == self.resolutions_and_use_call(
        ) + [
            call.put('pointsToBuild', 4),
            call.put('positionsA', pytest.approx([0.375, 0.5, 0.625, 0.6375])),
            call.put('positionsB', pytest.approx([0.0, 0.0, 0.0, 0.05])),
            call.put('timeArray', [500000, 500000, 500000, 200000]),
            call.put('userPrograms', [3, 4, 2, 8]),
            call.put('velocityMode', [0, 0, 1, 0]),
            call.post('appendProfile')
        ]
        assert self.o.end_index == 3
        assert len(self.o.completed_steps_lookup) == 9
        assert len(self.o.profile["time_array"]) == 1

    def test_run(self):
        update = Mock()
        self.o.run(self.context, update)
        assert self.child.handled_requests.mock_calls == [
            call.post('executeProfile')
        ]

    def test_reset(self):
        self.o.reset(self.context)
        assert self.child.handled_requests.mock_calls == [
            call.post('abortProfile'),
            call.put('numPoints', 10),
            call.put('useA', False),
            call.put('useB', False),
            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),
            call.put('pointsToBuild', 1),
            call.put('timeArray', [100000]),
            call.put('userPrograms', [8]),
            call.put('velocityMode', [3]),
            call.post('buildProfile'),
            call.post('executeProfile')
        ]

    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.put('numPoints', 4000000),
            call.put('cs', 'CS1'),
        ] + self.resolutions_and_use_call(useB=False) + [
            call.put('pointsToBuild', 8),
            call.put(
                'positionsA',
                pytest.approx(
                    [0.625, 0.5, 0.375, 0.25, 0.125, 0.0, -0.125, -0.1375])),
            call.put('timeArray', [
                100000, 500000, 500000, 500000, 500000, 500000, 500000, 100000
            ]),
            call.put('userPrograms', [3, 4, 3, 4, 3, 4, 2, 8]),
            call.put('velocityMode', [2, 0, 0, 0, 0, 0, 1, 3]),
            call.post('buildProfile')
        ]

    @patch(
        "malcolm.modules.pmac.parts.pmactrajectorypart.INTERPOLATE_INTERVAL",
        0.2)
    def test_long_steps_lookup(self):
        self.do_configure(axes_to_scan=["x"],
                          completed_steps=3,
                          x_pos=0.62506,
                          duration=14.0)
        assert self.child.handled_requests.mock_calls[-6:] == [
            call.put('pointsToBuild', 14),
            call.put(
                'positionsA',
                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
                ])),
            call.put('timeArray', [
                7143, 3500000, 3500000, 3500000, 3500000, 3500000, 3500000,
                3500000, 3500000, 3500000, 3500000, 3500000, 3500000, 7143
            ]),
            call.put('userPrograms',
                     [3, 0, 4, 0, 3, 0, 4, 0, 3, 0, 4, 0, 2, 8]),
            call.put('velocityMode',
                     [2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 3]),
            call.post('buildProfile')
        ]
        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.pmactrajectorypart.INTERPOLATE_INTERVAL",
        2.0)
    def test_long_move(self):
        self.do_configure(axes_to_scan=["x"], x_pos=-10.1375)
        assert self.child.handled_requests.mock_calls[13:18] == [
            call.put('pointsToBuild', 5),
            call.put(
                'positionsA',
                pytest.approx([-8.2575, -6.1775, -4.0975, -2.0175, -0.1375])),
            call.put('timeArray',
                     [2080000, 2080000, 2080000, 2080000, 2080000]),
            call.put('userPrograms', [8, 8, 8, 8, 8]),
            call.put('velocityMode', [0, 0, 0, 0, 2])
        ]
Beispiel #26
0
class TestRunnableControllerBreakpoints(unittest.TestCase):
    def setUp(self):
        self.p = Process("process1")
        self.context = Context(self.p)

        self.p2 = Process("process2")
        self.context2 = Context(self.p2)

        # Make a motion block to act as our child
        for c in motion_block(mri="childBlock", config_dir="/tmp"):
            self.p.add_controller(c)
        self.b_child = self.context.block_view("childBlock")

        # create a root block for the RunnableController block to reside in
        self.c = RunnableController(mri="mainBlock", config_dir="/tmp")
        self.p.add_controller(self.c)
        self.b = self.context.block_view("mainBlock")
        self.ss = self.c.state_set

        # start the process off
        self.checkState(self.ss.DISABLED)
        self.p.start()
        self.checkState(self.ss.READY)

    def tearDown(self):
        self.p.stop(timeout=1)

    def checkState(self, state):
        assert self.c.state.value == state

    def checkSteps(self, configured, completed, total):
        assert self.b.configuredSteps.value == configured
        assert self.b.completedSteps.value == completed
        assert self.b.totalSteps.value == total

    def test_steps_per_run_one_axis(self):
        line = LineGenerator("x", "mm", 0, 180, 10)
        duration = 0.01
        compound = CompoundGenerator([line], [], [], duration)
        compound.prepare()

        steps_per_run = self.c.get_steps_per_run(generator=compound,
                                                 axes_to_move=["x"],
                                                 breakpoints=[])
        assert steps_per_run == [10]

    def test_steps_per_run_concat(self):
        line1 = LineGenerator("x", "mm", -10, -10, 5)
        line2 = LineGenerator("x", "mm", 0, 180, 10)
        line3 = LineGenerator("x", "mm", 190, 190, 2)
        duration = 0.01
        concat = ConcatGenerator([line1, line2, line3])
        compound = CompoundGenerator([concat], [], [], duration)
        compound.prepare()
        breakpoints = [2, 3, 10, 2]

        steps_per_run = self.c.get_steps_per_run(generator=compound,
                                                 axes_to_move=["x"],
                                                 breakpoints=breakpoints)
        assert steps_per_run == breakpoints

    def test_breakpoints_tomo(self):
        line1 = LineGenerator("x", "mm", -10, -10, 5)
        line2 = LineGenerator("x", "mm", 0, 180, 10)
        line3 = LineGenerator("x", "mm", 190, 190, 2)
        duration = 0.01
        concat = ConcatGenerator([line1, line2, line3])
        breakpoints = [2, 3, 10, 2]
        self.b.configure(
            generator=CompoundGenerator([concat], [], [], duration),
            axesToMove=["x"],
            breakpoints=breakpoints,
        )

        assert self.c.configure_params.generator.size == 17
        self.checkSteps(2, 0, 17)
        self.checkState(self.ss.ARMED)

        self.b.run()
        self.checkSteps(5, 2, 17)
        self.checkState(self.ss.ARMED)

        self.b.run()
        self.checkSteps(15, 5, 17)
        self.checkState(self.ss.ARMED)

        self.b.run()
        self.checkSteps(17, 15, 17)
        self.checkState(self.ss.ARMED)

        self.b.run()
        self.checkSteps(17, 17, 17)
        self.checkState(self.ss.FINISHED)

    def test_breakpoints_sum_larger_than_total_steps_raises_AssertionError(
            self):
        line1 = LineGenerator("x", "mm", -10, -10, 5)
        line2 = LineGenerator("x", "mm", 0, 180, 10)
        line3 = LineGenerator("x", "mm", 190, 190, 2)
        duration = 0.01
        concat = ConcatGenerator([line1, line2, line3])

        breakpoints = [2, 3, 100, 2]

        self.assertRaises(
            AssertionError,
            self.b.configure,
            generator=CompoundGenerator([concat], [], [], duration),
            axesToMove=["x"],
            breakpoints=breakpoints,
        )

    def test_breakpoints_without_last(self):
        line1 = LineGenerator("x", "mm", -10, -10, 5)
        line2 = LineGenerator("x", "mm", 0, 180, 10)
        line3 = LineGenerator("x", "mm", 190, 190, 2)
        duration = 0.01
        concat = ConcatGenerator([line1, line2, line3])
        breakpoints = [2, 3, 10]
        self.b.configure(
            generator=CompoundGenerator([concat], [], [], duration),
            axesToMove=["x"],
            breakpoints=breakpoints,
        )

        assert self.c.configure_params.generator.size == 17
        self.checkSteps(2, 0, 17)
        self.checkState(self.ss.ARMED)

        self.b.run()
        self.checkSteps(5, 2, 17)
        self.checkState(self.ss.ARMED)

        self.b.run()
        self.checkSteps(15, 5, 17)
        self.checkState(self.ss.ARMED)

        self.b.run()
        self.checkSteps(17, 15, 17)
        self.checkState(self.ss.ARMED)

        self.b.run()
        self.checkSteps(17, 17, 17)
        self.checkState(self.ss.FINISHED)

    def test_breakpoints_rocking_tomo(self):
        line1 = LineGenerator("x", "mm", -10, -10, 5)
        line2 = LineGenerator("x", "mm", 0, 180, 10)
        line3 = LineGenerator("x", "mm", 190, 190, 2)
        line4 = LineGenerator("x", "mm", 180, 0, 10)
        duration = 0.01
        concat = ConcatGenerator([line1, line2, line3, line4])
        breakpoints = [2, 3, 10, 2]
        self.b.configure(
            generator=CompoundGenerator([concat], [], [], duration),
            axesToMove=["x"],
            breakpoints=breakpoints,
        )

        assert self.c.configure_params.generator.size == 27
        self.checkSteps(2, 0, 27)
        self.checkState(self.ss.ARMED)

        self.b.run()
        self.checkSteps(5, 2, 27)
        self.checkState(self.ss.ARMED)

        self.b.run()
        self.checkSteps(15, 5, 27)
        self.checkState(self.ss.ARMED)

        self.b.run()
        self.checkSteps(17, 15, 27)
        self.checkState(self.ss.ARMED)

        self.b.run()
        self.checkSteps(27, 17, 27)
        self.checkState(self.ss.ARMED)

        self.b.run()
        self.checkSteps(27, 27, 27)
        self.checkState(self.ss.FINISHED)

    def test_breakpoints_repeat_with_static(self):
        line1 = LineGenerator("x", "mm", -10, -10, 5)
        line2 = LineGenerator("x", "mm", 0, 180, 10)
        line3 = LineGenerator("x", "mm", 190, 190, 2)
        duration = 0.01
        concat = ConcatGenerator([line1, line2, line3])

        staticGen = StaticPointGenerator(2)
        breakpoints = [2, 3, 10, 2, 2, 3, 10, 2]

        self.b.configure(
            generator=CompoundGenerator([staticGen, concat], [], [], duration),
            axesToMove=["x"],
            breakpoints=breakpoints,
        )

        assert self.c.configure_params.generator.size == 34

        self.checkState(self.ss.ARMED)
        self.checkSteps(2, 0, 34)

        self.b.run()
        self.checkSteps(5, 2, 34)
        self.checkState(self.ss.ARMED)

        self.b.run()
        self.checkSteps(15, 5, 34)
        self.checkState(self.ss.ARMED)

        self.b.run()
        self.checkSteps(17, 15, 34)
        self.checkState(self.ss.ARMED)

        self.b.run()
        self.checkSteps(19, 17, 34)
        self.checkState(self.ss.ARMED)

        self.b.run()
        self.checkSteps(22, 19, 34)
        self.checkState(self.ss.ARMED)

        self.b.run()
        self.checkSteps(32, 22, 34)
        self.checkState(self.ss.ARMED)

        self.b.run()
        self.checkSteps(34, 32, 34)
        self.checkState(self.ss.ARMED)

        self.b.run()
        self.checkState(self.ss.FINISHED)

    def test_breakpoints_repeat_rocking_tomo(self):
        line1 = LineGenerator("x", "mm", -10, -10, 5)
        line2 = LineGenerator("x", "mm", 0, 180, 10)
        line3 = LineGenerator("x", "mm", 190, 190, 2)
        line4 = LineGenerator("x", "mm", 180, 0, 10)
        concat = ConcatGenerator([line1, line2, line3, line4])

        staticGen = StaticPointGenerator(2)

        duration = 0.01
        breakpoints = [2, 3, 10, 2, 10, 2, 3, 10, 2, 10]
        self.b.configure(
            generator=CompoundGenerator([staticGen, concat], [], [], duration),
            axesToMove=["x"],
            breakpoints=breakpoints,
        )

        assert self.c.configure_params.generator.size == 54

        self.checkState(self.ss.ARMED)
        self.checkSteps(2, 0, 54)

        self.b.run()
        self.checkSteps(5, 2, 54)
        self.checkState(self.ss.ARMED)

        self.b.run()
        self.checkSteps(15, 5, 54)
        self.checkState(self.ss.ARMED)

        self.b.run()
        self.checkSteps(17, 15, 54)
        self.checkState(self.ss.ARMED)

        self.b.run()
        self.checkSteps(27, 17, 54)
        self.checkState(self.ss.ARMED)

        self.b.run()
        self.checkSteps(29, 27, 54)
        self.checkState(self.ss.ARMED)

        self.b.run()
        self.checkSteps(32, 29, 54)
        self.checkState(self.ss.ARMED)

        self.b.run()
        self.checkSteps(42, 32, 54)
        self.checkState(self.ss.ARMED)

        self.b.run()
        self.checkSteps(44, 42, 54)
        self.checkState(self.ss.ARMED)

        self.b.run()
        self.checkSteps(54, 44, 54)
        self.checkState(self.ss.ARMED)

        self.b.run()
        self.checkState(self.ss.FINISHED)

    def test_breakpoints_helical_scan(self):
        line1 = LineGenerator(["y", "x"], ["mm", "mm"], [-0.555556, -10],
                              [-0.555556, -10], 5)
        line2 = LineGenerator(["y", "x"], ["mm", "mm"], [0, 0], [10, 180], 10)
        line3 = LineGenerator(["y", "x"], ["mm", "mm"], [10.555556, 190],
                              [10.555556, 190], 2)
        duration = 0.01
        concat = ConcatGenerator([line1, line2, line3])

        breakpoints = [2, 3, 10, 2]
        self.b.configure(
            generator=CompoundGenerator([concat], [], [], duration),
            axesToMove=["y", "x"],
            breakpoints=breakpoints,
        )

        assert self.c.configure_params.generator.size == 17

        self.checkState(self.ss.ARMED)
        self.checkSteps(2, 0, 17)

        self.b.run()
        self.checkSteps(5, 2, 17)
        self.checkState(self.ss.ARMED)

        self.b.run()
        self.checkSteps(15, 5, 17)
        self.checkState(self.ss.ARMED)

        self.b.run()
        self.checkSteps(17, 15, 17)
        self.checkState(self.ss.ARMED)

        self.b.run()
        self.checkState(self.ss.FINISHED)

    def test_breakpoints_with_pause(self):
        line1 = LineGenerator("x", "mm", -10, -10, 5)
        line2 = LineGenerator("x", "mm", 0, 180, 10)
        line3 = LineGenerator("x", "mm", 190, 190, 2)
        duration = 0.01
        concat = ConcatGenerator([line1, line2, line3])
        breakpoints = [2, 3, 10, 2]
        self.b.configure(
            generator=CompoundGenerator([concat], [], [], duration),
            axesToMove=["x"],
            breakpoints=breakpoints,
        )

        assert self.c.configure_params.generator.size == 17

        self.checkSteps(2, 0, 17)
        self.checkState(self.ss.ARMED)

        self.b.run()
        self.checkSteps(5, 2, 17)
        self.checkState(self.ss.ARMED)

        # rewind
        self.b.pause(lastGoodStep=1)
        self.checkSteps(2, 1, 17)
        self.checkState(self.ss.ARMED)
        self.b.run()
        self.checkSteps(5, 2, 17)
        self.checkState(self.ss.ARMED)

        self.b.run()
        self.checkSteps(15, 5, 17)
        self.checkState(self.ss.ARMED)

        self.b.run()
        self.checkSteps(17, 15, 17)
        self.checkState(self.ss.ARMED)

        # rewind
        self.b.pause(lastGoodStep=11)
        self.checkSteps(15, 11, 17)
        self.checkState(self.ss.ARMED)
        self.b.run()
        self.checkSteps(17, 15, 17)
        self.checkState(self.ss.ARMED)

        self.b.run()
        self.checkSteps(17, 17, 17)
        self.checkState(self.ss.FINISHED)

    def abort_after_1s(self):
        # Need a new context as in a different cothread
        c = Context(self.p)
        b = c.block_view("mainBlock")
        c.sleep(1.0)
        self.checkState(self.ss.RUNNING)
        b.abort()
        self.checkState(self.ss.ABORTED)

    def test_run_returns_in_ABORTED_state_when_aborted(self):
        # Add our forever running part
        forever_part = RunForeverPart(mri="childBlock",
                                      name="forever_part",
                                      initial_visibility=True)
        self.c.add_part(forever_part)

        # Configure our block
        duration = 0.1
        line1 = LineGenerator("y", "mm", 0, 2, 3)
        line2 = LineGenerator("x", "mm", 0, 2, 2, alternate=True)
        compound = CompoundGenerator([line1, line2], [], [], duration)
        self.b.configure(generator=compound, axesToMove=["x"])

        # Spawn the abort thread
        abort_thread = cothread.Spawn(self.abort_after_1s, raise_on_wait=True)

        # Do the run, which will be aborted
        with self.assertRaises(AbortedError):
            self.b.run()

        self.checkState(self.ss.ABORTED)

        # Check the abort thread didn't raise
        abort_thread.Wait(1.0)

    def test_breakpoints_tomo_with_outer_axis(self):
        # Outer axis we don't move
        outer_steps = 2
        line_outer = LineGenerator("y", "mm", 0, 1, outer_steps)

        # ConcatGenerator we do move
        line1 = LineGenerator("x", "mm", -10, -10, 5)
        line2 = LineGenerator("x", "mm", 0, 180, 10)
        line3 = LineGenerator("x", "mm", 190, 190, 2)
        concat = ConcatGenerator([line1, line2, line3])

        compound = CompoundGenerator([line_outer, concat], [], [],
                                     duration=0.01)
        breakpoints = [2, 3, 10, 2]
        inner_steps = sum(breakpoints)
        total_steps = inner_steps * outer_steps

        self.b.configure(generator=compound,
                         axesToMove=["x"],
                         breakpoints=breakpoints)
        # Configured, completed, total
        self.checkSteps(2, 0, total_steps)
        self.checkState(self.ss.ARMED)

        # Check we have the full configured steps
        assert self.c.configure_params.generator.size == total_steps

        # Check our breakpoints steps
        expected_breakpoint_steps = [2, 5, 15, 17, 19, 22, 32, 34]
        self.assertEqual(expected_breakpoint_steps, self.c.breakpoint_steps)

        # Run our controller through all but last breakpoint
        breakpoints = len(expected_breakpoint_steps)
        for index in range(breakpoints - 1):
            self.b.run()
            self.checkSteps(
                expected_breakpoint_steps[index + 1],
                expected_breakpoint_steps[index],
                total_steps,
            )
            self.checkState(self.ss.ARMED)

        # Final breakpoint
        self.b.run()
        self.checkSteps(total_steps, total_steps, total_steps)
        self.checkState(self.ss.FINISHED)

    def test_breakpoints_tomo_with_two_outer_axes(self):
        # Outer axes we don't move
        outer_steps = 2
        line_outer = LineGenerator("y", "mm", 0, 1, outer_steps)
        outer_outer_steps = 3
        line_outer_outer = LineGenerator("z", "mm", 0, 1, outer_outer_steps)

        # ConcatGenerator we do move
        line1 = LineGenerator("x", "mm", -10, -10, 5)
        line2 = LineGenerator("x", "mm", 0, 180, 10)
        concat = ConcatGenerator([line1, line2])

        compound = CompoundGenerator([line_outer_outer, line_outer, concat],
                                     [], [],
                                     duration=0.01)
        breakpoints = [2, 3, 10]
        inner_steps = sum(breakpoints)
        total_steps = inner_steps * outer_steps * outer_outer_steps

        self.b.configure(generator=compound,
                         axesToMove=["x"],
                         breakpoints=breakpoints)
        # Configured, completed, total
        self.checkSteps(2, 0, total_steps)
        self.checkState(self.ss.ARMED)

        # Check we have the full configured steps
        assert self.c.configure_params.generator.size == total_steps

        # Check our breakpoints steps
        expected_breakpoint_steps = [
            2,
            5,
            15,
            17,
            20,
            30,
            32,
            35,
            45,
            47,
            50,
            60,
            62,
            65,
            75,
            77,
            80,
            90,
        ]
        self.assertEqual(expected_breakpoint_steps, self.c.breakpoint_steps)

        # Run our controller through all but last breakpoint
        breakpoints = len(expected_breakpoint_steps)
        for index in range(breakpoints - 1):
            self.b.run()
            self.checkSteps(
                expected_breakpoint_steps[index + 1],
                expected_breakpoint_steps[index],
                total_steps,
            )
            self.checkState(self.ss.ARMED)

        # Final breakpoint
        self.b.run()
        self.checkSteps(total_steps, total_steps, total_steps)
        self.checkState(self.ss.FINISHED)

    def test_breakpoints_2d_inner_scan(self):
        # Y-axis
        outer_steps = 2
        line_y = LineGenerator("y", "mm", 0, 1, outer_steps)

        # X-axis
        line_x_1 = LineGenerator("x", "mm", -10, -10, 5)
        line_x_2 = LineGenerator("x", "mm", 0, 180, 10)
        line_x_3 = LineGenerator("x", "mm", 190, 190, 2)
        line_x = ConcatGenerator([line_x_1, line_x_2, line_x_3])

        compound = CompoundGenerator([line_y, line_x], [], [], duration=0.01)
        breakpoints = [2, 3, 10, 2, 17]
        total_steps = sum(breakpoints)

        # Configure the scan
        self.b.configure(generator=compound,
                         axesToMove=["x", "y"],
                         breakpoints=breakpoints)
        self.checkSteps(2, 0, total_steps)
        self.checkState(self.ss.ARMED)

        # Check we have the full amount of configured steps
        assert self.c.configure_params.generator.size == total_steps

        # Check our breakpoints steps
        expected_breakpoint_steps = [2, 5, 15, 17, 34]
        self.assertEqual(expected_breakpoint_steps, self.c.breakpoint_steps)

        # Run our controller through all but last breakpoint
        breakpoints = len(expected_breakpoint_steps)
        for index in range(breakpoints - 1):
            self.b.run()
            self.checkSteps(
                expected_breakpoint_steps[index + 1],
                expected_breakpoint_steps[index],
                total_steps,
            )
            self.checkState(self.ss.ARMED)

        # Final breakpoint
        self.b.run()
        self.checkSteps(total_steps, total_steps, total_steps)
        self.checkState(self.ss.FINISHED)

    def test_breakpoints_2d_inner_scan_with_outer_axis(self):
        # Outer axes we don't move
        outer_steps = 2
        line_outer = LineGenerator("z", "mm", 0, 1, outer_steps)

        # Y-axis
        line_y = LineGenerator("y", "mm", 0, 1, 2)

        # X-axis
        line_x_1 = LineGenerator("x", "mm", -10, -10, 5)
        line_x_2 = LineGenerator("x", "mm", 0, 180, 10)
        line_x_3 = LineGenerator("x", "mm", 190, 190, 2)
        line_x = ConcatGenerator([line_x_1, line_x_2, line_x_3])

        compound = CompoundGenerator([line_outer, line_y, line_x], [], [],
                                     duration=0.01)
        breakpoints = [2, 3, 10, 2, 17]
        total_steps = sum(breakpoints) * outer_steps

        # Configure the scan
        self.b.configure(generator=compound,
                         axesToMove=["x", "y"],
                         breakpoints=breakpoints)
        self.checkSteps(2, 0, total_steps)
        self.checkState(self.ss.ARMED)

        # Check we have the full amount of configured steps
        assert self.c.configure_params.generator.size == total_steps

        # Check our breakpoints steps
        expected_breakpoint_steps = [2, 5, 15, 17, 34, 36, 39, 49, 51, 68]
        self.assertEqual(expected_breakpoint_steps, self.c.breakpoint_steps)

        # Run our controller through all but last breakpoint
        breakpoints = len(expected_breakpoint_steps)
        for index in range(breakpoints - 1):
            self.b.run()
            self.checkSteps(
                expected_breakpoint_steps[index + 1],
                expected_breakpoint_steps[index],
                total_steps,
            )
            self.checkState(self.ss.ARMED)

        # Final breakpoint
        self.b.run()
        self.checkSteps(total_steps, total_steps, total_steps)
        self.checkState(self.ss.FINISHED)
Beispiel #27
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])
Beispiel #28
0
class TestPMACTrajectoryPart(ChildTestCase):
    def setUp(self):
        self.process = Process("Process")
        self.context = Context(self.process)
        self.cs = self.create_child_block(cs_block,
                                          self.process,
                                          mri="PMAC:CS1",
                                          prefix="PV:CSPRE")
        self.child = self.create_child_block(pmac_trajectory_block,
                                             self.process,
                                             mri="PMAC:TRAJ",
                                             prefix="PV:PRE")
        self.o = PmacTrajectoryPart(name="pmac", mri="PMAC:TRAJ")
        self.process.start()

    def tearDown(self):
        del self.context
        self.process.stop(timeout=1)

    def test_init(self):
        registrar = Mock()
        self.o.setup(registrar)
        registrar.add_attribute_model.assert_called_once_with(
            "minTurnaround", self.o.min_turnaround,
            self.o.min_turnaround.set_value)

    def test_bad_units(self):
        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 make_part_info(self, x_pos=0.5, y_pos=0.0, units="mm"):
        part_info = dict(xpart=[
            MotorInfo(cs_axis="A",
                      cs_port="CS1",
                      acceleration=2.5,
                      resolution=0.001,
                      offset=0.0,
                      max_velocity=1.0,
                      current_position=x_pos,
                      scannable="x",
                      velocity_settle=0.0,
                      units=units)
        ],
                         ypart=[
                             MotorInfo(cs_axis="B",
                                       cs_port="CS1",
                                       acceleration=2.5,
                                       resolution=0.001,
                                       offset=0.0,
                                       max_velocity=1.0,
                                       current_position=y_pos,
                                       scannable="y",
                                       velocity_settle=0.0,
                                       units=units)
                         ],
                         brick=[ControllerInfo(i10=1705244)],
                         cs1=[CSInfo(mri="PMAC:CS1", port="CS1")])
        return part_info

    def do_configure(self,
                     axes_to_scan,
                     completed_steps=0,
                     x_pos=0.5,
                     y_pos=0.0,
                     duration=1.0,
                     units="mm"):
        part_info = self.make_part_info(x_pos, y_pos, units)
        steps_to_do = 3 * len(axes_to_scan)
        xs = LineGenerator("x", "mm", 0.0, 0.5, 3, alternate=True)
        ys = LineGenerator("y", "mm", 0.0, 0.1, 2)
        generator = CompoundGenerator([ys, xs], [], [], duration)
        generator.prepare()
        self.o.configure(self.context, completed_steps, steps_to_do, part_info,
                         generator, axes_to_scan)

    def test_validate(self):
        generator = CompoundGenerator([], [], [], 0.0102)
        axesToMove = ["x"]
        part_info = self.make_part_info()
        ret = self.o.validate(part_info, generator, axesToMove)
        expected = 0.010166
        assert ret.value.duration == expected

    @patch(
        "malcolm.modules.pmac.parts.pmactrajectorypart.INTERPOLATE_INTERVAL",
        0.2)
    def test_configure(self):
        # Pretend to respond on demand values before they are actually set
        self.set_attributes(self.cs, demandA=-0.1375, demandB=0.0)
        self.do_configure(axes_to_scan=["x", "y"])
        assert self.cs.handled_requests.mock_calls == [
            call.put('deferMoves', True),
            call.put('csMoveTime', 0),
            call.put('demandA', -0.1375),
            call.put('demandB', 0.0),
            call.put('deferMoves', False)
        ]
        assert self.child.handled_requests.mock_calls == [
            call.put('numPoints', 4000000),
            call.put('cs', 'CS1'),
            call.put('useA', False),
            call.put('useB', False),
            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),
            call.put('pointsToBuild', 1),
            call.put('timeArray', pytest.approx([2000])),
            call.put('userPrograms', pytest.approx([8])),
            call.put('velocityMode', pytest.approx([3])),
            call.post('buildProfile'),
            call.post('executeProfile'),
        ] + self.resolutions_and_use_call() + [
            call.put('pointsToBuild', 16),
            # pytest.approx to allow sensible compare with numpy arrays
            call.put(
                'positionsA',
                pytest.approx([
                    -0.125, 0.0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.6375, 0.625,
                    0.5, 0.375, 0.25, 0.125, 0.0, -0.125, -0.1375
                ])),
            call.put(
                'positionsB',
                pytest.approx([
                    0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.05, 0.1, 0.1, 0.1,
                    0.1, 0.1, 0.1, 0.1, 0.1
                ])),
            call.put(
                'timeArray',
                pytest.approx([
                    100000, 500000, 500000, 500000, 500000, 500000, 500000,
                    200000, 200000, 500000, 500000, 500000, 500000, 500000,
                    500000, 100000
                ])),
            call.put(
                'userPrograms',
                pytest.approx([1, 4, 1, 4, 1, 4, 2, 8, 1, 4, 1, 4, 1, 4, 2, 8
                               ])),
            call.put(
                'velocityMode',
                pytest.approx([2, 0, 0, 0, 0, 0, 1, 0, 2, 0, 0, 0, 0, 0, 1, 3
                               ])),
            call.post('buildProfile')
        ]
        assert self.o.completed_steps_lookup == [
            0, 0, 1, 1, 2, 2, 3, 3, 3, 3, 4, 4, 5, 5, 6, 6
        ]

    @patch("malcolm.modules.pmac.parts.pmactrajectorypart.PROFILE_POINTS", 4)
    @patch(
        "malcolm.modules.pmac.parts.pmactrajectorypart.INTERPOLATE_INTERVAL",
        0.2)
    def test_update_step(self):
        # Pretend to respond on demand values before they are actually set
        self.set_attributes(self.cs, demandA=-0.1375, demandB=0.0)
        self.do_configure(axes_to_scan=["x", "y"], x_pos=0.0, y_pos=0.2)
        positionsA = self.child.handled_requests.put.call_args_list[-5][0][1]
        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["time_array"]) == 1
        self.o.registrar = Mock()
        self.child.handled_requests.reset_mock()
        self.o.update_step(3, self.context.block_view("PMAC:TRAJ"))
        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 == [
            call.put('pointsToBuild', 4),
            call.put('positionsA', pytest.approx([0.375, 0.5, 0.625, 0.6375])),
            call.put('positionsB', pytest.approx([0.0, 0.0, 0.0, 0.05])),
            call.put('timeArray',
                     pytest.approx([500000, 500000, 500000, 200000])),
            call.put('userPrograms', pytest.approx([1, 4, 2, 8])),
            call.put('velocityMode', pytest.approx([0, 0, 1, 0])),
            call.post('appendProfile')
        ]
        assert self.o.end_index == 3
        assert len(self.o.completed_steps_lookup) == 9
        assert len(self.o.profile["time_array"]) == 1

    def test_run(self):
        self.o.run(self.context)
        assert self.child.handled_requests.mock_calls == [
            call.post('executeProfile')
        ]

    def test_reset(self):
        self.o.reset(self.context)
        assert self.child.handled_requests.mock_calls == [
            call.post('abortProfile')
        ]

    def test_multi_run(self):
        # Pretend to respond on demand values before they are actually set
        self.set_attributes(self.cs, demandA=-0.1375)
        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()
        # Pretend to respond on demand values before they are actually set
        self.set_attributes(self.cs, demandA=0.6375)
        self.do_configure(axes_to_scan=["x"], completed_steps=3, x_pos=0.6375)
        assert self.child.handled_requests.mock_calls == [
            call.put('numPoints', 4000000),
            call.put('cs', 'CS1'),
            call.put('useA', False),
            call.put('useB', False),
            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),
            call.put('pointsToBuild', 1),
            call.put('timeArray', pytest.approx([2000])),
            call.put('userPrograms', pytest.approx([8])),
            call.put('velocityMode', pytest.approx([3])),
            call.post('buildProfile'),
            call.post('executeProfile'),
        ] + self.resolutions_and_use_call(useB=False) + [
            call.put('pointsToBuild', 8),
            call.put(
                'positionsA',
                pytest.approx(
                    [0.625, 0.5, 0.375, 0.25, 0.125, 0.0, -0.125, -0.1375])),
            call.put(
                'timeArray',
                pytest.approx([
                    100000, 500000, 500000, 500000, 500000, 500000, 500000,
                    100000
                ])),
            call.put('userPrograms', pytest.approx([1, 4, 1, 4, 1, 4, 2, 8])),
            call.put('velocityMode', pytest.approx([2, 0, 0, 0, 0, 0, 1, 3])),
            call.post('buildProfile')
        ]

    @patch(
        "malcolm.modules.pmac.parts.pmactrajectorypart.INTERPOLATE_INTERVAL",
        0.2)
    def test_long_steps_lookup(self):
        # Pretend to respond on demand values before they are actually set
        self.set_attributes(self.cs, demandA=0.6250637755102041)
        self.do_configure(axes_to_scan=["x"],
                          completed_steps=3,
                          x_pos=0.62506,
                          duration=14.0)
        assert self.child.handled_requests.mock_calls[-6:] == [
            call.put('pointsToBuild', 14),
            call.put(
                'positionsA',
                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
                ])),
            call.put(
                'timeArray',
                pytest.approx([
                    7143, 3500000, 3500000, 3500000, 3500000, 3500000, 3500000,
                    3500000, 3500000, 3500000, 3500000, 3500000, 3500000, 7143
                ])),
            call.put('userPrograms',
                     pytest.approx([1, 0, 4, 0, 1, 0, 4, 0, 1, 0, 4, 0, 2,
                                    8])),
            call.put('velocityMode',
                     pytest.approx([2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
                                    3])),
            call.post('buildProfile')
        ]
        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.pmactrajectorypart.PROFILE_POINTS", 9)
    def test_split_in_a_long_step_lookup(self):
        # Pretend to respond on demand values before they are actually set
        self.set_attributes(self.cs, demandA=0.6250637755102041)
        self.do_configure(axes_to_scan=["x"],
                          completed_steps=3,
                          x_pos=0.62506,
                          duration=14.0)
        # The last 6 calls show what trajectory we are building, ignore the
        # first 11 which are just the useX calls and cs selection
        assert self.child.handled_requests.mock_calls[-6:] == [
            call.put('pointsToBuild', 9),
            call.put(
                'positionsA',
                pytest.approx([
                    0.625, 0.5625, 0.5, 0.4375, 0.375, 0.3125, 0.25, 0.1875,
                    0.125
                ])),
            call.put(
                'timeArray',
                pytest.approx([
                    7143, 3500000, 3500000, 3500000, 3500000, 3500000, 3500000,
                    3500000, 3500000
                ])),
            call.put('userPrograms', pytest.approx([1, 0, 4, 0, 1, 0, 4, 0,
                                                    1])),
            call.put('velocityMode', pytest.approx([2, 0, 0, 0, 0, 0, 0, 0,
                                                    0])),
            call.post('buildProfile')
        ]
        # 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:TRAJ"))
        # Expect the rest of the points
        assert self.child.handled_requests.mock_calls[-6:] == [
            call.put('pointsToBuild', 5),
            call.put(
                'positionsA',
                pytest.approx(
                    [0.0625, 0.0, -0.0625, -0.125, -0.12506377551020409])),
            call.put('timeArray',
                     pytest.approx([3500000, 3500000, 3500000, 3500000,
                                    7143])),
            call.put('userPrograms', pytest.approx([0, 4, 0, 2, 8])),
            call.put('velocityMode', pytest.approx([0, 0, 0, 1, 3])),
            call.post('appendProfile')
        ]
        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
        ])
Beispiel #29
0
 def arm_detector(self, context: Context) -> None:
     child = context.block_view(self.mri)
     child.numImagesPerSeries.put_value(1)
     super().arm_detector(context)
     # Wait for the fan to be ready before returning from configure
     child.when_value_matches("fanStateReady", 1)
Beispiel #30
0
class TestDetectorDriverPart(ChildTestCase):
    def setUp(self):
        self.process = Process("Process")
        self.context = Context(self.process)

        def child_block():
            controllers, parts = adbase_parts(prefix="prefix")
            controller = StatefulController("mri")
            for part in parts:
                controller.add_part(part)
            return controllers + [controller]

        self.child = self.create_child_block(child_block, self.process)
        self.mock_when_value_matches(self.child)
        self.o = DetectorDriverPart(name="m",
                                    mri="mri",
                                    soft_trigger_modes=["Internal"])
        self.context.set_notify_dispatch_request(
            self.o.notify_dispatch_request)
        self.process.start()

    def tearDown(self):
        self.process.stop(timeout=2)

    def test_report(self):
        info = self.o.on_report_status()
        assert len(info) == 1
        assert info[0].rank == 2

    def test_configure(self):
        xs = LineGenerator("x", "mm", 0.0, 0.5, 3, alternate=True)
        ys = LineGenerator("y", "mm", 0.0, 0.1, 2)
        generator = CompoundGenerator([ys, xs], [], [], 0.1)
        generator.prepare()
        completed_steps = 0
        steps_to_do = 6
        info = ExposureDeadtimeInfo(0.01, 1000, 0.0)
        part_info = dict(anyname=[info])
        self.set_attributes(self.child, triggerMode="Internal")
        self.o.on_configure(
            self.context,
            completed_steps,
            steps_to_do,
            part_info,
            generator,
            fileDir="/tmp",
            exposure=info.calculate_exposure(generator.duration),
        )
        assert self.child.handled_requests.mock_calls == [
            call.put("arrayCallbacks", True),
            call.put("arrayCounter", 0),
            call.put("exposure", 0.1 - 0.01 - 0.0001),
            call.put("imageMode", "Multiple"),
            call.put("numImages", 6),
            call.put("acquirePeriod", 0.1 - 0.0001),
        ]
        assert not self.o.is_hardware_triggered

    def test_configure_with_extra_attributes(self):
        xs = LineGenerator("x", "mm", 0.0, 0.5, 3, alternate=True)
        ys = LineGenerator("y", "mm", 0.0, 0.1, 2)
        generator = CompoundGenerator([ys, xs], [], [], 0.1)
        generator.prepare()
        completed_steps = 0
        steps_to_do = 6
        expected_xml_filename = "/tmp/mri-attributes.xml"
        self.set_attributes(self.child, triggerMode="Internal")
        extra_attributes = ExtraAttributesTable(
            name=["test1", "test2", "test3"],
            sourceId=["PV1", "PV2", "PARAM1"],
            sourceType=[SourceType.PV, SourceType.PV, SourceType.PARAM],
            description=[
                "a test pv", "another test PV", "a param, for testing"
            ],
            dataType=[DataType.DBRNATIVE, DataType.DOUBLE, DataType.DOUBLE],
            datasetType=[
                AttributeDatasetType.MONITOR,
                AttributeDatasetType.DETECTOR,
                AttributeDatasetType.POSITION,
            ],
        )
        self.o.extra_attributes.set_value(extra_attributes)
        self.o.on_configure(self.context,
                            completed_steps,
                            steps_to_do, {},
                            generator,
                            fileDir="/tmp")
        assert self.child.handled_requests.mock_calls == [
            call.put("arrayCallbacks", True),
            call.put("arrayCounter", 0),
            call.put("imageMode", "Multiple"),
            call.put("numImages", 6),
            call.put("attributesFile", expected_xml_filename),
        ]
        assert not self.o.is_hardware_triggered
        with open(expected_xml_filename) as f:
            actual_xml = f.read().replace(">", ">\n")

        actual_tree = ElementTree.XML(actual_xml)
        expected_tree = ElementTree.XML(expected_xml)
        assert ElementTree.dump(actual_tree) == ElementTree.dump(expected_tree)

    def test_version_check(self):
        block = self.context.block_view("mri")
        self.o.required_version = "2.2"
        self.set_attributes(self.child, driverVersion="1.9")
        self.assertRaises(IncompatibleError, self.o.check_driver_version,
                          block)
        self.set_attributes(self.child, driverVersion="2.1")
        self.assertRaises(IncompatibleError, self.o.check_driver_version,
                          block)
        self.set_attributes(self.child, driverVersion="3.0")
        self.assertRaises(IncompatibleError, self.o.check_driver_version,
                          block)
        self.set_attributes(self.child, driverVersion="2.2")
        self.o.check_driver_version(block)
        self.set_attributes(self.child, driverVersion="2.2.3")
        self.o.check_driver_version(block)

    def test_run(self):
        self.o.registrar = MagicMock()
        # This would have been done by configure
        self.o.is_hardware_triggered = False
        self.o.on_run(self.context)
        assert self.child.handled_requests.mock_calls == [
            call.post("start"),
            call.when_value_matches("acquiring", True, None),
            call.when_value_matches("arrayCounterReadback", 0, None),
        ]
        assert self.o.registrar.report.call_count == 2
        assert self.o.registrar.report.call_args[0][0].steps == 0

    def test_abort(self):
        self.o.on_abort(self.context)
        assert self.child.handled_requests.mock_calls == [
            call.post("stop"),
            call.when_value_matches("acquiring", False, None),
        ]