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
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) ]
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
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"
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 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)
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
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, )
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)
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
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)
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
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
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]) ]
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)
class TestPMACChildPart(ChildTestCase): def setUp(self): self.process = Process("Process") self.context = Context(self.process) pmac_block = make_block_creator(__file__, "test_pmac_manager_block.yaml") self.child = self.create_child_block(pmac_block, self.process, mri_prefix="PMAC", config_dir="/tmp") # These are the child blocks we are interested in self.child_x = self.process.get_controller("BL45P-ML-STAGE-01:X") self.child_y = self.process.get_controller("BL45P-ML-STAGE-01:Y") self.child_cs1 = self.process.get_controller("PMAC:CS1") self.child_traj = self.process.get_controller("PMAC:TRAJ") self.child_status = self.process.get_controller("PMAC:STATUS") # CS1 needs to have the right port otherwise we will error self.set_attributes(self.child_cs1, port="CS1") self.o = PmacChildPart(name="pmac", mri="PMAC") self.context.set_notify_dispatch_request( self.o.notify_dispatch_request) self.process.start() def tearDown(self): del self.context self.process.stop(timeout=1) # TODO: restore this tests when GDA does units right def test_bad_units(self): pytest.skip("awaiting GDA units fix") with self.assertRaises(AssertionError) as cm: self.do_configure(["x", "y"], units="m") assert str(cm.exception) == "x: Expected scan units of 'm', got 'mm'" def resolutions_and_use_call(self, useB=True): return [ call.put("useA", True), call.put("useB", useB), call.put("useC", False), call.put("useU", False), call.put("useV", False), call.put("useW", False), call.put("useX", False), call.put("useY", False), call.put("useZ", False), ] def set_motor_attributes( self, x_pos=0.5, y_pos=0.0, units="mm", x_acceleration=2.5, y_acceleration=2.5, x_velocity=1.0, y_velocity=1.0, settle=0.0, ): # create some parts to mock the motion controller and 2 axes in a CS self.set_attributes( self.child_x, cs="CS1,A", accelerationTime=x_velocity / x_acceleration, resolution=0.001, offset=0.0, maxVelocity=x_velocity, readback=x_pos, velocitySettle=0.0, units=units, ) self.set_attributes( self.child_y, cs="CS1,B", accelerationTime=y_velocity / y_acceleration, resolution=0.001, offset=0.0, maxVelocity=y_velocity, readback=y_pos, velocitySettle=settle, units=units, ) def do_configure( self, axes_to_scan, completed_steps=0, x_pos=0.5, y_pos=0.0, duration=1.0, units="mm", infos=None, settle=0.0, continuous=True, ): self.set_motor_attributes(x_pos, y_pos, units, settle=settle) xs = LineGenerator("x", "mm", 0.0, 0.5, 3, alternate=True) ys = LineGenerator("y", "mm", 0.0, 0.1, 2) if len(axes_to_scan) == 1: # when only scanning one axis, we only do xs once steps_to_do = xs.size else: # otherwise we do xs for each step in ys steps_to_do = xs.size * ys.size generator = CompoundGenerator([ys, xs], [], [], duration, continuous=continuous) generator.prepare() self.o.on_configure( self.context, completed_steps, steps_to_do, {"part": infos}, generator, axes_to_scan, ) def test_validate(self): generator = CompoundGenerator([], [], [], 0.0102) axesToMove = ["x"] # servoFrequency() return value self.child.handled_requests.post.return_value = 4919.300698316487 ret = self.o.on_validate(self.context, generator, axesToMove, {}) expected = 0.010166 assert ret.value.duration == expected def do_check_output_quantized(self): assert self.child.handled_requests.mock_calls[:4] == [ call.post("writeProfile", csPort="CS1", timeArray=[0.002], userPrograms=[8]), call.post("executeProfile"), call.post("moveCS1", a=-0.1374875093687539, b=0.0, moveTime=1.0374875094), call.post( "writeProfile", csPort="CS1", timeArray=pytest.approx([ 99950, 500250, 500250, 500250, 500250, 500250, 500250, 100000, 101000, 101000, 100000, 500250, 500250, 500250, 500250, 500250, 500250, 99950, ]), velocityMode=pytest.approx( [1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 3]), userPrograms=pytest.approx( [1, 4, 1, 4, 1, 4, 2, 8, 8, 8, 1, 4, 1, 4, 1, 4, 2, 8]), a=pytest.approx([ -0.125, 0.0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.63749375, 0.63749375, 0.63749375, 0.625, 0.5, 0.375, 0.25, 0.125, 0.0, -0.125, -0.13748751, ]), b=pytest.approx([ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.01237593, 0.05, 0.08762407, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, ]), ), ] assert self.o.completed_steps_lookup == [ 0, 0, 1, 1, 2, 2, 3, 3, 3, 3, 3, 3, 4, 4, 5, 5, 6, 6, ] a_positions = [ -0.125, 0.0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.6375, 0.6375, 0.6375, 0.625, 0.5, 0.375, 0.25, 0.125, 0.0, -0.125, -0.1375, ] b_positions = [ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0125, 0.05, 0.0875, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, ] user_programs = [1, 4, 1, 4, 1, 4, 2, 8, 8, 8, 1, 4, 1, 4, 1, 4, 2, 8] velocity_mode = [1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 3] time_array = [ 100000, 500000, 500000, 500000, 500000, 500000, 500000, 100000, 100000, 100000, 100000, 500000, 500000, 500000, 500000, 500000, 500000, 100000, ] def do_check_output(self, user_programs=None): if user_programs is None: user_programs = self.user_programs # use a slice here because I'm getting calls to __str__ in debugger assert self.child.handled_requests.mock_calls[:4] == [ call.post("writeProfile", csPort="CS1", timeArray=[0.002], userPrograms=[8]), call.post("executeProfile"), call.post("moveCS1", a=-0.1375, b=0.0, moveTime=1.0375), # pytest.approx to allow sensible compare with numpy arrays call.post( "writeProfile", a=pytest.approx(self.a_positions), b=pytest.approx(self.b_positions), csPort="CS1", timeArray=pytest.approx(self.time_array), userPrograms=pytest.approx(user_programs), velocityMode=pytest.approx(self.velocity_mode), ), ] assert self.o.completed_steps_lookup == [ 0, 0, 1, 1, 2, 2, 3, 3, 3, 3, 3, 3, 4, 4, 5, 5, 6, 6, ] def do_check_output_slower(self): # use a slice here because I'm getting calls to __str__ in debugger assert self.child.handled_requests.mock_calls[:4] == [ call.post("writeProfile", csPort="CS1", timeArray=[0.002], userPrograms=[8]), call.post("executeProfile"), call.post("moveCS1", a=-0.1375, b=0.0, moveTime=1.0375), call.post( "writeProfile", a=pytest.approx([ -0.125, 0.0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.6375, 0.6375, 0.6375, 0.6375, 0.625, 0.5, 0.375, 0.25, 0.125, 0.0, -0.125, -0.1375, ]), b=pytest.approx([ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.00125, 0.02, 0.08, 0.09875, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, ]), csPort="CS1", timeArray=pytest.approx([ 100000, 500000, 500000, 500000, 500000, 500000, 500000, 100000, 300000, 600000, 300000, 100000, 500000, 500000, 500000, 500000, 500000, 500000, 100000, ]), userPrograms=pytest.approx( [1, 4, 1, 4, 1, 4, 2, 8, 8, 8, 8, 1, 4, 1, 4, 1, 4, 2, 8]), velocityMode=pytest.approx( [1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 3]), ), ] assert self.o.completed_steps_lookup == [ 0, 0, 1, 1, 2, 2, 3, 3, 3, 3, 3, 3, 3, 4, 4, 5, 5, 6, 6, ] def do_check_sparse_output(self, user_programs=None): if user_programs is None: user_programs = [1, 8, 0, 0, 0, 1, 8, 0] assert self.child.handled_requests.mock_calls == [ call.post("writeProfile", csPort="CS1", timeArray=[0.002], userPrograms=[8]), call.post("executeProfile"), call.post("moveCS1", a=-0.1375, b=0.0, moveTime=1.0375), # pytest.approx to allow sensible compare with numpy arrays call.post( "writeProfile", a=pytest.approx([ -0.125, 0.625, 0.6375, 0.6375, 0.6375, 0.625, -0.125, -0.1375 ]), b=pytest.approx( [0.0, 0.0, 0.0125, 0.05, 0.0875, 0.1, 0.1, 0.1]), csPort="CS1", timeArray=pytest.approx([ 100000, 3000000, 100000, 100000, 100000, 100000, 3000000, 100000 ]), userPrograms=pytest.approx(user_programs), # note the use of mode 2 AVERAGE_PREV_CURRENT at the end of each # sparse linear row velocityMode=pytest.approx([1, 2, 1, 1, 1, 1, 2, 3]), ), ] assert self.o.completed_steps_lookup == [0, 3, 3, 3, 3, 3, 6, 6] def test_configure(self): self.do_configure(axes_to_scan=["x", "y"]) self.do_check_output() def test_configure_quantize(self): m = [MinTurnaroundInfo(0.002, 0.001)] self.do_configure(axes_to_scan=["x", "y"], duration=1.0005, infos=m) self.do_check_output_quantized() def test_configure_slower_vmax(self): self.set_attributes(self.child_y, maxVelocityPercent=10) self.do_configure(axes_to_scan=["x", "y"]) self.do_check_output_slower() def test_configure_no_pulses(self): self.do_configure(axes_to_scan=["x", "y"], infos=[MotionTriggerInfo(MotionTrigger.NONE)]) self.do_check_sparse_output(user_programs=[0] * 8) def test_configure_start_of_row_pulses(self): self.do_configure(axes_to_scan=["x", "y"], infos=[MotionTriggerInfo(MotionTrigger.ROW_GATE)]) self.do_check_sparse_output() def test_configure_no_axes(self): self.set_motor_attributes() generator = CompoundGenerator([StaticPointGenerator(6)], [], [], duration=0.1) generator.prepare() self.o.on_configure(self.context, 0, 6, {}, generator, []) assert self.child.handled_requests.mock_calls == [ call.post("writeProfile", csPort="CS1", timeArray=[0.002], userPrograms=[8]), call.post("executeProfile"), # pytest.approx to allow sensible compare with numpy arrays call.post( "writeProfile", csPort="CS1", timeArray=pytest.approx([ 2000, 50000, 50000, 50000, 50000, 50000, 50000, 50000, 50000, 50000, 50000, 50000, 50000, 2000, ]), userPrograms=pytest.approx( [1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 2, 8]), velocityMode=pytest.approx( [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 3]), ), ] assert self.o.completed_steps_lookup == [ 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, ] @patch("malcolm.modules.pmac.parts.pmacchildpart.PROFILE_POINTS", 4) def test_update_step(self): self.do_configure(axes_to_scan=["x", "y"], x_pos=0.0, y_pos=0.2) assert len(self.child.handled_requests.mock_calls) == 4 # Check that the first trajectory moves to the first place positionsA = self.child.handled_requests.post.call_args_list[-1][1][ "a"] assert len(positionsA) == 4 assert positionsA[-1] == 0.25 assert self.o.end_index == 2 assert len(self.o.completed_steps_lookup) == 5 assert len(self.o.profile["timeArray"]) == 1 self.o.registrar = Mock() self.child.handled_requests.reset_mock() self.o.update_step(3, self.context.block_view("PMAC")) self.o.registrar.report.assert_called_once() assert self.o.registrar.report.call_args[0][0].steps == 1 assert not self.o.loading assert self.child.handled_requests.mock_calls == [ # pytest.approx to allow sensible compare with numpy arrays call.post( "writeProfile", a=pytest.approx([0.375, 0.5, 0.625, 0.6375]), b=pytest.approx([0.0, 0.0, 0.0, 0.0125]), timeArray=pytest.approx([500000, 500000, 500000, 100000]), userPrograms=pytest.approx([1, 4, 2, 8]), velocityMode=pytest.approx([0, 0, 1, 1]), ) ] assert self.o.end_index == 3 assert len(self.o.completed_steps_lookup) == 11 assert len(self.o.profile["timeArray"]) == 3 def test_run(self): self.o.generator = ANY self.o.on_run(self.context) assert self.child.handled_requests.mock_calls == [ call.post("executeProfile") ] def test_reset(self): self.o.generator = ANY self.o.on_reset(self.context) assert self.child.handled_requests.mock_calls == [ call.post("abortProfile") ] def test_multi_run(self): self.do_configure(axes_to_scan=["x"]) assert self.o.completed_steps_lookup == ([0, 0, 1, 1, 2, 2, 3, 3]) self.child.handled_requests.reset_mock() self.do_configure(axes_to_scan=["x"], completed_steps=3, x_pos=0.6375) assert self.child.handled_requests.mock_calls == [ call.post("writeProfile", csPort="CS1", timeArray=[0.002], userPrograms=[8]), call.post("executeProfile"), call.post("moveCS1", a=0.6375, moveTime=0.0), call.post( "writeProfile", csPort="CS1", a=pytest.approx( [0.625, 0.5, 0.375, 0.25, 0.125, 0.0, -0.125, -0.1375]), timeArray=pytest.approx([ 100000, 500000, 500000, 500000, 500000, 500000, 500000, 100000 ]), userPrograms=pytest.approx([1, 4, 1, 4, 1, 4, 2, 8]), velocityMode=pytest.approx([1, 0, 0, 0, 0, 0, 1, 3]), ), ] def test_long_steps_lookup(self): self.do_configure(axes_to_scan=["x"], completed_steps=3, x_pos=0.62506, duration=14.0) # Ignore the trigger reset and move to start, just look at the last call # which is the profile write assert self.child.handled_requests.mock_calls[-1] == call.post( "writeProfile", csPort="CS1", a=pytest.approx([ 0.625, 0.5625, 0.5, 0.4375, 0.375, 0.3125, 0.25, 0.1875, 0.125, 0.0625, 0.0, -0.0625, -0.125, -0.12506377551020409, ]), timeArray=pytest.approx([ 7143, 3500000, 3500000, 3500000, 3500000, 3500000, 3500000, 3500000, 3500000, 3500000, 3500000, 3500000, 3500000, 7143, ]), userPrograms=pytest.approx( [1, 0, 4, 0, 1, 0, 4, 0, 1, 0, 4, 0, 2, 8]), velocityMode=pytest.approx( [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 3]), ) assert self.o.completed_steps_lookup == ([ 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6 ]) @patch("malcolm.modules.pmac.parts.pmacchildpart.PROFILE_POINTS", 9) def test_split_in_a_long_step_lookup(self): self.do_configure(axes_to_scan=["x"], completed_steps=3, x_pos=0.62506, duration=14.0) # Ignore the trigger reset and move to start, just look at the last call # which is the profile write assert self.child.handled_requests.mock_calls[-1] == call.post( "writeProfile", csPort="CS1", a=pytest.approx([ 0.625, 0.5625, 0.5, 0.4375, 0.375, 0.3125, 0.25, 0.1875, 0.125 ]), timeArray=pytest.approx([ 7143, 3500000, 3500000, 3500000, 3500000, 3500000, 3500000, 3500000, 3500000, ]), userPrograms=pytest.approx([1, 0, 4, 0, 1, 0, 4, 0, 1]), velocityMode=pytest.approx([1, 0, 0, 0, 0, 0, 0, 0, 0]), ) # The completed steps works on complete (not split) steps, so we expect # the last value to be the end of step 6, even though it doesn't # actually appear in the velocity arrays assert self.o.completed_steps_lookup == ([ 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 6 ]) # Mock out the registrar that would have been registered when we # attached to a controller self.o.registrar = Mock() # Now call update step and get it to generate the next lot of points # scanned can be any index into completed_steps_lookup so that there # are less than PROFILE_POINTS left to go in it self.o.update_step(scanned=2, child=self.process.block_view("PMAC")) # Expect the rest of the points assert self.child.handled_requests.mock_calls[-1] == call.post( "writeProfile", a=pytest.approx( [0.0625, 0.0, -0.0625, -0.125, -0.12506377551020409]), timeArray=pytest.approx([3500000, 3500000, 3500000, 3500000, 7143]), userPrograms=pytest.approx([0, 4, 0, 2, 8]), velocityMode=pytest.approx([0, 0, 0, 1, 3]), ) assert self.o.registrar.report.call_count == 1 assert self.o.registrar.report.call_args[0][0].steps == 3 # And for the rest of the lookup table to be added assert self.o.completed_steps_lookup == ([ 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6 ]) def do_2d_trajectory_with_plot(self, gen, xv, yv, xa, ya, title): gen.prepare() self.set_motor_attributes(x_acceleration=xv / xa, y_acceleration=yv / ya, x_velocity=xv, y_velocity=yv) infos = [MotionTriggerInfo(MotionTrigger.ROW_GATE)] # infos = [MotionTriggerInfo(MotionTrigger.EVERY_POINT)] self.o.on_configure(self.context, 0, gen.size, {"part": infos}, gen, ["x", "y"]) name, args, kwargs = self.child.handled_requests.mock_calls[2] assert name == "post" assert args[0] == "moveCS1" # add in the start point to the position and time arrays xp = np.array([kwargs["a"]]) yp = np.array([kwargs["b"]]) tp = np.array([0]) # And the profile write name, args, kwargs = self.child.handled_requests.mock_calls[-1] assert name == "post" assert args[0] == "writeProfile" xp = np.append(xp, kwargs["a"]) yp = np.append(yp, kwargs["b"]) tp = np.append(tp, kwargs["timeArray"]) # if this test is run in pycharm then it plots some results # to help diagnose issues if environ.get("PLOTS") == "1": import matplotlib.pyplot as plt times = np.cumsum(tp / 1000) # show in millisecs plt.figure(figsize=(8, 6), dpi=300) plt.title("{} x/time {} points".format(title, xp.size)) plt.plot(xp, times, "+", ms=2.5) plt.figure(figsize=(8, 6), dpi=300) plt.title("{} x/y".format(title)) plt.plot(xp, yp, "+", ms=2.5) plt.show() return xp, yp def check_bounds(self, a, name): # tiny amounts of overshoot are acceptable npa = np.array(a) less_start = np.argmax((npa[0] - npa) > 0.000001) greater_end = np.argmax((npa - npa[-1]) > 0.000001) self.assertEqual( less_start, 0, "Position {} < start for {}\n{}".format(less_start, name, a)) self.assertEqual( greater_end, 0, "Position {} > end for {}\n{}".format(greater_end, name, a)) def test_turnaround_overshoot(self): """check for a previous bug in a sawtooth X,Y scan The issue was that the first point at the start of each rising edge overshot in Y. The parameters for each rising edge are below. Line Y, start=-2.5, stop= -2.5 +0.025, points=30 Line X, start=-0.95, stop= -0.95 +0.025, points=30 duration=0.15 X motor: VMAX=17, ACCL=0.1 (time to VMAX) Y motor: VMAX=1, ACCL=0.2 """ xs = LineGenerator("x", "mm", -2.5, -2.475, 30) ys = LineGenerator("y", "mm", -0.95, -0.925, 2) generator = CompoundGenerator([ys, xs], [], [], 0.15) x1, y1 = self.do_2d_trajectory_with_plot( generator, xv=17, yv=1, xa=0.1, ya=0.2, title="test_turnaround_overshoot 10 fast", ) self.child.handled_requests.reset_mock() x2, y2 = self.do_2d_trajectory_with_plot( generator, xv=17, yv=34, xa=10, ya=0.1, title="test_turnaround_overshoot 10 slower", ) self.child.handled_requests.reset_mock() # check all the points in the arrays are within their start and stop self.check_bounds(x1, "x1") self.check_bounds(x2, "x2") self.check_bounds(y1, "y1") self.check_bounds(y2, "y2") def test_step_scan(self): axes_to_scan = ["x", "y"] duration = 1.0005 self.set_motor_attributes(0.0, 0.0, "mm") steps_to_do = 3 * len(axes_to_scan) xs = LineGenerator("x", "mm", 0.0, 5, 3, alternate=True) ys = LineGenerator("y", "mm", 0.0, 10, 2) generator = CompoundGenerator([ys, xs], [], [], duration, continuous=False) generator.prepare() self.o.on_configure(self.context, 0, steps_to_do, {"part": None}, generator, axes_to_scan) action, func, args = self.child.handled_requests.mock_calls[-1] assert args["a"] == pytest.approx([ 0.0, 0.0, 0.0, 0.2, 2.3, 2.5, 2.5, 2.5, 2.7, 4.8, 5.0, 5.0, 5.0, 5.0, 5.0, 5.0, 5.0, 5.0, 5.0, 5.0, 4.8, 2.7, 2.5, 2.5, 2.5, 2.3, 0.2, 0, 0.0, 0.0, 0.0, ]) assert args["b"] == pytest.approx([ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.2, 3.4, 6.6, 9.8, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0, ]) assert args["timeArray"] == pytest.approx([ 2000, 500250, 500250, 400000, 2100000, 400000, 500250, 500250, 400000, 2100000, 400000, 500250, 500250, 400000, 3200000, 3200000, 3200000, 400000, 500250, 500250, 400000, 2100000, 400000, 500250, 500250, 400000, 2100000, 400000, 500250, 500250, 2000, ]) def test_minimum_profile(self): # tests that a turnaround that is >= minturnaround # is reduced to a start and end point only # this supports keeping the turnaround really short where # the motors are fast e.g. j15 axes_to_scan = ["x", "y"] duration = 0.005 self.set_motor_attributes( 0.0, 0.0, "mm", x_acceleration=100.0, x_velocity=2.0, y_acceleration=100.0, y_velocity=2.0, ) steps_to_do = 1 * len(axes_to_scan) xs = LineGenerator("x", "mm", 0, 0.0001, 2) ys = LineGenerator("y", "mm", 0, 0, 2) generator = CompoundGenerator([ys, xs], [], [], duration, continuous=False) generator.prepare() m = [MinTurnaroundInfo(0.002, 0.002)] self.o.on_configure(self.context, 0, steps_to_do, {"part": m}, generator, axes_to_scan) action, func, args = self.child.handled_requests.mock_calls[-1] assert args["a"] == pytest.approx( [0, 0, 0, 0.0001, 0.0001, 0.0001, 0.0001]) assert args["b"] == pytest.approx([0, 0, 0, 0, 0, 0, 0]) assert args["timeArray"] == pytest.approx( [2000, 2500, 2500, 2000, 2500, 2500, 2000]) # now make the acceleration slower so that the turnaround takes # longer than the minimum interval self.set_motor_attributes( 0.0, 0.0, "mm", x_acceleration=10.0, x_velocity=2.0, y_acceleration=100.0, y_velocity=2.0, ) self.o.on_configure(self.context, 0, steps_to_do, {"part": m}, generator, axes_to_scan) # this generates one mid point between the 1st upper and 2nd lower # bounds. Note that the point is not exactly central due to time # quantization action, func, args = self.child.handled_requests.mock_calls[-1] assert args["a"] == pytest.approx( [0, 0, 0, 5.0e-05, 0.0001, 0.0001, 0.0001, 0.0001]) assert args["b"] == pytest.approx([0, 0, 0, 0, 0, 0, 0, 0]) assert args["timeArray"] == pytest.approx( [2000, 2500, 2500, 6000, 6000, 2500, 2500, 2000]) def test_settle_time(self): """ Test the standard do_configure but with motor settle time """ self.do_configure(axes_to_scan=["x", "y"], settle=0.01) self.do_check_output_settle() def do_check_output_settle(self): wp = self.child.handled_requests.mock_calls[3] # this is the same as do_check_output but with an extra time slot in the # turnaround: I jump through some hoops here to make it clear what the # difference is (rather than writing out the writeProfile parameters in full) a_pos = self.a_positions a_pos = a_pos[:10] + [0.627375] + a_pos[10:] b_pos = self.b_positions b_pos = b_pos[:9] + [0.089875, 0.1] + b_pos[10:] up = self.user_programs up = up[:10] + [8] + up[10:] ta = self.time_array ta = ta[:9] + [110000, 90000, 10000] + ta[11:] vm = self.velocity_mode vm = vm[:10] + [1] + vm[10:] assert wp == call.post( "writeProfile", a=pytest.approx(a_pos), b=pytest.approx(b_pos), csPort="CS1", timeArray=pytest.approx(ta), userPrograms=pytest.approx(up), velocityMode=pytest.approx(vm), ) def long_configure(self, row_gate=False): # test 4,000,000 points configure - used to check performance if row_gate: infos = [ MotionTriggerInfo(MotionTrigger.ROW_GATE), ] else: infos = None self.set_motor_attributes( 0, 0, "mm", x_velocity=300, y_velocity=300, x_acceleration=30, y_acceleration=30, ) axes_to_scan = ["x", "y"] x_steps, y_steps = 4000, 1000 steps_to_do = x_steps * y_steps xs = LineGenerator("x", "mm", 0.0, 10, x_steps, alternate=True) ys = LineGenerator("y", "mm", 0.0, 8, y_steps) generator = CompoundGenerator([ys, xs], [], [], 0.0001) generator.prepare() start = datetime.now() self.o.on_configure(self.context, 0, steps_to_do, {"part": infos}, generator, axes_to_scan) elapsed = datetime.now() - start # todo goal was sub 1 second but we achieved sub 3 secs assert elapsed.total_seconds() < 3.5 def test_configure_long_trajectory(self): # Skip on GitHub Actions and GitLab CI if "CI" in environ: pytest.skip("performance test only") # brick triggered self.long_configure(False) # 'sparse' trajectory linear point removal self.long_configure(True) def test_long_turnaround(self): """ Verify that if the turnaround time exceeds maximum time between PVT points then addtional points are added """ duration = 2 axes_to_scan = ["x"] # make the motors slow so that it takes 10 secs to do the turnaround self.set_motor_attributes(x_pos=0, y_pos=0, units="mm", x_velocity=0.1, y_velocity=0.1) # very simple trajectory with two points and a turnaround between them xs = LineGenerator("x", "mm", 0.0, 1.0, 2) generator = CompoundGenerator([xs], [], [], duration, continuous=False) generator.prepare() self.o.on_configure(self.context, 0, 2, {"part": None}, generator, axes_to_scan) assert self.child.handled_requests.mock_calls[-1] == call.post( "writeProfile", csPort="CS1", a=pytest.approx([ 0.0, 0.0, 0.0, 0.002, 0.334, 0.666, 0.998, 1.0, 1.0, 1.0, 1.0 ]), timeArray=pytest.approx([ 2000, 1000000, 1000000, 40000, 3320000, 3320000, 3320000, 40000, 1000000, 1000000, 2000, ]), userPrograms=pytest.approx([1, 4, 2, 8, 0, 0, 8, 1, 4, 2, 8]), velocityMode=pytest.approx([1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 3]), ) def test_configure_delay_after(self): # a test to show that delay_after inserts a "loop_back" turnaround delay = 1.0 axes_to_scan = ["x", "y"] self.set_motor_attributes(0.5, 0.0, "mm") steps_to_do = 3 xs = LineGenerator("x", "mm", 0.0, 0.5, 3, alternate=True) ys = LineGenerator("y", "mm", 0.0, 0.1, 2) generator = CompoundGenerator([ys, xs], [], [], 1.0, delay_after=delay) generator.prepare() self.o.on_configure(self.context, 0, steps_to_do, {"part": None}, generator, axes_to_scan) action, func, args = self.child.handled_requests.mock_calls[-1] assert args["a"] == pytest.approx([ -0.125, 0.0, 0.125, 0.13742472, 0.11257528, 0.125, 0.25, 0.375, 0.38742472, 0.36257528, 0.375, 0.5, 0.625, 0.6375, ]) assert args["b"] == pytest.approx([ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 ]) assert args["timeArray"] == pytest.approx([ 100000, 500000, 500000, 114000, 776000, 114000, 500000, 500000, 114000, 776000, 114000, 500000, 500000, 100000, ]) # check the delay times are correct (in microsecs) t = args["timeArray"] assert t[3] + t[4] + t[5] >= delay * 1000000 assert t[8] + t[9] + t[10] >= delay * 1000000 assert args["userPrograms"] == pytest.approx( [1, 4, 2, 8, 8, 1, 4, 2, 8, 8, 1, 4, 2, 8]) assert args["velocityMode"] == pytest.approx( [1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 3])
class 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 ])
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)
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), ]