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/objectives.py ADDED
@@ -0,0 +1,275 @@
1
+ import tensorflow as tf
2
+ import numpy as np
3
+ import gpflow
4
+ from typing import Type, Any, Dict
5
+
6
+
7
+ def jitter_fn(cov: tf.Tensor, jitter: float = 1e-6) -> tf.Tensor:
8
+ """
9
+ Adds a small positive value (jitter) to the diagonal of a covariance matrix
10
+ for numerical stability. This prevents issues with ill-conditioned matrices
11
+ during computations like Cholesky decomposition or determinant calculation.
12
+
13
+ Args:
14
+ cov (tf.Tensor): The input covariance matrix. Expected to be a square matrix.
15
+ jitter (float): The small positive value to add to the diagonal. Defaults to 1e-6.
16
+
17
+ Returns:
18
+ tf.Tensor: The covariance matrix with jitter added to its diagonal.
19
+
20
+ Usage:
21
+ ```python
22
+ # Example covariance matrix
23
+ cov_matrix = tf.constant([[1.0, 0.5], [0.5, 1.0]], dtype=tf.float64)
24
+ # Add default jitter
25
+ jittered_cov = jitter_fn(cov_matrix)
26
+ # Add custom jitter
27
+ custom_jittered_cov = jitter_fn(cov_matrix, jitter=1e-5)
28
+ ```
29
+ """
30
+ cov = tf.linalg.set_diag(cov, tf.linalg.diag_part(cov) + jitter)
31
+ return cov
32
+
33
+
34
+ class Objective:
35
+ """
36
+ Base class for objective functions used in optimization.
37
+ Subclasses must implement the `__call__` method to define the objective.
38
+ """
39
+
40
+ def __init__(self, X_objective: np.ndarray, kernel: gpflow.kernels.Kernel,
41
+ noise_variance: float, **kwargs: Any):
42
+ """
43
+ Initializes the base objective. This constructor primarily serves to define
44
+ the expected parameters for all objective subclasses.
45
+
46
+ Args:
47
+ X_objective (np.ndarray): The input data points that define the context or
48
+ environment for which the objective is calculated.
49
+ Shape: (N, D) where N is number of points, D is dimension.
50
+ kernel (gpflow.kernels.Kernel): The GPflow kernel function used in the objective.
51
+ noise_variance (float): The observed data noise variance.
52
+ **kwargs: Arbitrary keyword arguments.
53
+ """
54
+ pass
55
+
56
+ def __call__(self, X: tf.Tensor) -> tf.Tensor:
57
+ """
58
+ Computes the objective value for a given set of input points `X`.
59
+ This method must be implemented by subclasses.
60
+
61
+ Args:
62
+ X (tf.Tensor): The input points for which the objective is to be computed.
63
+ Shape: (M, D) where M is number of points, D is dimension.
64
+
65
+ Returns:
66
+ tf.Tensor: The computed objective value.
67
+
68
+ Raises:
69
+ NotImplementedError: If the method is not implemented by a subclass.
70
+ """
71
+ raise NotImplementedError
72
+
73
+ def update(self, kernel: gpflow.kernels.Kernel,
74
+ noise_variance: float) -> None:
75
+ """
76
+ Updates the kernel and noise variance parameters used by the objective function.
77
+ This method should be overridden by subclasses if they maintain internal state
78
+ that needs updating (e.g., cached kernel matrices or jitter values).
79
+
80
+ Args:
81
+ kernel (gpflow.kernels.Kernel): The updated GPflow kernel function.
82
+ noise_variance (float): The updated data noise variance.
83
+ """
84
+ pass
85
+
86
+
87
+ class MI(Objective):
88
+ """
89
+ Computes the Mutual Information (MI) between a fixed set of objective points
90
+ (`X_objective`) and a variable set of input points (`X`).
91
+
92
+ MI is calculated as:
93
+ $MI(X; X_{objective}) = log|K(X,X)| + log|K(X_{objective},X_{objective})| - log|K(X \cup X_{objective}, X \cup X_{objective})|$
94
+
95
+ Jitter is added to the diagonal of the covariance matrices to ensure numerical stability.
96
+ """
97
+
98
+ def __init__(self,
99
+ X_objective: np.ndarray,
100
+ kernel: gpflow.kernels.Kernel,
101
+ noise_variance: float,
102
+ jitter: float = 1e-6,
103
+ **kwargs: Any):
104
+ """
105
+ Initializes the Mutual Information (MI) objective.
106
+
107
+ Args:
108
+ X_objective (np.ndarray): The fixed set of data points (e.g., candidate locations
109
+ or training data points) against which MI is computed.
110
+ Shape: (N, D).
111
+ kernel (gpflow.kernels.Kernel): The GPflow kernel function to compute covariances.
112
+ noise_variance (float): The observed data noise variance, which is added to the jitter.
113
+ jitter (float): A small positive value to add for numerical stability to covariance
114
+ matrix diagonals. Defaults to 1e-6.
115
+ **kwargs: Arbitrary keyword arguments.
116
+ """
117
+ self.X_objective = tf.constant(X_objective, dtype=tf.float64)
118
+ self.kernel = kernel
119
+ self.noise_variance = noise_variance
120
+ # Total jitter includes the noise variance
121
+ self._base_jitter = jitter
122
+ self.jitter_fn = lambda cov: jitter_fn(
123
+ cov, jitter=self._base_jitter + self.noise_variance)
124
+
125
+ def __call__(self, X: tf.Tensor) -> tf.Tensor:
126
+ """
127
+ Computes the Mutual Information for the given input points `X`.
128
+
129
+ Args:
130
+ X (tf.Tensor): The input points (e.g., sensing locations) for which
131
+ MI is to be computed. Shape: (M, D).
132
+
133
+ Returns:
134
+ tf.Tensor: The computed Mutual Information value.
135
+
136
+ Usage:
137
+ ```python
138
+ import gpflow
139
+ import numpy as np
140
+ # Assume X_objective and kernel are defined
141
+ # X_objective = np.random.rand(100, 2)
142
+ # kernel = gpflow.kernels.SquaredExponential()
143
+ # noise_variance = 0.1
144
+
145
+ mi_objective = MI(X_objective=X_objective, kernel=kernel, noise_variance=noise_variance)
146
+ X_sensing = tf.constant(np.random.rand(10, 2), dtype=tf.float64)
147
+ mi_value = mi_objective(X_sensing)
148
+ ```
149
+ """
150
+ # K(X_objective, X_objective)
151
+ K_obj_obj = self.kernel(self.X_objective)
152
+ # K(X, X)
153
+ K_X_X = self.kernel(X)
154
+ # K(X_objective U X, X_objective U X)
155
+ K_combined = self.kernel(tf.concat([self.X_objective, X], axis=0))
156
+
157
+ # Compute log determinants
158
+ logdet_K_obj_obj = tf.math.log(tf.linalg.det(
159
+ self.jitter_fn(K_obj_obj)))
160
+ logdet_K_X_X = tf.math.log(tf.linalg.det(self.jitter_fn(K_X_X)))
161
+ logdet_K_combined = tf.math.log(
162
+ tf.linalg.det(self.jitter_fn(K_combined)))
163
+
164
+ # MI formula
165
+ mi = logdet_K_obj_obj + logdet_K_X_X - logdet_K_combined
166
+
167
+ return mi
168
+
169
+ def update(self, kernel: gpflow.kernels.Kernel,
170
+ noise_variance: float) -> None:
171
+ """
172
+ Updates the kernel and noise variance for the MI objective.
173
+ This method is crucial for optimizing the GP hyperparameters externally
174
+ and having the objective function reflect those changes.
175
+
176
+ Args:
177
+ kernel (gpflow.kernels.Kernel): The updated GPflow kernel function.
178
+ noise_variance (float): The updated data noise variance.
179
+ """
180
+ # Update kernel's trainable variables (e.g., lengthscales, variance)
181
+ for self_var, var in zip(self.kernel.trainable_variables,
182
+ kernel.trainable_variables):
183
+ self_var.assign(var)
184
+
185
+ self.noise_variance = noise_variance
186
+ # Update the jitter function to reflect the new noise variance
187
+ self.jitter_fn = lambda cov: jitter_fn(
188
+ cov, jitter=self._base_jitter + self.noise_variance)
189
+
190
+
191
+ class SLogMI(MI):
192
+ """
193
+ Computes the Mutual Information (MI) using `tf.linalg.slogdet` for numerical stability,
194
+ especially for large or ill-conditioned covariance matrices.
195
+
196
+ The slogdet (sign and log determinant) method computes the sign and the natural
197
+ logarithm of the absolute value of the determinant of a square matrix.
198
+ This is more numerically stable than computing the determinant directly and then
199
+ taking the logarithm, as `tf.linalg.det` can return very small or very large
200
+ numbers that lead to underflow/overflow when `tf.math.log` is applied.
201
+
202
+ Jitter is also added to the diagonal for additional numerical stability.
203
+ """
204
+
205
+ def __call__(self, X: tf.Tensor) -> tf.Tensor:
206
+ """
207
+ Computes the Mutual Information for the given input points `X` using `tf.linalg.slogdet`.
208
+
209
+ Args:
210
+ X (tf.Tensor): The input points (e.g., sensing locations) for which
211
+ MI is to be computed. Shape: (M, D).
212
+
213
+ Returns:
214
+ tf.Tensor: The computed Mutual Information value.
215
+
216
+ Usage:
217
+ ```python
218
+ import gpflow
219
+ import numpy as np
220
+ # Assume X_objective and kernel are defined
221
+ # X_objective = np.random.rand(100, 2)
222
+ # kernel = gpflow.kernels.SquaredExponential()
223
+ # noise_variance = 0.1
224
+
225
+ slogmi_objective = SLogMI(X_objective=X_objective, kernel=kernel, noise_variance=noise_variance)
226
+ X_sensing = tf.constant(np.random.rand(10, 2), dtype=tf.float64)
227
+ mi_value = slogmi_objective(X_sensing)
228
+ ```
229
+ """
230
+ # K(X_objective, X_objective)
231
+ K_obj_obj = self.kernel(self.X_objective)
232
+ # K(X, X)
233
+ K_X_X = self.kernel(X)
234
+ # K(X_objective U X, X_objective U X)
235
+ K_combined = self.kernel(tf.concat([self.X_objective, X], axis=0))
236
+
237
+ # Compute log determinants using slogdet for numerical stability
238
+ _, logdet_K_obj_obj = tf.linalg.slogdet(self.jitter_fn(K_obj_obj))
239
+ _, logdet_K_X_X = tf.linalg.slogdet(self.jitter_fn(K_X_X))
240
+ _, logdet_K_combined = tf.linalg.slogdet(self.jitter_fn(K_combined))
241
+
242
+ # MI formula
243
+ mi = logdet_K_obj_obj + logdet_K_X_X - logdet_K_combined
244
+
245
+ return mi
246
+
247
+
248
+ OBJECTIVES: Dict[str, Type[Objective]] = {
249
+ 'MI': MI,
250
+ 'SLogMI': SLogMI,
251
+ }
252
+
253
+
254
+ def get_objective(objective_name: str) -> Type[Objective]:
255
+ """
256
+ Retrieves an objective function class by its string name.
257
+
258
+ Args:
259
+ objective_name (str): The name of the objective function (e.g., 'MI', 'SLogMI').
260
+
261
+ Returns:
262
+ Type[Objective]: The class of the requested objective function.
263
+
264
+ Raises:
265
+ KeyError: If the objective name is not found in the registered OBJECTIVES.
266
+
267
+ Usage:
268
+ ```python
269
+ # Get the Mutual Information objective class
270
+ MIObjectiveClass = get_objective('MI')
271
+ # You can then instantiate it:
272
+ # mi_instance = MIObjectiveClass(X_objective=..., kernel=..., noise_variance=...)
273
+ ```
274
+ """
275
+ return OBJECTIVES[objective_name]
@@ -1,10 +1 @@
1
1
  # sgptools/utils/__init__.py
2
-
3
- """General utilities to support the functionalities of this package:
4
-
5
- - `data`: Provides utilities to preprocess datasets
6
- - `gpflow`: Provides utilities to interface with GPflow
7
- - `metrics`: Provides utilities to quantify the solution quality
8
- - `misc`: Provides miscellaneous helper functions
9
- - `tsp`: Provides utilities to run TSP/VRP solver
10
- """