-
Notifications
You must be signed in to change notification settings - Fork 1
/
Agents.py
370 lines (303 loc) · 16.2 KB
/
Agents.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
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
import numpy as np
import matplotlib.pyplot as plt
import json
import _pickle as cpickle
"""
Collection of agents that can be used by generate-sensorimotor-data.py.
Agents are used to generate motor configurations and get the corresponding egocentric position of the sensor.
"""
class Agent:
"""
type: str
type of agent
n_motors : int
number of independent motor components in [-1, 1]
size_regular_grid: int
number of samples that form the regular sampling of the motor space
generate_random_sampling(k):
randomly explores the motor space and returns the motor samples and corresponding sensor egocentric positions
generate_regular_sampling():
returns a regular sampling of the motor space and the corresponding sensor egocentric positions
display(motor):
displays the agent's configuration associated with an input motor command
log(dir):
logs the agent's parameters
"""
def __init__(self, type_agent, n_motors, size_regular_grid):
self.type = type_agent
self.n_motors = n_motors
self.size_regular_grid = size_regular_grid
def generate_random_sampling(self, k):
return None, None
def generate_regular_sampling(self):
return None, None
def display(self, motor_configuration):
pass
def save(self, destination):
"""
Save the agent to the disk.
"""
try:
# save a readable log of the agent
serializable_dict = self.__dict__.copy()
for key, value in serializable_dict.items():
if type(value) is np.ndarray:
serializable_dict[key] = value.tolist()
with open(destination + "/agent_params.txt", "w") as f:
json.dump(serializable_dict, f, indent=2, sort_keys=True)
# save the object on disk
with open(destination + "/agent.pkl", "wb") as f:
cpickle.dump(self, f)
except:
print("ERROR: saving the agent in {} failed".format(destination))
return False
class GridExplorer(Agent):
"""
A ``discrete'' agent that can move its sensor in a 5x5 grid using a non-linear redundant mapping
from motors configurations to positions in the 2D grid.
Attributes
----------
n_states: int
number of possible motor states
state2motor_mapping : nd.array of float of size (n_states, n_motors)
mapping between the states and the motor configurations
state2pos_mapping : nd.array of int of size (n_states, 2)
mapping between the states and the sensor position (x, y) in [-2, 2]² in the grid
"""
def __init__(self, type_agent, n_motors, size_regular_grid):
super().__init__(type_agent, n_motors, size_regular_grid)
self.state2motor_mapping = None
self.state2pos_mapping = None
self.n_states = size_regular_grid
def generate_random_sampling(self, k=1):
"""
Draw a set of k randomly selected motor configurations and associated sensor positions
Returns:
motor - (k, self.n_motors) array
position - (k, 2) array
"""
state_index = np.random.randint(0, self.n_states, k)
motor = self.state2motor_mapping[state_index, :]
position = self.state2pos_mapping[state_index, :]
return motor, position
def generate_regular_sampling(self):
"""
Returns all the motor configurations and associated sensor positions.
"""
return self.state2motor_mapping, self.state2pos_mapping
def display(self, motor):
"""
Displays the position associated with a motor configuration
"""
plt.plot(0, 0, 'ks')
for i in range(motor.shape[0]):
# get the state from the motor command
state_index = np.where(np.all(self.state2motor_mapping == motor[i, :], axis=1))[0][0]
position = self.state2pos_mapping[state_index, :]
plt.plot(position[0], position[1], 'ro')
class GridExplorer3dof(GridExplorer):
"""
A ``discrete'' agent that can move its sensor in a 5x5 grid using a non-linear redundant mapping from 3 motors to positions in the 2D grid.
Attributes
----------
state2motor_mapping : nd.array of float of size (n_states, n_motors)
mapping between the states and the motor configurations
state2pos_mapping : nd.array of int of size (n_states, 2)
mapping between the states and the sensor position (x, y) in [-2, 2]² in the grid
"""
def __init__(self, number_motors=3, resolution=5):
super().__init__(type_agent="GridExplorer3dof", n_motors=number_motors, size_regular_grid=resolution**number_motors)
self.state2motor_mapping, self.state2pos_mapping = self.create_discrete_mapping_3dmotor_to_2dpos(reso=resolution)
def create_discrete_mapping_3dmotor_to_2dpos(self, reso):
"""
Create a random mapping from motor configurations to egocentric positions of the sensor.
The mapping is discrete, such that a limited set of motor configurations is associated with a limited set of sensors positions (of smaller
cardinality due to redundancy). For convenience, the mapping is split into a state2motor_mapping and a state2pos_mapping, where a state
corresponds to the index given to a motor configuration.
Returns:
state2motor_mapping - (reso**3, 3) array
state2pos_mapping - (reso**3, 2) array
"""
# scan all possible states
coordinates = np.array(np.meshgrid(*list([np.linspace(0, 1, reso)]) * self.n_motors))
# reshape the coordinates into matrix of size (reso**3, 3)
mapping = np.array([coord.reshape((-1)) for coord in coordinates]).T
# apply a transformation to the mapping to create the non-linear state2motor_mapping
state2motor_mapping = np.power(mapping, 3, dtype=float)
# rescale the motor commands in the state2motor_mapping in [-1, 1]
state2motor_mapping = state2motor_mapping * 2 - 1
# apply a trivial transformation (drop the last coordinate) to the mapping to create the redundant state2pos_mapping
state2pos_mapping = mapping[:, 0:2]
# rescale the positions in [-2, 2] and format it as integer
state2pos_mapping = (state2pos_mapping * 4 - 2).astype(int)
return state2motor_mapping, state2pos_mapping
class GridExplorer6dof(GridExplorer):
"""
A ``discrete'' agent that can move its sensor in a 5x5 grid using a non-linear redundant mapping from 6 motors to positions in the 2D grid.
Attributes
----------
state2motor_mapping : nd.array of float of size (n_states, n_motors)
mapping between the states and the motor configurations
state2pos_mapping : nd.array of int of size (n_states, 2)
mapping between the states and the sensor position (x, y) in [-2, 2]² in the grid
"""
def __init__(self, number_motors=6, resolution=4):
super().__init__(type_agent="GridExplorer3dof", n_motors=number_motors, size_regular_grid=resolution**number_motors)
self.state2motor_mapping, self.state2pos_mapping = self.create_discrete_mapping_6dmotor_to_2dpos(reso=resolution)
def create_discrete_mapping_6dmotor_to_2dpos(self, reso):
"""
Create a random mapping from motor configurations to egocentric positions of the sensor.
The mapping is discrete, such that a limited set of motor configurations is associated with a limited set of sensors positions (of smaller
cardinality due to redundancy). For convenience, the mapping is split into a state2motor_mapping and a state2pos_mapping, where a state
corresponds to the index given to a motor configuration.
Returns:
state2motor_mapping - (reso**n_motors, 6) array
state2pos_mapping - (reso**n_motors, 2) array
"""
# scan all possible states
coordinates = np.array(np.meshgrid(*list([np.linspace(0, 1, reso)]) * self.n_motors))
# reshape the coordinates into matrix of size (reso**n_motors, n_motors)
mapping = np.array([coord.reshape((-1)) for coord in coordinates]).T
# mixing matrix
mixing_matrix = 4 * np.random.rand(self.n_motors, self.n_motors) - 2
state2motor_mapping = np.matmul(mapping, np.linalg.inv(mixing_matrix))
# normalization of the values into [0, 1] for easy application of the non-linearities
state2motor_mapping = state2motor_mapping - np.min(state2motor_mapping, axis=0)
state2motor_mapping = state2motor_mapping / np.max(state2motor_mapping, axis=0)
# non-linear transformation from [0, 1] to [0, 1]
state2motor_mapping[:, 0] = np.power(state2motor_mapping[:, 0], 0.5, dtype=float)
state2motor_mapping[:, 1] = np.power(state2motor_mapping[:, 1], 2, dtype=float)
state2motor_mapping[:, 2] = np.power(state2motor_mapping[:, 2], 3, dtype=float)
state2motor_mapping[:, 3] = (np.log((state2motor_mapping[:, 3] + 0.1) / 1.1) - np.log(0.1 / 1.1)) / (-np.log(0.1 / 1.1))
state2motor_mapping[:, 4] = (np.exp(state2motor_mapping[:, 4]) - 1) / (np.exp(1) - 1)
state2motor_mapping[:, 5] = np.power(state2motor_mapping[:, 5], 1, dtype=float)
# rescale the motor commands in the state2motor_mapping in [-1, 1]
state2motor_mapping = state2motor_mapping * 2 - 1
# apply a trivial transformation (drop the last coordinates) to the mapping to create the state2pos_mapping
state2pos_mapping = mapping[:, 0:2]
# rescale the positions in [-2, 2] and format it as integer
state2pos_mapping = np.ceil(state2pos_mapping * (reso - 1) - (reso / 2)).astype(int)
return state2motor_mapping, state2pos_mapping
class HingeArm(Agent):
"""
An arm that can move its end-effector in a 2D space, while keeping its orientation fixed.
Attributes
----------
motor_amplitude : float
scale by which the motor components are multiplied to command the joints
segments_length : list/tuple of 3 float/int
lengths of the arm segments
"""
def __init__(self, type_agent, n_motors, size_regular_grid):
super().__init__(type_agent, n_motors, size_regular_grid)
self.motor_amplitude = None
self.segments_length = None
def get_position_from_motor(self, motor):
return None
def generate_random_sampling(self, k=1):
"""
Draw a set of k randomly selected motor configurations and associated egocentric sensor positions
Returns:
motor - (k, self.n_motors) array
position - (k, 2) array
"""
# draw random motor components in [-1, 1]
motor = 2 * np.random.rand(k, self.n_motors) - 1
# get the associated egocentric positions
position = self.get_position_from_motor(motor)
return motor, position
def generate_regular_sampling(self):
"""
Generates a regular grid of motor configurations in the motor space.
"""
resolution = int(self.size_regular_grid**(1/self.n_motors))
# create a grid of coordinates
coordinates = np.array(np.meshgrid(*list([np.arange(-1, 1, 2/resolution)]) * self.n_motors))
# reshape the coordinates into matrix of size (reso**n_motors, n_motors)
motor_grid = np.array([coord.reshape((-1)) for coord in coordinates]).T
# get the corresponding positions
pos_grid = self.get_position_from_motor(motor_grid)
return motor_grid, pos_grid
class HingeArm3dof(HingeArm):
"""
A three-segment arm with hinge joints that can move its end-effector in a 2D space.
The arm has three segments of size 12 and covers a working space of radius 36.
Note that only the position of the end-effector is considered; its orientation is considered fixed.
Attributes
----------
motor_amplitude : float
scale by which the motor components are multiplied
segments_length : list/tuple of 3 float/int
lengths of the arm segments
"""
def __init__(self):
super().__init__(type_agent="HingeArm3dof", n_motors=3, size_regular_grid=7**3)
self.motor_amplitude = [np.pi] * self.n_motors
self.segments_length = [0.5] * self.n_motors # the arm covers a working space working space of radius 1.5 in an environment of size size 7
def get_position_from_motor(self, motor):
"""
Get the coordinates of the sensor via trigonometry.
"""
x = np.sum(np.multiply(self.segments_length, np.cos(np.cumsum(self.motor_amplitude * motor, axis=1))), axis=1, keepdims=True)
y = np.sum(np.multiply(self.segments_length, np.sin(np.cumsum(self.motor_amplitude * motor, axis=1))), axis=1, keepdims=True)
return np.hstack((x, y))
def display(self, motor):
"""
Displays the position associated with a motor configuration
"""
# get the joints positions
x = np.cumsum(np.multiply(self.segments_length, np.cos(np.cumsum(self.motor_amplitude * motor, axis=1))), axis=1)
y = np.cumsum(np.multiply(self.segments_length, np.sin(np.cumsum(self.motor_amplitude * motor, axis=1))), axis=1)
# add the agent's base
x = np.hstack((np.zeros((x.shape[0], 1)), x))
y = np.hstack((np.zeros((y.shape[0], 1)), y))
# unify the cases where motor is a unique input or multiple inputs
motor = motor.reshape((-1, self.n_motors))
# display the different motor configurations
for i in range(motor.shape[0]):
plt.plot(x[i, :], y[i, :], '-o')
plt.axis("equal")
class HingeArm6dof(HingeArm):
"""
A four-segment arm with 4 hinge and 2 translational joints that can move its end-effector in a 2D space.
The arm has four segments of size 9 and covers a working space of radius 36.
Note that only the position of the end-effector is considered; its orientation is considered fixed.
Attributes
----------
motor_amplitude : list of float
scale by which the motor components are multiplied
segments_length : list/tuple of 3 float/int
lengths of the arm segments
"""
def __init__(self):
super().__init__(type_agent="HingeArm6dof", n_motors=6, size_regular_grid=5**6)
self.motor_amplitude = [np.pi, np.pi, np.pi, np.pi, 1, 1]
self.segments_length = [0.375] * 4 # the arm covers a working space of radius 1.5 in an environment of size size 7
def get_position_from_motor(self, motor):
"""
Get the coordinates of the sensor via trigonometry.
"""
seg_lengths = np.array(self.segments_length) * np.hstack((np.ones((motor.shape[0], 1)),
self.motor_amplitude[4:] * motor[:, 4:],
np.ones((motor.shape[0], 1))))
x = np.sum(np.multiply(seg_lengths, np.cos(np.cumsum(self.motor_amplitude[0:4] * motor[:, 0:4], axis=1))), axis=1, keepdims=True)
y = np.sum(np.multiply(seg_lengths, np.sin(np.cumsum(self.motor_amplitude[0:4] * motor[:, 0:4], axis=1))), axis=1, keepdims=True)
return np.hstack((x, y))
def display(self, motor):
"""
Displays the position associated with a motor configuration
"""
# get the joints positions
seg_lengths = np.array(self.segments_length) * np.hstack((np.ones((motor.shape[0], 1)),
self.motor_amplitude[4:] * motor[:, 4:],
np.ones((motor.shape[0], 1))))
x = np.cumsum(np.multiply(seg_lengths, np.cos(np.cumsum(self.motor_amplitude[0:4] * motor[:, 0:4], axis=1))), axis=1)
y = np.cumsum(np.multiply(seg_lengths, np.sin(np.cumsum(self.motor_amplitude[0:4] * motor[:, 0:4], axis=1))), axis=1)
# add the agent's base
x = np.hstack((np.zeros((x.shape[0], 1)), x))
y = np.hstack((np.zeros((y.shape[0], 1)), y))
# unify the cases where motor is a unique input or multiple inputs
motor = motor.reshape((-1, self.n_motors))
# display the different motor configurations
for i in range(motor.shape[0]):
plt.plot(x[i, :], y[i, :], '-o')