Laboratory Automation

In laboratory automation, bimanual robots are utilized to perform complex tasks that require the coordination of two robotic arms, such as handling delicate samples, operating instruments, or transferring materials between devices. The adoption of bimanual robots in labs can significantly improve efficiency and precision, reducing human error and increasing throughput in research and testing processes. However, the advanced capabilities of these robots also introduce greater complexity in programming and optimization, leading to higher costs for engineering, system integration, and ongoing maintenance. Here we introduce a short program created using the Jacobi platform, designed to be easily configurable for lab automation tasks involving dual-arm robots.

Video

Application Code

  1import math
  2import copy
  3import time
  4
  5from jacobi import Obstacle, Motion, Frame, Planner, Studio, LinearSection, JacobiError
  6
  7
  8class LabRobot:
  9    def __init__(self, studio: Studio, planner: Planner, dual_arm):
 10        self.studio = studio
 11        self.planner = planner
 12        self.dual_arm = dual_arm
 13        self.all_obstacles = planner.environment.get_obstacles()
 14        for obstacle in self.all_obstacles:
 15            if 'lab_short_table' in obstacle.name:
 16                self.table_obs = obstacle
 17        self._initialize_test_tubes()
 18        self._initialize_obstacles()
 19        self._initialize_reference_frames()
 20        self._initialize_offsets()
 21
 22    def _initialize_test_tubes(self):
 23        self.uncapped_test_tubes = [f'test_tube_0_{i}' for i in range(5)]
 24        self.test_tube_with_caps = [
 25            f'test_tube_cap_{i}_{j}' for i in range(5) for j in range(5)]
 26
 27    def _initialize_obstacles(self):
 28        self.filled_rack = self.planner.environment.get_obstacle('filled_rack')
 29        self.empty_rack = self.planner.environment.get_obstacle('empty_rack')
 30        self.cap_obstacle = self.planner.environment.get_obstacle('cap.glb')
 31        self.test_tube_only_obstacle = self.planner.environment.get_obstacle(
 32            'test_tube_only_arm')
 33
 34    def _initialize_reference_frames(self):
 35        self.left_arm_home_position = [0, -0.52, 0, -1.57, 0, 0, 0]
 36        self.right_arm_home_position = [
 37            0.109, -1.489, 0.202, 0.067, -0.886, 2.141, -1.693]
 38
 39        self.left_arm_idle_frame = Frame(
 40            x=0.3, y=0.385, z=0.50, a=math.pi / 2, b=math.pi / 4, c=0)
 41        self.left_arm_center_frame = Frame(
 42            x=0.42, y=-0.02, z=0.50, a=math.pi / 2, b=math.pi / 4, c=0)
 43
 44        self.right_arm_idle_frame = Frame(
 45            x=0.3, y=-0.385, z=0.625, a=-math.pi, b=0, c=-math.pi / 2)
 46        self.right_arm_center_frame = Frame(
 47            x=0.42, y=-0.02, z=0.535, a=-math.pi, b=0, c=-math.pi / 2)
 48
 49    def _initialize_offsets(self):
 50        self.test_tube_grab_z_offset = 0.035
 51        self.test_tube_grab_z_offset_rack = 0.132
 52        self.rack_hole_diameter = 0.0171
 53        self.rack_hole_clearance = 0.0100
 54        self.space_between_holes = self.rack_hole_diameter + self.rack_hole_clearance
 55
 56        self.empty_rack_origin = self.empty_rack.origin * \
 57            Frame(x=-self.space_between_holes * 2,
 58                  y=self.space_between_holes * 2)
 59        self.filled_rack_origin = self.filled_rack.origin * \
 60            Frame(x=-self.space_between_holes * 2,
 61                  y=self.space_between_holes * 2)
 62
 63    def run_trajectory(self, trajectory, robot):
 64        self.studio.run_trajectory(trajectory, robot=robot)
 65
 66    def set_to_home(self):
 67        self.studio.set_joint_position(
 68            joint_position=self.left_arm_home_position, robot=self.dual_arm.left)
 69        self.studio.set_joint_position(
 70            joint_position=self.right_arm_home_position, robot=self.dual_arm.right)
 71
 72    def arms_go_home(self):
 73        robot = self.dual_arm
 74        start = self.studio.get_joint_position(
 75            robot=self.dual_arm.left) + self.studio.get_joint_position(robot=self.dual_arm.right)
 76        goal = self.left_arm_home_position + self.right_arm_home_position
 77        motion_to_go_home = Motion(
 78            name='goinghome', robot=robot, start=start, goal=goal)
 79        trajectory_to_go_home = self.planner.plan(motion_to_go_home)
 80        self.run_trajectory(trajectory_to_go_home, robot=robot)
 81
 82    def arm_go_home(self, side: str):
 83        robot = self.dual_arm.left if side == 'left' else self.dual_arm.right
 84        goal = self.left_arm_home_position if side == 'left' else self.right_arm_home_position
 85
 86        start = self.studio.get_joint_position(robot=robot)
 87        motion_to_go_home = Motion(
 88            name='gohome', robot=robot, start=start, goal=goal)
 89        trajectory_to_go_home = self.planner.plan(motion_to_go_home)
 90        self.run_trajectory(trajectory_to_go_home, robot=robot)
 91
 92    def arms_go_idle(self, side: str):
 93        robot = self.dual_arm.left if side == 'left' else self.dual_arm.right
 94        goal = self.left_arm_idle_frame if side == 'left' else self.right_arm_idle_frame
 95
 96        start = self.studio.get_joint_position(robot=robot)
 97        motion_to_go_idle = Motion(
 98            name='goidle', robot=robot, start=start, goal=goal)
 99        self.planner.environment.add_obstacle(self.table_obs)
100        trajectory_to_go_idle = self.planner.plan(motion_to_go_idle)
101        self.planner.environment.remove_obstacle(self.table_obs)
102        self.run_trajectory(trajectory_to_go_idle, robot=robot)
103
104    def grab_filled_test_tube(self, cap_test_tube: Obstacle):
105        robot = self.dual_arm.left
106        test_tube_position = cap_test_tube.origin
107
108        start = self.studio.get_joint_position(robot=robot)
109        goal = test_tube_position * \
110            Frame(z=-self.test_tube_grab_z_offset) * \
111            Frame(a=math.pi / 2, b=math.pi / 4, c=0)
112        motion_to_grab = Motion(
113            name='grab', robot=robot, start=start, goal=goal)
114        motion_to_grab.linear_approach = LinearSection(
115            offset=Frame(z=-0.05), speed=0.15)
116        motion_to_grab.linear_retraction = LinearSection(
117            offset=Frame(z=0.001), speed=0.15)
118
119        self._remove_obstacles_for_planning()
120        self.planner.environment.remove_obstacle(cap_test_tube)
121        trajectory_to_grab = self.planner.plan(motion_to_grab)
122        self.run_trajectory(trajectory_to_grab, robot=robot)
123
124        self._attach_test_tube_to_arm(cap_test_tube, robot)
125        self._move_arm_to_center(robot)
126
127    def _remove_obstacles_for_planning(self):
128        try:
129            self.planner.environment.remove_obstacle(
130                self.planner.environment.get_obstacle('lab_short_table'))
131            self.planner.environment.remove_obstacle(
132                self.planner.environment.get_obstacle('filled_rack'))
133        # Do not throw error if obstacle is not found
134        except JacobiError:
135            pass
136
137    def _attach_test_tube_to_arm(self, test_tube: Obstacle, robot):
138        self.studio.remove_obstacle(test_tube)
139        test_tube.name = 'item'
140        test_tube.origin = Frame(x=0.0, y=0.035, z=0.0, a=-1.57, b=0.0, c=0.0)
141        robot.item_obstacle = test_tube
142        self.studio.set_item(test_tube, robot=robot)
143
144    def _move_arm_to_center(self, robot):
145        start = self.studio.get_joint_position(robot=robot)
146        goal = self.left_arm_center_frame
147        motion_to_center = Motion(
148            name='leftarm2center', robot=robot, start=start, goal=goal)
149        motion_to_center.linear_retraction = LinearSection(
150            offset=Frame(y=0.2), speed=0.15)
151        trajectory = self.planner.plan(motion_to_center)
152        self.run_trajectory(trajectory, robot=robot)
153
154    def uncap_test_tube(self):
155        robot = self.dual_arm.right
156        start = self.studio.get_joint_position(self.dual_arm.right)
157        goal = self.right_arm_center_frame
158        goal_ik = self.dual_arm.right.inverse_kinematics(goal)
159        goal_ik[-1] = goal_ik[-1] + math.pi
160
161        planner_cf = Planner(self.dual_arm.right)
162        motion_to_center = Motion(
163            name='rightarm2center', robot=robot, start=start, goal=goal_ik)
164        motion_to_center.linear_approach = LinearSection(offset=Frame(z=-0.05))
165        trajectory_to_center = planner_cf.plan(motion_to_center)
166        self.run_trajectory(trajectory_to_center, robot=robot)
167
168        start = self.studio.get_joint_position(robot=robot)
169        goal = copy.deepcopy(start)
170        goal[-1] = goal[-1] - math.pi
171        planner_cf = Planner(self.dual_arm.right)
172        motion_to_uncap = Motion(
173            name='uncap', robot=robot, start=start, goal=goal)
174        trajectory_to_uncap = planner_cf.plan(motion_to_uncap)
175        self.run_trajectory(trajectory_to_uncap, robot=robot)
176
177    def switch_obstacles(self):
178        self._remove_item_from_arm(self.dual_arm.left)
179        self._attach_test_tube_to_arm(
180            self.test_tube_only_obstacle, self.dual_arm.left)
181        self._attach_cap_to_arm(self.dual_arm.right)
182
183    def _remove_item_from_arm(self, robot):
184        robot.item_obstacle = None
185        self.studio.set_item(None, robot=robot)
186
187    def _attach_cap_to_arm(self, robot):
188        cap_obs = self.cap_obstacle
189        cap_obs.name = 'cap_item'
190        cap_obs.origin = Frame(x=0.0, y=0.0, z=0.015, a=3.14, b=0.0, c=0.0)
191        robot.item_obstacle = cap_obs
192        self.studio.set_item(cap_obs, robot=robot)
193
194    def right_arm_dispose_cap(self):
195        time.sleep(1)
196        robot = self.dual_arm.right
197        start = self.studio.get_joint_position(robot=robot)
198        goal = self.right_arm_idle_frame
199        motion_to_dispose = Motion(
200            name='dispose', robot=robot, start=start, goal=goal)
201        motion_to_dispose.linear_retraction = LinearSection(
202            offset=Frame(z=-0.1), speed=0.15)
203        planner_cf = Planner(self.dual_arm.right)
204        trajectory_to_dispose = planner_cf.plan(motion_to_dispose)
205        self.run_trajectory(trajectory_to_dispose, robot=robot)
206        self._remove_item_from_arm(self.dual_arm.right)
207
208    def place_test_tube_in_rack(self, row: int, col: int):
209        common_goal = self.empty_rack_origin * Frame(x=self.space_between_holes * col, y=self.space_between_holes * row) \
210            * Frame(z=self.test_tube_grab_z_offset_rack)
211        goal_for_robot = common_goal * \
212            Frame(a=-math.pi / 2, b=math.pi, c=math.pi) * Frame(y=-0.015)
213        goal_for_tube = common_goal
214
215        robot = self.dual_arm.left
216        planner_cf = Planner(robot)
217        start = self.studio.get_joint_position(robot=robot)
218
219        motion_to_place = Motion(
220            name='place', robot=robot, start=start, goal=goal_for_robot)
221        motion_to_place.linear_approach = LinearSection(
222            offset=Frame(y=0.1), speed=0.15)
223        trajectory_to_place = planner_cf.plan(motion_to_place)
224        self.run_trajectory(trajectory_to_place, robot=robot)
225        self._remove_item_from_arm(self.dual_arm.left)
226        self._spawn_test_tube_in_rack(goal_for_tube)
227
228    def _spawn_test_tube_in_rack(self, goal_for_tube):
229        test_tube = self.uncapped_test_tubes.pop(0)
230        test_tube = self.planner.environment.get_obstacle(test_tube)
231        test_tube.origin = goal_for_tube
232        self.studio.add_obstacle(test_tube)
233
234
235def main():
236    project_path = 'lab_automation.jacobi-project'
237    time.sleep(4)
238    planner = Planner.load_from_project_file(project_path)
239    abb_yumi = planner.environment.get_robot('abb_yumi')
240    abb_yumi.set_speed(0.6)
241    abb_yumi.right.set_speed(0.6)
242    abb_yumi.left.set_speed(0.6)
243    studio = Studio()
244    studio.reset()
245
246    my_lab_robot = LabRobot(studio=studio, planner=planner, dual_arm=abb_yumi)
247    my_lab_robot.set_to_home()
248
249    for i in range(4):
250        capped_test_tube = my_lab_robot.planner.environment.get_obstacle(
251            f'test_tube_cap_0_{i}')
252        my_lab_robot.grab_filled_test_tube(capped_test_tube)
253
254        if i == 0:
255            my_lab_robot.arms_go_idle('right')
256
257        my_lab_robot.uncap_test_tube()
258
259        my_lab_robot.switch_obstacles()
260
261        my_lab_robot.right_arm_dispose_cap()
262
263        row, col = divmod(i, 5)
264        my_lab_robot.place_test_tube_in_rack(row, col)
265
266        my_lab_robot.arms_go_idle('left')
267
268    my_lab_robot.arm_go_home('left')
269    print('Lab automation sequence completed successfully!')
270
271
272if __name__ == '__main__':
273    main()