Skip to content

Commit

Permalink
Merge pull request #182 from yannachen/robot_sample
Browse files Browse the repository at this point in the history
Robot transfer sample
  • Loading branch information
yannachen authored Mar 22, 2024
2 parents ea28d7d + 4f9a7a5 commit b2795d1
Show file tree
Hide file tree
Showing 7 changed files with 213 additions and 5 deletions.
8 changes: 8 additions & 0 deletions src/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
from haven.instrument.dxp import DxpDetector
from haven.instrument.dxp import add_mcas as add_dxp_mcas
from haven.instrument.ion_chamber import IonChamber
from haven.instrument.robot import Robot
from haven.instrument.shutter import Shutter
from haven.instrument.slits import ApertureSlits, BladeSlits
from haven.instrument.xspress import Xspress3Detector
Expand Down Expand Up @@ -264,6 +265,13 @@ def aerotech():
return stage


@pytest.fixture()
def robot():
RobotClass = make_fake_device(Robot)
robot = RobotClass(name="robotA", prefix="255idA:")
return robot


@pytest.fixture()
def aerotech_flyer(aerotech):
flyer = aerotech.horiz
Expand Down
1 change: 1 addition & 0 deletions src/haven/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
from .plans.mono_gap_calibration import calibrate_mono_gap # noqa: F401
from .plans.mono_ID_calibration import mono_ID_calibration # noqa: F401
from .plans.record_dark_current import record_dark_current # noqa: F401
from .plans.robot_transfer_sample import robot_transfer_sample # noqa: F401
from .plans.set_energy import set_energy # noqa: F401
from .plans.shutters import close_shutters, open_shutters # noqa: F401
from .plans.xafs_scan import xafs_scan # noqa: F401
Expand Down
3 changes: 2 additions & 1 deletion src/haven/instrument/aerotech.py
Original file line number Diff line number Diff line change
Expand Up @@ -483,7 +483,8 @@ def send_command(self, cmd: str):
return status

def disable_pso(self):
self.send_command(f"PSOCONTROL {self.axis} OFF")
for axis in range(2):
self.send_command(f"PSOCONTROL @{axis} OFF")

def check_flyscan_bounds(self):
"""Check that the fly-scan params are sane at the scan start and end.
Expand Down
2 changes: 1 addition & 1 deletion src/haven/instrument/heater.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from apstools.devices import PTC10AioChannel as PTC10AioChannelBase
from apstools.devices import PTC10PositionerMixin, PTC10TcChannel
from ophyd import Component as Cpt
from ophyd import EpicsSignalRO, EpicsSignalWithRBV, PVPositioner, EpicsSignal
from ophyd import EpicsSignal, EpicsSignalRO, EpicsSignalWithRBV, PVPositioner

from .._iconfig import load_config
from .device import aload_devices, make_device
Expand Down
56 changes: 53 additions & 3 deletions src/haven/instrument/robot.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,55 @@
log = logging.getLogger(__name__)


LOAD_TIMEOUT = 40


class Sample(Device):
"""An individual robot sample that can be loaded.
Signals
=======
present
Whether or not a sample is physically present on the stage.
empty
Whether or not no sample is physically present on the stage.
load
Direct the robot to physically move this sample to the loading
position. Can be slow (~25-30 seconds).
unload
Direct the robot to physically remove this sample to the loading
position. Can be slow (~25-30 seconds).
x
The x position of the robot in Cartesian coordinates.
y
The y position of the robot in Cartesian coordinates.
z
The z position of the robot in Cartesian coordinates.
rx
The rx position of the robot in Cartesian coordinates.
ry
The ry position of the robot in Cartesian coordinates.
rz
The position of the robot in Cartesian coordinates.
"""

present = Cpt(EpicsSignalRO, ":present")
empty = Cpt(EpicsSignalRO, ":empty")
load = Cpt(EpicsSignal, ":load", kind="omitted")
unload = Cpt(EpicsSignal, ":unload", kind="omitted")
load = Cpt(
EpicsSignal,
":load",
kind="omitted",
write_timeout=LOAD_TIMEOUT,
put_complete=True,
)
unload = Cpt(
EpicsSignal,
":unload",
kind="omitted",
write_timeout=LOAD_TIMEOUT,
put_complete=True,
)
x = Cpt(EpicsSignalRO, ":x")
y = Cpt(EpicsSignalRO, ":y")
z = Cpt(EpicsSignalRO, ":z")
Expand Down Expand Up @@ -94,7 +138,13 @@ class Robot(Device):

# sample transfer
current_sample = Cpt(EpicsSignalRO, ":current_sample", kind="config")
unload_current_sample = Cpt(EpicsSignal, ":unload_current_sample", kind="omitted")
unload_current_sample = Cpt(
EpicsSignal,
":unload_current_sample",
kind="omitted",
write_timeout=LOAD_TIMEOUT,
put_complete=True,
)
current_sample_reset = Cpt(EpicsSignal, ":current_sample_reset", kind="omitted")
home = Cpt(EpicsSignal, ":home", kind="config")
cal_stage = Cpt(EpicsSignal, ":cal_stage", kind="config")
Expand Down
105 changes: 105 additions & 0 deletions src/haven/plans/robot_transfer_sample.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import logging

from bluesky import plan_stubs as bps

from ..instrument.instrument_registry import registry
from ..motor_position import rbv

log = logging.getLogger(__name__)


__all__ = ["robot_transfer_sample"]


ON = 1


def robot_transfer_sample(robot, sampleN, *args):
"""
Use robot to load sampleN at a fixed Aerotech stage position or any
motors.
e.g.
Load sample:
robot_sample(robot, 9, motor1, 100, motor2, 200, motor3, 50)
Unload sample:
robot_sample(robot, None, motor1, 100, motor2, 200, motor3, 50)
Parameters:
- robot: robot device
- sampleN: Sample number
- *args: multiple pairs of motor and pos
"""

robot = registry.find(robot)

# Check if power is on
# if robot.power_rbv.get() == Off:
# raise ValueError("Robot is NOT powered. Please turn on the power.")

# Check if remote control is on
# if not robot.remote_control.get() == Off:
# raise ValueError(
# "Robot is NOT in reomete control mode. Please click on Pad for remote control."
# )

# Check if gripper is on
# if robot.gripper_activated.get() == Off:
# raise ValueError("Gripper is not activated. Please activate the gripper.")

# Check if robot is running
# if robot.grogram_running.get() == On:
# raise ValueError("Robot is running now.")

# Find motors
motor_list = [registry.find(motor) for motor in args[::2]]
new_positions = [pos for pos in args[1::2]]
# Record the motor positions before load sampls
initial_positions = [rbv(motor) for motor in motor_list]

# Move the aerotech to the loading position
for motor, pos in zip(motor_list, new_positions):
yield from bps.mv(motor, pos)

if sampleN == None:
# Unload sample
yield from bps.mv(robot.unload_current_sample, ON)

else:
# Load sampleN after all the other motor arrive at (pos1, pos2, pos3...)
sample = getattr(
robot.samples, f"sample{sampleN}"
) # Access the Sample device corresponding to sampleN
print(sample.load)
yield from bps.mv(sample.load, ON) # Assuming '1' initiates the loading action

# Return to the initial position
for motor, pos in zip(motor_list[-1::-1], initial_positions[-1::-1]):
yield from bps.mv(motor, pos)


# -----------------------------------------------------------------------------
# :author: Yanna Chen
# :email: [email protected]
# :copyright: Copyright © 2024, UChicago Argonne, LLC
#
# Distributed under the terms of the 3-Clause BSD License
#
# The full license is in the file LICENSE, distributed with this software.
#
# DISCLAIMER
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
# -----------------------------------------------------------------------------
43 changes: 43 additions & 0 deletions src/haven/tests/test_robot_transfer_sample.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from ophyd.sim import motor1, motor2, motor3

from haven import robot_transfer_sample


def test_robot_sample(robot):

plan = robot_transfer_sample(robot, 9, motor1, 100, motor2, 200, motor3, 50)

msgs = list(plan)

assert robot.name == "robotA"
assert len(msgs) == 14

unload = robot_transfer_sample(robot, None, motor1, 100)
msgs = list(unload)
assert len(msgs) == 6


# -----------------------------------------------------------------------------
# :author: Yanna Chen
# :email: [email protected]
# :copyright: Copyright © 2024, UChicago Argonne, LLC
#
# Distributed under the terms of the 3-Clause BSD License
#
# The full license is in the file LICENSE, distributed with this software.
#
# DISCLAIMER
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
# -----------------------------------------------------------------------------

0 comments on commit b2795d1

Please sign in to comment.