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()