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
@@ -0,0 +1,699 @@
|
|
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
|
+
"""Provides transforms to model complex sensor field of views and handle informative path planning
|
15
|
+
"""
|
16
|
+
|
17
|
+
import tensorflow as tf
|
18
|
+
from gpflow.config import default_float
|
19
|
+
|
20
|
+
float_type = default_float()
|
21
|
+
|
22
|
+
import numpy as np
|
23
|
+
from typing import Optional, Union, Any, Tuple, List # Import necessary types for type hinting
|
24
|
+
|
25
|
+
|
26
|
+
class Transform:
|
27
|
+
"""
|
28
|
+
Base class for transformations applied to inducing points in sparse Gaussian process models.
|
29
|
+
This class defines common interfaces for expanding inducing points (e.g., to model
|
30
|
+
sensor fields of view or continuous paths) and aggregating kernel matrices.
|
31
|
+
It also provides a base for adding constraint terms to the optimization objective.
|
32
|
+
|
33
|
+
Refer to the following papers for more details:
|
34
|
+
- Efficient Sensor Placement from Regression with Sparse Gaussian Processes in Continuous and Discrete Spaces [Jakkala and Akella, 2023]
|
35
|
+
- Multi-Robot Informative Path Planning from Regression with Sparse Gaussian Processes [Jakkala and Akella, 2024]
|
36
|
+
"""
|
37
|
+
|
38
|
+
def __init__(self,
|
39
|
+
aggregation_size: Optional[int] = None,
|
40
|
+
constraint_weight: float = 1.0,
|
41
|
+
**kwargs: Any):
|
42
|
+
"""
|
43
|
+
Initializes the base Transform class.
|
44
|
+
|
45
|
+
Args:
|
46
|
+
aggregation_size (Optional[int]): Number of consecutive inducing points to aggregate
|
47
|
+
when transforming kernel matrices. If None, no aggregation
|
48
|
+
is performed. Defaults to None.
|
49
|
+
constraint_weight (float): A scalar weight that controls the importance of the
|
50
|
+
constraint terms in the SGP's optimization objective function.
|
51
|
+
A higher weight means stronger enforcement of constraints.
|
52
|
+
Defaults to 1.0.
|
53
|
+
**kwargs (Any): Additional keyword arguments to be passed to the constructor.
|
54
|
+
"""
|
55
|
+
self.aggregation_size = aggregation_size
|
56
|
+
self.constraint_weight = constraint_weight
|
57
|
+
|
58
|
+
def expand(
|
59
|
+
self, Xu: Union[np.ndarray,
|
60
|
+
tf.Tensor]) -> Union[np.ndarray, tf.Tensor]:
|
61
|
+
"""
|
62
|
+
Applies an expansion transform to the inducing points.
|
63
|
+
In this base class, it simply returns the input inducing points unchanged.
|
64
|
+
Subclasses should override this method to implement specific expansion logic.
|
65
|
+
|
66
|
+
Args:
|
67
|
+
Xu (Union[np.ndarray, tf.Tensor]): The input inducing points.
|
68
|
+
Shape: (m, d) where `m` is the number of inducing points
|
69
|
+
and `d` is their dimensionality.
|
70
|
+
|
71
|
+
Returns:
|
72
|
+
Union[np.ndarray, tf.Tensor]: The expanded inducing points.
|
73
|
+
"""
|
74
|
+
return Xu
|
75
|
+
|
76
|
+
def aggregate(self, k: tf.Tensor) -> tf.Tensor:
|
77
|
+
"""
|
78
|
+
Applies an aggregation transform to kernel matrices. This is typically used
|
79
|
+
to reduce the size of kernel matrices after expansion, by averaging or summing
|
80
|
+
over groups of expanded points. This can reduce computational cost for
|
81
|
+
matrix inversions (e.g., in `Kuu`).
|
82
|
+
|
83
|
+
Args:
|
84
|
+
k (tf.Tensor): The input kernel matrix.
|
85
|
+
Can be (mp, mp) for `Kuu` (square matrix for inducing points
|
86
|
+
against themselves), or (mp, n) for `Kuf` (rectangular matrix
|
87
|
+
for inducing points against training data).
|
88
|
+
`m` is the number of original inducing points.
|
89
|
+
`p` is the number of points each inducing point is mapped to
|
90
|
+
by the expansion transform.
|
91
|
+
`n` is the number of training data points.
|
92
|
+
|
93
|
+
Returns:
|
94
|
+
tf.Tensor: The aggregated kernel matrix.
|
95
|
+
Shape: (m, m) if input was (mp, mp), or (m, n) if input was (mp, n).
|
96
|
+
"""
|
97
|
+
if self.aggregation_size is None:
|
98
|
+
return k
|
99
|
+
|
100
|
+
# The aggregation logic assumes `k` has leading dimensions that are
|
101
|
+
# multiples of `self.aggregation_size`.
|
102
|
+
if k.shape[0] == k.shape[
|
103
|
+
1]: # This is K(U, U) or K(U_expanded, U_expanded)
|
104
|
+
# Reshape for `tf.nn.avg_pool`: [batch, height, width, channels]
|
105
|
+
# Here, we treat the matrix as a 1-channel image.
|
106
|
+
k_reshaped = tf.expand_dims(tf.expand_dims(k, axis=0),
|
107
|
+
axis=-1) # (1, mp, mp, 1)
|
108
|
+
|
109
|
+
# Apply average pooling. `ksize` and `strides` define the window size
|
110
|
+
# and movement for aggregation. This effectively averages blocks.
|
111
|
+
k_aggregated = tf.nn.avg_pool(
|
112
|
+
k_reshaped,
|
113
|
+
ksize=[1, self.aggregation_size, self.aggregation_size, 1],
|
114
|
+
strides=[1, self.aggregation_size, self.aggregation_size, 1],
|
115
|
+
padding='VALID')
|
116
|
+
# Squeeze back to (m, m)
|
117
|
+
k = tf.squeeze(k_aggregated, axis=[0, -1])
|
118
|
+
else: # This is K(U, F) or K(U_expanded, F)
|
119
|
+
# Reshape for `tf.nn.avg_pool`: (1, mp, n) -> (1, mp, n, 1) if channels are 1
|
120
|
+
# Or (1, mp, n) directly for 1D pooling if `n` is treated as a feature dimension.
|
121
|
+
# Here, we're pooling along the inducing point dimension.
|
122
|
+
k_reshaped = tf.expand_dims(k, axis=0) # (1, mp, n)
|
123
|
+
k_aggregated = tf.nn.avg_pool(
|
124
|
+
k_reshaped,
|
125
|
+
ksize=[1, self.aggregation_size, 1], # Pool along height (mp)
|
126
|
+
strides=[1, self.aggregation_size, 1],
|
127
|
+
padding='VALID')
|
128
|
+
# Squeeze back to (m, n)
|
129
|
+
k = tf.squeeze(k_aggregated, axis=[0])
|
130
|
+
return k
|
131
|
+
|
132
|
+
def constraints(self, Xu: tf.Tensor) -> tf.Tensor:
|
133
|
+
"""
|
134
|
+
Computes constraint terms that are added to the SGP's optimization function (ELBO).
|
135
|
+
This base implementation returns a zero tensor, implying no constraints by default.
|
136
|
+
Subclasses should override this to implement specific constraints (e.g., path length budget).
|
137
|
+
|
138
|
+
Args:
|
139
|
+
Xu (tf.Tensor): The inducing points, from which to compute the constraints.
|
140
|
+
Shape: (m, d).
|
141
|
+
|
142
|
+
Returns:
|
143
|
+
tf.Tensor: A scalar tensor representing the constraint penalty. Defaults to 0.0.
|
144
|
+
"""
|
145
|
+
return tf.constant(0.0, dtype=float_type)
|
146
|
+
|
147
|
+
|
148
|
+
class IPPTransform(Transform):
|
149
|
+
"""
|
150
|
+
Transform to model Informative Path Planning (IPP) problems for single or multiple robots.
|
151
|
+
It handles continuous sensing, non-point fields of view (FoV), and distance constraints.
|
152
|
+
|
153
|
+
* For point sensing (discrete waypoints), set `sampling_rate = 2`.
|
154
|
+
* For continuous sensing along paths, set `sampling_rate > 2` to interpolate
|
155
|
+
additional points between waypoints for information gathering.
|
156
|
+
* For continuous sensing with aggregation for computational efficiency,
|
157
|
+
set `sampling_rate > 2` and `aggregate_fov = True`. This averages
|
158
|
+
covariances from interpolated points, potentially diminishing solution quality slightly.
|
159
|
+
* If using a non-point FoV model (e.g., `SquareTransform`) with continuous sampling,
|
160
|
+
only the FoV inducing points are aggregated.
|
161
|
+
* For multi-robot scenarios, set `num_robots > 1`.
|
162
|
+
* For online IPP where some visited waypoints are fixed, use `update_Xu_fixed`
|
163
|
+
to freeze these waypoints from further optimization.
|
164
|
+
"""
|
165
|
+
|
166
|
+
def __init__(self,
|
167
|
+
sampling_rate: int = 2,
|
168
|
+
distance_budget: Optional[float] = None,
|
169
|
+
num_robots: int = 1,
|
170
|
+
Xu_fixed: Optional[np.ndarray] = None,
|
171
|
+
num_dim: int = 2,
|
172
|
+
sensor_model: Optional[Transform] = None,
|
173
|
+
aggregate_fov: bool = False,
|
174
|
+
**kwargs: Any):
|
175
|
+
"""
|
176
|
+
Initializes the IPPTransform.
|
177
|
+
|
178
|
+
Args:
|
179
|
+
sampling_rate (int): Number of points to sample along each segment between two
|
180
|
+
consecutive inducing points. `sampling_rate=2` implies
|
181
|
+
only the two endpoints are used (point sensing).
|
182
|
+
`sampling_rate > 2` implies continuous sensing via interpolation.
|
183
|
+
Must be $\ge 2$. Defaults to 2.
|
184
|
+
distance_budget (Optional[float]): The maximum allowable total path length for each robot.
|
185
|
+
If None, no distance constraint is applied. Defaults to None.
|
186
|
+
num_robots (int): The number of robots or agents involved in the IPP problem. Defaults to 1.
|
187
|
+
Xu_fixed (Optional[np.ndarray]): (num_robots, num_visited, num_dim);
|
188
|
+
An array of pre-defined, fixed waypoints that should
|
189
|
+
not be optimized (e.g., already visited locations in online IPP).
|
190
|
+
If None, all waypoints are optimizable. Defaults to None.
|
191
|
+
num_dim (int): The dimensionality of the inducing points (e.g., 2 for (x,y), 3 for (x,y,angle)).
|
192
|
+
Defaults to 2.
|
193
|
+
sensor_model (Optional[Transform]): A `Transform` object that defines a non-point
|
194
|
+
Field of View (FoV) for the sensor (e.g., `SquareTransform`).
|
195
|
+
If None, a point sensor model is assumed. Defaults to None.
|
196
|
+
aggregate_fov (bool): If True, and `sampling_rate > 2` (continuous sensing is enabled),
|
197
|
+
or if `sensor_model` is provided, aggregation will be enabled.
|
198
|
+
This reduces computation by averaging expanded points' covariances.
|
199
|
+
Defaults to False.
|
200
|
+
**kwargs (Any): Additional keyword arguments passed to the base `Transform` constructor.
|
201
|
+
|
202
|
+
Raises:
|
203
|
+
ValueError: If `sampling_rate` is less than 2.
|
204
|
+
|
205
|
+
Usage:
|
206
|
+
```python
|
207
|
+
# Single robot, point sensing
|
208
|
+
transform_point = IPPTransform(num_robots=1, num_dim=2, sampling_rate=2)
|
209
|
+
|
210
|
+
# Single robot, continuous sensing
|
211
|
+
transform_continuous = IPPTransform(num_robots=1, num_dim=2, sampling_rate=10)
|
212
|
+
|
213
|
+
# Multi-robot, continuous sensing with distance budget
|
214
|
+
transform_multi_budget = IPPTransform(num_robots=2, num_dim=2, sampling_rate=5, distance_budget=50.0, constraint_weight=100.0)
|
215
|
+
```
|
216
|
+
"""
|
217
|
+
super().__init__(**kwargs)
|
218
|
+
if sampling_rate < 2:
|
219
|
+
raise ValueError(
|
220
|
+
'Sampling rate must be greater than or equal to 2.')
|
221
|
+
|
222
|
+
self.sampling_rate = sampling_rate
|
223
|
+
self.distance_budget = distance_budget
|
224
|
+
self.num_robots = num_robots
|
225
|
+
self.num_dim = num_dim
|
226
|
+
self.sensor_model = sensor_model
|
227
|
+
|
228
|
+
# Enable aggregation if `aggregate_fov` is True, potentially leveraging the sensor_model's aggregation
|
229
|
+
if aggregate_fov:
|
230
|
+
if self.sensor_model is not None:
|
231
|
+
# If a sensor model exists, let it handle its own aggregation settings.
|
232
|
+
# This assumes `sensor_model` has an `enable_aggregation` method.
|
233
|
+
if hasattr(self.sensor_model, 'enable_aggregation'):
|
234
|
+
self.sensor_model.enable_aggregation()
|
235
|
+
elif self.sampling_rate > 2:
|
236
|
+
# If no specific sensor model but continuous sensing, aggregate based on sampling rate.
|
237
|
+
self.aggregation_size = self.sampling_rate
|
238
|
+
|
239
|
+
# Initialize TensorFlow Variable for fixed waypoints if provided, for online IPP.
|
240
|
+
if Xu_fixed is not None:
|
241
|
+
self.Xu_fixed = tf.Variable(
|
242
|
+
Xu_fixed,
|
243
|
+
shape=tf.TensorShape(
|
244
|
+
None), # Shape can be inferred dynamically
|
245
|
+
trainable=False, # Fixed points are not optimized
|
246
|
+
dtype=float_type)
|
247
|
+
else:
|
248
|
+
self.Xu_fixed = None
|
249
|
+
|
250
|
+
def update_Xu_fixed(self, Xu_fixed: np.ndarray) -> None:
|
251
|
+
"""
|
252
|
+
Updates the set of visited (fixed) waypoints for online IPP scenarios.
|
253
|
+
These waypoints will not be optimized in subsequent steps.
|
254
|
+
|
255
|
+
Args:
|
256
|
+
Xu_fixed (np.ndarray): A NumPy array of shape (num_robots, num_visited_waypoints, num_dim)
|
257
|
+
representing the new set of fixed waypoints.
|
258
|
+
"""
|
259
|
+
self.num_fixed = Xu_fixed.shape[
|
260
|
+
1] # Store number of fixed waypoints per robot
|
261
|
+
if self.Xu_fixed is not None:
|
262
|
+
self.Xu_fixed.assign(tf.constant(Xu_fixed, dtype=float_type))
|
263
|
+
else:
|
264
|
+
self.Xu_fixed = tf.Variable(tf.constant(Xu_fixed,
|
265
|
+
dtype=float_type),
|
266
|
+
shape=tf.TensorShape(None),
|
267
|
+
trainable=False)
|
268
|
+
|
269
|
+
def expand(self,
|
270
|
+
Xu: tf.Tensor,
|
271
|
+
expand_sensor_model: bool = True) -> tf.Tensor:
|
272
|
+
"""
|
273
|
+
Applies the expansion transform to the inducing points based on the IPP settings.
|
274
|
+
This can involve:
|
275
|
+
1. Adding fixed (already visited) waypoints.
|
276
|
+
2. Interpolating points between waypoints for continuous sensing.
|
277
|
+
3. Expanding each point into a sensor's Field of View (FoV) if a `sensor_model` is present.
|
278
|
+
|
279
|
+
Args:
|
280
|
+
Xu (tf.Tensor): The current set of optimizable inducing points.
|
281
|
+
Shape: (num_robots * num_optimizable_waypoints, num_dim).
|
282
|
+
Note: `num_dim` might include angle if `sensor_model` requires it.
|
283
|
+
expand_sensor_model (bool): If True, applies the `sensor_model`'s expansion
|
284
|
+
(if a `sensor_model` is configured). If False,
|
285
|
+
only the path interpolation and fixed point handling
|
286
|
+
are performed, useful for internal calculations like distance.
|
287
|
+
Defaults to True.
|
288
|
+
|
289
|
+
Returns:
|
290
|
+
tf.Tensor: The expanded inducing points, ready for kernel computations.
|
291
|
+
Shape: (total_expanded_points, d_output), where `d_output`
|
292
|
+
is typically 2 for (x,y) coordinates used in the kernel.
|
293
|
+
"""
|
294
|
+
# If using single-robot offline IPP with point sensing, return inducing points as is.
|
295
|
+
if self.sampling_rate == 2 and self.Xu_fixed is None and self.sensor_model is None:
|
296
|
+
return Xu
|
297
|
+
|
298
|
+
# Reshape Xu to (num_robots, num_waypoints_per_robot, num_dim)
|
299
|
+
Xu = tf.reshape(Xu, (self.num_robots, -1, self.num_dim))
|
300
|
+
|
301
|
+
# If using online IPP, add visited waypoints that won't be optimized anymore
|
302
|
+
if self.Xu_fixed is not None:
|
303
|
+
Xu = tf.concat([self.Xu_fixed, Xu[:, self.num_fixed:]], axis=1)
|
304
|
+
|
305
|
+
if not expand_sensor_model:
|
306
|
+
return tf.reshape(Xu, (-1, self.num_dim))
|
307
|
+
|
308
|
+
# Interpolate additional inducing points between waypoints to approximate
|
309
|
+
# the continuous data sensing model
|
310
|
+
if self.sampling_rate > 2:
|
311
|
+
Xu = tf.linspace(Xu[:, :-1], Xu[:, 1:], self.sampling_rate)
|
312
|
+
Xu = tf.transpose(Xu, perm=[1, 2, 0, 3])
|
313
|
+
Xu = tf.reshape(Xu, (self.num_robots, -1, self.num_dim))
|
314
|
+
|
315
|
+
# If a sensor model is defined and `expand_sensor_model` is True,
|
316
|
+
# apply the sensor model's expansion for each robot.
|
317
|
+
if self.sensor_model is not None:
|
318
|
+
Xu_ = []
|
319
|
+
for i in range(self.num_robots):
|
320
|
+
Xu_.append(self.sensor_model.expand(Xu[i]))
|
321
|
+
Xu = tf.concat(Xu_, axis=0)
|
322
|
+
return Xu
|
323
|
+
|
324
|
+
Xu = tf.reshape(Xu, (-1, self.num_dim))
|
325
|
+
return Xu
|
326
|
+
|
327
|
+
def aggregate(self, k: tf.Tensor) -> tf.Tensor:
|
328
|
+
"""
|
329
|
+
Applies the aggregation transform to kernel matrices.
|
330
|
+
If a `sensor_model` is defined, it delegates aggregation to the sensor model.
|
331
|
+
Otherwise, it uses the base class's aggregation logic (which depends on `self.aggregation_size`).
|
332
|
+
|
333
|
+
Args:
|
334
|
+
k (tf.Tensor): The input kernel matrix (e.g., K_expanded_expanded, K_expanded_training).
|
335
|
+
|
336
|
+
Returns:
|
337
|
+
tf.Tensor: The aggregated kernel matrix.
|
338
|
+
"""
|
339
|
+
if self.sensor_model is not None:
|
340
|
+
return self.sensor_model.aggregate(k)
|
341
|
+
else:
|
342
|
+
return super().aggregate(k)
|
343
|
+
|
344
|
+
def constraints(self, Xu: tf.Tensor) -> tf.Tensor:
|
345
|
+
"""
|
346
|
+
Computes the distance constraint term that is added to the SGP's optimization function.
|
347
|
+
Each robot's path length is constrained by `distance_budget`. A penalty is applied
|
348
|
+
if the path length exceeds the budget.
|
349
|
+
|
350
|
+
Args:
|
351
|
+
Xu (tf.Tensor): The inducing points (waypoints) from which to compute path lengths.
|
352
|
+
Shape: (num_robots * num_waypoints, num_dim).
|
353
|
+
|
354
|
+
Returns:
|
355
|
+
tf.Tensor: A scalar tensor representing the total distance constraint penalty.
|
356
|
+
This value is negative, and its magnitude increases with constraint violation.
|
357
|
+
"""
|
358
|
+
if self.distance_budget is None:
|
359
|
+
return tf.constant(0.0, dtype=float_type) # No distance constraint
|
360
|
+
else:
|
361
|
+
# Expand Xu without sensor model to get the true path points for distance calculation.
|
362
|
+
# Xu is the optimizable part; self.expand will add fixed points if any.
|
363
|
+
Xu_for_distance = self.expand(Xu, expand_sensor_model=False)
|
364
|
+
|
365
|
+
# Calculate distances for each robot's path
|
366
|
+
individual_robot_distances = self.distance(Xu_for_distance)
|
367
|
+
|
368
|
+
# Compute the positive violation for each robot's path
|
369
|
+
violations = individual_robot_distances - self.distance_budget
|
370
|
+
|
371
|
+
# Apply ReLU to ensure only positive violations contribute to the penalty
|
372
|
+
# Sum all violations and apply the constraint weight
|
373
|
+
penalty = -tf.reduce_sum(
|
374
|
+
tf.nn.relu(violations)) * self.constraint_weight
|
375
|
+
return penalty
|
376
|
+
|
377
|
+
def distance(self, Xu: tf.Tensor) -> tf.Tensor:
|
378
|
+
"""
|
379
|
+
Computes the total path length(s) incurred by sequentially visiting the inducing points.
|
380
|
+
For multiple robots, returns a tensor of individual path lengths.
|
381
|
+
|
382
|
+
Args:
|
383
|
+
Xu (tf.Tensor): The inducing points (waypoints) from which to compute the path lengths.
|
384
|
+
Shape: (total_waypoints, num_dim). This input is typically already
|
385
|
+
expanded to include fixed points if `Xu_fixed` is used.
|
386
|
+
|
387
|
+
Returns:
|
388
|
+
tf.Tensor: A scalar tensor if `num_robots=1`, or a 1D tensor of floats
|
389
|
+
(shape: `(num_robots,)`) representing individual path lengths.
|
390
|
+
"""
|
391
|
+
# Reshape to (num_robots, num_waypoints_per_robot, num_dim)
|
392
|
+
Xu_reshaped = tf.reshape(Xu, (self.num_robots, -1, self.num_dim))
|
393
|
+
|
394
|
+
if self.sensor_model is not None:
|
395
|
+
# If a sensor model is present, delegate distance calculation to it,
|
396
|
+
# as it might have specific logic for its FoV's contribution to distance.
|
397
|
+
dists: List[tf.Tensor] = []
|
398
|
+
for i in range(self.num_robots):
|
399
|
+
# Pass each robot's path (which includes position and potentially angle)
|
400
|
+
dists.append(self.sensor_model.distance(Xu_reshaped[i]))
|
401
|
+
return tf.concat(
|
402
|
+
dists, axis=0) # Concatenate distances if multiple robots
|
403
|
+
else:
|
404
|
+
# For point/continuous sensing without a special FoV model:
|
405
|
+
# Calculate Euclidean distance between consecutive waypoints.
|
406
|
+
# Assuming first two dimensions are (x,y) for distance calculation.
|
407
|
+
# `Xu_reshaped[:, 1:, :2]` are points from the second to last.
|
408
|
+
# `Xu_reshaped[:, :-1, :2]` are points from the first to second to last.
|
409
|
+
segment_distances = tf.norm(Xu_reshaped[:, 1:, :2] -
|
410
|
+
Xu_reshaped[:, :-1, :2],
|
411
|
+
axis=-1)
|
412
|
+
total_distances = tf.reduce_sum(
|
413
|
+
segment_distances, axis=1) # Sum distances for each robot
|
414
|
+
return total_distances
|
415
|
+
|
416
|
+
|
417
|
+
class SquareTransform(Transform):
|
418
|
+
"""
|
419
|
+
Non-point Transform to model a square Field of View (FoV) for a sensor.
|
420
|
+
This transform expands each inducing point (waypoint with position and orientation)
|
421
|
+
into a grid of points approximating a square area, which is then used in kernel computations.
|
422
|
+
This typically applies to single-robot cases as part of an `IPPTransform`.
|
423
|
+
"""
|
424
|
+
|
425
|
+
def __init__(self,
|
426
|
+
side_length: float,
|
427
|
+
pts_per_side: int,
|
428
|
+
aggregate_fov: bool = False,
|
429
|
+
**kwargs: Any):
|
430
|
+
"""
|
431
|
+
Initializes the SquareTransform for a square FoV.
|
432
|
+
|
433
|
+
Args:
|
434
|
+
side_length (float): The side length of the square FoV.
|
435
|
+
pts_per_side (int): The number of points to sample along each side of the square.
|
436
|
+
A `pts_per_side` of 3 will create a 3x3 grid of 9 points to approximate the FoV.
|
437
|
+
aggregate_fov (bool): If True, aggregation will be enabled for the expanded FoV points.
|
438
|
+
This averages covariances from the FoV points to reduce computational cost.
|
439
|
+
Defaults to False.
|
440
|
+
**kwargs (Any): Additional keyword arguments passed to the base `Transform` constructor.
|
441
|
+
|
442
|
+
Usage:
|
443
|
+
```python
|
444
|
+
# Create a square FoV of side length 10.0, approximated by a 5x5 grid of points
|
445
|
+
square_fov_transform = SquareTransform(length=10.0, pts_per_side=5, aggregate_fov=True)
|
446
|
+
```
|
447
|
+
"""
|
448
|
+
super().__init__(**kwargs)
|
449
|
+
self.side_length = side_length
|
450
|
+
self.pts_per_side = pts_per_side
|
451
|
+
# Calculate the spacing between points along each side
|
452
|
+
self.side_length_factor = side_length / (self.pts_per_side)
|
453
|
+
|
454
|
+
if aggregate_fov:
|
455
|
+
self.enable_aggregation()
|
456
|
+
|
457
|
+
def enable_aggregation(self, size: Optional[int] = None) -> None:
|
458
|
+
"""
|
459
|
+
Enables FoV covariance aggregation. This reduces the covariance matrix inversion
|
460
|
+
cost by effectively reducing the covariance matrix size.
|
461
|
+
|
462
|
+
Args:
|
463
|
+
size (Optional[int]): If None, all the interpolated inducing points within the FoV
|
464
|
+
(i.e., `pts_per_side^2` points) are aggregated into a single aggregated point.
|
465
|
+
Alternatively, the number of inducing points to aggregate can be
|
466
|
+
explicitly defined using this variable (e.g., if a custom
|
467
|
+
aggregation strategy is desired that groups `size` points).
|
468
|
+
Defaults to None.
|
469
|
+
"""
|
470
|
+
if size is None:
|
471
|
+
self.aggregation_size = self.pts_per_side**2 # Aggregate all points within a FoV
|
472
|
+
else:
|
473
|
+
self.aggregation_size = size
|
474
|
+
|
475
|
+
def expand(self, Xu: tf.Tensor) -> tf.Tensor:
|
476
|
+
"""
|
477
|
+
Applies the expansion transformation to the inducing points, modeling a square FoV.
|
478
|
+
Each input inducing point, which includes position (x, y) and orientation (theta),
|
479
|
+
is expanded into a grid of `pts_per_side` x `pts_per_side` points representing the FoV.
|
480
|
+
|
481
|
+
Args:
|
482
|
+
Xu (tf.Tensor): Inducing points in the position and orientation space.
|
483
|
+
Shape: (m, 3) where `m` is the number of inducing points,
|
484
|
+
and `3` corresponds to (x, y, angle in radians).
|
485
|
+
|
486
|
+
Returns:
|
487
|
+
tf.Tensor: The expanded inducing points in 2D input space (x,y).
|
488
|
+
Shape: (m * pts_per_side * pts_per_side, 2).
|
489
|
+
`m` is the number of original inducing points.
|
490
|
+
`pts_per_side * pts_per_side` is the number of points each inducing
|
491
|
+
point is mapped to in order to form the FoV.
|
492
|
+
"""
|
493
|
+
# Split Xu into x, y coordinates and orientation (theta)
|
494
|
+
x_coords, y_coords, angles = tf.split(Xu, num_or_size_splits=3, axis=1)
|
495
|
+
x = tf.reshape(x_coords, [
|
496
|
+
-1,
|
497
|
+
]) # Flatten to (m,)
|
498
|
+
y = tf.reshape(y_coords, [
|
499
|
+
-1,
|
500
|
+
])
|
501
|
+
theta = tf.reshape(angles, [
|
502
|
+
-1,
|
503
|
+
])
|
504
|
+
|
505
|
+
points: List[tf.Tensor] = []
|
506
|
+
# Iterate to create `pts_per_side` lines forming the square grid.
|
507
|
+
# The loop runs from -floor(pts_per_side/2) to ceil(pts_per_side/2) to center the grid.
|
508
|
+
for i in range(-int(np.floor((self.pts_per_side - 1) / 2)),
|
509
|
+
int(np.ceil((self.pts_per_side - 1) / 2)) + 1):
|
510
|
+
# Calculate start and end points for each line segment of the square grid.
|
511
|
+
# `(i * self.side_length_factor)` shifts the line perpendicular to the orientation `theta`.
|
512
|
+
# `self.side_length/2` extends the line segment along the orientation `theta`.
|
513
|
+
|
514
|
+
# Start point (x,y) for the current line
|
515
|
+
start_x = x + (i * self.side_length_factor) * tf.cos(
|
516
|
+
theta + np.pi / 2) - self.side_length / 2 * tf.cos(theta)
|
517
|
+
start_y = y + (i * self.side_length_factor) * tf.sin(
|
518
|
+
theta + np.pi / 2) - self.side_length / 2 * tf.sin(theta)
|
519
|
+
|
520
|
+
# End point (x,y) for the current line
|
521
|
+
end_x = x + (i * self.side_length_factor) * tf.cos(
|
522
|
+
theta + np.pi / 2) + self.side_length / 2 * tf.cos(theta)
|
523
|
+
end_y = y + (i * self.side_length_factor) * tf.sin(
|
524
|
+
theta + np.pi / 2) + self.side_length / 2 * tf.sin(theta)
|
525
|
+
|
526
|
+
# Stack start and end points for linspace
|
527
|
+
line_starts = tf.stack([start_x, start_y], axis=-1) # (m, 2)
|
528
|
+
line_ends = tf.stack([end_x, end_y], axis=-1) # (m, 2)
|
529
|
+
|
530
|
+
# Generate `self.pts_per_side` points along each line segment.
|
531
|
+
# `axis=1` ensures interpolation is done column-wise for each (start, end) pair.
|
532
|
+
# The result is (m, pts_per_side, 2) for each `i`.
|
533
|
+
points.append(
|
534
|
+
tf.linspace(line_starts, line_ends, self.pts_per_side, axis=1))
|
535
|
+
|
536
|
+
# Concatenate all generated line segments.
|
537
|
+
# `tf.concat` will stack them along a new axis, forming (num_lines, m, pts_per_side, 2)
|
538
|
+
xy = tf.concat(
|
539
|
+
points, axis=1
|
540
|
+
) # (m, pts_per_side * pts_per_side, 2) after the transpose in the original code.
|
541
|
+
|
542
|
+
xy = tf.reshape(xy, (-1, 2))
|
543
|
+
return xy
|
544
|
+
|
545
|
+
def distance(self, Xu: tf.Tensor) -> tf.Tensor:
|
546
|
+
"""
|
547
|
+
Computes the Euclidean distance incurred by sequentially visiting the inducing points.
|
548
|
+
For a Square FoV, the distance is typically only based on the (x,y) movement,
|
549
|
+
ignoring the angle.
|
550
|
+
|
551
|
+
Args:
|
552
|
+
Xu (tf.Tensor): Inducing points.
|
553
|
+
Shape: (m, 3) where `m` is the number of inducing points,
|
554
|
+
and `3` corresponds to (x, y, angle).
|
555
|
+
|
556
|
+
Returns:
|
557
|
+
tf.Tensor: A scalar tensor representing the total path length.
|
558
|
+
"""
|
559
|
+
# Reshape to (number_of_points, 3) and take only the (x,y) coordinates
|
560
|
+
Xu_xy = tf.reshape(
|
561
|
+
Xu, (-1, self.num_dim))[:, :2] # Assuming num_dim is 3 (x,y,angle)
|
562
|
+
|
563
|
+
if Xu_xy.shape[0] < 2:
|
564
|
+
return tf.constant(0.0, dtype=float_type)
|
565
|
+
|
566
|
+
# Calculate Euclidean distance between consecutive (x,y) points
|
567
|
+
segment_distances = tf.norm(Xu_xy[1:] - Xu_xy[:-1], axis=-1)
|
568
|
+
total_distance = tf.reduce_sum(segment_distances, axis=0)
|
569
|
+
return total_distance
|
570
|
+
|
571
|
+
|
572
|
+
class SquareHeightTransform(Transform):
|
573
|
+
"""
|
574
|
+
Non-point Transform to model a height-dependent square Field of View (FoV).
|
575
|
+
The size of the square FoV changes with the 'height' (z-dimension) of the sensor.
|
576
|
+
This transform expands each inducing point (waypoint with x, y, z coordinates)
|
577
|
+
into a grid of points approximating a square area whose size depends on 'z'.
|
578
|
+
"""
|
579
|
+
|
580
|
+
def __init__(self,
|
581
|
+
pts_per_side: int,
|
582
|
+
aggregate_fov: bool = False,
|
583
|
+
**kwargs: Any):
|
584
|
+
"""
|
585
|
+
Initializes the SquareHeightTransform for a height-dependent square FoV.
|
586
|
+
|
587
|
+
Args:
|
588
|
+
pts_per_side (int): The number of points to sample along each side of the square FoV.
|
589
|
+
A `pts_per_side` of 3 will create a 3x3 grid of 9 points to approximate the FoV.
|
590
|
+
aggregate_fov (bool): If True, aggregation will be enabled for the expanded FoV points.
|
591
|
+
This averages covariances from the FoV points to reduce computational cost.
|
592
|
+
Defaults to False.
|
593
|
+
**kwargs (Any): Additional keyword arguments passed to the base `Transform` constructor.
|
594
|
+
|
595
|
+
Usage:
|
596
|
+
```python
|
597
|
+
# Create a height-dependent square FoV approximated by a 7x7 grid
|
598
|
+
square_height_fov_transform = SquareHeightTransform(pts_per_side=7, aggregate_fov=True)
|
599
|
+
```
|
600
|
+
"""
|
601
|
+
super().__init__(**kwargs)
|
602
|
+
self.pts_per_side = pts_per_side
|
603
|
+
|
604
|
+
if aggregate_fov:
|
605
|
+
self.enable_aggregation()
|
606
|
+
|
607
|
+
def enable_aggregation(self, size: Optional[int] = None) -> None:
|
608
|
+
"""
|
609
|
+
Enables FoV covariance aggregation, which reduces the covariance matrix inversion
|
610
|
+
cost by effectively reducing the covariance matrix size.
|
611
|
+
|
612
|
+
Args:
|
613
|
+
size (Optional[int]): If None, all the interpolated inducing points within the FoV
|
614
|
+
(i.e., `pts_per_side^2` points) are aggregated into a single aggregated point.
|
615
|
+
Alternatively, the number of inducing points to aggregate can be
|
616
|
+
explicitly defined using this variable. Defaults to None.
|
617
|
+
"""
|
618
|
+
if size is None:
|
619
|
+
self.aggregation_size = self.pts_per_side**2
|
620
|
+
else:
|
621
|
+
self.aggregation_size = size
|
622
|
+
|
623
|
+
def expand(self, Xu):
|
624
|
+
"""
|
625
|
+
Applies the expansion transform to the inducing points
|
626
|
+
|
627
|
+
Args:
|
628
|
+
Xu (ndarray): (m, 3); Inducing points in the 3D position space.
|
629
|
+
`m` is the number of inducing points,
|
630
|
+
`3` is the dimension of the space (x, y, z)
|
631
|
+
|
632
|
+
Returns:
|
633
|
+
Xu (ndarray): (mp, 2); Inducing points in input space.
|
634
|
+
`p` is the number of points each inducing point is mapped
|
635
|
+
to in order to form the FoV.
|
636
|
+
"""
|
637
|
+
x, y, h = tf.split(Xu, num_or_size_splits=3, axis=1)
|
638
|
+
x = tf.reshape(x, [
|
639
|
+
-1,
|
640
|
+
])
|
641
|
+
y = tf.reshape(y, [
|
642
|
+
-1,
|
643
|
+
])
|
644
|
+
h = tf.reshape(h, [
|
645
|
+
-1,
|
646
|
+
])
|
647
|
+
|
648
|
+
delta = h / (self.pts_per_side - 1)
|
649
|
+
|
650
|
+
pts = []
|
651
|
+
for i in range(self.pts_per_side):
|
652
|
+
line_starts = [x - h / 2, y - (h / 2) + (delta * i)]
|
653
|
+
line_ends = [x + h / 2, y - (h / 2) + (delta * i)]
|
654
|
+
pts.append(
|
655
|
+
tf.linspace(line_starts, line_ends, self.pts_per_side, axis=1))
|
656
|
+
xy = tf.concat(pts, axis=1)
|
657
|
+
xy = tf.transpose(xy, [2, 1, 0])
|
658
|
+
xy = tf.reshape(xy, [-1, 2])
|
659
|
+
xy = self._reshape(xy, tf.shape(Xu)[0])
|
660
|
+
return xy
|
661
|
+
|
662
|
+
def _reshape(self, X: tf.Tensor, num_inducing: tf.Tensor) -> tf.Tensor:
|
663
|
+
"""
|
664
|
+
Reorders the expanded inducing points.
|
665
|
+
|
666
|
+
Args:
|
667
|
+
X (tf.Tensor): Expanded inducing points.
|
668
|
+
num_inducing (tf.Tensor): Original number of inducing points.
|
669
|
+
|
670
|
+
Returns:
|
671
|
+
tf.Tensor: Reordered expanded inducing points.
|
672
|
+
"""
|
673
|
+
X = tf.reshape(
|
674
|
+
X, (num_inducing, -1, self.pts_per_side, self.pts_per_side, 2))
|
675
|
+
X = tf.transpose(X, (0, 2, 1, 3, 4))
|
676
|
+
X = tf.reshape(X, (-1, 2))
|
677
|
+
return X
|
678
|
+
|
679
|
+
def distance(self, Xu: tf.Tensor) -> tf.Tensor:
|
680
|
+
"""
|
681
|
+
Computes the Euclidean distance incurred by sequentially visiting the inducing points.
|
682
|
+
For a height-dependent Square FoV, the distance is typically only based on the
|
683
|
+
(x,y,z) movement.
|
684
|
+
|
685
|
+
Args:
|
686
|
+
Xu (tf.Tensor): Inducing points.
|
687
|
+
Shape: (m, 3) where `m` is the number of inducing points,
|
688
|
+
and `3` corresponds to (x, y, z).
|
689
|
+
|
690
|
+
Returns:
|
691
|
+
tf.Tensor: A scalar tensor representing the total path length.
|
692
|
+
"""
|
693
|
+
# Reshape to (number_of_points, 3)
|
694
|
+
Xu_xyz = tf.reshape(Xu, (-1, 3))
|
695
|
+
|
696
|
+
# Calculate Euclidean distance between consecutive (x,y,z) points
|
697
|
+
segment_distances = tf.norm(Xu_xyz[1:] - Xu_xyz[:-1], axis=-1)
|
698
|
+
total_distance = tf.reduce_sum(segment_distances, axis=0)
|
699
|
+
return total_distance
|