-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathsimulator.py
331 lines (267 loc) · 13.5 KB
/
simulator.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
import pybullet_utils.bullet_client as bc
import pybullet as p
import pybullet_data
import numpy as np
import time
import os
class Simulator:
"""This class is a wrapper for simulating the RARL Hexapod with the PyBullet physics engine
Note
----
This class is configured to automatically start a PyBullet instance on an available CPU core.
Each initialised class will use a separate CPU aiding parallelisation
Attributes
----------
urdf : str
Filename of URDF model of hexapod relative to simulator.
controller : :obj:'Controller'
Positional controller object responsible for providing joint angles and velocities at given time
visualiser_enabled : bool
Whether to display a GUI
collision_fatal : bool
Whether collisions between links raise an exception
locked_joints : :obj:`list` of :obj:`int`
A list of joint numbers which should be fixed in a retracted position
failed_joints : :obj:`list` of :obj:`int`
A list of joint numbers which should simulated as unpowered.
"""
def __init__(self, controller, urdf='/urdf/hexapod_simplified.urdf', visualiser=False, follow=True, collision_fatal=True, failed_legs=[], camera_position=[0, 0, 0], camera_distance=0.7, camera_yaw=20, camera_pitch=-30):
self.t = 0 #: float: Current time of the simulator
self.dt = 1.0/240.0 #: float: Timestep of simulator. Default is 1/240s for PyBullet.
self.n_step = 0
self.gravity = -9.81 #: float: Magnitude of gravity vector in the positive z direction
self.foot_friction = 0.7
self.controller = controller
self.visualiser_enabled = visualiser
self.follow = follow
self.collision_fatal = collision_fatal
self.failed_joints = []
# set up joints to be locked to simulate failure
self.locked_joints = []
for failed_leg in failed_legs:
self.locked_joints += [failed_leg*3-3, failed_leg*3-2, failed_leg*3-1]
self.camera_position = [0.7, 0, 0] #: list of float: GUI camera focus position in cartesian coordinates (self.controller.body_height)
self.camera_distance = 0.8 #: float: GUI camera distance from camera position
self.camera_yaw = 0 #: float: GUI camera yaw in degrees (-20)
self.camera_pitch = -45 #: float: GUI camera pitch in degrees (-30)
self.camera_position = camera_position
self.camera_distance = camera_distance
self.camera_yaw = camera_yaw
self.camera_pitch = camera_pitch
# connect a client to a pybullet physics server
if self.visualiser_enabled:
self.client = bc.BulletClient(connection_mode=p.GUI)
self.client.resetDebugVisualizerCamera(cameraDistance=self.camera_distance, cameraYaw=self.camera_yaw, cameraPitch=self.camera_pitch, cameraTargetPosition=self.camera_position)
self.client.configureDebugVisualizer(p.COV_ENABLE_GUI, False) # somestimes useful to turn on
self.client.configureDebugVisualizer(p.COV_ENABLE_RENDERING, True)
self.client.configureDebugVisualizer(p.COV_ENABLE_SHADOWS, False, shadowMapResolution=8192) # for nice rendering
self.client.configureDebugVisualizer(p.COV_ENABLE_WIREFRAME, False) # somestimes useful to turn on
self.client.configureDebugVisualizer(p.COV_ENABLE_RGB_BUFFER_PREVIEW, False)
self.client.configureDebugVisualizer(p.COV_ENABLE_DEPTH_BUFFER_PREVIEW, False)
self.client.configureDebugVisualizer(p.COV_ENABLE_SEGMENTATION_MARK_PREVIEW, False)
else:
self.client = bc.BulletClient(connection_mode=p.DIRECT)
self.client.setAdditionalSearchPath(pybullet_data.getDataPath())
self.client.setGravity(0, 0, self.gravity)
self.client.setRealTimeSimulation(False) # simulation needs to be explicitly stepped
# Add ground plane and set lateral friction coefficient
self.groundId = self.client.loadURDF('plane.urdf') #: int: Body ID of ground
self.client.changeDynamics(self.groundId, -1, lateralFriction=self.foot_friction)
# Add hexapod URDF
position = [0, 0, self.controller.body_height]
orientation = self.client.getQuaternionFromEuler([0, 0, -controller.crab_angle])
filepath = os.path.abspath(os.path.dirname(__file__)) + urdf
self.hexId = self.client.loadURDF(filepath, position, orientation, flags=p.URDF_USE_INERTIA_FROM_FILE | p.URDF_USE_SELF_COLLISION) #: int: Body ID of hexapod robot
# get joint and link info from model
self.joints = self.__get_joints(self.hexId) #: list of int: List of joint indeces
self.links = self.__get_links(self.joints) #: list of int: List of link indeces
# initialise joints and links
self.__init_joints(self.controller, self.joints, self.locked_joints)
self.__init_links(self.links)
# self.fps = 24
# self.pipeline = Popen(['ffmpeg', '-y', '-f', 'image2pipe', '-vcodec', 'mjpeg', '-r', str(self.fps), '-i', '-', '-vcodec', 'mpeg4', '-qscale', '5', '-r', str(self.fps), 'video.mp4'], stdin=PIPE)
# set joints to their initial positions
def __init_joints(self, controller, joints, locked_joints):
joint_angles = controller.joint_angles(t=0)
for index, joint in enumerate(joints):
joint_angle = joint_angles[index]
joint_index, lower_limit, upper_limit, max_torque, max_speed = joint
# not guaranteed that joint is present
if joint_index is None: continue
# if joint is locked, set it to fixed angle
if index in locked_joints:
joint_speed = 0
if joint_index in np.arange(18)[0::3]: # coxa
joint_angle = np.radians(0)
elif joint_index in joints[1::3]: # femur
joint_angle = np.radians(90)
elif joint_index in joints[2::3]: # tibia
joint_angle = np.radians(-150)
# set joints to their starting position
self.client.resetJointState(self.hexId, joint_index, targetValue=joint_angle)
if index in locked_joints: continue
# ensure actuator behaves as unpowered servo
# assign small friction force to joint to simulate servo friction
self.client.setJointMotorControl2(self.hexId, joint_index, p.VELOCITY_CONTROL, force=0.1)
def __init_links(self, links):
tibia_links = links[:, 2]
for link_index in tibia_links:
# assign friction to feet to ensure they are not dragged
self.client.changeDynamics(self.hexId, link_index, lateralFriction=self.foot_friction)
# remove collisions between femur and base as this was preventing full coxa range of motion
femur_links = links[:, 1]
for link_index in femur_links:
self.client.setCollisionFilterPair(self.hexId, self.hexId, linkIndexA=-1, linkIndexB=link_index, enableCollision=0)
# Fetches and stores the joint index and joint information in the expected order
def __get_joints(self, robotId):
# A lot of the joint information in the URDF is not used in setJointMotorControl and needs to be manually applied
joint_names = [b'joint_1_1', b'joint_1_2', b'joint_1_3', b'joint_2_1', b'joint_2_2', b'joint_2_3', b'joint_3_1', b'joint_3_2', b'joint_3_3', b'joint_4_1', b'joint_4_2', b'joint_4_3', b'joint_5_1', b'joint_5_2', b'joint_5_3', b'joint_6_1', b'joint_6_2', b'joint_6_3']
joints = np.full((len(joint_names), 5), None)
for joint_index in range(self.client.getNumJoints(robotId)):
info = self.client.getJointInfo(robotId, joint_index)
try:
index = joint_names.index(info[1])
# [ joint_index, lower_limit, upper_limit, max_torque, max_velocity ]
joints[index] = [info[0], info[8], info[9], info[10], info[11]]
except ValueError:
print('Unexpected joint name in URDF')
return joints
def __get_links(self, joints):
# In pybullet the linkIndex is the jointIndex
link_indices = self.joints[:,0]
links = link_indices.reshape(6,3)
return links
def set_foot_friction(self, foot_friction):
self.foot_friction = foot_friction
self.client.changeDynamics(self.groundId, -1, lateralFriction=foot_friction)
def terminate(self):
"""Closes PyBullet physics engine and frees up system resources
Note
----
Prints the PyBullet error if termination failed
"""
try:
# self.client.stopStateLogging(self.logId)
# self.pipeline.stdin.close()
# self.pipeline.wait()
self.client.disconnect()
except p.error as e:
print('Termination of simulation failed:', e)
def step(self):
"""Steps the simulation by dt
Note
----
This will request the joint angles and speeds from the assigned controller
"""
start_time = time.perf_counter()
# using setJointMotorControl2 (slightly slower but allows setting of max velocity)
joint_angles = self.controller.joint_angles(t=self.t)
for index, joint_properties in enumerate(self.joints):
joint_index, lower_limit, upper_limit, max_torque, max_speed = joint_properties
# skip if joint is not present of if failed
if (joint_index is None) or (index in self.locked_joints) or (index in self.failed_joints): continue
joint_angle = joint_angles[index]
# ensure controller does not attempt to exceed joint limits
joint_angle = min(max(lower_limit, joint_angle), upper_limit)
# max velocity in URDF isn't used and needs to be assigned
self.client.setJointMotorControl2(self.hexId, joint_index, p.POSITION_CONTROL, targetPosition=joint_angle, force=max_torque, maxVelocity=max_speed)
if self.collision_fatal:
if self.__link_collision() or self.__ground_collision():
raise RuntimeError('Link collision during simulation')
end_time = time.perf_counter()
# follow robot with camera
if self.visualiser_enabled:
if self.follow:
self.client.resetDebugVisualizerCamera(cameraDistance=self.camera_distance, cameraYaw=self.camera_yaw, cameraPitch=self.camera_pitch, cameraTargetPosition=self.base_pos())
time.sleep(max(self.dt - end_time + start_time, 0))
# controller feedback running at 10Hz
if ((self.n_step % 24) == 0):
self.controller.IMU_feedback(self.base_orientation())
self.client.stepSimulation()
self.n_step += 1
self.t += self.dt
def supporting_legs(self):
"""Determines the supporting legs for the hexapod
Note
----
A leg is considered to be supporting if it is in constact with the ground.
This method is used in plotting the gait sequence diagram.
Returns
----
list of bool: A list of booleans where the index is the leg number and where 'True' represents in contact with the ground.
"""
tibia_links = self.links[:, 2]
# Get contact points between hex and ground for the last stepSimulation call
contact_points = np.array(self.client.getContactPoints(self.hexId, self.groundId), dtype=object)
# get links for contact between ground and hexapod
try:
contact_links = contact_points[:, 3]
except IndexError as e:
contact_links = np.array([])
supporting_legs = np.isin(tibia_links, contact_links)
return supporting_legs
# Returns true for collision between links in robot
def __link_collision(self):
# Get contact points between hex links
# Collision between child and parent in URDF is disabled by default so shouldn't need to ignore this
contact_points = np.asarray(self.client.getContactPoints(self.hexId, self.hexId), dtype=object)
# also need to add in collision detection between robot and ground excluding tibia links
return contact_points.size > 0
# Returns true for collisions between the robot and the ground
def __ground_collision(self):
tibia_links = self.links[:, 2]
# Get contact points between hex and ground for the last stepSimulation call
contact_points = np.array(self.client.getContactPoints(self.hexId, self.groundId), dtype=object)
# get links for contact between ground and hexapod excluding the tibia links as these can contact ground
try:
contact_links = contact_points[:, 3]
except IndexError as e:
contact_links = np.array([])
# filter out tibia contacts
contact_links = contact_links[~np.isin(contact_links, tibia_links)]
# returns a boolean array of whether tibia is in contact with the ground or not
return contact_links.size > 0
def base_orientation(self):
quaternion = self.client.getBasePositionAndOrientation(self.hexId)[1]
return p.getEulerFromQuaternion(quaternion)
# returns the base position without the orientation
def base_pos(self):
"""Returns the position of the hexapod base
Note
----
The base orientation is not returned
Returns
----
list of float: The position of the hexapod base in cartesian coordinates
"""
return self.client.getBasePositionAndOrientation(self.hexId)[0]
def evaulate_gait(leg_params, body_height=0.14, velocity=0.3, duration=5.0, visualiser=True, collisions=False, failed_legs=[]):
# controller will return an error if parameters are not feasible
controller = Controller(leg_params, body_height=body_height, velocity=velocity, crab_angle=-np.pi/6)
# initialise simulator
simulator = Simulator(controller, follow=True, visualiser=visualiser, collision_fatal=collisions, failed_legs=failed_legs)
# initialise fitness and descriptor
contact_sequence = np.full((6, 0), False)
# simulator returns error if collision occurs
for t in np.arange(0, duration, step=simulator.dt):
try:
simulator.step()
except RuntimeError as error:
print(error)
fitness = 0
break
contact_sequence = np.append(contact_sequence, simulator.supporting_legs().reshape(-1,1), axis=1)
fitness = simulator.base_pos()[0]
# summarise descriptor
descriptor = np.sum(contact_sequence, axis=1) / np.size(contact_sequence, axis=1)
# plot footfall diagram
plot_footfall(contact_sequence)
simulator.terminate()
return fitness, descriptor
if __name__ == "__main__":
from controllers.kinematic import Controller
from controllers.kinematic import stationary
controller = Controller(stationary, body_height=0.11, velocity=0.0, crab_angle=-np.pi/6)
simulator = Simulator(controller=controller, follow=False, visualiser=True, collision_fatal=False, camera_distance=1.0, camera_yaw=90, camera_pitch=-55)
while True:
simulator.step()