sgptools 1.2.0__py3-none-any.whl → 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sgptools/__init__.py +3 -4
- sgptools/core/__init__.py +1 -0
- sgptools/{models/core → core}/augmented_gpr.py +11 -17
- sgptools/{models/core → core}/augmented_sgpr.py +27 -34
- sgptools/core/osgpr.py +417 -0
- sgptools/core/transformations.py +699 -0
- sgptools/kernels/__init__.py +0 -8
- sgptools/kernels/attentive_kernel.py +214 -69
- sgptools/kernels/neural_kernel.py +268 -92
- sgptools/kernels/neural_network.py +127 -28
- sgptools/methods.py +1047 -0
- sgptools/objectives.py +275 -0
- sgptools/utils/__init__.py +0 -9
- sgptools/utils/data.py +452 -149
- sgptools/utils/gpflow.py +335 -174
- sgptools/utils/metrics.py +375 -102
- sgptools/utils/misc.py +145 -111
- sgptools/utils/tsp.py +224 -84
- sgptools-2.0.0.dist-info/METADATA +216 -0
- sgptools-2.0.0.dist-info/RECORD +23 -0
- {sgptools-1.2.0.dist-info → sgptools-2.0.0.dist-info}/WHEEL +1 -1
- sgptools/models/__init__.py +0 -10
- sgptools/models/bo.py +0 -118
- sgptools/models/cma_es.py +0 -121
- sgptools/models/continuous_sgp.py +0 -68
- sgptools/models/core/__init__.py +0 -9
- sgptools/models/core/osgpr.py +0 -291
- sgptools/models/core/transformations.py +0 -434
- sgptools/models/greedy_mi.py +0 -115
- sgptools/models/greedy_sgp.py +0 -97
- sgptools-1.2.0.dist-info/METADATA +0 -39
- sgptools-1.2.0.dist-info/RECORD +0 -27
- {sgptools-1.2.0.dist-info → sgptools-2.0.0.dist-info/licenses}/LICENSE.txt +0 -0
- {sgptools-1.2.0.dist-info → sgptools-2.0.0.dist-info}/top_level.txt +0 -0
@@ -1,434 +0,0 @@
|
|
1
|
-
# Copyright 2024 The SGP-Tools Contributors. All Rights Reserved.
|
2
|
-
#
|
3
|
-
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
-
# you may not use this file except in compliance with the License.
|
5
|
-
# You may obtain a copy of the License at
|
6
|
-
#
|
7
|
-
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
-
#
|
9
|
-
# Unless required by applicable law or agreed to in writing, software
|
10
|
-
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
-
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
-
# See the License for the specific language governing permissions and
|
13
|
-
# limitations under the License.
|
14
|
-
|
15
|
-
"""Provides transforms to model complex sensor field of views and handle informative path planning
|
16
|
-
"""
|
17
|
-
|
18
|
-
import tensorflow as tf
|
19
|
-
import numpy as np
|
20
|
-
|
21
|
-
|
22
|
-
class Transform:
|
23
|
-
"""Base class for transformations of the inducing points, including expansion and aggregation transforms.
|
24
|
-
|
25
|
-
Refer to the following papers for more details:
|
26
|
-
- Efficient Sensor Placement from Regression with Sparse Gaussian Processes in Continuous and Discrete Spaces [Jakkala and Akella, 2023]
|
27
|
-
- Multi-Robot Informative Path Planning from Regression with Sparse Gaussian Processes [Jakkala and Akella, 2024]
|
28
|
-
|
29
|
-
Args:
|
30
|
-
aggregation_size (int): Number of consecutive inducing points to aggregate
|
31
|
-
constraint_weight (float): Weight term that controls the importance of the
|
32
|
-
constraint terms in the SGP's optimization objective
|
33
|
-
"""
|
34
|
-
def __init__(self,
|
35
|
-
aggregation_size=None,
|
36
|
-
constraint_weight=1.0,
|
37
|
-
**kwargs):
|
38
|
-
self.aggregation_size = aggregation_size
|
39
|
-
self.constraint_weight = constraint_weight
|
40
|
-
|
41
|
-
def expand(self, Xu):
|
42
|
-
"""Applies the expansion transform to the inducing points
|
43
|
-
|
44
|
-
Args:
|
45
|
-
Xu (ndarray): Expansion transformed inducing points
|
46
|
-
"""
|
47
|
-
return Xu
|
48
|
-
|
49
|
-
def aggregate(self, k):
|
50
|
-
"""Applies the aggregation transform to kernel matrices
|
51
|
-
|
52
|
-
Args:
|
53
|
-
k (tensor): (mp, mp)/(mp, n); Kernel matrix.
|
54
|
-
`m` is the number of inducing points,
|
55
|
-
`p` is the number of points each inducing point is mapped,
|
56
|
-
`n` is the number of training data points.
|
57
|
-
|
58
|
-
Returns:
|
59
|
-
k (tensor): (m, m)/(m, n); Aggregated kernel matrix
|
60
|
-
"""
|
61
|
-
if self.aggregation_size is None:
|
62
|
-
return k
|
63
|
-
|
64
|
-
if k.shape[0] == k.shape[1]:
|
65
|
-
# Handle Kuu which is a square matrix
|
66
|
-
k = tf.expand_dims(tf.expand_dims(k, axis=0), axis=-1)
|
67
|
-
k = tf.nn.avg_pool(k,
|
68
|
-
ksize=[1, self.aggregation_size, self.aggregation_size, 1],
|
69
|
-
strides=[1, self.aggregation_size, self.aggregation_size, 1],
|
70
|
-
padding='VALID')
|
71
|
-
k = tf.squeeze(k, axis=[0, -1])
|
72
|
-
else:
|
73
|
-
# Handle Kuf which is a rectangular matrix
|
74
|
-
k = tf.expand_dims(k, axis=0)
|
75
|
-
k = tf.nn.avg_pool(k,
|
76
|
-
ksize=[1, self.aggregation_size, 1],
|
77
|
-
strides=[1, self.aggregation_size, 1],
|
78
|
-
padding='VALID')
|
79
|
-
k = tf.squeeze(k, axis=[0])
|
80
|
-
return k
|
81
|
-
|
82
|
-
def constraints(self, Xu):
|
83
|
-
"""Computes the constraint terms that are added to the SGP's optimization function
|
84
|
-
|
85
|
-
Args:
|
86
|
-
Xu (ndarray): Inducing points from which to compute the constraints
|
87
|
-
|
88
|
-
Returns:
|
89
|
-
c (float): constraint terms (eg., distance constraint)
|
90
|
-
"""
|
91
|
-
return 0.
|
92
|
-
|
93
|
-
|
94
|
-
class IPPTransform(Transform):
|
95
|
-
"""Transform to model IPP problems
|
96
|
-
|
97
|
-
Usage details:
|
98
|
-
* For point sensing, set `sampling_rate = 2`
|
99
|
-
* For continuous sensing, set `sampling_rate > 2` (account for the information along the path)
|
100
|
-
* For continuous sensing with aggregation, set `sampling_rate > 2` and `aggregate_fov = True` (faster but solution quality is a bit diminished)
|
101
|
-
* If using a non-point FoV model with continuous sampling, only the FoV inducing points are aggregated
|
102
|
-
* For multi-robot case, set `num_robots > 1`
|
103
|
-
* For onlineIPP use `update_fixed` to freeze the visited waypoints
|
104
|
-
|
105
|
-
Args:
|
106
|
-
sampling_rate (int): Number of points to sample between each pair of inducing points
|
107
|
-
distance_budget (float): Distance budget for the path
|
108
|
-
num_robots (int): Number of robots
|
109
|
-
Xu_fixed (ndarray): (num_robots, num_visited, num_dim); Visited waypoints that don't need to be optimized
|
110
|
-
num_dim (int): Number of dimensions of the inducing points
|
111
|
-
sensor_model (Transform): Transform object to expand each inducing point to `p` points
|
112
|
-
approximating each sensor's FoV
|
113
|
-
aggregate_fov (bool): Used only when sampling_rate > 2, i.e., when using a continuous sensing model.
|
114
|
-
If `True`, covariances corresponding to interpolated inducing points along each edge
|
115
|
-
of the path are aggregated to reduce the matrix inversion cost.
|
116
|
-
"""
|
117
|
-
def __init__(self,
|
118
|
-
sampling_rate=2,
|
119
|
-
distance_budget=None,
|
120
|
-
num_robots=1,
|
121
|
-
Xu_fixed=None,
|
122
|
-
num_dim=2,
|
123
|
-
sensor_model=None,
|
124
|
-
aggregate_fov=False,
|
125
|
-
**kwargs):
|
126
|
-
super().__init__(**kwargs)
|
127
|
-
if sampling_rate < 2:
|
128
|
-
raise ValueError('Sampling rate must be greater than 2.')
|
129
|
-
|
130
|
-
self.sampling_rate = sampling_rate
|
131
|
-
self.distance_budget = distance_budget
|
132
|
-
self.num_robots = num_robots
|
133
|
-
self.num_dim = num_dim
|
134
|
-
self.sensor_model = sensor_model
|
135
|
-
|
136
|
-
# Set aggregation size to sampling rate if aggregate_fov is True
|
137
|
-
# and sampling rate is enabled (greater than 2)
|
138
|
-
if aggregate_fov:
|
139
|
-
if self.sensor_model is not None:
|
140
|
-
self.sensor_model.enable_aggregation()
|
141
|
-
elif sampling_rate > 2:
|
142
|
-
self.aggregation_size = sampling_rate
|
143
|
-
|
144
|
-
# Initilize variable to store visited waypoints for onlineIPP
|
145
|
-
if Xu_fixed is not None:
|
146
|
-
self.Xu_fixed = tf.Variable(Xu_fixed,
|
147
|
-
shape=tf.TensorShape(None),
|
148
|
-
trainable=False)
|
149
|
-
else:
|
150
|
-
self.Xu_fixed = None
|
151
|
-
|
152
|
-
def update_Xu_fixed(self, Xu_fixed):
|
153
|
-
"""Function to update the visited waypoints
|
154
|
-
|
155
|
-
Args:
|
156
|
-
Xu_fixed (ndarray): numpy array (num_robots, num_visited_waypoints, num_dim)
|
157
|
-
"""
|
158
|
-
self.num_fixed = Xu_fixed.shape[1]
|
159
|
-
if self.Xu_fixed is not None:
|
160
|
-
self.Xu_fixed.assign(Xu_fixed)
|
161
|
-
else:
|
162
|
-
self.Xu_fixed = tf.Variable(Xu_fixed,
|
163
|
-
shape=tf.TensorShape(None),
|
164
|
-
trainable=False)
|
165
|
-
|
166
|
-
def expand(self, Xu, expand_sensor_model=True):
|
167
|
-
"""Sample points between each pair of inducing points to form the path
|
168
|
-
|
169
|
-
Args:
|
170
|
-
Xu (ndarray): (num_robots x num_inducing, num_dim); Inducing points in the num_dim dimensional space
|
171
|
-
expand_sensor_model (bool): Only add the fixed inducing points without other sensor/path transforms,
|
172
|
-
used for online IPP
|
173
|
-
|
174
|
-
Returns:
|
175
|
-
Xu (ndarray): Expansion transformed inducing points
|
176
|
-
"""
|
177
|
-
# If using single-robot offline IPP with point sensing, return inducing points as is.
|
178
|
-
if self.sampling_rate == 2 and self.Xu_fixed is None and self.sensor_model is None:
|
179
|
-
return Xu
|
180
|
-
|
181
|
-
Xu = tf.reshape(Xu, (self.num_robots, -1, self.num_dim))
|
182
|
-
|
183
|
-
# If using online IPP, add visited waypoints that won't be optimized anymore
|
184
|
-
if self.Xu_fixed is not None:
|
185
|
-
Xu = tf.concat([self.Xu_fixed, Xu[:, self.num_fixed:]], axis=1)
|
186
|
-
|
187
|
-
if not expand_sensor_model:
|
188
|
-
return tf.reshape(Xu, (-1, self.num_dim))
|
189
|
-
|
190
|
-
# Interpolate additional inducing points between waypoints to approximate
|
191
|
-
# the continuous data sensing model
|
192
|
-
if self.sampling_rate > 2:
|
193
|
-
Xu = tf.linspace(Xu[:, :-1], Xu[:, 1:], self.sampling_rate)
|
194
|
-
Xu = tf.transpose(Xu, perm=[1, 2, 0, 3])
|
195
|
-
Xu = tf.reshape(Xu, (self.num_robots, -1, self.num_dim))
|
196
|
-
|
197
|
-
if self.sensor_model is not None:
|
198
|
-
Xu_ = []
|
199
|
-
for i in range(self.num_robots):
|
200
|
-
Xu_.append(self.sensor_model.expand(Xu[i]))
|
201
|
-
Xu = tf.concat(Xu_, axis=0)
|
202
|
-
return Xu
|
203
|
-
|
204
|
-
Xu = tf.reshape(Xu, (-1, self.num_dim))
|
205
|
-
return Xu
|
206
|
-
|
207
|
-
def aggregate(self, k):
|
208
|
-
"""Applies the aggregation transform to kernel matrices. Checks `sensor_model`
|
209
|
-
and uses the appropriate aggregation transform.
|
210
|
-
|
211
|
-
Args:
|
212
|
-
k (tensor): (mp, mp)/(mp, n); Kernel matrix.
|
213
|
-
`m` is the number of inducing points,
|
214
|
-
`p` is the number of points each inducing point is mapped,
|
215
|
-
`n` is the number of training data points.
|
216
|
-
|
217
|
-
Returns:
|
218
|
-
k (tensor): (m, m)/(m, n); Aggregated kernel matrix
|
219
|
-
"""
|
220
|
-
if self.sensor_model is not None:
|
221
|
-
return self.sensor_model.aggregate(k)
|
222
|
-
else:
|
223
|
-
return super().aggregate(k)
|
224
|
-
|
225
|
-
def constraints(self, Xu):
|
226
|
-
"""Computes the distance constraint term that is added to the SGP's optimization function.
|
227
|
-
Each robot can be assigned a different distance budget.
|
228
|
-
|
229
|
-
Args:
|
230
|
-
Xu (ndarray): Inducing points from which to compute the distance constraints
|
231
|
-
|
232
|
-
Returns:
|
233
|
-
loss (float): distance constraint term
|
234
|
-
"""
|
235
|
-
if self.distance_budget is None:
|
236
|
-
return 0.
|
237
|
-
else:
|
238
|
-
# Only do fixed points expansion transform
|
239
|
-
Xu = self.expand(Xu, expand_sensor_model=False)
|
240
|
-
dist = self.distance(Xu)-self.distance_budget
|
241
|
-
dist = tf.reduce_sum(tf.nn.relu(dist))
|
242
|
-
loss = -dist*self.constraint_weight
|
243
|
-
return loss
|
244
|
-
|
245
|
-
def distance(self, Xu):
|
246
|
-
"""Computes the distance incured by sequentially visiting the inducing points
|
247
|
-
Args:
|
248
|
-
Xu (ndarray): (m, num_dim); Inducing points from which to compute the path lengths
|
249
|
-
`m` is the number of inducing points
|
250
|
-
`num_dim` dimension of the data collection environment
|
251
|
-
Returns:
|
252
|
-
dist (float or tensor of floats): path length(s)
|
253
|
-
"""
|
254
|
-
Xu = tf.reshape(Xu, (self.num_robots, -1, self.num_dim))
|
255
|
-
if self.sensor_model is not None:
|
256
|
-
dists = []
|
257
|
-
for i in range(self.num_robots):
|
258
|
-
dists.append(self.sensor_model.distance(Xu[i]))
|
259
|
-
dists = tf.concat(dists, axis=0)
|
260
|
-
return dists
|
261
|
-
else:
|
262
|
-
# Assumes 2D waypoints by default
|
263
|
-
dist = tf.norm(Xu[:, 1:, :2] - Xu[:, :-1, :2], axis=-1)
|
264
|
-
dist = tf.reduce_sum(dist, axis=1)
|
265
|
-
return dist
|
266
|
-
|
267
|
-
|
268
|
-
class SquareTransform(Transform):
|
269
|
-
"""Non-point Transform to model a square FoV. Only works for single robot cases.
|
270
|
-
|
271
|
-
Args:
|
272
|
-
length (float): Length of the square FoV
|
273
|
-
num_side (int): Number of points along each side of the FoV
|
274
|
-
aggregate_fov (bool): If `True`, covariances corresponding to interpolated inducing points used to
|
275
|
-
approximate the sensor FoV are aggregated to reduce the matrix inversion cost
|
276
|
-
"""
|
277
|
-
def __init__(self, length, num_side, aggregate_fov=False, **kwargs):
|
278
|
-
super().__init__(**kwargs)
|
279
|
-
self.length = length
|
280
|
-
self.num_side = num_side
|
281
|
-
self.length_factor=length/(self.num_side)
|
282
|
-
self.num_length = int(length/self.length_factor)
|
283
|
-
|
284
|
-
if aggregate_fov:
|
285
|
-
self.enable_aggregation()
|
286
|
-
|
287
|
-
def enable_aggregation(self, size=None):
|
288
|
-
"""Enable FoV covariance aggregation, which reduces the covariance matrix inversion cost by reducing the
|
289
|
-
covariance matrix size.
|
290
|
-
|
291
|
-
Args:
|
292
|
-
size (int): If None, all the interpolated inducing points within the FoV are aggregated. Alternatively,
|
293
|
-
the number of inducing points to aggregate can be explicitly defined using this variable.
|
294
|
-
"""
|
295
|
-
if size is None:
|
296
|
-
self.aggregation_size = self.num_side**2
|
297
|
-
else:
|
298
|
-
self.aggregation_size = size
|
299
|
-
|
300
|
-
def expand(self, Xu):
|
301
|
-
"""Applies the expansion transformation to the inducing points
|
302
|
-
|
303
|
-
Args:
|
304
|
-
Xu (ndarray): (m, 3); Inducing points in the position and orientation space.
|
305
|
-
`m` is the number of inducing points,
|
306
|
-
`3` is the dimension of the space (x, y, angle in radians)
|
307
|
-
|
308
|
-
Returns:
|
309
|
-
Xu (ndarray): (mp, 2); Inducing points in input space.
|
310
|
-
`p` is the number of points each inducing point is mapped
|
311
|
-
to in order to form the FoV.
|
312
|
-
"""
|
313
|
-
x, y, theta = tf.split(Xu, num_or_size_splits=3, axis=1)
|
314
|
-
x = tf.reshape(x, [-1,])
|
315
|
-
y = tf.reshape(y, [-1,])
|
316
|
-
theta = tf.reshape(theta, [-1,])
|
317
|
-
|
318
|
-
points = []
|
319
|
-
for i in range(-int(np.floor((self.num_side)/2)), int(np.ceil((self.num_side)/2))):
|
320
|
-
points.append(tf.linspace([(x + (i * self.length_factor) * tf.cos(theta)) - self.length/2 * tf.cos(theta+np.pi/2),
|
321
|
-
(y + (i * self.length_factor) * tf.sin(theta)) - self.length/2 * tf.sin(theta+np.pi/2)],
|
322
|
-
[(x + (i * self.length_factor) * tf.cos(theta)) + self.length/2 * tf.cos(theta+np.pi/2),
|
323
|
-
(y + (i * self.length_factor) * tf.sin(theta)) + self.length/2 * tf.sin(theta+np.pi/2)],
|
324
|
-
self.num_side, axis=1))
|
325
|
-
xy = tf.concat(points, axis=1)
|
326
|
-
xy = tf.transpose(xy, [2, 1, 0])
|
327
|
-
xy = tf.reshape(xy, (-1, 2))
|
328
|
-
return xy
|
329
|
-
|
330
|
-
def distance(self, Xu):
|
331
|
-
"""Computes the distance incured by sequentially visiting the inducing points
|
332
|
-
Args:
|
333
|
-
Xu (ndarray): (m, 3); Inducing points from which to compute the path lengths.
|
334
|
-
`m` is the number of inducing points.
|
335
|
-
|
336
|
-
Returns:
|
337
|
-
dist (float): path lengths
|
338
|
-
"""
|
339
|
-
Xu = tf.reshape(Xu, (-1, 3))[:, :2]
|
340
|
-
dist = tf.norm(Xu[1:] - Xu[:-1], axis=-1)
|
341
|
-
dist = tf.reduce_sum(dist, axis=0)
|
342
|
-
return dist
|
343
|
-
|
344
|
-
|
345
|
-
class SquareHeightTransform(Transform):
|
346
|
-
"""Non-point Transform to model a height-dependent square FoV
|
347
|
-
|
348
|
-
Args:
|
349
|
-
num_side (int): Number of points along each side of the FoV
|
350
|
-
aggregate_fov (bool): If `True`, covariances corresponding to interpolated inducing points used to
|
351
|
-
approximate the sensor FoV are aggregated to reduce the matrix inversion cost
|
352
|
-
"""
|
353
|
-
def __init__(self, num_side, aggregate_fov=False, **kwargs):
|
354
|
-
super().__init__(**kwargs)
|
355
|
-
self.num_side = num_side
|
356
|
-
|
357
|
-
if aggregate_fov:
|
358
|
-
self.enable_aggregation()
|
359
|
-
|
360
|
-
def enable_aggregation(self, size=None):
|
361
|
-
"""Enable FoV covariance aggregation, which reduces the covariance matrix inversion cost by reducing the
|
362
|
-
covariance matrix size.
|
363
|
-
|
364
|
-
Args:
|
365
|
-
size (int): If None, all the interpolated inducing points within the FoV are aggregated. Alternatively,
|
366
|
-
the number of inducing points to aggregate can be explicitly defined using this variable.
|
367
|
-
"""
|
368
|
-
if size is None:
|
369
|
-
self.aggregation_size = self.num_side**2
|
370
|
-
else:
|
371
|
-
self.aggregation_size = size
|
372
|
-
|
373
|
-
def expand(self, Xu):
|
374
|
-
"""
|
375
|
-
Applies the expansion transform to the inducing points
|
376
|
-
|
377
|
-
Args:
|
378
|
-
Xu (ndarray): (m, 3); Inducing points in the 3D position space.
|
379
|
-
`m` is the number of inducing points,
|
380
|
-
`3` is the dimension of the space (x, y, z)
|
381
|
-
|
382
|
-
Returns:
|
383
|
-
Xu (ndarray): (mp, 2); Inducing points in input space.
|
384
|
-
`p` is the number of points each inducing point is mapped
|
385
|
-
to in order to form the FoV.
|
386
|
-
"""
|
387
|
-
x, y, h = tf.split(Xu, num_or_size_splits=3, axis=1)
|
388
|
-
x = tf.reshape(x, [-1,])
|
389
|
-
y = tf.reshape(y, [-1,])
|
390
|
-
h = tf.reshape(h, [-1,])
|
391
|
-
|
392
|
-
delta = h / (self.num_side - 1)
|
393
|
-
|
394
|
-
pts = []
|
395
|
-
for i in range(self.num_side):
|
396
|
-
pts.append(tf.linspace([x - h/2, y - (h/2) + (delta * i)],
|
397
|
-
[x + h/2, y - (h/2) + (delta * i)],
|
398
|
-
self.num_side,
|
399
|
-
axis=1))
|
400
|
-
xy = tf.concat(pts, axis=1)
|
401
|
-
xy = tf.transpose(xy, [2, 1, 0])
|
402
|
-
xy = tf.reshape(xy, [-1, 2])
|
403
|
-
xy = self._reshape(xy, tf.shape(Xu)[0])
|
404
|
-
return xy
|
405
|
-
|
406
|
-
def _reshape(self, X, num_inducing):
|
407
|
-
"""Reorder the inducing points to be in the correct order for aggregation with square height FoV
|
408
|
-
|
409
|
-
Args:
|
410
|
-
X (ndarray): (mp, 2); Inducing points in input space. `p` is the number of points each
|
411
|
-
inducing point is mapped to in order to form the FoV.
|
412
|
-
|
413
|
-
Returns:
|
414
|
-
Xu (ndarray): (mp, 2); Reorder inducing points
|
415
|
-
"""
|
416
|
-
X = tf.reshape(X, (num_inducing, -1, self.num_side, self.num_side, 2))
|
417
|
-
X = tf.transpose(X, (0, 2, 1, 3, 4))
|
418
|
-
X = tf.reshape(X, (-1, 2))
|
419
|
-
return X
|
420
|
-
|
421
|
-
def distance(self, Xu):
|
422
|
-
"""Computes the distance incured by sequentially visiting the inducing points
|
423
|
-
Args:
|
424
|
-
Xu (ndarray): (m, 3); Inducing points from which to compute the path lengths.
|
425
|
-
`m` is the number of inducing points.
|
426
|
-
|
427
|
-
Returns:
|
428
|
-
dist (float): path lengths
|
429
|
-
"""
|
430
|
-
Xu = tf.reshape(Xu, (-1, 3))
|
431
|
-
dist = tf.norm(Xu[1:] - Xu[:-1], axis=-1)
|
432
|
-
dist = tf.reduce_sum(dist, axis=0)
|
433
|
-
return dist
|
434
|
-
|
sgptools/models/greedy_mi.py
DELETED
@@ -1,115 +0,0 @@
|
|
1
|
-
# Copyright 2024 The SGP-Tools Contributors. All Rights Reserved.
|
2
|
-
#
|
3
|
-
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
-
# you may not use this file except in compliance with the License.
|
5
|
-
# You may obtain a copy of the License at
|
6
|
-
#
|
7
|
-
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
-
#
|
9
|
-
# Unless required by applicable law or agreed to in writing, software
|
10
|
-
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
-
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
-
# See the License for the specific language governing permissions and
|
13
|
-
# limitations under the License.
|
14
|
-
|
15
|
-
from apricot import CustomSelection
|
16
|
-
from gpflow.models.gpr import GPR
|
17
|
-
import numpy as np
|
18
|
-
|
19
|
-
|
20
|
-
class GreedyMI:
|
21
|
-
"""Helper class to compute mutual information using a Gaussian process for a given set of sensor locations.
|
22
|
-
Used by `get_greedy_mi_sol` function to compute the solution sensor placements using the Greedy-MI method.
|
23
|
-
|
24
|
-
Refer to the following papers for more details:
|
25
|
-
- Near-Optimal Sensor Placements in Gaussian Processes: Theory, Efficient Algorithms and Empirical Studies [Krause et al., 2008]
|
26
|
-
- Data-driven learning and planning for environmental sampling [Ma et al., 2018]
|
27
|
-
|
28
|
-
Args:
|
29
|
-
S (ndarray): (n, d); Candidate sensor placement locations
|
30
|
-
V (ndarray): (n, d); Locations in the environment used to approximate the monitoring regions
|
31
|
-
noise_variance (float): data variance
|
32
|
-
kernel (gpflow.kernels.Kernel): gpflow kernel function
|
33
|
-
transform (Transform): Transform object
|
34
|
-
"""
|
35
|
-
def __init__(self, S, V, noise_variance, kernel,
|
36
|
-
transform=None):
|
37
|
-
self.S = S
|
38
|
-
self.V = V
|
39
|
-
self.kernel = kernel
|
40
|
-
self.input_dim = S.shape[1]
|
41
|
-
self.noise_variance = noise_variance
|
42
|
-
self.transform = transform
|
43
|
-
|
44
|
-
def mutual_info(self, x):
|
45
|
-
"""Computes mutual information using the points `x`
|
46
|
-
|
47
|
-
Args:
|
48
|
-
x (ndarray): (n); Indices of the solution placement locations
|
49
|
-
|
50
|
-
Returns:
|
51
|
-
MI (float): Mutual information between the placement x and candidate locations
|
52
|
-
"""
|
53
|
-
x = np.array(x).reshape(-1).astype(int)
|
54
|
-
A = self.S[x[:-1]].reshape(-1, self.input_dim)
|
55
|
-
y = self.S[x[-1]].reshape(-1, self.input_dim)
|
56
|
-
|
57
|
-
if len(A) == 0:
|
58
|
-
sigma_a = 1.0
|
59
|
-
else:
|
60
|
-
if self.transform is not None:
|
61
|
-
A = self.transform.expand(A)
|
62
|
-
a_gp = GPR(data=(A, np.zeros((len(A), 1))),
|
63
|
-
kernel=self.kernel,
|
64
|
-
noise_variance=self.noise_variance)
|
65
|
-
_, sigma_a = a_gp.predict_f(y)
|
66
|
-
|
67
|
-
# Remove locations in A∪y from V to build A bar (Refer to Krause et al., 2008)
|
68
|
-
V_ = self.V.copy()
|
69
|
-
V_rows = V_.view([('', V_.dtype)] * V_.shape[1])
|
70
|
-
|
71
|
-
if self.transform is not None:
|
72
|
-
solution = self.S[x].reshape(-1, self.input_dim)
|
73
|
-
A_ = self.transform.expand(solution)
|
74
|
-
else:
|
75
|
-
A_ = self.S[x]
|
76
|
-
A_rows = A_.view([('', V_.dtype)] * A_.shape[1])
|
77
|
-
|
78
|
-
V_ = np.setdiff1d(V_rows, A_rows).view(V_.dtype).reshape(-1, V_.shape[1])
|
79
|
-
|
80
|
-
self.v_gp = GPR(data=(V_, np.zeros((len(V_), 1))),
|
81
|
-
kernel=self.kernel,
|
82
|
-
noise_variance=self.noise_variance)
|
83
|
-
_, sigma_v = self.v_gp.predict_f(y)
|
84
|
-
|
85
|
-
return (sigma_a/sigma_v).numpy().squeeze()
|
86
|
-
|
87
|
-
|
88
|
-
def get_greedy_mi_sol(num_sensors, candidates, X_train, noise_variance, kernel,
|
89
|
-
transform=None, optimizer='naive'):
|
90
|
-
"""Get sensor placement solutions using the GP-based mutual information approach (submodular objective function).
|
91
|
-
Uses a greedy algorithm to select sensor placements from a given discrete set of candidates locations.
|
92
|
-
|
93
|
-
Refer to the following papers for more details:
|
94
|
-
- Near-Optimal Sensor Placements in Gaussian Processes: Theory, Efficient Algorithms and Empirical Studies [Krause et al., 2008]
|
95
|
-
- Data-driven learning and planning for environmental sampling [Ma et al., 2018]
|
96
|
-
|
97
|
-
Args:
|
98
|
-
num_sensors (int): Number of sensor locations to optimize
|
99
|
-
candidates (ndarray): (n, d); Candidate sensor placement locations
|
100
|
-
X_train (ndarray): (n, d); Locations in the environment used to approximate the monitoring regions
|
101
|
-
noise_variance (float): data variance
|
102
|
-
kernel (gpflow.kernels.Kernel): gpflow kernel function
|
103
|
-
transform (Transform): Transform object
|
104
|
-
optimizer (str): Name of an optimizer available in the apricot library
|
105
|
-
|
106
|
-
Returns:
|
107
|
-
Xu (ndarray): (m, d); Solution sensor placement locations
|
108
|
-
"""
|
109
|
-
mi_model = GreedyMI(candidates, X_train, noise_variance, kernel, transform)
|
110
|
-
model = CustomSelection(num_sensors,
|
111
|
-
mi_model.mutual_info,
|
112
|
-
optimizer=optimizer,
|
113
|
-
verbose=False)
|
114
|
-
sol = model.fit_transform(np.arange(len(candidates)).reshape(-1, 1))
|
115
|
-
return candidates[sol.reshape(-1)]
|
sgptools/models/greedy_sgp.py
DELETED
@@ -1,97 +0,0 @@
|
|
1
|
-
# Copyright 2024 The SGP-Tools Contributors. All Rights Reserved.
|
2
|
-
#
|
3
|
-
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
-
# you may not use this file except in compliance with the License.
|
5
|
-
# You may obtain a copy of the License at
|
6
|
-
#
|
7
|
-
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
-
#
|
9
|
-
# Unless required by applicable law or agreed to in writing, software
|
10
|
-
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
-
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
-
# See the License for the specific language governing permissions and
|
13
|
-
# limitations under the License.
|
14
|
-
|
15
|
-
from .core.augmented_sgpr import AugmentedSGPR
|
16
|
-
from apricot import CustomSelection
|
17
|
-
import numpy as np
|
18
|
-
|
19
|
-
|
20
|
-
class GreedySGP:
|
21
|
-
"""Helper class to compute SGP's ELBO/optimization bound for a given set of sensor locations.
|
22
|
-
Used by `get_greedy_sgp_sol` function to compute the solution sensor placements using the Greedy-SGP method.
|
23
|
-
|
24
|
-
Refer to the following papers for more details:
|
25
|
-
- Efficient Sensor Placement from Regression with Sparse Gaussian Processes in Continuous and Discrete Spaces [[Jakkala and Akella, 2023](https://www.itskalvik.com/publication/sgp-sp/)]
|
26
|
-
|
27
|
-
Args:
|
28
|
-
num_inducing (int): Number of inducing points
|
29
|
-
S (ndarray): (n, d); Candidate sensor placement locations
|
30
|
-
V (ndarray): (n, d); Locations in the environment used to approximate the monitoring regions
|
31
|
-
noise_variance (float): Data noise variance
|
32
|
-
kernel (gpflow.kernels.Kernel): gpflow kernel function
|
33
|
-
transform (Transform): Transform object
|
34
|
-
"""
|
35
|
-
def __init__(self, num_inducing, S, V, noise_variance, kernel,
|
36
|
-
transform=None):
|
37
|
-
self.sgp = AugmentedSGPR((V, np.zeros((len(V), 1))),
|
38
|
-
noise_variance=noise_variance,
|
39
|
-
kernel=kernel,
|
40
|
-
inducing_variable=S[:num_inducing],
|
41
|
-
transform=transform)
|
42
|
-
self.locs = S
|
43
|
-
self.num_inducing = num_inducing
|
44
|
-
self.inducing_dim = S.shape[1]
|
45
|
-
|
46
|
-
def bound(self, x):
|
47
|
-
"""Computes the SGP's optimization bound using the inducing points `x`
|
48
|
-
|
49
|
-
Args:
|
50
|
-
x (ndarray): (n); Indices of the solution placement locations
|
51
|
-
|
52
|
-
Returns:
|
53
|
-
elbo (float): Evidence lower bound/SGP's optimization bound value
|
54
|
-
"""
|
55
|
-
x = np.array(x).reshape(-1).astype(int)
|
56
|
-
Xu = np.ones((self.num_inducing, self.inducing_dim), dtype=np.float32)
|
57
|
-
|
58
|
-
# Initialize all inducing points at the first solution placement location.
|
59
|
-
# Ensures that the number of inducing points is always fixed and no additional
|
60
|
-
# information is passed to the SGP
|
61
|
-
Xu *= self.locs[x][0]
|
62
|
-
|
63
|
-
# Copy all given solution placements to the inducing points set
|
64
|
-
Xu[-len(x):] = self.locs[x]
|
65
|
-
|
66
|
-
# Update the SGP inducing points
|
67
|
-
self.sgp.inducing_variable.Z.assign(Xu)
|
68
|
-
return self.sgp.elbo().numpy() # return the ELBO
|
69
|
-
|
70
|
-
|
71
|
-
def get_greedy_sgp_sol(num_sensors, candidates, X_train, noise_variance, kernel,
|
72
|
-
transform=None):
|
73
|
-
"""Get sensor placement solutions using the Greedy-SGP method. Uses a greedy algorithm to
|
74
|
-
select sensor placements from a given discrete set of candidates locations.
|
75
|
-
|
76
|
-
Refer to the following papers for more details:
|
77
|
-
- Efficient Sensor Placement from Regression with Sparse Gaussian Processes in Continuous and Discrete Spaces [[Jakkala and Akella, 2023](https://www.itskalvik.com/publication/sgp-sp/)]
|
78
|
-
|
79
|
-
Args:
|
80
|
-
num_sensors (int): Number of sensor locations to optimize
|
81
|
-
candidates (ndarray): (n, d); Candidate sensor placement locations
|
82
|
-
X_train (ndarray): (n, d); Locations in the environment used to approximate the monitoring regions
|
83
|
-
noise_variance (float): data variance
|
84
|
-
kernel (gpflow.kernels.Kernel): gpflow kernel function
|
85
|
-
transform (Transform): Transform object
|
86
|
-
|
87
|
-
Returns:
|
88
|
-
Xu (ndarray): (m, d); Solution sensor placement locations
|
89
|
-
"""
|
90
|
-
sgp_model = GreedySGP(num_sensors, candidates, X_train,
|
91
|
-
noise_variance, kernel, transform=transform)
|
92
|
-
model = CustomSelection(num_sensors,
|
93
|
-
sgp_model.bound,
|
94
|
-
optimizer='naive',
|
95
|
-
verbose=False)
|
96
|
-
sol = model.fit_transform(np.arange(len(candidates)).reshape(-1, 1))
|
97
|
-
return candidates[sol.reshape(-1)]
|
@@ -1,39 +0,0 @@
|
|
1
|
-
Metadata-Version: 2.2
|
2
|
-
Name: sgptools
|
3
|
-
Version: 1.2.0
|
4
|
-
Summary: Software Suite for Sensor Placement and Informative Path Planning
|
5
|
-
Home-page: https://www.itskalvik.com/sgp-tools
|
6
|
-
Author: Kalvik
|
7
|
-
Author-email: itskalvik@gmail.com
|
8
|
-
License: Apache-2.0
|
9
|
-
Requires-Python: >=3.6
|
10
|
-
License-File: LICENSE.txt
|
11
|
-
Requires-Dist: apricot-select
|
12
|
-
Requires-Dist: matplotlib
|
13
|
-
Requires-Dist: pandas
|
14
|
-
Requires-Dist: scikit-learn
|
15
|
-
Requires-Dist: scipy
|
16
|
-
Requires-Dist: numpy<2.0.0
|
17
|
-
Requires-Dist: ortools
|
18
|
-
Requires-Dist: scikit-image
|
19
|
-
Requires-Dist: shapely
|
20
|
-
Requires-Dist: cma
|
21
|
-
Requires-Dist: bayesian-optimization
|
22
|
-
Requires-Dist: hkb_diamondsquare
|
23
|
-
Requires-Dist: tensorflow-probability[tf]>=0.21.0
|
24
|
-
Requires-Dist: tensorflow>=2.13.0; platform_machine != "arm64"
|
25
|
-
Requires-Dist: tensorflow-aarch64>=2.13.0; platform_machine == "arm64"
|
26
|
-
Requires-Dist: tensorflow-macos>=2.13.0; platform_system == "Darwin" and platform_machine == "arm64"
|
27
|
-
Requires-Dist: typing_extensions
|
28
|
-
Requires-Dist: gpflow>=2.7.0
|
29
|
-
Requires-Dist: pillow
|
30
|
-
Dynamic: author
|
31
|
-
Dynamic: author-email
|
32
|
-
Dynamic: description
|
33
|
-
Dynamic: home-page
|
34
|
-
Dynamic: license
|
35
|
-
Dynamic: requires-dist
|
36
|
-
Dynamic: requires-python
|
37
|
-
Dynamic: summary
|
38
|
-
|
39
|
-
Software Suite for Sensor Placement and Informative Path Planning
|