def test_run_hook_table(self): hook = MagicMock() func = MagicMock() func.return_value = {"foo": ["bar", "bat", "baz"], "two": 2} func2 = MagicMock() func2.return_value = {"foo": ["bar2", "bat2"], "two": 3} task = MagicMock() task2 = MagicMock() part = MagicMock() part2 = MagicMock() part_tasks = {part: task, part2: task2} hook_queue = self.c.process.create_queue.return_value hook_queue.get.side_effect = [(func, func.return_value), (func2, func2.return_value)] hook.find_func_tasks.return_value = {func: task, func2: task2} return_table = hook.make_return_table.return_value return_table.endpoints = ["name", "foo", "two"] return_table.meta.elements = dict( name=StringArrayMeta(), foo=StringArrayMeta(tags=["hook:return_array"]), two=NumberArrayMeta("int32")) self.c.hook_names = {hook: "test_hook"} self.c.parts = {"test_part": part, "part2": part2} result = self.c.run_hook(hook, part_tasks) self.assertEquals(hook.make_return_table.return_value, result) result.append.assert_has_calls([ call(["test_part", "bar", 2]), call(["test_part", "bat", 2]), call(["test_part", "baz", 2]), call(["part2", "bar2", 3]), call(["part2", "bat2", 3]), ], any_order=True)
class TestStringArrayMeta(unittest.TestCase): def setUp(self): self.meta = StringArrayMeta("test description") def test_init(self): self.assertEqual("test description", self.meta.description) self.assertEqual(self.meta.label, "") self.assertEqual(self.meta.typeid, "malcolm:core/StringArrayMeta:1.0") def test_validate_none(self): self.assertEquals(self.meta.validate(None), []) def test_validate_array(self): array = ["test_string", 123, 123.456] self.assertEquals( ["test_string", "123", "123.456"], self.meta.validate(array)) def test_not_iterable_raises(self): value = 12346 self.assertRaises(ValueError, self.meta.validate, value) def test_null_element_raises(self): array = ["test", None] self.assertRaises(ValueError, self.meta.validate, array)
def test_set_elements_from_serialized(self): tm = self.tm elements = OrderedDict() elements["col1"] = StringArrayMeta() elements["col2"] = StringArrayMeta() elements = TableElementMap(elements) serialized = elements.to_dict() tm.set_elements(serialized) self.assertEqual(serialized, tm.elements.to_dict())
def test_set_elements(self): tm = self.tm elements = OrderedDict() elements["col1"] = StringArrayMeta() elements["col2"] = StringArrayMeta() elements = TableElementMap(elements) tm.set_elements(elements) self.assertEqual(elements, tm.elements) tm.process.report_changes.assert_called_once_with([["elements"], elements.to_dict()])
def setUp(self): self.sam = StringArrayMeta() self.serialized = OrderedDict() self.serialized["typeid"] = "malcolm:core/TableMeta:1.0" self.serialized["elements"] = TableElementMap( dict(c1=self.sam)).to_dict() self.serialized["description"] = "desc" self.serialized["tags"] = () self.serialized["writeable"] = True self.serialized["label"] = "Name"
def setUp(self): self.sam = StringArrayMeta() self.serialized = OrderedDict() self.serialized["typeid"] = "malcolm:core/MapMeta:1.0" self.serialized["elements"] = ElementMap(dict(c1=self.sam)).to_dict() self.serialized["description"] = "desc" self.serialized["tags"] = () self.serialized["writeable"] = False self.serialized["label"] = "" self.serialized["required"] = ("c1",)
def create_process_block(self): self.process_block = Block() # TODO: add a meta here children = OrderedDict() children["blocks"] = StringArrayMeta( description="Blocks hosted by this Process").make_attribute([]) children["remoteBlocks"] = StringArrayMeta( description="Blocks reachable via ClientComms").make_attribute([]) self.process_block.replace_endpoints(children) self.process_block.set_process_path(self, [self.name]) self.add_block(self.process_block, self)
def test_init_with_dict(self): meta = Mock() meta.elements = { "e1": NumberArrayMeta("int32"), "e2": StringArrayMeta(), "e3": StringArrayMeta() } d = {"e1": [0, 1], "e3": ["value"]} t = Table(meta, d) self.assertEquals([0, 1], list(t.e1)) self.assertEquals((), t.e2) self.assertEquals(("value", ), t.e3) self.assertEquals("malcolm:core/Table:1.0", t.typeid)
def test_to_dict(self): columns = OrderedDict() columns["foo"] = StringArrayMeta(label="Foo") columns["bar"] = StringArrayMeta() meta = TableMeta(description="desc", tags=[], writeable=True, label="my label", columns=columns) value = Table(meta) value.foo = ["foo1", "foo2"] value.bar = ["bar1", "bar2"] o = meta.make_attribute(value) self.assertEqual(o.to_dict(), self.serialized)
class TestSerialization(unittest.TestCase): def setUp(self): self.sam = StringArrayMeta() self.serialized = OrderedDict() self.serialized["typeid"] = "malcolm:core/MapMeta:1.0" self.serialized["elements"] = ElementMap(dict(c1=self.sam)).to_dict() self.serialized["description"] = "desc" self.serialized["tags"] = [] self.serialized["writeable"] = False self.serialized["label"] = "" self.serialized["required"] = ["c1"] def test_to_dict(self): tm = MapMeta("desc") tm.set_elements(ElementMap(dict(c1=self.sam))) tm.set_required(["c1"]) self.assertEqual(tm.to_dict(), self.serialized) def test_from_dict(self): tm = MapMeta.from_dict(self.serialized) self.assertEquals(tm.description, "desc") self.assertEquals(len(tm.elements), 1) self.assertEquals(tm.elements["c1"].to_dict(), self.sam.to_dict()) self.assertEquals(tm.tags, []) self.assertEquals(tm.required, ["c1"])
def setUp(self): meta = Mock() meta.elements = OrderedDict() meta.elements["e1"] = StringArrayMeta() meta.elements["e2"] = NumberArrayMeta("int32") meta.elements["e3"] = NumberArrayMeta("int32") self.meta = meta
class TestTableMetaSerialization(unittest.TestCase): def setUp(self): self.sam = StringArrayMeta() self.serialized = OrderedDict() self.serialized["typeid"] = "malcolm:core/TableMeta:1.0" self.serialized["elements"] = TableElementMap( dict(c1=self.sam)).to_dict() self.serialized["description"] = "desc" self.serialized["tags"] = [] self.serialized["writeable"] = True self.serialized["label"] = "Name" def test_to_dict(self): tm = TableMeta("desc") tm.set_label("Name") tm.set_elements(TableElementMap(dict(c1=self.sam))) tm.set_writeable(True) self.assertEqual(tm.to_dict(), self.serialized) def test_from_dict(self): tm = TableMeta.from_dict(self.serialized) self.assertEquals(tm.description, "desc") self.assertEquals(len(tm.elements), 1) self.assertEquals(tm.elements["c1"].to_dict(), self.sam.to_dict()) self.assertEquals(tm.tags, []) self.assertEquals(tm.writeable, True) self.assertEquals(tm.label, "Name")
def make_return_table(self, part_tasks): # Filter part tasks so that we only run the ones hooked to us columns = OrderedDict(name=StringArrayMeta("Part name")) for part in part_tasks: hooked = [ method_name for (method_name, hook, _) in get_hook_decorated(part) if hook is self ] for method_name, method_meta, func in get_method_decorated(part): if method_name in hooked: # Add return metas to the table columns for arg_name in method_meta.returns.elements: md = method_meta.returns.elements[arg_name].to_dict() if "ArrayMeta" in md["typeid"]: md["tags"] = md["tags"] + ["hook:return_array"] else: md["typeid"] = md["typeid"].replace( "Meta", "ArrayMeta") meta = deserialize_object(md, VArrayMeta) if arg_name in columns: column_d = columns[arg_name].to_dict() assert column_d == md, \ "%s != %s" % (column_d, md) columns[arg_name] = meta meta = TableMeta("Part returns from hook", columns=columns) return_table = Table(meta) return return_table
class TestTableMetaSerialization(unittest.TestCase): def setUp(self): self.sam = StringArrayMeta() self.serialized = OrderedDict() self.serialized["typeid"] = "malcolm:core/TableMeta:1.0" self.serialized["elements"] = TableElementMap( dict(c1=self.sam)).to_dict() self.serialized["description"] = "desc" self.serialized["tags"] = () self.serialized["writeable"] = True self.serialized["label"] = "Name" def test_to_dict(self): tm = TableMeta("desc") tm.set_label("Name") tm.set_elements(TableElementMap(dict(c1=self.sam))) tm.set_writeable(True) self.assertEqual(tm.to_dict(), self.serialized) def test_from_dict(self): tm = TableMeta.from_dict(self.serialized) self.assertEquals(tm.description, "desc") self.assertEquals(len(tm.elements), 1) self.assertEquals(tm.elements["c1"].to_dict(), self.sam.to_dict()) self.assertEquals(tm.tags, ()) self.assertEquals(tm.writeable, True) self.assertEquals(tm.label, "Name")
class TestSerialization(unittest.TestCase): def setUp(self): self.sam = StringArrayMeta() self.serialized = OrderedDict() self.serialized["typeid"] = "malcolm:core/MapMeta:1.0" self.serialized["elements"] = ElementMap(dict(c1=self.sam)).to_dict() self.serialized["description"] = "desc" self.serialized["tags"] = () self.serialized["writeable"] = False self.serialized["label"] = "" self.serialized["required"] = ("c1",) def test_to_dict(self): tm = MapMeta("desc") tm.set_elements(ElementMap(dict(c1=self.sam))) tm.set_required(["c1"]) self.assertEqual(tm.to_dict(), self.serialized) def test_from_dict(self): tm = MapMeta.from_dict(self.serialized) self.assertEquals(tm.description, "desc") self.assertEquals(len(tm.elements), 1) self.assertEquals(tm.elements["c1"].to_dict(), self.sam.to_dict()) self.assertEquals(tm.tags, ()) self.assertEquals(tm.required, ("c1",))
def create_attributes(self): for data in super(RunnableController, self).create_attributes(): yield data self.completed_steps = NumberMeta( "int32", "Readback of number of scan steps").make_attribute(0) self.completed_steps.meta.set_writeable_in(sm.PAUSED, sm.READY) yield "completedSteps", self.completed_steps, self.set_completed_steps self.configured_steps = NumberMeta( "int32", "Number of steps currently configured").make_attribute(0) yield "configuredSteps", self.configured_steps, None self.total_steps = NumberMeta( "int32", "Readback of number of scan steps").make_attribute(0) yield "totalSteps", self.total_steps, None self.axes_to_move = StringArrayMeta( "Default axis names to scan for configure()").make_attribute( self.params.axesToMove) self.axes_to_move.meta.set_writeable_in(sm.EDITABLE) yield "axesToMove", self.axes_to_move, self.set_axes_to_move
def test_init(self): meta = Mock() s = StringArrayMeta() meta.elements = {"e1": s, "e2": s, "e3": s} t = Table(meta) self.assertEquals((), t.e1) self.assertEquals((), t.e2) self.assertEquals((), t.e3) self.assertEquals("malcolm:core/Table:1.0", t.typeid)
def setUp(self): self.sam = StringArrayMeta() self.serialized = OrderedDict() self.serialized["typeid"] = "malcolm:core/MapMeta:1.0" self.serialized["elements"] = ElementMap(dict(c1=self.sam)).to_dict() self.serialized["description"] = "desc" self.serialized["tags"] = [] self.serialized["writeable"] = False self.serialized["label"] = "" self.serialized["required"] = ["c1"]
def setUp(self): self.sam = StringArrayMeta() self.serialized = OrderedDict() self.serialized["typeid"] = "malcolm:core/TableMeta:1.0" self.serialized["elements"] = TableElementMap( dict(c1=self.sam)).to_dict() self.serialized["description"] = "desc" self.serialized["tags"] = [] self.serialized["writeable"] = True self.serialized["label"] = "Name"
class ScanTickerPart(ChildPart): # Generator instance generator = None # Where to start completed_steps = None # How many steps to do steps_to_do = None # When to blow up exception_step = None @RunnableController.Configure @RunnableController.PostRunReady @RunnableController.Seek @method_takes( "generator", PointGeneratorMeta("Generator instance"), REQUIRED, "axesToMove", StringArrayMeta( "List of axes in inner dimension of generator that should be moved" ), REQUIRED, "exceptionStep", NumberMeta("int32", "If >0, raise an exception at the end of this step"), 0) def configure(self, task, completed_steps, steps_to_do, part_info, params): # If we are being asked to move if self.name in params.axesToMove: # Just store the generator and place we need to start self.generator = params.generator self.completed_steps = completed_steps self.steps_to_do = steps_to_do self.exception_step = params.exceptionStep else: # Flag nothing to do self.generator = None @RunnableController.Run @RunnableController.Resume def run(self, task, update_completed_steps): # Start time so everything is relative point_time = time.time() if self.generator: for i in range(self.completed_steps, self.completed_steps + self.steps_to_do): # Get the point we are meant to be scanning point = self.generator.get_point(i) # Update the child counter to be the demand position position = point.positions[self.name] task.put(self.child["counter"], position) # Wait until the next point is due point_time += point.duration wait_time = point_time - time.time() task.sleep(wait_time) # Update the point as being complete update_completed_steps(i + 1, self) # If this is the exception step then blow up assert i +1 != self.exception_step, \ "Raising exception at step %s" % self.exception_step
def setUp(self): elements = OrderedDict() elements["typeid"] = "malcolm:core/TableElementMap:1.0" elements["foo"] = StringArrayMeta(label="Foo").to_dict() elements["bar"] = StringArrayMeta().to_dict() meta = OrderedDict() meta["typeid"] = "malcolm:core/TableMeta:1.0" meta["elements"] = elements meta["description"] = "desc" meta["tags"] = [] meta["writeable"] = True meta["label"] = "my label" value = OrderedDict() value["typeid"] = "malcolm:core/Table:1.0" value["foo"] = ["foo1", "foo2"] value["bar"] = ["bar1", "bar2"] self.serialized = OrderedDict() self.serialized["typeid"] = "epics:nt/NTTable:1.0" self.serialized["labels"] = ["Foo", "bar"] self.serialized["meta"] = meta self.serialized["value"] = value
def create_attributes(self): for data in super(ManagerController, self).create_attributes(): yield data # Make a table for the layout info we need columns = OrderedDict() columns["name"] = StringArrayMeta("Name of layout part") columns["mri"] = StringArrayMeta("Malcolm full name of child block") columns["x"] = NumberArrayMeta("float64", "X Coordinate of child block") columns["y"] = NumberArrayMeta("float64", "Y Coordinate of child block") columns["visible"] = BooleanArrayMeta("Whether child block is visible") layout_table_meta = TableMeta("Layout of child blocks", columns=columns) layout_table_meta.set_writeable_in(sm.EDITABLE) self.layout = layout_table_meta.make_attribute() yield "layout", self.layout, self.set_layout self.layout_name = ChoiceMeta( "Saved layout name to load", []).make_attribute() self.layout_name.meta.set_writeable_in( self.stateMachine.AFTER_RESETTING) yield "layoutName", self.layout_name, self.load_layout assert os.path.isdir(self.params.configDir), \ "%s is not a directory" % self.params.configDir
class TestStringArrayMeta(unittest.TestCase): def setUp(self): self.meta = StringArrayMeta("test description") def test_init(self): self.assertEqual("test description", self.meta.description) self.assertEqual(self.meta.label, "") self.assertEqual(self.meta.typeid, "malcolm:core/StringArrayMeta:1.0") def test_validate_none(self): self.assertIsNone(self.meta.validate(None)) def test_validate_array(self): array = ["test_string", 123, 123.456] self.assertEquals(["test_string", "123", "123.456"], self.meta.validate(array)) def test_not_iterable_raises(self): value = 12346 self.assertRaises(ValueError, self.meta.validate, value) def test_null_element_raises(self): array = ["test", None] self.assertRaises(ValueError, self.meta.validate, array)
def create_attributes(self): for data in super(RunnableController, self).create_attributes(): yield data self.completed_steps = NumberMeta( "int32", "Readback of number of scan steps").make_attribute(0) self.completed_steps.meta.set_writeable_in(sm.PAUSED, sm.READY) yield "completedSteps", self.completed_steps, self.set_completed_steps self.configured_steps = NumberMeta( "int32", "Number of steps currently configured").make_attribute(0) yield "configuredSteps", self.configured_steps, None self.total_steps = NumberMeta( "int32", "Readback of number of scan steps" ).make_attribute(0) yield "totalSteps", self.total_steps, None self.axes_to_move = StringArrayMeta( "Default axis names to scan for configure()" ).make_attribute(self.params.axesToMove) self.axes_to_move.meta.set_writeable_in(sm.EDITABLE) yield "axesToMove", self.axes_to_move, self.set_axes_to_move
def setUp(self): self.meta = StringArrayMeta("test description")
from collections import OrderedDict from malcolm.controllers.defaultcontroller import DefaultController from malcolm.core import RunnableDeviceStateMachine, REQUIRED, method_returns, \ method_only_in, method_takes, ElementMap, Attribute, Task, Hook, Table from malcolm.core.vmetas import PointGeneratorMeta, StringArrayMeta, \ NumberMeta, NumberArrayMeta, BooleanArrayMeta, TableMeta sm = RunnableDeviceStateMachine configure_args = [ "generator", PointGeneratorMeta("Generator instance"), REQUIRED, "axes_to_move", StringArrayMeta("Axes that should be moved"), REQUIRED, "exposure", NumberMeta("float64", "How long to remain at each point"), REQUIRED ] # Make a table for the layout info we need columns = OrderedDict() columns["name"] = StringArrayMeta("Name of layout part") columns["mri"] = StringArrayMeta("Malcolm full name of child block") columns["x"] = NumberArrayMeta("float64", "X Co-ordinate of child block") columns["y"] = NumberArrayMeta("float64", "X Co-ordinate of child block") columns["visible"] = BooleanArrayMeta("Whether child block is visible") layout_table_meta = TableMeta("Layout of child blocks", columns=columns) # Make a table for the port info we need columns = OrderedDict() columns["name"] = StringArrayMeta("Name of layout part") columns["type"] = StringArrayMeta("Type of outport (e.g. bit or pos)") columns["value"] = StringArrayMeta("Value of outport (e.g. PULSE1.OUT)")
class RunnableController(ManagerController): """RunnableDevice implementer that also exposes GUI for child parts""" # The stateMachine that this controller implements stateMachine = sm() Validate = Hook() """Called at validate() to check parameters are valid Args: task (Task): The task used to perform operations on child blocks part_info (dict): {part_name: [Info]} returned from ReportStatus params (Map): Any configuration parameters asked for by part validate() method_takes() decorator Returns: [`ParameterTweakInfo`] - any parameters tweaks that have occurred to make them compatible with this part. If any are returned, Validate will be re-run with the modified parameters. """ ReportStatus = Hook() """Called before Validate, Configure, PostRunReady and Seek hooks to report the current configuration of all parts Args: task (Task): The task used to perform operations on child blocks Returns: [`Info`] - any configuration Info objects relevant to other parts """ Configure = Hook() """Called at configure() to configure child block for a run Args: task (Task): The task used to perform operations on child blocks completed_steps (int): Number of steps already completed steps_to_do (int): Number of steps we should configure for part_info (dict): {part_name: [Info]} returned from ReportStatus params (Map): Any configuration parameters asked for by part configure() method_takes() decorator Returns: [`Info`] - any Info objects that need to be passed to other parts for storing in attributes """ PostConfigure = Hook() """Called at the end of configure() to store configuration info calculated in the Configure hook Args: task (Task): The task used to perform operations on child blocks part_info (dict): {part_name: [Info]} returned from Configure hook """ Run = Hook() """Called at run() to start the configured steps running Args: task (Task): The task used to perform operations on child blocks update_completed_steps (callable): If part can report progress, this part should call update_completed_steps(completed_steps, self) with the integer step value each time progress is updated """ PostRunReady = Hook() """Called at the end of run() when there are more steps to be run Args: task (Task): The task used to perform operations on child blocks completed_steps (int): Number of steps already completed steps_to_do (int): Number of steps we should configure for part_info (dict): {part_name: [Info]} returned from ReportStatus params (Map): Any configuration parameters asked for by part configure() method_takes() decorator """ PostRunIdle = Hook() """Called at the end of run() when there are no more steps to be run Args: task (Task): The task used to perform operations on child blocks """ Pause = Hook() """Called at pause() to pause the current scan before Seek is called Args: task (Task): The task used to perform operations on child blocks """ Seek = Hook() """Called at seek() or at the end of pause() to reconfigure for a different number of completed_steps Args: task (Task): The task used to perform operations on child blocks completed_steps (int): Number of steps already completed steps_to_do (int): Number of steps we should configure for part_info (dict): {part_name: [Info]} returned from ReportStatus params (Map): Any configuration parameters asked for by part configure() method_takes() decorator """ Resume = Hook() """Called at resume() to continue a paused scan Args: task (Task): The task used to perform operations on child blocks update_completed_steps (callable): If part can report progress, this part should call update_completed_steps(completed_steps, self) with the integer step value each time progress is updated """ Abort = Hook() """Called at abort() to stop the current scan Args: task (Task): The task used to perform operations on child blocks """ # Attributes completed_steps = None configured_steps = None total_steps = None axes_to_move = None # Params passed to configure() configure_params = None # Stored for pause steps_per_run = 0 # Progress reporting dict # {part: completed_steps for that part} progress_reporting = None @method_writeable_in(sm.IDLE) def edit(self): # Override edit to only work from Idle super(RunnableController, self).edit() @method_writeable_in(sm.FAULT, sm.DISABLED, sm.ABORTED, sm.READY) def reset(self): # Override reset to work from aborted and ready too super(RunnableController, self).reset() def create_attributes(self): for data in super(RunnableController, self).create_attributes(): yield data self.completed_steps = NumberMeta( "int32", "Readback of number of scan steps").make_attribute(0) self.completed_steps.meta.set_writeable_in(sm.PAUSED, sm.READY) yield "completedSteps", self.completed_steps, self.set_completed_steps self.configured_steps = NumberMeta( "int32", "Number of steps currently configured").make_attribute(0) yield "configuredSteps", self.configured_steps, None self.total_steps = NumberMeta( "int32", "Readback of number of scan steps" ).make_attribute(0) yield "totalSteps", self.total_steps, None self.axes_to_move = StringArrayMeta( "Default axis names to scan for configure()" ).make_attribute(self.params.axesToMove) self.axes_to_move.meta.set_writeable_in(sm.EDITABLE) yield "axesToMove", self.axes_to_move, self.set_axes_to_move def do_reset(self): super(RunnableController, self).do_reset() self._update_configure_args() self.configured_steps.set_value(0) self.completed_steps.set_value(0) self.total_steps.set_value(0) def go_to_error_state(self, exception): if isinstance(exception, AbortedError): self.log_info("Got AbortedError in %s" % self.state.value) else: super(RunnableController, self).go_to_error_state(exception) def _update_configure_args(self): # Look for all parts that hook into Configure configure_funcs = self.Configure.find_hooked_functions(self.parts) method_metas = [] for part, func_name in configure_funcs.items(): method_metas.append(part.method_metas[func_name]) # Update takes with the things we need default_configure = MethodMeta.from_dict( RunnableController.configure.MethodMeta.to_dict()) default_configure.defaults["axesToMove"] = self.axes_to_move.value method_metas.append(default_configure) # Decorate validate and configure with the sum of its parts self.block["validate"].recreate_from_others(method_metas) self.block["validate"].set_returns(self.block["validate"].takes) self.block["configure"].recreate_from_others(method_metas) def set_axes_to_move(self, value): self.axes_to_move.set_value(value) self._update_configure_args() @method_takes(*configure_args) def validate(self, params, returns): iterations = 10 # Make some tasks just for validate part_tasks = self.create_part_tasks() # Get any status from all parts status_part_info = self.run_hook(self.ReportStatus, part_tasks) while iterations > 0: # Try up to 10 times to get a valid set of parameters iterations -= 1 # Validate the params with all the parts validate_part_info = self.run_hook( self.Validate, part_tasks, status_part_info, **params) tweaks = ParameterTweakInfo.filter_values(validate_part_info) if tweaks: for tweak in tweaks: params[tweak.parameter] = tweak.value self.log_debug( "Tweaking %s to %s", tweak.parameter, tweak.value) else: # Consistent set, just return the params return params raise ValueError("Could not get a consistent set of parameters") @method_takes(*configure_args) @method_writeable_in(sm.IDLE) def configure(self, params): """Configure for a scan""" self.validate(params, params) self.try_stateful_function( sm.CONFIGURING, sm.READY, self.do_configure, params) def do_configure(self, params): # These are the part tasks that abort() and pause() will operate on self.part_tasks = self.create_part_tasks() # Store the params for use in seek() self.configure_params = params # Set the steps attributes that we will do across many run() calls self.total_steps.set_value(params.generator.num) self.completed_steps.set_value(0) self.configured_steps.set_value(0) # TODO: this should come from tne generator self.steps_per_run = self._get_steps_per_run( params.generator, params.axesToMove) # Get any status from all parts part_info = self.run_hook(self.ReportStatus, self.part_tasks) # Use the ProgressReporting classes for ourselves self.progress_reporting = {} # Run the configure command on all parts, passing them info from # ReportStatus. Parts should return any reporting info for PostConfigure completed_steps = 0 steps_to_do = self.steps_per_run part_info = self.run_hook( self.Configure, self.part_tasks, completed_steps, steps_to_do, part_info, **self.configure_params) # Take configuration info and reflect it as attribute updates self.run_hook(self.PostConfigure, self.part_tasks, part_info) # Update the completed and configured steps self.configured_steps.set_value(steps_to_do) def _get_steps_per_run(self, generator, axes_to_move): steps = 1 axes_set = set(axes_to_move) for g in reversed(generator.generators): # If the axes_set is empty then we are done if not axes_set: break # Consume the axes that this generator scans for axis in g.position_units: assert axis in axes_set, \ "Axis %s is not in %s" % (axis, axes_to_move) axes_set.remove(axis) # Now multiply by the dimensions to get the number of steps for dim in g.index_dims: steps *= dim return steps @method_writeable_in(sm.READY) def run(self): """Run an already configured scan""" if self.configured_steps.value < self.total_steps.value: next_state = sm.READY else: next_state = sm.IDLE self.try_stateful_function(sm.RUNNING, next_state, self._call_do_run) def _call_do_run(self): hook = self.Run while True: try: self.do_run(hook) except AbortedError: # Work out if it was an abort or pause state = self.state.value self.log_debug("Do run got AbortedError from %s", state) if state in (sm.SEEKING, sm.PAUSED): # Wait to be restarted task = Task("StateWaiter", self.process) bad_states = [sm.DISABLING, sm.ABORTING, sm.FAULT] try: task.when_matches(self.state, sm.RUNNING, bad_states) except BadValueError: # raise AbortedError so we don't try to transition raise AbortedError() # Restart it hook = self.Resume self.status.set_value("Run resumed") else: # just drop out raise else: return def do_run(self, hook): self.run_hook(hook, self.part_tasks, self.update_completed_steps) self.transition(sm.POSTRUN, "Finishing run") completed_steps = self.configured_steps.value if completed_steps < self.total_steps.value: steps_to_do = self.steps_per_run part_info = self.run_hook(self.ReportStatus, self.part_tasks) self.completed_steps.set_value(completed_steps) self.run_hook( self.PostRunReady, self.part_tasks, completed_steps, steps_to_do, part_info, **self.configure_params) self.configured_steps.set_value(completed_steps + steps_to_do) else: self.run_hook(self.PostRunIdle, self.part_tasks) def update_completed_steps(self, completed_steps, part): # This is run in the child thread, so make sure it is thread safe self.progress_reporting[part] = completed_steps min_completed_steps = min(self.progress_reporting.values()) if min_completed_steps > self.completed_steps.value: self.completed_steps.set_value(min_completed_steps) @method_writeable_in( sm.IDLE, sm.CONFIGURING, sm.READY, sm.RUNNING, sm.POSTRUN, sm.PAUSED, sm.SEEKING) def abort(self): self.try_stateful_function( sm.ABORTING, sm.ABORTED, self.do_abort, self.Abort) def do_abort(self, hook): for task in self.part_tasks.values(): task.stop() self.run_hook(hook, self.create_part_tasks()) for task in self.part_tasks.values(): task.wait() def set_completed_steps(self, completed_steps): params = self.pause.MethodMeta.prepare_input_map( completedSteps=completed_steps) self.pause(params) @method_writeable_in(sm.READY, sm.PAUSED, sm.RUNNING) @method_takes("completedSteps", NumberMeta( "int32", "Step to mark as the last completed step, -1 for current"), -1) def pause(self, params): current_state = self.state.value if params.completedSteps < 0: completed_steps = self.completed_steps.value else: completed_steps = params.completedSteps if current_state == sm.RUNNING: next_state = sm.PAUSED else: next_state = current_state assert completed_steps < self.total_steps.value, \ "Cannot seek to after the end of the scan" self.try_stateful_function( sm.SEEKING, next_state, self.do_pause, completed_steps) def do_pause(self, completed_steps): self.do_abort(self.Pause) in_run_steps = completed_steps % self.steps_per_run steps_to_do = self.steps_per_run - in_run_steps part_info = self.run_hook(self.ReportStatus, self.part_tasks) self.completed_steps.set_value(completed_steps) self.run_hook( self.Seek, self.part_tasks, completed_steps, steps_to_do, part_info, **self.configure_params) self.configured_steps.set_value(completed_steps + steps_to_do) @method_writeable_in(sm.PAUSED) def resume(self): self.transition(sm.RUNNING, "Resuming run")
class LayoutPart(Part): # Child block object child = None # {part_name: visible} saying whether part_name is visible part_visible = None # Layout options x = 0 y = 0 visible = False mri = None name = None def store_params(self, params): self.name = params.name self.child = self.process.get_block(params.child) self.mri = params.child self.part_visible = {} @ManagerController.UpdateLayout @method_takes("layout_table", layout_table_meta, REQUIRED, "outport_table", outport_table_meta, REQUIRED) @method_returns("mri", StringMeta("Malcolm full name of child block"), REQUIRED, "x", NumberMeta("float64", "X Co-ordinate of child block"), REQUIRED, "y", NumberMeta("float64", "X Co-ordinate of child block"), REQUIRED, "visible", BooleanMeta("Whether child block is visible"), REQUIRED) def update_layout_table(self, task, params, returns): for i, name in enumerate(params.layout_table.name): _, _, x, y, visible = params.layout_table[i] if name == self.name: if self.visible and not visible: self.sever_inports(task) self.x = x self.y = y self.visible = visible else: was_visible = self.part_visible.get(name, True) if was_visible and not visible: self.sever_outports(task, name, params.outport_table) self.part_visible[name] = visible returns.mri = self.mri returns.x = self.x returns.y = self.y returns.visible = self.visible return returns def _get_flowgraph_ports(self, direction="out"): # {attr_name: port_tag} ports = OrderedDict() for attr_name in self.child.endpoints: attr = self.child[attr_name] if isinstance(attr, Attribute): for tag in attr.meta.tags: if tag.startswith("flowgraph:%sport" % direction): ports[attr] = tag return ports def sever_inports(self, task): inports = self._get_flowgraph_ports("in") futures = [] for attr in inports: futures += task.put_async(attr, attr.meta.choices[0]) task.wait_all(futures) def sever_outports(self, task, name, outport_table): # Find the outports of this part # {outport_value: typ} e.g. "PCOMP.OUT" -> "bit" outports = {} for i, n in enumerate(outport_table.name): if n == name: outports[outport_table.value[i]] = outport_table.type[i] inports = self._get_flowgraph_ports("in") futures = [] for attr, port_tag in inports.items(): typ = port_tag.split(":")[2] if outports.get(attr.value, None) == typ: futures += task.put_async(attr, attr.meta.choices[0]) task.wait_all(futures) @ManagerController.ListOutports @method_returns("type", StringArrayMeta("Type of outport (e.g. bit or pos)"), REQUIRED, "value", StringArrayMeta("Value of outport (e.g. PULSE1.OUT)"), REQUIRED) def list_outports(self, _, returns): outports = self._get_flowgraph_ports("out") types = [] values = [] for port_tag in outports.values(): _, _, typ, name = port_tag.split(":", 4) types.append(typ) values.append(name) returns.type = types returns.value = values return returns
from malcolm.parts.builtin.attributepart import AttributePart from malcolm.core import method_also_takes, REQUIRED from malcolm.core.vmetas import ChoiceMeta, StringMeta, StringArrayMeta @method_also_takes( "choices", StringArrayMeta("Possible choices for this attribute"), REQUIRED, "initialValue", StringMeta("Initial value of attribute"), REQUIRED, ) class ChoicePart(AttributePart): def get_initial_value(self): return self.params.initialValue def create_meta(self, description, tags): return ChoiceMeta(choices=self.params.choices, description=description, tags=tags)
def setUp(self): self.tm = TableMeta("desc") self.tm.set_elements(TableElementMap(dict(c1=StringArrayMeta())))
NO_PROGRAM = 0 # Do nothing TRIG_CAPTURE = 4 # Capture 1, Frame 0, Detector 0 TRIG_DEAD_FRAME = 2 # Capture 0, Frame 1, Detector 0 TRIG_LIVE_FRAME = 3 # Capture 0, Frame 1, Detector 1 TRIG_ZERO = 8 # Capture 0, Frame 0, Detector 0 # How many generator points to load each time POINTS_PER_BUILD = 4000 # All possible PMAC CS axis assignment cs_axis_names = list("ABCUVWXYZ") # Args for configure and validate configure_args = [ "generator", PointGeneratorMeta("Generator instance"), REQUIRED, "axesToMove", StringArrayMeta( "List of axes in inner dimension of generator that should be moved"), []] # Class for these motor variables class MotorInfo(Info): def __init__(self, cs_axis, cs_port, acceleration, resolution, offset, max_velocity, current_position, scannable, velocity_settle): self.cs_axis = cs_axis self.cs_port = cs_port self.acceleration = acceleration self.resolution = resolution self.offset = offset self.max_velocity = max_velocity self.current_position = current_position self.scannable = scannable
def test_init_with_none(self): meta = Mock() meta.elements = {"e1": StringArrayMeta()} t = Table(meta, None) self.assertEquals((), t.e1) self.assertEquals("malcolm:core/Table:1.0", t.typeid)
from malcolm.compat import OrderedDict from malcolm.core import Part, Table, Info from malcolm.core.vmetas import StringArrayMeta, ChoiceArrayMeta, TableMeta, \ NumberArrayMeta from malcolm.controllers.runnablecontroller import RunnableController # Make a table for the dataset info we produce dataset_types = [ "primary", "secondary", "monitor", "position_set", "position_value"] columns = OrderedDict() columns["name"] = StringArrayMeta("Dataset name") columns["filename"] = StringArrayMeta( "Filename of HDF file relative to fileDir") columns["type"] = ChoiceArrayMeta("Type of dataset", dataset_types) columns["rank"] = NumberArrayMeta("int32", "Rank (number of dimensions)") columns["path"] = StringArrayMeta("Dataset path within HDF file") columns["uniqueid"] = StringArrayMeta("UniqueID array path within HDF file") dataset_table_meta = TableMeta("Datsets produced in HDF file", columns=columns) # Produced by plugins in part_info class DatasetProducedInfo(Info): def __init__(self, name, filename, type, rank, path, uniqueid): self.name = name self.filename = filename assert type in dataset_types, \ "Dataset type %s not in %s" % (type, dataset_types) self.type = type self.rank = rank self.path = path self.uniqueid = uniqueid
from malcolm.core.vmetas import PointGeneratorMeta, NumberMeta, StringArrayMeta class ParameterTweakInfo(Info): """Tweaks""" def __init__(self, parameter, value): self.parameter = parameter self.value = value sm = RunnableStateMachine configure_args = [ "generator", PointGeneratorMeta("Generator instance"), REQUIRED, "axesToMove", StringArrayMeta( "List of axes in inner dimension of generator that should be moved"), [] ] @method_also_takes("axesToMove", StringArrayMeta("Default value for configure() axesToMove"), []) class RunnableController(ManagerController): """RunnableDevice implementer that also exposes GUI for child parts""" # The stateMachine that this controller implements stateMachine = sm() Validate = Hook() """Called at validate() to check parameters are valid
class RunnableController(ManagerController): """RunnableDevice implementer that also exposes GUI for child parts""" # The stateMachine that this controller implements stateMachine = sm() Validate = Hook() """Called at validate() to check parameters are valid Args: task (Task): The task used to perform operations on child blocks part_info (dict): {part_name: [Info]} returned from ReportStatus params (Map): Any configuration parameters asked for by part validate() method_takes() decorator Returns: [`ParameterTweakInfo`] - any parameters tweaks that have occurred to make them compatible with this part. If any are returned, Validate will be re-run with the modified parameters. """ ReportStatus = Hook() """Called before Validate, Configure, PostRunReady and Seek hooks to report the current configuration of all parts Args: task (Task): The task used to perform operations on child blocks Returns: [`Info`] - any configuration Info objects relevant to other parts """ Configure = Hook() """Called at configure() to configure child block for a run Args: task (Task): The task used to perform operations on child blocks completed_steps (int): Number of steps already completed steps_to_do (int): Number of steps we should configure for part_info (dict): {part_name: [Info]} returned from ReportStatus params (Map): Any configuration parameters asked for by part configure() method_takes() decorator Returns: [`Info`] - any Info objects that need to be passed to other parts for storing in attributes """ PostConfigure = Hook() """Called at the end of configure() to store configuration info calculated in the Configure hook Args: task (Task): The task used to perform operations on child blocks part_info (dict): {part_name: [Info]} returned from Configure hook """ Run = Hook() """Called at run() to start the configured steps running Args: task (Task): The task used to perform operations on child blocks update_completed_steps (callable): If part can report progress, this part should call update_completed_steps(completed_steps, self) with the integer step value each time progress is updated """ PostRunReady = Hook() """Called at the end of run() when there are more steps to be run Args: task (Task): The task used to perform operations on child blocks completed_steps (int): Number of steps already completed steps_to_do (int): Number of steps we should configure for part_info (dict): {part_name: [Info]} returned from ReportStatus params (Map): Any configuration parameters asked for by part configure() method_takes() decorator """ PostRunIdle = Hook() """Called at the end of run() when there are no more steps to be run Args: task (Task): The task used to perform operations on child blocks """ Pause = Hook() """Called at pause() to pause the current scan before Seek is called Args: task (Task): The task used to perform operations on child blocks """ Seek = Hook() """Called at seek() or at the end of pause() to reconfigure for a different number of completed_steps Args: task (Task): The task used to perform operations on child blocks completed_steps (int): Number of steps already completed steps_to_do (int): Number of steps we should configure for part_info (dict): {part_name: [Info]} returned from ReportStatus params (Map): Any configuration parameters asked for by part configure() method_takes() decorator """ Resume = Hook() """Called at resume() to continue a paused scan Args: task (Task): The task used to perform operations on child blocks update_completed_steps (callable): If part can report progress, this part should call update_completed_steps(completed_steps, self) with the integer step value each time progress is updated """ Abort = Hook() """Called at abort() to stop the current scan Args: task (Task): The task used to perform operations on child blocks """ # Attributes completed_steps = None configured_steps = None total_steps = None axes_to_move = None # Params passed to configure() configure_params = None # Stored for pause steps_per_run = 0 # Progress reporting dict # {part: completed_steps for that part} progress_reporting = None @method_writeable_in(sm.IDLE) def edit(self): # Override edit to only work from Idle super(RunnableController, self).edit() @method_writeable_in(sm.FAULT, sm.DISABLED, sm.ABORTED, sm.READY) def reset(self): # Override reset to work from aborted and ready too super(RunnableController, self).reset() def create_attributes(self): for data in super(RunnableController, self).create_attributes(): yield data self.completed_steps = NumberMeta( "int32", "Readback of number of scan steps").make_attribute(0) self.completed_steps.meta.set_writeable_in(sm.PAUSED, sm.READY) yield "completedSteps", self.completed_steps, self.set_completed_steps self.configured_steps = NumberMeta( "int32", "Number of steps currently configured").make_attribute(0) yield "configuredSteps", self.configured_steps, None self.total_steps = NumberMeta( "int32", "Readback of number of scan steps").make_attribute(0) yield "totalSteps", self.total_steps, None self.axes_to_move = StringArrayMeta( "Default axis names to scan for configure()").make_attribute( self.params.axesToMove) self.axes_to_move.meta.set_writeable_in(sm.EDITABLE) yield "axesToMove", self.axes_to_move, self.set_axes_to_move def do_reset(self): super(RunnableController, self).do_reset() self._update_configure_args() self.configured_steps.set_value(0) self.completed_steps.set_value(0) self.total_steps.set_value(0) def go_to_error_state(self, exception): if isinstance(exception, AbortedError): self.log_info("Got AbortedError in %s" % self.state.value) else: super(RunnableController, self).go_to_error_state(exception) def _update_configure_args(self): # Look for all parts that hook into Configure configure_funcs = self.Configure.find_hooked_functions(self.parts) method_metas = [] for part, func_name in configure_funcs.items(): method_metas.append(part.method_metas[func_name]) # Update takes with the things we need default_configure = MethodMeta.from_dict( RunnableController.configure.MethodMeta.to_dict()) default_configure.defaults["axesToMove"] = self.axes_to_move.value method_metas.append(default_configure) # Decorate validate and configure with the sum of its parts self.block["validate"].recreate_from_others(method_metas) self.block["validate"].set_returns(self.block["validate"].takes) self.block["configure"].recreate_from_others(method_metas) def set_axes_to_move(self, value): self.axes_to_move.set_value(value) self._update_configure_args() @method_takes(*configure_args) def validate(self, params, returns): iterations = 10 # Make some tasks just for validate part_tasks = self.create_part_tasks() # Get any status from all parts status_part_info = self.run_hook(self.ReportStatus, part_tasks) while iterations > 0: # Try up to 10 times to get a valid set of parameters iterations -= 1 # Validate the params with all the parts validate_part_info = self.run_hook(self.Validate, part_tasks, status_part_info, **params) tweaks = ParameterTweakInfo.filter_values(validate_part_info) if tweaks: for tweak in tweaks: params[tweak.parameter] = tweak.value self.log_debug("Tweaking %s to %s", tweak.parameter, tweak.value) else: # Consistent set, just return the params return params raise ValueError("Could not get a consistent set of parameters") @method_takes(*configure_args) @method_writeable_in(sm.IDLE) def configure(self, params): """Configure for a scan""" self.validate(params, params) self.try_stateful_function(sm.CONFIGURING, sm.READY, self.do_configure, params) def do_configure(self, params): # These are the part tasks that abort() and pause() will operate on self.part_tasks = self.create_part_tasks() # Load the saved settings first self.run_hook(self.Load, self.part_tasks, self.load_structure) # Store the params for use in seek() self.configure_params = params # This will calculate what we need from the generator, possibly a long # call params.generator.prepare() # Set the steps attributes that we will do across many run() calls self.total_steps.set_value(params.generator.size) self.completed_steps.set_value(0) self.configured_steps.set_value(0) # TODO: We can be cleverer about this and support a different number # of steps per run for each run by examining the generator structure self.steps_per_run = self._get_steps_per_run(params.generator, params.axesToMove) # Get any status from all parts part_info = self.run_hook(self.ReportStatus, self.part_tasks) # Use the ProgressReporting classes for ourselves self.progress_reporting = {} # Run the configure command on all parts, passing them info from # ReportStatus. Parts should return any reporting info for PostConfigure completed_steps = 0 steps_to_do = self.steps_per_run part_info = self.run_hook(self.Configure, self.part_tasks, completed_steps, steps_to_do, part_info, **self.configure_params) # Take configuration info and reflect it as attribute updates self.run_hook(self.PostConfigure, self.part_tasks, part_info) # Update the completed and configured steps self.configured_steps.set_value(steps_to_do) def _get_steps_per_run(self, generator, axes_to_move): steps = 1 axes_set = set(axes_to_move) for dim in reversed(generator.dimensions): # If the axes_set is empty then we are done if not axes_set: break # Consume the axes that this generator scans for axis in dim.axes: assert axis in axes_set, \ "Axis %s is not in %s" % (axis, axes_to_move) axes_set.remove(axis) # Now multiply by the dimensions to get the number of steps steps *= dim.size return steps @method_writeable_in(sm.READY) def run(self): """Run an already configured scan""" if self.configured_steps.value < self.total_steps.value: next_state = sm.READY else: next_state = sm.IDLE self.try_stateful_function(sm.RUNNING, next_state, self._call_do_run) def _call_do_run(self): hook = self.Run while True: try: self.do_run(hook) except AbortedError: # Work out if it was an abort or pause state = self.state.value self.log_debug("Do run got AbortedError from %s", state) if state in (sm.SEEKING, sm.PAUSED): # Wait to be restarted task = Task("StateWaiter", self.process) bad_states = [sm.DISABLING, sm.ABORTING, sm.FAULT] try: task.when_matches(self.state, sm.RUNNING, bad_states) except BadValueError: # raise AbortedError so we don't try to transition raise AbortedError() # Restart it hook = self.Resume self.status.set_value("Run resumed") else: # just drop out raise else: return def do_run(self, hook): self.run_hook(hook, self.part_tasks, self.update_completed_steps) self.transition(sm.POSTRUN, "Finishing run") completed_steps = self.configured_steps.value if completed_steps < self.total_steps.value: steps_to_do = self.steps_per_run part_info = self.run_hook(self.ReportStatus, self.part_tasks) self.completed_steps.set_value(completed_steps) self.run_hook(self.PostRunReady, self.part_tasks, completed_steps, steps_to_do, part_info, **self.configure_params) self.configured_steps.set_value(completed_steps + steps_to_do) else: self.run_hook(self.PostRunIdle, self.part_tasks) def update_completed_steps(self, completed_steps, part): # This is run in the child thread, so make sure it is thread safe self.progress_reporting[part] = completed_steps min_completed_steps = min(self.progress_reporting.values()) if min_completed_steps > self.completed_steps.value: self.completed_steps.set_value(min_completed_steps) @method_writeable_in(sm.IDLE, sm.CONFIGURING, sm.READY, sm.RUNNING, sm.POSTRUN, sm.PAUSED, sm.SEEKING) def abort(self): self.try_stateful_function(sm.ABORTING, sm.ABORTED, self.do_abort, self.Abort) def do_abort(self, hook): for task in self.part_tasks.values(): task.stop() self.run_hook(hook, self.create_part_tasks()) for task in self.part_tasks.values(): task.wait() def set_completed_steps(self, completed_steps): params = self.pause.MethodMeta.prepare_input_map( completedSteps=completed_steps) self.pause(params) @method_writeable_in(sm.READY, sm.PAUSED, sm.RUNNING) @method_takes( "completedSteps", NumberMeta("int32", "Step to mark as the last completed step, -1 for current"), -1) def pause(self, params): current_state = self.state.value if params.completedSteps < 0: completed_steps = self.completed_steps.value else: completed_steps = params.completedSteps if current_state == sm.RUNNING: next_state = sm.PAUSED else: next_state = current_state assert completed_steps < self.total_steps.value, \ "Cannot seek to after the end of the scan" self.try_stateful_function(sm.SEEKING, next_state, self.do_pause, completed_steps) def do_pause(self, completed_steps): self.do_abort(self.Pause) in_run_steps = completed_steps % self.steps_per_run steps_to_do = self.steps_per_run - in_run_steps part_info = self.run_hook(self.ReportStatus, self.part_tasks) self.completed_steps.set_value(completed_steps) self.run_hook(self.Seek, self.part_tasks, completed_steps, steps_to_do, part_info, **self.configure_params) self.configured_steps.set_value(completed_steps + steps_to_do) @method_writeable_in(sm.PAUSED) def resume(self): self.transition(sm.RUNNING, "Resuming run")
def test_set_elements(self): els = ElementMap(dict(sam=StringArrayMeta())) self.mm.set_elements(els) self.assertEqual(self.mm.elements, els)