{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Grouping Motors with `mb_creator`\n", "\n", "It started with a question:\n", "\n", "> How to create a Device simply with just motors?\n", "\n", "This document shows how to use the {func}`apstools.devices.mb_creator()`\n", "function to create an ophyd object that has a set of motors.\n", "\n", "An object created by `mb_creator()` is intended to group its component motors. It does not provide any methods (other than those provided by its base class) that act on the components. For example, there is no `.move()` method or `.position` property.\n", "\n", ":::{seealso}\n", "\n", "* {doc}`Quick Start `\n", "* {func}`~apstools.devices.motor_factory.mb_creator()`" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Background\n", "\n", "Often, we encounter equipment consisting of a set of related motors, whether it\n", "be a positioning stage, a support table, a set of slits, or some other\n", "apparatus. Take, for example the *neat stage* at APS station 3-ID-D. The *neat\n", "stage* has two translations and one rotation.\n", "\n", "![3-ID-D neat stage](/resources/3idd_neat_stage.png)\n", "\n", "To create an ophyd object that describes the *neat stage*, we need a custom\n", "subclass of `ophyd.Device` that names the three motors.\n", "\n", "```py\n", "from ophyd import Component, Device, EpicsMotor\n", "\n", "class NeatStage_3IDD(Device):\n", " x = Component(EpicsMotor, \"m1\")\n", " y = Component(EpicsMotor, \"m2\")\n", " theta = Component(EpicsMotor, \"m3\")\n", "```\n", "\n", "Then, we create the ophyd object as an instance of the custom subclass:\n", "\n", "```py\n", "neat_stage = NeatStage_3IDD(\"3idd:\", name=\"neat_stage\")\n", "```\n", "\n", "We need a new custom class for every stage. Often, they are very similar except\n", "for the axis names and EPICS PV names for the motors. In most cases, each of\n", "these custom classes is used exactly once.\n", "\n", "**Summary**\n", "\n", "We have identified a pattern and an opportunity. It started with the question:\n", "\n", "> Make (or already exists?) a device that just combines a bunch of motors using just kwargs." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Example using `mb_creator()`\n", "\n", "Let's make that same stage using {func}`~apstools.devices.motor_factory.mb_creator()`, a\n", "factory function to create such structures. It is not necessary to create a\n", "custom class, the factory function does that for us, then builds the ophyd\n", "object.\n", "\n", "```py\n", "from apstools.devices import mb_creator\n", "\n", "neat_stage = mb_creator(\n", " prefix=\"3idd:\",\n", " motors={\n", " \"x\": \"m1\",\n", " \"y\": \"m2\",\n", " \"theta\": \"m3\",\n", " },\n", " name=\"neat_stage\",\n", ")\n", "```" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Let's start\n", "\n", "The simplest object we can create is a Device structure with no motors at all.\n", "We must provide a `name=` keyword argument (kwarg) for ophyd. The convention is\n", "to use the same name as the ophyd we object to be created." ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "MB_Device(prefix='', name='bundle', read_attrs=[], configuration_attrs=[])" ] }, "execution_count": 9, "metadata": {}, "output_type": "execute_result" } ], "source": [ "from apstools.devices import mb_creator\n", "bundle = mb_creator(name=\"bundle\")\n", "bundle" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## The `motors` keyword\n", "\n", "Let's add one motor axis. Call it `x`. Here, we provide the names of the motors as a list of names.\n", "\n", "
\n", "\n", "If `motors` is a list, it is converted into a dictionary where each axis is a key with an empty dictionary as its value. This allows for a consistent structure regardless of the input type. An empty dictionary is a shortcut for `{\"prefix\": None, \"class\": \"ophyd.SoftPositioner\"}`.\n", "\n", "
" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "MB_Device(prefix='', name='bundle', read_attrs=['x'], configuration_attrs=[])" ] }, "execution_count": 10, "metadata": {}, "output_type": "execute_result" } ], "source": [ "bundle = mb_creator(name=\"bundle\", motors=[\"x\"])\n", "bundle" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can learn more about this axis:" ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "SoftPositioner(name='bundle_x', parent='bundle', settle_time=0.0, timeout=None, egu='', limits=(0, 0), source='computed')" ] }, "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], "source": [ "bundle.x" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The {func}`apstools.devices.motor_factory.mb_creator()` created a\n", "`SoftPositioner` (simulated motor) since we did not provide an EPICS PV name to\n", "be used.\n", "\n", "### Use an EPICS motor\n", "\n", "Let's make a motor bundle with an EPICS motor record. We'll need to supply the\n", "process variable (PV) name. A Python dictionary is used to provide the axis\n", "names (as the dictionary keys) and the PV value for each key. Here, we start\n", "with just one motor." ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "bundle=MB_Device(prefix='', name='bundle', read_attrs=['x', 'x.user_readback', 'x.user_setpoint'], configuration_attrs=['x', 'x.user_offset', 'x.user_offset_dir', 'x.velocity', 'x.acceleration', 'x.motor_egu'])\n", "bundle.x=EpicsMotor(prefix='gp:m1', name='bundle_x', parent='bundle', settle_time=0.0, timeout=None, read_attrs=['user_readback', 'user_setpoint'], configuration_attrs=['user_offset', 'user_offset_dir', 'velocity', 'acceleration', 'motor_egu'])\n" ] } ], "source": [ "bundle = mb_creator(name=\"bundle\", motors={\"x\": \"gp:m1\"})\n", "bundle.wait_for_connection()\n", "print(f\"{bundle=}\")\n", "print(f\"{bundle.x=}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "It's easy to see how to extend this to any number of motor axes. Here is a motor bundle with 6 EPICS motors:" ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "bundle=MB_Device(prefix='', name='bundle', read_attrs=['x', 'x.user_readback', 'x.user_setpoint', 'y', 'y.user_readback', 'y.user_setpoint', 'z', 'z.user_readback', 'z.user_setpoint', 'roll', 'roll.user_readback', 'roll.user_setpoint', 'pitch', 'pitch.user_readback', 'pitch.user_setpoint', 'yaw', 'yaw.user_readback', 'yaw.user_setpoint'], configuration_attrs=['x', 'x.user_offset', 'x.user_offset_dir', 'x.velocity', 'x.acceleration', 'x.motor_egu', 'y', 'y.user_offset', 'y.user_offset_dir', 'y.velocity', 'y.acceleration', 'y.motor_egu', 'z', 'z.user_offset', 'z.user_offset_dir', 'z.velocity', 'z.acceleration', 'z.motor_egu', 'roll', 'roll.user_offset', 'roll.user_offset_dir', 'roll.velocity', 'roll.acceleration', 'roll.motor_egu', 'pitch', 'pitch.user_offset', 'pitch.user_offset_dir', 'pitch.velocity', 'pitch.acceleration', 'pitch.motor_egu', 'yaw', 'yaw.user_offset', 'yaw.user_offset_dir', 'yaw.velocity', 'yaw.acceleration', 'yaw.motor_egu'])\n", "bundle.read()=OrderedDict({'bundle_x': {'value': 0.05, 'timestamp': 1747171397.856864}, 'bundle_x_user_setpoint': {'value': 0.04999999999999987, 'timestamp': 1747171397.856864}, 'bundle_y': {'value': -0.16670000000000001, 'timestamp': 1747170542.707431}, 'bundle_y_user_setpoint': {'value': -0.16670000000000001, 'timestamp': 1747170542.707431}, 'bundle_z': {'value': 0.0, 'timestamp': 1747170542.707435}, 'bundle_z_user_setpoint': {'value': 0.0, 'timestamp': 1747170542.707435}, 'bundle_roll': {'value': 0.0, 'timestamp': 1747170542.707438}, 'bundle_roll_user_setpoint': {'value': 0.0, 'timestamp': 1747170542.707438}, 'bundle_pitch': {'value': 0.0, 'timestamp': 1746817545.842815}, 'bundle_pitch_user_setpoint': {'value': 0.0, 'timestamp': 1746817545.842815}, 'bundle_yaw': {'value': 0.0, 'timestamp': 1746817545.842817}, 'bundle_yaw_user_setpoint': {'value': 0.0, 'timestamp': 1746817545.842817}})\n" ] } ], "source": [ "bundle = mb_creator(\n", " name=\"bundle\", \n", " motors={\n", " \"x\": \"gp:m1\",\n", " \"y\": \"gp:m2\",\n", " \"z\": \"gp:m3\",\n", " \"roll\": \"gp:m4\",\n", " \"pitch\": \"gp:m5\",\n", " \"yaw\": \"gp:m6\",\n", " }\n", ")\n", "bundle.wait_for_connection()\n", "print(f\"{bundle=}\")\n", "print(f\"{bundle.read()=}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can turn any of these axes into a simulator (`SoftPositioner`) by providing `None` instead of an EPICS PV. Let's show that for `roll`, `pitch`, and `yaw`. Note that `motors` must still be a dictionary so that we can specify PVs for the `x`, `y`, and `z` axes." ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "bundle=MB_Device(prefix='', name='bundle', read_attrs=['x', 'x.user_readback', 'x.user_setpoint', 'y', 'y.user_readback', 'y.user_setpoint', 'z', 'z.user_readback', 'z.user_setpoint', 'roll', 'pitch', 'yaw'], configuration_attrs=['x', 'x.user_offset', 'x.user_offset_dir', 'x.velocity', 'x.acceleration', 'x.motor_egu', 'y', 'y.user_offset', 'y.user_offset_dir', 'y.velocity', 'y.acceleration', 'y.motor_egu', 'z', 'z.user_offset', 'z.user_offset_dir', 'z.velocity', 'z.acceleration', 'z.motor_egu'])\n", "bundle.x=EpicsMotor(prefix='gp:m1', name='bundle_x', parent='bundle', settle_time=0.0, timeout=None, read_attrs=['user_readback', 'user_setpoint'], configuration_attrs=['user_offset', 'user_offset_dir', 'velocity', 'acceleration', 'motor_egu'])\n", "bundle.roll=SoftPositioner(name='bundle_roll', parent='bundle', settle_time=0.0, timeout=None, egu='', limits=(0, 0), source='computed')\n" ] } ], "source": [ "bundle = mb_creator(\n", " name=\"bundle\", \n", " motors={\n", " \"x\": \"gp:m1\",\n", " \"y\": \"gp:m2\",\n", " \"z\": \"gp:m3\",\n", " \"roll\": None,\n", " \"pitch\": None,\n", " \"yaw\": None,\n", " }\n", ")\n", "bundle.wait_for_connection()\n", "print(f\"{bundle=}\")\n", "print(f\"{bundle.x=}\")\n", "print(f\"{bundle.roll=}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## The `prefix` keyword\n", "\n", "When all the EPICS PVs share a common starting sequence of symbols, we can\n", "provide that in the `prefix` kwarg. Here, we modify from the previous structure\n", "to show how to use this kwarg. Note that we remove it from each of the PV\n", "names:" ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "bundle=MB_Device(prefix='gp:', name='bundle', read_attrs=['x', 'x.user_readback', 'x.user_setpoint', 'y', 'y.user_readback', 'y.user_setpoint', 'z', 'z.user_readback', 'z.user_setpoint', 'roll', 'pitch', 'yaw'], configuration_attrs=['x', 'x.user_offset', 'x.user_offset_dir', 'x.velocity', 'x.acceleration', 'x.motor_egu', 'y', 'y.user_offset', 'y.user_offset_dir', 'y.velocity', 'y.acceleration', 'y.motor_egu', 'z', 'z.user_offset', 'z.user_offset_dir', 'z.velocity', 'z.acceleration', 'z.motor_egu'])\n", "bundle.x=EpicsMotor(prefix='gp:m1', name='bundle_x', parent='bundle', settle_time=0.0, timeout=None, read_attrs=['user_readback', 'user_setpoint'], configuration_attrs=['user_offset', 'user_offset_dir', 'velocity', 'acceleration', 'motor_egu'])\n", "bundle.roll=SoftPositioner(name='bundle_roll', parent='bundle', settle_time=0.0, timeout=None, egu='', limits=(0, 0), source='computed')\n" ] } ], "source": [ "bundle = mb_creator(\n", " name=\"bundle\",\n", " prefix=\"gp:\",\n", " motors={\n", " \"x\": \"m1\",\n", " \"y\": \"m2\",\n", " \"z\": \"m3\",\n", " \"roll\": None,\n", " \"pitch\": None,\n", " \"yaw\": None,\n", " }\n", ")\n", "bundle.wait_for_connection()\n", "print(f\"{bundle=}\")\n", "print(f\"{bundle.x=}\")\n", "print(f\"{bundle.roll=}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## The `labels` keyword\n", "\n", "Ophyd provides a `labels` kwarg to help categorize the various ophyd objects. It\n", "is a Python list. There are very few reserved labels (`\"detectors\"` is used by\n", "the `%ct` bluesky magic function, `\"motors\"` is also used by some convention.)\n", "Use your own labels as best fits the situation. Multiple labels are allowed.\n", "\n", "The ophyd registry (`oregistry`) can search for ophyd objects by label. Let's show that, too." ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "oregistry.device_names={'signal', 'bundle'}\n", "oregistry.findall(label='example')=[MB_Device(prefix='gp:', name='bundle', read_attrs=['x', 'x.user_readback', 'x.user_setpoint', 'y', 'y.user_readback', 'y.user_setpoint'], configuration_attrs=['x', 'x.user_offset', 'x.user_offset_dir', 'x.velocity', 'x.acceleration', 'x.motor_egu', 'y', 'y.user_offset', 'y.user_offset_dir', 'y.velocity', 'y.acceleration', 'y.motor_egu'])]\n", "oregistry.findall(label='not', allow_none=True)=[]\n", "oregistry.findall(label='not EPICS')=[Signal(name='signal', value=0.0, timestamp=1747267101.8723898)]\n" ] } ], "source": [ "from ophyd import Signal\n", "from ophydregistry import Registry\n", "\n", "oregistry = Registry()\n", "oregistry.auto_register = True\n", "\n", "bundle = mb_creator(\n", " name=\"bundle\",\n", " prefix=\"gp:\",\n", " motors={\"x\": \"m1\", \"y\": \"m2\"},\n", " labels=[\"bundle\", \"example\"],\n", ")\n", "signal = Signal(name=\"signal\", labels=[\"not EPICS\"])\n", "print(f\"{oregistry.device_names=}\")\n", "print(f\"{oregistry.findall(label='example')=}\")\n", "print(f\"{oregistry.findall(label='not', allow_none=True)=}\")\n", "print(f\"{oregistry.findall(label='not EPICS')=}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## The `class_bases` keyword\n", "\n", "The factory uses `ophyd.MotorBundle` by default to create the ophyd object. If you want to use a different base class (or more than one), the `class_bases` kwarg allows you to define a *list* of base classes. Here, we use the `ophyd.Device` class:" ] }, { "cell_type": "code", "execution_count": 17, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "bundle.__class__.__name__='MB_Device'\n" ] } ], "source": [ "bundle = mb_creator(\n", " name=\"bundle\",\n", " motors=[\"x\", \"y\", \"theta\"],\n", " class_bases=[\"ophyd.Device\"],\n", ")\n", "print(f\"{bundle.__class__.__name__=}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "To see how `ophyd.Device` has been used, we need to investigate the order of\n", "base classes used by the `bundle` object. We can see this by printing the\n", "*Python class Method Resolution Order* ([MRO](https://www.geeksforgeeks.org/method-resolution-order-in-python-inheritance/))." ] }, { "cell_type": "code", "execution_count": 18, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "(apstools.devices.motor_factory.MB_Device,\n", " ophyd.device.Device,\n", " ophyd.device.BlueskyInterface,\n", " ophyd.ophydobj.OphydObject,\n", " object)" ] }, "execution_count": 18, "metadata": {}, "output_type": "execute_result" } ], "source": [ "bundle.__class__.__mro__" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As noted above, the factory does not provide additional methods such as\n", "`.move()` or `.position`. To move any of the component motors, move them individually, such as: `bundle.x.move(5)`. In a bluesky plan stub, they can be moved together, such as\n", "\n", "```py\n", " yield from bps.move(\n", " bundle.x, 5,\n", " bundle.y, 2,\n", " bundle.theta, 25.251,\n", " )\n", "```\n", "\n", "The `class_bases` kwarg is a list, allowing you to\n", "provide an additional mixin class where such methods could be added.\n", "\n", "Let's show an example with a custom mixin class that provides a `.position`\n", "property." ] }, { "cell_type": "code", "execution_count": 19, "metadata": {}, "outputs": [], "source": [ "from ophyd import Device\n", "\n", "class PositionMixin(Device):\n", " \"\"\"Report position of any component that has a position property.\"\"\"\n", " \n", " @property\n", " def position(self) -> dict:\n", " pos = {}\n", " for attr in self.component_names:\n", " cpt = getattr(self, attr)\n", " if hasattr(cpt, \"position\"):\n", " pos[attr] = cpt.position\n", " return pos" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Create our `bundle` object, defining the `class_bases` kwarg to include both\n", "`\"ophyd.MotorBundle\"` (which will be imported by\n", "{func}`~apstools.devices.motor_factory.mb_creator()`) and our custom\n", "`PositionMixin`." ] }, { "cell_type": "code", "execution_count": 20, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "bundle.position={'x': 0.05, 'y': -0.16670000000000001, 'theta': 0.0}\n" ] } ], "source": [ "bundle = mb_creator(\n", " name=\"bundle\",\n", " prefix=\"gp:\",\n", " motors={\"x\": \"m1\", \"y\": \"m2\", \"theta\": \"m3\"},\n", " class_bases=[\"ophyd.MotorBundle\", PositionMixin],\n", ")\n", "bundle.wait_for_connection()\n", "print(f\"{bundle.position=}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## The `class_name` keyword\n", "\n", "All the examples above created ophyd objects as instances of `MB_Device`. It is\n", "possible to change that using the `class_name` kwarg. Let's make an example\n", "simulated neat stage (see above), then look at its MRO (order of base devices)." ] }, { "cell_type": "code", "execution_count": 21, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "(apstools.devices.motor_factory.NeatStage_Simulator,\n", " ophyd.epics_motor.MotorBundle,\n", " ophyd.device.Device,\n", " ophyd.device.BlueskyInterface,\n", " ophyd.ophydobj.OphydObject,\n", " object)" ] }, "execution_count": 21, "metadata": {}, "output_type": "execute_result" } ], "source": [ "bundle = mb_creator(\n", " name=\"bundle\",\n", " motors=[\"x\", \"y\", \"theta\"],\n", " class_name=\"NeatStage_Simulator\",\n", ")\n", "bundle.__class__.__mro__" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Advanced use\n", "\n", "Above, we saw that the `motors` kwarg can be either a list (names of the\n", "simulated axes) or a dictionary (names and PV names). Here we show advanced use\n", "of that dictionary. Instead of providing a PV name as a string value, we can\n", "provide a **dictionary** with `prefix` keyword for the PV and any other keywords\n", "accepted by the class for the axis.\n", "\n", "Let's demonstrate for an EPICS motor axis and a simulated axis. Comments were\n", "added for the additional terms that are not so obvious." ] }, { "cell_type": "code", "execution_count": 22, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "bundle=MB_Device(prefix='', name='bundle', read_attrs=['motor', 'motor.user_readback', 'motor.user_setpoint'], configuration_attrs=['motor', 'motor.user_offset', 'motor.user_offset_dir', 'motor.velocity', 'motor.acceleration', 'motor.motor_egu', 'soft'])\n", "bundle.motor=EpicsMotor(prefix='gp:m1', name='bundle_motor', parent='bundle', settle_time=0.0, timeout=None, read_attrs=['user_readback', 'user_setpoint'], configuration_attrs=['user_offset', 'user_offset_dir', 'velocity', 'acceleration', 'motor_egu'])\n", "bundle.soft=SoftPositioner(name='bundle_soft', parent='bundle', settle_time=0.0, timeout=None, egu='', limits=(-100, 100), source='computed')\n" ] } ], "source": [ "bundle = mb_creator(\n", " name=\"bundle\",\n", " motors={\n", " \"motor\": {\n", " \"prefix\": \"gp:m1\",\n", " \"labels\": [\"motors\", \"EPICS\"],\n", " \"kind\": \"hinted\", # reported as data, plotted if named\n", " },\n", " \"soft\": {\n", " \"init_pos\": 0,\n", " \"limits\": [-100, 100],\n", " \"labels\": [\"motors\", \"EPICS\"],\n", " \"kind\": \"config\", # reported as configuration, not data\n", " },\n", " },\n", ")\n", "print(f\"{bundle=}\")\n", "print(f\"{bundle.motor=}\")\n", "print(f\"{bundle.soft=}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In addition to `EpicsMotor` and `SoftPositioner`, it's possible to use other\n", "positioner classes for a \"motor\"." ] }, { "cell_type": "code", "execution_count": 23, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "OrderedDict([('bundle_positioner', {'value': 0.0, 'timestamp': 631152000.0}),\n", " ('bundle_positioner_setpoint',\n", " {'value': 0.0, 'timestamp': 631152000.0})])" ] }, "execution_count": 23, "metadata": {}, "output_type": "execute_result" } ], "source": [ "bundle = mb_creator(\n", " name=\"bundle\",\n", " prefix=\"zgp:\",\n", " motors={\n", " \"positioner\": {\n", " \"class\": \"apstools.devices.PVPositionerSoftDoneWithStop\",\n", " \"prefix\": \"gp:\",\n", " \"readback_pv\": \"float14\",\n", " \"setpoint_pv\": \"float4\",\n", " },\n", " },\n", ")\n", "bundle.wait_for_connection()\n", "bundle.read()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "It's even possible to specify an axis with the `EpicsSignal` class. Let's try\n", "with just a couple PVs:" ] }, { "cell_type": "code", "execution_count": 24, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "OrderedDict([('bundle_calculated',\n", " {'value': 25.30143434805829, 'timestamp': 1747267101.806678}),\n", " ('bundle_b', {'value': 25.0, 'timestamp': 1747267101.806678})])" ] }, "execution_count": 24, "metadata": {}, "output_type": "execute_result" } ], "source": [ "bundle = mb_creator(\n", " name=\"bundle\",\n", " motors={\n", " \"calculated\": {\n", " \"class\": \"ophyd.EpicsSignalRO\",\n", " \"prefix\": \"gp:userCalc8.VAL\",\n", " },\n", " \"b\": {\n", " \"class\": \"ophyd.EpicsSignal\",\n", " \"prefix\": \"gp:userCalc8.B\",\n", " },\n", " },\n", ")\n", "bundle.wait_for_connection()\n", "bundle.read()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The factory-generated class will be used to create ophyd objects (instances of\n", "that class).\n", "\n", "Here, we demonstrate a bundle that uses two different factories. It is typical\n", "at the APS where a diffractometer is mounted on a multi-axis table.\n", "\n", "Keep in mind that a [Python class\n", "factory](https://www.geeksforgeeks.org/class-factories-a-powerful-pattern-in-python/)\n", "is different from on object factory:\n", "\n", "factory | produces\n", "--- | ---\n", "`mb_class_factory()` | Creates a custom Python class which will be used to create one or more ophyd objects.\n", "`mb_creator()` | Creates an ophyd object (using {func}`~apstools.devices.motor_factory.mb_class_factory()`)\n", "\n", "
\n", "\n", "
\n", "\n", "----\n", "**NOTE**\n", "\n", "This example is contrived. Most diffractometer installations will not expose\n", "the table motors to use within bluesky since the table axes are used to align\n", "the diffractometer in the beamline. Once aligned, the table motors are moved if\n", "the table is found to require additional alignment.\n", "\n", "----\n", "\n", "
" ] }, { "cell_type": "code", "execution_count": 25, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "bundle=MB_Device(prefix='', name='bundle', read_attrs=['psic', 'psic.beam', 'psic.beam.wavelength', 'psic.beam.energy', 'psic.h', 'psic.h.readback', 'psic.h.setpoint', 'psic.k', 'psic.k.readback', 'psic.k.setpoint', 'psic.l', 'psic.l.readback', 'psic.l.setpoint', 'psic.mu', 'psic.eta', 'psic.chi', 'psic.phi', 'psic.nu', 'psic.delta', 'table', 'table.x1', 'table.x2', 'table.y1', 'table.y2', 'table.y3', 'table.z'], configuration_attrs=['psic', 'psic.solver_signature', 'psic.beam', 'psic.beam.source_type', 'psic.beam.wavelength_units', 'psic.beam.wavelength_deadband', 'psic.beam.energy_units', 'psic.h', 'psic.k', 'psic.l', 'table'])\n", "bundle.psic=Hklpy2Diffractometer(prefix='', name='bundle_psic', parent='bundle', settle_time=0.0, timeout=None, egu='', limits=(0, 0), source='computed', read_attrs=['beam', 'beam.wavelength', 'beam.energy', 'h', 'h.readback', 'h.setpoint', 'k', 'k.readback', 'k.setpoint', 'l', 'l.readback', 'l.setpoint', 'mu', 'eta', 'chi', 'phi', 'nu', 'delta'], configuration_attrs=['solver_signature', 'beam', 'beam.source_type', 'beam.wavelength_units', 'beam.wavelength_deadband', 'beam.energy_units', 'h', 'k', 'l'], concurrent=True)\n", "bundle.table=MB_Device(prefix='', name='bundle_table', parent='bundle', read_attrs=['x1', 'x2', 'y1', 'y2', 'y3', 'z'], configuration_attrs=[])\n" ] } ], "source": [ "bundle = mb_creator(\n", " name=\"bundle\",\n", " labels=[\"diffractometer\"],\n", " motors={\n", " \"psic\": {\n", " \"factory\": {\n", " \"function\": \"hklpy2.diffract.diffractometer_class_factory\",\n", " \"geometry\": \"APS POLAR\",\n", " \"solver\": \"hkl_soleil\",\n", " \"solver_kwargs\": {\"engine\": \"hkl\"},\n", " \"reals\": [\"mu\", \"eta\", \"chi\", \"phi\", \"nu\", \"delta\"],\n", " }\n", " },\n", " \"table\": {\n", " \"factory\": {\n", " \"function\": \"apstools.devices.mb_class_factory\",\n", " \"motors\": [\"x1\", \"x2\", \"y1\", \"y2\", \"y3\", \"z\"],\n", " }\n", " },\n", " },\n", ")\n", "bundle.wait_for_connection()\n", "print(f\"{bundle=}\")\n", "print(f\"{bundle.psic=}\")\n", "print(f\"{bundle.table=}\")" ] } ], "metadata": { "kernelspec": { "display_name": "bluesky_2025_1", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.13.3" } }, "nbformat": 4, "nbformat_minor": 2 }