CUQIpy 1.3.0.post0.dev86__py3-none-any.whl → 1.3.0.post0.dev237__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.
Potentially problematic release.
This version of CUQIpy might be problematic. Click here for more details.
- cuqi/_version.py +3 -3
- cuqi/experimental/geometry/_productgeometry.py +3 -3
- cuqi/experimental/mcmc/_rto.py +23 -15
- cuqi/model/_model.py +1051 -347
- cuqi/pde/_pde.py +14 -10
- cuqi/solver/_solver.py +6 -2
- cuqi/testproblem/_testproblem.py +2 -3
- {cuqipy-1.3.0.post0.dev86.dist-info → cuqipy-1.3.0.post0.dev237.dist-info}/METADATA +1 -1
- {cuqipy-1.3.0.post0.dev86.dist-info → cuqipy-1.3.0.post0.dev237.dist-info}/RECORD +12 -12
- {cuqipy-1.3.0.post0.dev86.dist-info → cuqipy-1.3.0.post0.dev237.dist-info}/WHEEL +1 -1
- {cuqipy-1.3.0.post0.dev86.dist-info → cuqipy-1.3.0.post0.dev237.dist-info}/licenses/LICENSE +0 -0
- {cuqipy-1.3.0.post0.dev86.dist-info → cuqipy-1.3.0.post0.dev237.dist-info}/top_level.txt +0 -0
cuqi/model/_model.py
CHANGED
|
@@ -5,48 +5,55 @@ from scipy.sparse import hstack
|
|
|
5
5
|
from scipy.linalg import solve
|
|
6
6
|
from cuqi.samples import Samples
|
|
7
7
|
from cuqi.array import CUQIarray
|
|
8
|
-
from cuqi.geometry import Geometry, _DefaultGeometry1D, _DefaultGeometry2D
|
|
8
|
+
from cuqi.geometry import Geometry, _DefaultGeometry1D, _DefaultGeometry2D,\
|
|
9
|
+
_get_identity_geometries
|
|
9
10
|
import cuqi
|
|
10
11
|
import matplotlib.pyplot as plt
|
|
11
12
|
from copy import copy
|
|
13
|
+
from functools import partial
|
|
14
|
+
from cuqi.utilities import force_ndarray
|
|
12
15
|
|
|
13
16
|
class Model(object):
|
|
14
17
|
"""Generic model defined by a forward operator.
|
|
15
18
|
|
|
16
19
|
Parameters
|
|
17
20
|
-----------
|
|
18
|
-
forward :
|
|
19
|
-
Forward operator.
|
|
21
|
+
forward : callable function
|
|
22
|
+
Forward operator of the model. It takes one or more inputs and returns the model output.
|
|
20
23
|
|
|
21
|
-
range_geometry : integer or cuqi.geometry.Geometry
|
|
22
|
-
If integer is given, a cuqi.geometry.
|
|
24
|
+
range_geometry : integer, a 1D or 2D tuple of integers, cuqi.geometry.Geometry
|
|
25
|
+
If integer or 1D tuple of integers is given, a cuqi.geometry._DefaultGeometry1D is created with dimension of the integer.
|
|
26
|
+
If 2D tuple of integers is given, a cuqi.geometry._DefaultGeometry2D is created with dimensions of the tuple.
|
|
27
|
+
If cuqi.geometry.Geometry object is given, it is used as the range geometry of the model.
|
|
23
28
|
|
|
24
|
-
domain_geometry : integer or cuqi.geometry.Geometry
|
|
25
|
-
If integer is given, a cuqi.geometry.
|
|
29
|
+
domain_geometry : integer, a 1D or 2D tuple of integers, cuqi.geometry.Geometry or a tuple with items of any of the listed types
|
|
30
|
+
If integer or 1D tuple of integers is given, a cuqi.geometry._DefaultGeometry1D is created with dimension of the integer.
|
|
31
|
+
If 2D tuple of integers is given (and the forward model has one input only), a cuqi.geometry._DefaultGeometry2D is created with dimensions of the tuple.
|
|
32
|
+
If cuqi.geometry.Geometry is given, it is used as the domain geometry.
|
|
33
|
+
If tuple of the above types is given, a cuqi.geometry._ProductGeometry is created based on the tuple entries. This is used for models with multiple inputs where each entry in the tuple represents the geometry of each input.
|
|
34
|
+
|
|
35
|
+
gradient : callable function, a tuple of callable functions or None, optional
|
|
36
|
+
The direction-Jacobian product of the forward model Jacobian with respect to the model input, evaluated at the model input. For example, if the forward model inputs are `x` and `y`, the gradient callable signature should be (`direction`, `x`, `y`), in that order, where `direction` is the direction by which the Jacobian matrix is multiplied and `x` and `y` are the parameters at which the Jacobian is computed.
|
|
37
|
+
|
|
38
|
+
If the gradient function is a single callable function, it returns a 1D ndarray if the model has only one input. If the model has multiple inputs, this gradient function should return a tuple of 1D ndarrays, each representing the gradient with respect to each input.
|
|
26
39
|
|
|
27
|
-
|
|
28
|
-
The direction-Jacobian product of the forward operator Jacobian with
|
|
29
|
-
respect to the forward operator input, evaluated at a point (`wrt`).
|
|
30
|
-
The signature of the gradient function should be (`direction`, `wrt`),
|
|
31
|
-
where `direction` is the direction by which the Jacobian matrix is
|
|
32
|
-
multiplied and `wrt` is the point at which the Jacobian is computed.
|
|
40
|
+
If the gradient function is a tuple of callable functions, each callable function should return a 1D ndarray representing the gradient with respect to each input. The order of the callable functions in the tuple should match the order of the model inputs.
|
|
33
41
|
|
|
34
|
-
jacobian : callable function, optional
|
|
35
|
-
The Jacobian of the forward
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
function is specified.
|
|
42
|
+
jacobian : callable function, a tuple of callable functions or None, optional
|
|
43
|
+
The Jacobian of the forward model with respect to the forward model input, evaluated at the model input. For example, if the forward model inputs are `x` and `y`, the jacobian signature should be (`x`, `y`), in that order, where `x` and `y` are the parameters at which the Jacobian is computed.
|
|
44
|
+
|
|
45
|
+
If the Jacobian function is a single callable function, it should return a 2D ndarray of shape (range_dim, domain_dim) if the model has only one input. If the model has multiple inputs, this Jacobian function should return a tuple of 2D ndarrays, each representing the Jacobian with respect to each input.
|
|
46
|
+
|
|
47
|
+
If the Jacobian function is a tuple of callable functions, each callable function should return a 2D ndarray representing the Jacobian with respect to each input. The order of the callable functions in the tuple should match the order of the model inputs.
|
|
48
|
+
|
|
49
|
+
The Jacobian function is used to specify the gradient function by computing the vector-Jacobian product (VJP), here we refer to the vector in the VJP as the `direction` since it is the direction at which the gradient is computed. Either the gradient or the Jacobian can be specified, but not both.
|
|
43
50
|
|
|
44
51
|
|
|
45
52
|
:ivar range_geometry: The geometry representing the range.
|
|
46
53
|
:ivar domain_geometry: The geometry representing the domain.
|
|
47
54
|
|
|
48
|
-
Example
|
|
49
|
-
|
|
55
|
+
Example 1
|
|
56
|
+
----------
|
|
50
57
|
|
|
51
58
|
Consider a forward model :math:`F: \mathbb{R}^2 \\rightarrow \mathbb{R}` defined by the following forward operator:
|
|
52
59
|
|
|
@@ -75,6 +82,9 @@ class Model(object):
|
|
|
75
82
|
|
|
76
83
|
model = Model(forward, range_geometry=1, domain_geometry=2, jacobian=jacobian)
|
|
77
84
|
|
|
85
|
+
print(model(np.array([1, 1])))
|
|
86
|
+
print(model.gradient(np.array([1]), np.array([1, 1])))
|
|
87
|
+
|
|
78
88
|
Alternatively, the gradient information in the forward model can be defined by direction-Jacobian product using the gradient keyword argument.
|
|
79
89
|
|
|
80
90
|
This may be more efficient if forming the Jacobian matrix is expensive.
|
|
@@ -87,64 +97,158 @@ class Model(object):
|
|
|
87
97
|
def forward(x):
|
|
88
98
|
return 10*x[1] - 10*x[0]**3 + 5*x[0]**2 + 6*x[0]
|
|
89
99
|
|
|
90
|
-
def gradient(direction,
|
|
91
|
-
# Direction-Jacobian product direction@jacobian(
|
|
92
|
-
return direction@np.array([[-30*
|
|
100
|
+
def gradient(direction, x):
|
|
101
|
+
# Direction-Jacobian product direction@jacobian(x)
|
|
102
|
+
return direction@np.array([[-30*x[0]**2 + 10*x[0] + 6, 10]])
|
|
93
103
|
|
|
94
104
|
model = Model(forward, range_geometry=1, domain_geometry=2, gradient=gradient)
|
|
95
105
|
|
|
106
|
+
print(model(np.array([1, 1])))
|
|
107
|
+
print(model.gradient(np.array([1]), np.array([1, 1])))
|
|
108
|
+
|
|
109
|
+
Example 2
|
|
110
|
+
----------
|
|
111
|
+
Alternatively, the example above can be defined as a model with multiple inputs: :math:`x` and :math:`y`:
|
|
112
|
+
|
|
113
|
+
.. code-block:: python
|
|
114
|
+
|
|
115
|
+
import numpy as np
|
|
116
|
+
from cuqi.model import Model
|
|
117
|
+
from cuqi.geometry import Discrete
|
|
118
|
+
|
|
119
|
+
def forward(x, y):
|
|
120
|
+
return 10 * y - 10 * x**3 + 5 * x**2 + 6 * x
|
|
121
|
+
|
|
122
|
+
def jacobian(x, y):
|
|
123
|
+
return (np.array([[-30 * x**2 + 10 * x + 6]]), np.array([[10]]))
|
|
124
|
+
|
|
125
|
+
model = Model(
|
|
126
|
+
forward,
|
|
127
|
+
range_geometry=1,
|
|
128
|
+
domain_geometry=(Discrete(1), Discrete(1)),
|
|
129
|
+
jacobian=jacobian,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
print(model(1, 1))
|
|
133
|
+
print(model.gradient(np.array([1]), 1, 1))
|
|
96
134
|
"""
|
|
97
135
|
def __init__(self, forward, range_geometry, domain_geometry, gradient=None, jacobian=None):
|
|
98
136
|
|
|
99
|
-
#Check if input is callable
|
|
137
|
+
# Check if input is callable
|
|
100
138
|
if callable(forward) is not True:
|
|
101
139
|
raise TypeError("Forward needs to be callable function.")
|
|
102
|
-
|
|
140
|
+
|
|
141
|
+
# Store forward func
|
|
142
|
+
self._forward_func = forward
|
|
143
|
+
self._stored_non_default_args = None
|
|
144
|
+
|
|
145
|
+
# Store range_geometry
|
|
146
|
+
self.range_geometry = range_geometry
|
|
147
|
+
|
|
148
|
+
# Store domain_geometry
|
|
149
|
+
self.domain_geometry = domain_geometry
|
|
150
|
+
|
|
151
|
+
# Additional checks for the forward operator
|
|
152
|
+
self._check_domain_geometry_consistent_with_forward()
|
|
153
|
+
|
|
103
154
|
# Check if only one of gradient and jacobian is given
|
|
104
155
|
if (gradient is not None) and (jacobian is not None):
|
|
105
156
|
raise TypeError("Only one of gradient and jacobian should be specified")
|
|
106
|
-
|
|
107
|
-
#Check
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
#
|
|
157
|
+
|
|
158
|
+
# Check correct gradient form (check type, signature, etc.)
|
|
159
|
+
self._check_correct_gradient_jacobian_form(gradient, "gradient")
|
|
160
|
+
|
|
161
|
+
# Check correct jacobian form (check type, signature, etc.)
|
|
162
|
+
self._check_correct_gradient_jacobian_form(jacobian, "jacobian")
|
|
163
|
+
|
|
164
|
+
# If jacobian is provided, use it to specify gradient function
|
|
165
|
+
# (vector-Jacobian product)
|
|
115
166
|
if jacobian is not None:
|
|
116
|
-
gradient =
|
|
117
|
-
|
|
118
|
-
#Store forward func
|
|
119
|
-
self._forward_func = forward
|
|
167
|
+
gradient = self._use_jacobian_to_specify_gradient(jacobian)
|
|
168
|
+
|
|
120
169
|
self._gradient_func = gradient
|
|
121
|
-
|
|
122
|
-
#
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
170
|
+
|
|
171
|
+
# Set gradient output stacked flag to False
|
|
172
|
+
self._gradient_output_stacked = False
|
|
173
|
+
|
|
174
|
+
@property
|
|
175
|
+
def _non_default_args(self):
|
|
176
|
+
if self._stored_non_default_args is None:
|
|
177
|
+
# Store non_default_args of the forward operator for faster caching
|
|
178
|
+
# when checking for those arguments.
|
|
179
|
+
self._stored_non_default_args =\
|
|
180
|
+
cuqi.utilities.get_non_default_args(self._forward_func)
|
|
181
|
+
return self._stored_non_default_args
|
|
182
|
+
|
|
183
|
+
@property
|
|
184
|
+
def number_of_inputs(self):
|
|
185
|
+
""" The number of inputs of the model. """
|
|
186
|
+
return len(self._non_default_args)
|
|
187
|
+
|
|
188
|
+
@property
|
|
189
|
+
def range_geometry(self):
|
|
190
|
+
""" The geometry representing the range of the model. """
|
|
191
|
+
return self._range_geometry
|
|
192
|
+
|
|
193
|
+
@range_geometry.setter
|
|
194
|
+
def range_geometry(self, value):
|
|
195
|
+
""" Update the range geometry of the model. """
|
|
196
|
+
if isinstance(value, Geometry):
|
|
197
|
+
self._range_geometry = value
|
|
198
|
+
elif isinstance(value, int):
|
|
199
|
+
self._range_geometry = self._create_default_geometry(value)
|
|
200
|
+
elif isinstance(value, tuple):
|
|
201
|
+
self._range_geometry = self._create_default_geometry(value)
|
|
202
|
+
elif value is None:
|
|
203
|
+
raise AttributeError(
|
|
204
|
+
"The parameter 'range_geometry' is not specified by the user and it cannot be inferred from the attribute 'forward'."
|
|
205
|
+
)
|
|
143
206
|
else:
|
|
144
|
-
raise TypeError(
|
|
207
|
+
raise TypeError(
|
|
208
|
+
" The allowed types for 'range_geometry' are: 'cuqi.geometry.Geometry', int, 1D tuple of int, or 2D tuple of int."
|
|
209
|
+
)
|
|
145
210
|
|
|
146
|
-
|
|
147
|
-
|
|
211
|
+
@property
|
|
212
|
+
def domain_geometry(self):
|
|
213
|
+
""" The geometry representing the domain of the model. """
|
|
214
|
+
return self._domain_geometry
|
|
215
|
+
|
|
216
|
+
@domain_geometry.setter
|
|
217
|
+
def domain_geometry(self, value):
|
|
218
|
+
""" Update the domain geometry of the model. """
|
|
219
|
+
|
|
220
|
+
if isinstance(value, Geometry):
|
|
221
|
+
self._domain_geometry = value
|
|
222
|
+
elif isinstance(value, int):
|
|
223
|
+
self._domain_geometry = self._create_default_geometry(value)
|
|
224
|
+
elif isinstance(value, tuple) and self.number_of_inputs == 1:
|
|
225
|
+
self._domain_geometry = self._create_default_geometry(value)
|
|
226
|
+
elif isinstance(value, tuple) and self.number_of_inputs > 1:
|
|
227
|
+
geometries = [item if isinstance(item, Geometry) else self._create_default_geometry(item) for item in value]
|
|
228
|
+
self._domain_geometry = cuqi.experimental.geometry._ProductGeometry(*geometries)
|
|
229
|
+
elif value is None:
|
|
230
|
+
raise AttributeError(
|
|
231
|
+
"The parameter 'domain_geometry' is not specified by the user and it cannot be inferred from the attribute 'forward'."
|
|
232
|
+
)
|
|
233
|
+
else:
|
|
234
|
+
raise TypeError(
|
|
235
|
+
"For forward model with 1 input, the allowed types for 'domain_geometry' are: 'cuqi.geometry.Geometry', int, 1D tuple of int, or 2D tuple of int. For forward model with multiple inputs, the 'domain_geometry' should be a tuple with items of any of the above types."
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
def _create_default_geometry(self, value):
|
|
239
|
+
"""Private function that creates default geometries for the model."""
|
|
240
|
+
if isinstance(value, tuple) and len(value) == 1:
|
|
241
|
+
value = value[0]
|
|
242
|
+
if isinstance(value, Geometry):
|
|
243
|
+
return value
|
|
244
|
+
if isinstance(value, int):
|
|
245
|
+
return _DefaultGeometry1D(grid=value)
|
|
246
|
+
elif isinstance(value, tuple) and len(value) == 2:
|
|
247
|
+
return _DefaultGeometry2D(im_shape=value)
|
|
248
|
+
else:
|
|
249
|
+
raise ValueError(
|
|
250
|
+
"Default geometry creation can be specified by an integer or a 2D tuple of integers."
|
|
251
|
+
)
|
|
148
252
|
|
|
149
253
|
@property
|
|
150
254
|
def domain_dim(self):
|
|
@@ -160,341 +264,842 @@ class Model(object):
|
|
|
160
264
|
"""
|
|
161
265
|
return self.range_geometry.par_dim
|
|
162
266
|
|
|
163
|
-
def
|
|
164
|
-
"""
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
267
|
+
def _check_domain_geometry_consistent_with_forward(self):
|
|
268
|
+
"""Private function that checks if the domain geometry of the model is
|
|
269
|
+
consistent with the forward operator."""
|
|
270
|
+
if (
|
|
271
|
+
not isinstance(
|
|
272
|
+
self.domain_geometry, cuqi.experimental.geometry._ProductGeometry
|
|
273
|
+
)
|
|
274
|
+
and self.number_of_inputs > 1
|
|
275
|
+
):
|
|
276
|
+
raise ValueError(
|
|
277
|
+
"The forward operator input is specified by more than one argument. This is only supported for domain geometry of type tuple with items of type: cuqi.geometry.Geometry object, int, or 2D tuple of int."
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
def _check_correct_gradient_jacobian_form(self, func, func_type):
|
|
281
|
+
"""Private function that checks if the gradient/jacobian parameter is
|
|
282
|
+
in the correct form. That is, check if the gradient/jacobian has the
|
|
283
|
+
correct type, signature, etc."""
|
|
284
|
+
|
|
285
|
+
if func is None:
|
|
286
|
+
return
|
|
287
|
+
|
|
288
|
+
# gradient/jacobian should be callable (for single input and multiple input case)
|
|
289
|
+
# or a tuple of callables (for multiple inputs case)
|
|
290
|
+
if isinstance(func, tuple):
|
|
291
|
+
# tuple length should be same as the number of inputs
|
|
292
|
+
if len(func) != self.number_of_inputs:
|
|
293
|
+
raise ValueError(
|
|
294
|
+
f"The "
|
|
295
|
+
+ func_type.lower()
|
|
296
|
+
+ f" tuple length should be {self.number_of_inputs} for model with inputs {self._non_default_args}"
|
|
297
|
+
)
|
|
298
|
+
# tuple items should be callables or None
|
|
299
|
+
if not all([callable(func_i) or func_i is None for func_i in func]):
|
|
300
|
+
raise TypeError(
|
|
301
|
+
func_type.capitalize()
|
|
302
|
+
+ " tuple should contain callable functions or None."
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
elif callable(func):
|
|
306
|
+
# temporarily convert gradient/jacobian to tuple for checking only
|
|
307
|
+
func = (func,)
|
|
308
|
+
|
|
309
|
+
else:
|
|
310
|
+
raise TypeError(
|
|
311
|
+
"Gradient needs to be callable function or tuple of callable functions."
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
expected_func_non_default_args = self._non_default_args
|
|
315
|
+
if func_type.lower() == "gradient":
|
|
316
|
+
# prepend 'direction' to the expected gradient non default args
|
|
317
|
+
expected_func_non_default_args = [
|
|
318
|
+
"direction"
|
|
319
|
+
] + expected_func_non_default_args
|
|
320
|
+
|
|
321
|
+
for func_i in func:
|
|
322
|
+
# make sure the signature of the gradient/jacobian function is correct
|
|
323
|
+
# that is, the same as the expected_func_non_default_args
|
|
324
|
+
if func_i is not None:
|
|
325
|
+
func_non_default_args = cuqi.utilities.get_non_default_args(func_i)
|
|
326
|
+
|
|
327
|
+
if list(func_non_default_args) != list(expected_func_non_default_args):
|
|
328
|
+
raise ValueError(
|
|
329
|
+
func_type.capitalize()
|
|
330
|
+
+ f" function signature should be {expected_func_non_default_args}"
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
def _use_jacobian_to_specify_gradient(self, jacobian):
|
|
334
|
+
"""Private function that uses the jacobian function to specify the
|
|
335
|
+
gradient function."""
|
|
336
|
+
# if jacobian is a single function and model has multiple inputs
|
|
337
|
+
if callable(jacobian) and self.number_of_inputs > 1:
|
|
338
|
+
gradient = self._create_gradient_lambda_function_from_jacobian_with_correct_signature(
|
|
339
|
+
jacobian, form='one_callable_multiple_inputs'
|
|
340
|
+
)
|
|
341
|
+
# Elif jacobian is a single function and model has only one input
|
|
342
|
+
elif callable(jacobian):
|
|
343
|
+
gradient = self._create_gradient_lambda_function_from_jacobian_with_correct_signature(
|
|
344
|
+
jacobian, form='one_callable_one_input'
|
|
345
|
+
)
|
|
346
|
+
# Else, jacobian is a tuple of jacobian functions
|
|
347
|
+
else:
|
|
348
|
+
gradient = []
|
|
349
|
+
for jac in jacobian:
|
|
350
|
+
if jac is not None:
|
|
351
|
+
gradient.append(
|
|
352
|
+
self._create_gradient_lambda_function_from_jacobian_with_correct_signature(
|
|
353
|
+
jac, form='tuple_of_callables'
|
|
354
|
+
)
|
|
355
|
+
)
|
|
356
|
+
else:
|
|
357
|
+
gradient.append(None)
|
|
358
|
+
return tuple(gradient) if isinstance(gradient, list) else gradient
|
|
359
|
+
|
|
360
|
+
def _create_gradient_lambda_function_from_jacobian_with_correct_signature(
|
|
361
|
+
self, jacobian, form
|
|
362
|
+
):
|
|
363
|
+
"""Private function that creates gradient lambda function from the
|
|
364
|
+
jacobian function, with the correct signature (based on the model
|
|
365
|
+
non_default_args).
|
|
366
|
+
"""
|
|
367
|
+
# create the string representation of the lambda function
|
|
368
|
+
# for different forms of jacobian
|
|
369
|
+
if form=='one_callable_multiple_inputs':
|
|
370
|
+
grad_fun_str = (
|
|
371
|
+
"lambda direction, "
|
|
372
|
+
+ ", ".join(self._non_default_args)
|
|
373
|
+
+ ", jacobian: tuple([direction@jacobian("
|
|
374
|
+
+ ", ".join(self._non_default_args)
|
|
375
|
+
+ ")[i] for i in range("+str(self.number_of_inputs)+")])"
|
|
376
|
+
)
|
|
377
|
+
elif form=='tuple_of_callables' or form=='one_callable_one_input':
|
|
378
|
+
grad_fun_str = (
|
|
379
|
+
"lambda direction, "
|
|
380
|
+
+ ", ".join(self._non_default_args)
|
|
381
|
+
+ ", jacobian: direction@jacobian("
|
|
382
|
+
+ ", ".join(self._non_default_args)
|
|
383
|
+
+ ")"
|
|
384
|
+
)
|
|
385
|
+
else:
|
|
386
|
+
raise ValueError("form should be either 'one_callable' or 'tuple_of_callables'.")
|
|
387
|
+
|
|
388
|
+
# create the lambda function from the string
|
|
389
|
+
grad_func = eval(grad_fun_str)
|
|
390
|
+
|
|
391
|
+
# create partial function from the lambda function with jacobian as a
|
|
392
|
+
# fixed argument
|
|
393
|
+
grad_func = partial(grad_func, jacobian=jacobian)
|
|
394
|
+
|
|
395
|
+
return grad_func
|
|
396
|
+
|
|
397
|
+
def _2fun(self, geometry=None, is_par=True, **kwargs):
|
|
398
|
+
""" Converts `kwargs` to function values (if needed) using the geometry. For example, `kwargs` can be the model input which need to be converted to function value before being passed to :class:`~cuqi.model.Model` operators (e.g. _forward_func, _adjoint_func, _gradient_func).
|
|
169
399
|
|
|
170
400
|
Parameters
|
|
171
401
|
----------
|
|
172
|
-
x : ndarray or cuqi.array.CUQIarray
|
|
173
|
-
The value to be converted.
|
|
174
|
-
|
|
175
402
|
geometry : cuqi.geometry.Geometry
|
|
176
|
-
The geometry representing `
|
|
403
|
+
The geometry representing the values in `kwargs`.
|
|
177
404
|
|
|
178
|
-
is_par : bool
|
|
179
|
-
If True, `
|
|
180
|
-
If False, `
|
|
405
|
+
is_par : bool or a tuple of bools
|
|
406
|
+
If `is_par` is True, the values in `kwargs` are assumed to be parameters.
|
|
407
|
+
If `is_par` is False, the values in `kwargs` are assumed to be function values.
|
|
408
|
+
If `is_par` is a tuple of bools, the values in `kwargs` are assumed to be parameters or function values based on the corresponding boolean value in the tuple.
|
|
409
|
+
|
|
410
|
+
**kwargs : keyword arguments to be converted to function values.
|
|
181
411
|
|
|
182
412
|
Returns
|
|
183
413
|
-------
|
|
184
|
-
|
|
185
|
-
`x` represented as a function.
|
|
414
|
+
dict of the converted values
|
|
186
415
|
"""
|
|
187
|
-
#
|
|
188
|
-
#
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
416
|
+
# Check kwargs and geometry are consistent and set up geometries list and
|
|
417
|
+
# is_par tuple
|
|
418
|
+
geometries, is_par = self._helper_pre_conversion_checks_and_processing(geometry, is_par, **kwargs)
|
|
419
|
+
|
|
420
|
+
# Convert to function values
|
|
421
|
+
for i, (k, v) in enumerate(kwargs.items()):
|
|
422
|
+
# Use CUQIarray funvals if geometry is consistent
|
|
423
|
+
if isinstance(v, CUQIarray) and v.geometry == geometries[i]:
|
|
424
|
+
kwargs[k] = v.funvals
|
|
425
|
+
# Else, if we still need to convert to function value (is_par[i] is True)
|
|
426
|
+
# we use the geometry par2fun method
|
|
427
|
+
elif is_par[i] and v is not None:
|
|
428
|
+
kwargs[k] = geometries[i].par2fun(v)
|
|
429
|
+
else:
|
|
430
|
+
# No need to convert
|
|
431
|
+
pass
|
|
432
|
+
|
|
433
|
+
return kwargs
|
|
434
|
+
|
|
435
|
+
def _helper_pre_conversion_checks_and_processing(self, geometry=None, is_par=True, **kwargs):
|
|
436
|
+
""" Helper function that checks if kwargs and geometry are consistent
|
|
437
|
+
and sets up geometries list and is_par tuple.
|
|
438
|
+
"""
|
|
439
|
+
# If len of kwargs is larger than 1, the geometry needs to be of type
|
|
440
|
+
# _ProductGeometry
|
|
441
|
+
if (
|
|
442
|
+
not isinstance(geometry, cuqi.experimental.geometry._ProductGeometry)
|
|
443
|
+
and len(kwargs) > 1
|
|
444
|
+
):
|
|
445
|
+
raise ValueError(
|
|
446
|
+
"The input is specified by more than one argument. This is only "
|
|
447
|
+
+ "supported for domain geometry of type "
|
|
448
|
+
+ f"{cuqi.experimental.geometry._ProductGeometry.__name__}."
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
# If is_par is bool, make it a tuple of bools of the same length as
|
|
452
|
+
# kwargs
|
|
453
|
+
is_par = (is_par,) * len(kwargs) if isinstance(is_par, bool) else is_par
|
|
454
|
+
|
|
455
|
+
# Set up geometries list
|
|
456
|
+
geometries = (
|
|
457
|
+
geometry.geometries
|
|
458
|
+
if isinstance(geometry, cuqi.experimental.geometry._ProductGeometry)
|
|
459
|
+
else [geometry]
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
return geometries, is_par
|
|
463
|
+
|
|
464
|
+
def _2par(self, geometry=None, to_CUQIarray=False, is_par=False, **kwargs):
|
|
465
|
+
""" Converts `kwargs` to parameters using the geometry. For example, `kwargs` can be the output of :class:`~cuqi.model.Model` operators (e.g. _forward_func, _adjoint_func, _gradient_func) which need to be converted to parameters before being returned.
|
|
202
466
|
|
|
203
467
|
Parameters
|
|
204
468
|
----------
|
|
205
|
-
val : ndarray or cuqi.array.CUQIarray
|
|
206
|
-
The value to be converted to parameters.
|
|
207
|
-
|
|
208
469
|
geometry : cuqi.geometry.Geometry
|
|
209
|
-
The geometry representing the
|
|
470
|
+
The geometry representing the values in `kwargs`.
|
|
210
471
|
|
|
211
|
-
to_CUQIarray : bool
|
|
212
|
-
If True, the
|
|
472
|
+
to_CUQIarray : bool or a tuple of bools
|
|
473
|
+
If `to_CUQIarray` is True, the values in `kwargs` will be wrapped in `CUQIarray`.
|
|
474
|
+
If `to_CUQIarray` is False, the values in `kwargs` will not be wrapped in `CUQIarray`.
|
|
475
|
+
If `to_CUQIarray` is a tuple of bools, the values in `kwargs` will be wrapped in `CUQIarray` or not based on the corresponding boolean value in the tuple.
|
|
213
476
|
|
|
214
|
-
is_par : bool
|
|
215
|
-
If True, `
|
|
216
|
-
|
|
477
|
+
is_par : bool or a tuple of bools
|
|
478
|
+
If `is_par` is True, the values in `kwargs` are assumed to be parameters.
|
|
479
|
+
If `is_par` is False, the values in `kwargs` are assumed to be function values.
|
|
480
|
+
If `is_par` is a tuple of bools, the values in `kwargs` are assumed to be parameters or function values based on the corresponding boolean value in the tuple.
|
|
217
481
|
|
|
218
482
|
Returns
|
|
219
483
|
-------
|
|
220
|
-
|
|
221
|
-
The value `val` represented as parameters.
|
|
484
|
+
dict of the converted values
|
|
222
485
|
"""
|
|
486
|
+
# Check kwargs and geometry are consistent and set up geometries list and
|
|
487
|
+
# is_par tuple
|
|
488
|
+
geometries, is_par = self._helper_pre_conversion_checks_and_processing(geometry, is_par, **kwargs)
|
|
489
|
+
|
|
490
|
+
# if to_CUQIarray is bool, make it a tuple of bools of the same length
|
|
491
|
+
# as kwargs
|
|
492
|
+
to_CUQIarray = (to_CUQIarray,) * len(kwargs) if isinstance(to_CUQIarray, bool) else to_CUQIarray
|
|
493
|
+
|
|
223
494
|
# Convert to parameters
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
495
|
+
for i , (k, v) in enumerate(kwargs.items()):
|
|
496
|
+
# Use CUQIarray parameters if geometry is consistent
|
|
497
|
+
if isinstance(v, CUQIarray) and v.geometry == geometries[i]:
|
|
498
|
+
v = v.parameters
|
|
499
|
+
# Else, if we still need to convert to parameter value (is_par[i] is False)
|
|
500
|
+
# we use the geometry fun2par method
|
|
501
|
+
elif not is_par[i] and v is not None:
|
|
502
|
+
v = geometries[i].fun2par(v)
|
|
503
|
+
else:
|
|
504
|
+
# No need to convert
|
|
505
|
+
pass
|
|
506
|
+
|
|
507
|
+
# Wrap the value v in CUQIarray if requested
|
|
508
|
+
if to_CUQIarray[i] and v is not None:
|
|
509
|
+
v = CUQIarray(v, is_par=True, geometry=geometries[i])
|
|
510
|
+
|
|
511
|
+
kwargs[k] = v
|
|
239
512
|
|
|
240
|
-
|
|
241
|
-
""" Private function that applies the given function `func` to the input value `x`. It converts the input to function values (if needed) using the given `func_domain_geometry` and converts the output function values to parameters using the given `func_range_geometry`. It additionally handles the case of applying the function `func` to the cuqi.samples.Samples object.
|
|
513
|
+
return kwargs
|
|
242
514
|
|
|
243
|
-
|
|
515
|
+
def _apply_func(self, func=None, fwd=True, is_par=True, **kwargs):
|
|
516
|
+
""" Private function that applies the given function `func` to the input `kwargs`. It converts the input to function values (if needed) and converts the output to parameter values. It additionally handles the case of applying the function `func` to cuqi.samples.Samples objects.
|
|
244
517
|
|
|
245
518
|
Parameters
|
|
246
519
|
----------
|
|
247
520
|
func: function handler
|
|
248
521
|
The function to be applied.
|
|
249
522
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
The geometry representing the function `func` domain.
|
|
255
|
-
|
|
256
|
-
x : ndarray or cuqi.array.CUQIarray
|
|
257
|
-
The input value to the operator.
|
|
523
|
+
fwd : bool
|
|
524
|
+
Flag indicating the direction of the operator to determine the range and domain geometries of the function.
|
|
525
|
+
If True the function is a forward operator.
|
|
526
|
+
If False the function is an adjoint operator.
|
|
258
527
|
|
|
259
|
-
is_par : bool
|
|
260
|
-
If True the
|
|
261
|
-
If False the input
|
|
528
|
+
is_par : bool or list of bool
|
|
529
|
+
If True, the inputs in `kwargs` are assumed to be parameters.
|
|
530
|
+
If False, the input in `kwargs` are assumed to be function values.
|
|
531
|
+
If `is_par` is a list of bools, the inputs are assumed to be parameters or function values based on the corresponding boolean value in the list.
|
|
262
532
|
|
|
263
533
|
Returns
|
|
264
534
|
-------
|
|
265
|
-
ndarray or cuqi.array.CUQIarray
|
|
266
|
-
The output of the function
|
|
535
|
+
ndarray or cuqi.array.CUQIarray or cuqi.samples.Samples object
|
|
536
|
+
The output of the function.
|
|
267
537
|
"""
|
|
538
|
+
# Specify the range and domain geometries of the function
|
|
539
|
+
# If forward operator, range geometry is the model range geometry and
|
|
540
|
+
# domain geometry is the model domain geometry
|
|
541
|
+
if fwd:
|
|
542
|
+
func_range_geometry = self.range_geometry
|
|
543
|
+
func_domain_geometry = self.domain_geometry
|
|
544
|
+
# If adjoint operator, range geometry is the model domain geometry and
|
|
545
|
+
# domain geometry is the model range geometry
|
|
546
|
+
else:
|
|
547
|
+
func_range_geometry = self.domain_geometry
|
|
548
|
+
func_domain_geometry = self.range_geometry
|
|
549
|
+
|
|
268
550
|
# If input x is Samples we apply func for each sample
|
|
269
551
|
# TODO: Check if this can be done all-at-once for computational speed-up
|
|
270
|
-
if isinstance(x,Samples):
|
|
271
|
-
|
|
272
|
-
# Recursively apply func to each sample
|
|
273
|
-
for idx, item in enumerate(x):
|
|
274
|
-
out[:,idx] = self._apply_func(func,
|
|
275
|
-
func_range_geometry,
|
|
276
|
-
func_domain_geometry,
|
|
277
|
-
item, is_par=True,
|
|
278
|
-
**kwargs)
|
|
279
|
-
return Samples(out, geometry=func_range_geometry)
|
|
280
|
-
|
|
281
|
-
# store if input x is CUQIarray
|
|
282
|
-
is_CUQIarray = type(x) is CUQIarray
|
|
552
|
+
if any(isinstance(x, Samples) for x in kwargs.values()):
|
|
553
|
+
return self._handle_case_when_model_input_is_samples(func, fwd, **kwargs)
|
|
283
554
|
|
|
284
|
-
|
|
285
|
-
|
|
555
|
+
# store if any input x is CUQIarray
|
|
556
|
+
is_CUQIarray = any(isinstance(x, CUQIarray) for x in kwargs.values())
|
|
286
557
|
|
|
287
|
-
#
|
|
288
|
-
|
|
289
|
-
return self._2par(out, func_range_geometry,
|
|
290
|
-
to_CUQIarray=is_CUQIarray)
|
|
558
|
+
# Convert input to function values
|
|
559
|
+
kwargs = self._2fun(geometry=func_domain_geometry, is_par=is_par, **kwargs)
|
|
291
560
|
|
|
292
|
-
|
|
293
|
-
|
|
561
|
+
# Apply the function
|
|
562
|
+
out = func(**kwargs)
|
|
294
563
|
|
|
295
|
-
|
|
564
|
+
# Return output as parameters
|
|
565
|
+
# (wrapped in CUQIarray if any input was CUQIarray)
|
|
566
|
+
return self._2par(
|
|
567
|
+
geometry=func_range_geometry, to_CUQIarray=is_CUQIarray, **{"out": out}
|
|
568
|
+
)["out"]
|
|
296
569
|
|
|
570
|
+
def _handle_case_when_model_input_is_samples(self, func=None, fwd=True, **kwargs):
|
|
571
|
+
"""Private function that calls apply_func for samples in the
|
|
572
|
+
Samples object(s).
|
|
573
|
+
"""
|
|
574
|
+
# All kwargs should be Samples objects
|
|
575
|
+
if not all(isinstance(x, Samples) for x in kwargs.values()):
|
|
576
|
+
raise TypeError(
|
|
577
|
+
"If applying the function to Samples, all inputs should be Samples."
|
|
578
|
+
)
|
|
579
|
+
|
|
580
|
+
# All Samples objects should have the same number of samples
|
|
581
|
+
Ns = list(kwargs.values())[0].Ns
|
|
582
|
+
if not all(x.Ns == Ns for x in kwargs.values()):
|
|
583
|
+
raise ValueError(
|
|
584
|
+
"If applying the function to Samples, all inputs should have the same number of samples."
|
|
585
|
+
)
|
|
586
|
+
|
|
587
|
+
# Specify the range dimension of the function
|
|
588
|
+
range_dim = self.range_dim if fwd else self.domain_dim
|
|
589
|
+
|
|
590
|
+
# Create empty array to store the output
|
|
591
|
+
out = np.zeros((range_dim, Ns))
|
|
592
|
+
|
|
593
|
+
# Recursively apply func to each sample
|
|
594
|
+
for i in range(Ns):
|
|
595
|
+
kwargs_i = {
|
|
596
|
+
k: CUQIarray(v.samples[..., i], is_par=v.is_par, geometry=v.geometry)
|
|
597
|
+
for k, v in kwargs.items()
|
|
598
|
+
}
|
|
599
|
+
out[:, i] = self._apply_func(func=func, fwd=fwd, **kwargs_i)
|
|
600
|
+
# Specify the range geometries of the function
|
|
601
|
+
func_range_geometry = self.range_geometry if fwd else self.domain_geometry
|
|
602
|
+
return Samples(out, geometry=func_range_geometry)
|
|
603
|
+
|
|
604
|
+
def _parse_args_add_to_kwargs(
|
|
605
|
+
self, *args, is_par=True, non_default_args=None, map_name="model", **kwargs
|
|
606
|
+
):
|
|
607
|
+
""" Private function that parses the input arguments and adds them as
|
|
608
|
+
keyword arguments matching (the order of) the non default arguments of
|
|
609
|
+
the forward function or other specified non_default_args list.
|
|
610
|
+
"""
|
|
611
|
+
# If non_default_args is not specified, use the non_default_args of the
|
|
612
|
+
# model
|
|
613
|
+
if non_default_args is None:
|
|
614
|
+
non_default_args = self._non_default_args
|
|
615
|
+
|
|
616
|
+
# If any args are given, add them to kwargs
|
|
617
|
+
if len(args) > 0:
|
|
297
618
|
if len(kwargs) > 0:
|
|
298
|
-
raise ValueError(
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
619
|
+
raise ValueError(
|
|
620
|
+
"The "
|
|
621
|
+
+ map_name.lower()
|
|
622
|
+
+ " input is specified both as positional and keyword arguments. This is not supported."
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
appending_error_message = ""
|
|
626
|
+
# Check if the input is for multiple input case and is stacked,
|
|
627
|
+
# then split it
|
|
628
|
+
if len(args)==1 and len(non_default_args)>1:
|
|
629
|
+
# If the argument is a Sample object, splitting is not supported
|
|
630
|
+
if isinstance(args[0], Samples):
|
|
631
|
+
raise ValueError(
|
|
632
|
+
"The "
|
|
633
|
+
+ map_name.lower()
|
|
634
|
+
+ f" input is specified by a Samples object that cannot be split into multiple arguments corresponding to the non_default_args {non_default_args}."
|
|
635
|
+
)
|
|
636
|
+
split_succeeded, split_args = self._is_stacked_args(*args, is_par=is_par)
|
|
637
|
+
if split_succeeded:
|
|
638
|
+
args = split_args
|
|
639
|
+
else:
|
|
640
|
+
appending_error_message = (
|
|
641
|
+
" Additionally, the "
|
|
642
|
+
+ map_name.lower()
|
|
643
|
+
+ f" input is specified by a single argument that cannot be split into multiple arguments matching the expected non_default_args {non_default_args}."
|
|
644
|
+
)
|
|
645
|
+
|
|
646
|
+
# Check if the number of args does not match the number of
|
|
647
|
+
# non_default_args of the model
|
|
648
|
+
if len(args) != len(non_default_args):
|
|
649
|
+
raise ValueError(
|
|
650
|
+
"The number of positional arguments does not match the number of non-default arguments of the "
|
|
651
|
+
+ map_name.lower()
|
|
652
|
+
+ "."
|
|
653
|
+
+ appending_error_message
|
|
654
|
+
)
|
|
655
|
+
|
|
303
656
|
# Add args to kwargs following the order of non_default_args
|
|
304
657
|
for idx, arg in enumerate(args):
|
|
305
|
-
kwargs[
|
|
658
|
+
kwargs[non_default_args[idx]] = arg
|
|
659
|
+
|
|
660
|
+
# Check kwargs matches non_default_args
|
|
661
|
+
if set(list(kwargs.keys())) != set(non_default_args):
|
|
662
|
+
if map_name == "gradient":
|
|
663
|
+
error_msg = f"The gradient input is specified by a direction and keywords arguments {list(kwargs.keys())} that does not match the non_default_args of the model {non_default_args}."
|
|
664
|
+
else:
|
|
665
|
+
error_msg = (
|
|
666
|
+
"The "
|
|
667
|
+
+ map_name.lower()
|
|
668
|
+
+ f" input is specified by a keywords arguments {list(kwargs.keys())} that does not match the non_default_args of the "
|
|
669
|
+
+ map_name
|
|
670
|
+
+ f" {non_default_args}."
|
|
671
|
+
)
|
|
672
|
+
|
|
673
|
+
raise ValueError(error_msg)
|
|
674
|
+
|
|
675
|
+
# Make sure order of kwargs is the same as non_default_args
|
|
676
|
+
kwargs = {k: kwargs[k] for k in non_default_args}
|
|
306
677
|
|
|
307
678
|
return kwargs
|
|
308
|
-
|
|
679
|
+
|
|
680
|
+
def _is_stacked_args(self, *args, is_par=True):
|
|
681
|
+
"""Private function that checks if the input arguments are stacked
|
|
682
|
+
and splits them if they are."""
|
|
683
|
+
# Length of args should be 1 if the input is stacked (no partial
|
|
684
|
+
# stacking is supported)
|
|
685
|
+
if len(args) > 1:
|
|
686
|
+
return False, args
|
|
687
|
+
|
|
688
|
+
# Type of args should be parameter
|
|
689
|
+
if not is_par:
|
|
690
|
+
return False, args
|
|
691
|
+
|
|
692
|
+
# args[0] should be numpy array or CUQIarray
|
|
693
|
+
is_CUQIarray = isinstance(args[0], CUQIarray)
|
|
694
|
+
is_numpy_array = isinstance(args[0], np.ndarray)
|
|
695
|
+
if not is_CUQIarray and not is_numpy_array:
|
|
696
|
+
return False, args
|
|
697
|
+
|
|
698
|
+
# Shape of args[0] should be (domain_dim,)
|
|
699
|
+
if not args[0].shape == (self.domain_dim,):
|
|
700
|
+
return False, args
|
|
701
|
+
|
|
702
|
+
# Ensure domain geometry is _ProductGeometry
|
|
703
|
+
if not isinstance(
|
|
704
|
+
self.domain_geometry, cuqi.experimental.geometry._ProductGeometry
|
|
705
|
+
):
|
|
706
|
+
return False, args
|
|
707
|
+
|
|
708
|
+
# Split the stacked input
|
|
709
|
+
split_args = np.split(args[0], self.domain_geometry.stacked_par_split_indices)
|
|
710
|
+
|
|
711
|
+
# Covert split args to CUQIarray if input is CUQIarray
|
|
712
|
+
if is_CUQIarray:
|
|
713
|
+
split_args = [
|
|
714
|
+
CUQIarray(arg, is_par=True, geometry=self.domain_geometry.geometries[i])
|
|
715
|
+
for i, arg in enumerate(split_args)
|
|
716
|
+
]
|
|
717
|
+
|
|
718
|
+
return True, split_args
|
|
719
|
+
|
|
309
720
|
def forward(self, *args, is_par=True, **kwargs):
|
|
310
721
|
""" Forward function of the model.
|
|
311
722
|
|
|
312
|
-
Forward converts the input to function values (if needed) using the domain geometry of the model.
|
|
313
|
-
Forward converts the output function values to parameters using the range geometry of the model.
|
|
723
|
+
Forward converts the input to function values (if needed) using the domain geometry of the model. Then it applies the forward operator to the function values and converts the output to parameters using the range geometry of the model.
|
|
314
724
|
|
|
315
725
|
Parameters
|
|
316
726
|
----------
|
|
317
|
-
*args :
|
|
318
|
-
The
|
|
727
|
+
*args : ndarrays or cuqi.array.CUQIarray objects or cuqi.samples.Samples objects
|
|
728
|
+
Positional arguments for the forward operator. The forward operator input can be specified as either positional arguments or keyword arguments but not both.
|
|
319
729
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
730
|
+
If the input is specified as positional arguments, the order of the arguments should match the non_default_args of the model.
|
|
731
|
+
|
|
732
|
+
is_par : bool or a tuple of bools
|
|
733
|
+
If True, the inputs in `args` or `kwargs` are assumed to be parameters.
|
|
734
|
+
If False, the inputs in `args` or `kwargs` are assumed to be function values.
|
|
735
|
+
If `is_par` is a tuple of bools, the inputs are assumed to be parameters or function values based on the corresponding boolean value in the tuple.
|
|
323
736
|
|
|
324
|
-
**kwargs : keyword arguments
|
|
325
|
-
|
|
737
|
+
**kwargs : keyword arguments
|
|
738
|
+
keyword arguments for the forward operator. The forward operator input can be specified as either positional arguments or keyword arguments but not both.
|
|
739
|
+
|
|
740
|
+
If the input is specified as keyword arguments, the keys should match the non_default_args of the model.
|
|
326
741
|
|
|
327
742
|
Returns
|
|
328
743
|
-------
|
|
329
|
-
ndarray or cuqi.array.CUQIarray
|
|
744
|
+
ndarray or cuqi.array.CUQIarray or cuqi.samples.Samples object
|
|
330
745
|
The model output. Always returned as parameters.
|
|
331
746
|
"""
|
|
332
747
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
# For now only support one input
|
|
340
|
-
if len(kwargs) > 1:
|
|
341
|
-
raise ValueError("The model input is specified by more than one argument. This is not supported.")
|
|
748
|
+
# Add args to kwargs and ensure the order of the arguments matches the
|
|
749
|
+
# non_default_args of the forward function
|
|
750
|
+
kwargs = self._parse_args_add_to_kwargs(
|
|
751
|
+
*args, **kwargs, is_par=is_par, map_name="model"
|
|
752
|
+
)
|
|
342
753
|
|
|
343
|
-
#
|
|
344
|
-
|
|
754
|
+
# extract args from kwargs
|
|
755
|
+
args = list(kwargs.values())
|
|
345
756
|
|
|
346
|
-
# If input is a distribution, we simply change the parameter name of
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
new_model._non_default_args = [x.name] # Defaults to x if distribution had no name
|
|
352
|
-
return new_model
|
|
757
|
+
# If input is a distribution, we simply change the parameter name of
|
|
758
|
+
# model to match the distribution name
|
|
759
|
+
if all(isinstance(x, cuqi.distribution.Distribution)
|
|
760
|
+
for x in kwargs.values()):
|
|
761
|
+
return self._handle_case_when_model_input_is_distributions(kwargs)
|
|
353
762
|
|
|
354
763
|
# If input is a random variable, we handle it separately
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
764
|
+
elif all(isinstance(x, cuqi.experimental.algebra.RandomVariable)
|
|
765
|
+
for x in kwargs.values()):
|
|
766
|
+
return self._handle_case_when_model_input_is_random_variables(kwargs)
|
|
767
|
+
|
|
358
768
|
# If input is a Node from internal abstract syntax tree, we let the Node handle the operation
|
|
359
769
|
# We use NotImplemented to indicate that the operation is not supported from the Model class
|
|
360
770
|
# in case of operations such as "@" that can be interpreted as both __matmul__ and __rmatmul__
|
|
361
771
|
# the operation may be delegated to the Node class.
|
|
362
|
-
|
|
772
|
+
elif any(isinstance(args_i, cuqi.experimental.algebra.Node) for args_i in args):
|
|
363
773
|
return NotImplemented
|
|
364
774
|
|
|
365
775
|
# Else we apply the forward operator
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
776
|
+
# if model has _original_non_default_args, we use it to replace the
|
|
777
|
+
# kwargs keys so that it matches self._forward_func signature
|
|
778
|
+
if hasattr(self, '_original_non_default_args'):
|
|
779
|
+
kwargs = {k:v for k,v in zip(self._original_non_default_args, args)}
|
|
780
|
+
return self._apply_func(func=self._forward_func,
|
|
781
|
+
fwd=True,
|
|
782
|
+
is_par=is_par,
|
|
783
|
+
**kwargs)
|
|
784
|
+
|
|
785
|
+
def _correct_distribution_dimension(self, distributions):
|
|
786
|
+
"""Private function that checks if the dimension of the
|
|
787
|
+
distributions matches the domain dimension of the model."""
|
|
788
|
+
if len(distributions) == 1:
|
|
789
|
+
return list(distributions)[0].dim == self.domain_dim
|
|
790
|
+
elif len(distributions) > 1 and isinstance(
|
|
791
|
+
self.domain_geometry, cuqi.experimental.geometry._ProductGeometry
|
|
792
|
+
):
|
|
793
|
+
return all(
|
|
794
|
+
d.dim == self.domain_geometry.par_dim_list[i]
|
|
795
|
+
for i, d in enumerate(distributions)
|
|
796
|
+
)
|
|
797
|
+
else:
|
|
798
|
+
return False
|
|
370
799
|
|
|
371
|
-
def
|
|
372
|
-
|
|
800
|
+
def _handle_case_when_model_input_is_distributions(self, kwargs):
|
|
801
|
+
"""Private function that handles the case of the input being a
|
|
802
|
+
distribution or multiple distributions."""
|
|
803
|
+
|
|
804
|
+
if not self._correct_distribution_dimension(kwargs.values()):
|
|
805
|
+
raise ValueError(
|
|
806
|
+
"Attempting to match parameter name of Model with given distribution(s), but distribution(s) dimension(s) does not match model input dimension(s)."
|
|
807
|
+
)
|
|
808
|
+
new_model = copy(self)
|
|
809
|
+
|
|
810
|
+
# Store the original non_default_args of the model
|
|
811
|
+
new_model._original_non_default_args = self._non_default_args
|
|
812
|
+
|
|
813
|
+
# Update the non_default_args of the model to match the distribution
|
|
814
|
+
# names. Defaults to x in the case of only one distribution that has no
|
|
815
|
+
# name
|
|
816
|
+
new_model._stored_non_default_args = [x.name for x in kwargs.values()]
|
|
817
|
+
|
|
818
|
+
# If there is a repeated name, raise an error
|
|
819
|
+
if len(set(new_model._stored_non_default_args)) != len(
|
|
820
|
+
new_model._stored_non_default_args
|
|
821
|
+
):
|
|
822
|
+
raise ValueError(
|
|
823
|
+
"Attempting to match parameter name of Model with given distributions, but distribution names are not unique. Please provide unique names for the distributions."
|
|
824
|
+
)
|
|
825
|
+
|
|
826
|
+
return new_model
|
|
827
|
+
|
|
828
|
+
def _handle_case_when_model_input_is_random_variables(self, kwargs):
|
|
829
|
+
""" Private function that handles the case of the input being a random variable. """
|
|
830
|
+
# If random variable is not a leaf-type node (e.g. internal node) we return NotImplemented
|
|
831
|
+
if any(not isinstance(x.tree, cuqi.experimental.algebra.VariableNode) for x in kwargs.values()):
|
|
832
|
+
return NotImplemented
|
|
833
|
+
|
|
834
|
+
# Extract the random variable distributions and check dimensions consistency with domain geometry
|
|
835
|
+
distributions = [value.distribution for value in kwargs.values()]
|
|
836
|
+
if not self._correct_distribution_dimension(distributions):
|
|
837
|
+
raise ValueError("Attempting to match parameter name of Model with given random variable(s), but random variable dimension(s) does not match model input dimension(s).")
|
|
838
|
+
|
|
839
|
+
new_model = copy(self)
|
|
840
|
+
|
|
841
|
+
# Store the original non_default_args of the model
|
|
842
|
+
new_model._original_non_default_args = self._non_default_args
|
|
843
|
+
|
|
844
|
+
# Update the non_default_args of the model to match the random variable
|
|
845
|
+
# names. Defaults to x in the case of only one random variable that has
|
|
846
|
+
# no name
|
|
847
|
+
new_model._stored_non_default_args = [x.name for x in distributions]
|
|
848
|
+
|
|
849
|
+
# If there is a repeated name, raise an error
|
|
850
|
+
if len(set(new_model._stored_non_default_args)) != len(
|
|
851
|
+
new_model._stored_non_default_args
|
|
852
|
+
):
|
|
853
|
+
raise ValueError(
|
|
854
|
+
"Attempting to match parameter name of Model with given random variables, but random variables names are not unique. Please provide unique names for the random variables."
|
|
855
|
+
)
|
|
856
|
+
|
|
857
|
+
return new_model
|
|
373
858
|
|
|
374
|
-
def gradient(
|
|
375
|
-
|
|
859
|
+
def gradient(
|
|
860
|
+
self, direction, *args, is_direction_par=True, is_var_par=True, **kwargs
|
|
861
|
+
):
|
|
862
|
+
"""Gradient of the forward operator (Direction-Jacobian product)
|
|
376
863
|
|
|
377
|
-
|
|
378
|
-
forward operator and the Jacobian of the forward operator.
|
|
864
|
+
The gradient computes the Vector-Jacobian product (VJP) of the forward operator evaluated at the given model input and the given vector (direction).
|
|
379
865
|
|
|
380
866
|
Parameters
|
|
381
867
|
----------
|
|
382
|
-
direction : ndarray
|
|
383
|
-
The direction to compute the gradient.
|
|
868
|
+
direction : ndarray or cuqi.array.CUQIarray
|
|
869
|
+
The direction at which to compute the gradient.
|
|
384
870
|
|
|
385
|
-
|
|
386
|
-
|
|
871
|
+
*args : ndarrays or cuqi.array.CUQIarray objects
|
|
872
|
+
Positional arguments for the values at which to compute the gradient. The gradient operator input can be specified as either positional arguments or keyword arguments but not both.
|
|
873
|
+
|
|
874
|
+
If the input is specified as positional arguments, the order of the arguments should match the non_default_args of the model.
|
|
387
875
|
|
|
388
876
|
is_direction_par : bool
|
|
389
877
|
If True, `direction` is assumed to be parameters.
|
|
390
878
|
If False, `direction` is assumed to be function values.
|
|
391
879
|
|
|
392
|
-
|
|
393
|
-
If True, `
|
|
394
|
-
If False, `
|
|
395
|
-
|
|
880
|
+
is_var_par : bool or a tuple of bools
|
|
881
|
+
If True, the inputs in `args` or `kwargs` are assumed to be parameters.
|
|
882
|
+
If False, the inputs in `args` or `kwargs` are assumed to be function values.
|
|
883
|
+
If `is_var_par` is a tuple of bools, the inputs in `args` or `kwargs` are assumed to be parameters or function values based on the corresponding boolean value in the tuple.
|
|
396
884
|
"""
|
|
397
|
-
#
|
|
398
|
-
#
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
885
|
+
# Add args to kwargs and ensure the order of the arguments matches the
|
|
886
|
+
# non_default_args of the forward function
|
|
887
|
+
kwargs = self._parse_args_add_to_kwargs(
|
|
888
|
+
*args, **kwargs, is_par=is_var_par, map_name="gradient"
|
|
889
|
+
)
|
|
890
|
+
|
|
891
|
+
# Obtain the parameters representation of the variables and raise an
|
|
892
|
+
# error if it cannot be obtained
|
|
893
|
+
error_message = (
|
|
894
|
+
"For the gradient to be computed, is_var_par needs to be True and the variables in kwargs needs to be parameter value, not function value. Alternatively, the model domain_geometry:"
|
|
895
|
+
+ f" {self.domain_geometry} "
|
|
896
|
+
+ "should have an implementation of the method fun2par"
|
|
897
|
+
)
|
|
405
898
|
try:
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
899
|
+
kwargs_par = self._2par(
|
|
900
|
+
geometry=self.domain_geometry,
|
|
901
|
+
is_par=is_var_par,
|
|
902
|
+
to_CUQIarray=False,
|
|
903
|
+
**kwargs,
|
|
904
|
+
)
|
|
411
905
|
# NotImplementedError will be raised if fun2par of the geometry is not
|
|
412
906
|
# implemented and ValueError will be raised when imap is not set in
|
|
413
907
|
# MappedGeometry
|
|
414
908
|
except ValueError as e:
|
|
415
|
-
raise ValueError(
|
|
416
|
-
|
|
417
|
-
|
|
909
|
+
raise ValueError(
|
|
910
|
+
error_message
|
|
911
|
+
+ " ,including an implementation of imap for MappedGeometry"
|
|
912
|
+
)
|
|
418
913
|
except NotImplementedError as e:
|
|
419
914
|
raise NotImplementedError(error_message)
|
|
420
|
-
|
|
421
|
-
# Check for other errors that may prevent computing the gradient
|
|
422
|
-
self._check_gradient_can_be_computed(direction, wrt)
|
|
423
|
-
|
|
424
|
-
wrt = self._2fun(wrt, self.domain_geometry, is_par=is_wrt_par)
|
|
425
|
-
|
|
426
|
-
# Store if the input direction is CUQIarray
|
|
427
|
-
is_direction_CUQIarray = type(direction) is CUQIarray
|
|
428
915
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
#
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
916
|
+
# Check for other errors that may prevent computing the gradient
|
|
917
|
+
self._check_gradient_can_be_computed(direction, kwargs)
|
|
918
|
+
|
|
919
|
+
# Also obtain the function values representation of the variables
|
|
920
|
+
kwargs_fun = self._2fun(
|
|
921
|
+
geometry=self.domain_geometry, is_par=is_var_par, **kwargs
|
|
922
|
+
)
|
|
923
|
+
|
|
924
|
+
# Store if any of the inputs is a CUQIarray
|
|
925
|
+
to_CUQIarray = isinstance(direction, CUQIarray) or any(
|
|
926
|
+
isinstance(x, CUQIarray) for x in kwargs_fun.values()
|
|
927
|
+
)
|
|
928
|
+
|
|
929
|
+
# Turn to_CUQIarray to a tuple of bools of the same length as kwargs_fun
|
|
930
|
+
to_CUQIarray = tuple([to_CUQIarray] * len(kwargs_fun))
|
|
931
|
+
|
|
932
|
+
# Convert direction to function value
|
|
933
|
+
direction_fun = self._2fun(
|
|
934
|
+
direction=direction, geometry=self.range_geometry, is_par=is_direction_par
|
|
935
|
+
)
|
|
936
|
+
|
|
937
|
+
# If model has _original_non_default_args, we use it to replace the
|
|
938
|
+
# kwargs keys so that it matches self._gradient_func signature
|
|
939
|
+
if hasattr(self, '_original_non_default_args'):
|
|
940
|
+
args_fun = list(kwargs_fun.values())
|
|
941
|
+
kwargs_fun = {
|
|
942
|
+
k: v for k, v in zip(self._original_non_default_args, args_fun)
|
|
943
|
+
}
|
|
944
|
+
# Append the direction to the kwargs_fun as first input
|
|
945
|
+
kwargs_fun_grad_input = {**direction_fun, **kwargs_fun}
|
|
946
|
+
|
|
947
|
+
# Form 1 of gradient (callable)
|
|
948
|
+
if callable(self._gradient_func):
|
|
949
|
+
grad = self._gradient_func(**kwargs_fun_grad_input)
|
|
950
|
+
grad_is_par = False # Assume gradient is function value
|
|
951
|
+
|
|
952
|
+
# Form 2 of gradient (tuple of callables)
|
|
953
|
+
elif isinstance(self._gradient_func, tuple):
|
|
954
|
+
grad = []
|
|
955
|
+
for i, grad_func in enumerate(self._gradient_func):
|
|
956
|
+
if grad_func is not None:
|
|
957
|
+
grad.append(grad_func(**kwargs_fun_grad_input))
|
|
958
|
+
else:
|
|
959
|
+
grad.append(None)
|
|
960
|
+
# set the ith item of to_CUQIarray tuple to False
|
|
961
|
+
# because the ith gradient is None
|
|
962
|
+
to_CUQIarray = to_CUQIarray[:i] + (False,) + to_CUQIarray[i + 1 :]
|
|
963
|
+
grad_is_par = False # Assume gradient is function value
|
|
964
|
+
|
|
965
|
+
grad = self._apply_chain_rule_to_account_for_domain_geometry_gradient(
|
|
966
|
+
kwargs_par, grad, grad_is_par, to_CUQIarray
|
|
967
|
+
)
|
|
968
|
+
|
|
969
|
+
if len(grad) == 1:
|
|
970
|
+
return list(grad.values())[0]
|
|
971
|
+
elif self._gradient_output_stacked:
|
|
972
|
+
return np.hstack(
|
|
973
|
+
[
|
|
974
|
+
(
|
|
975
|
+
v.to_numpy()
|
|
976
|
+
if isinstance(v, CUQIarray)
|
|
977
|
+
else force_ndarray(v, flatten=True)
|
|
978
|
+
)
|
|
979
|
+
for v in list(grad.values())
|
|
980
|
+
]
|
|
981
|
+
)
|
|
448
982
|
|
|
449
983
|
return grad
|
|
450
|
-
|
|
451
|
-
def _check_gradient_can_be_computed(self, direction,
|
|
452
|
-
"""
|
|
984
|
+
|
|
985
|
+
def _check_gradient_can_be_computed(self, direction, kwargs_dict):
|
|
986
|
+
"""Private function that checks if the gradient can be computed. By
|
|
453
987
|
raising an error for the cases where the gradient cannot be computed."""
|
|
454
988
|
|
|
455
989
|
# Raise an error if _gradient_func function is not set
|
|
456
990
|
if self._gradient_func is None:
|
|
457
991
|
raise NotImplementedError("Gradient is not implemented for this model.")
|
|
458
|
-
|
|
459
|
-
# Raise error if either the direction or
|
|
460
|
-
if isinstance(direction, Samples) or
|
|
461
|
-
|
|
462
|
-
|
|
992
|
+
|
|
993
|
+
# Raise an error if either the direction or kwargs are Samples objects
|
|
994
|
+
if isinstance(direction, Samples) or any(
|
|
995
|
+
isinstance(x, Samples) for x in kwargs_dict.values()
|
|
996
|
+
):
|
|
997
|
+
raise NotImplementedError(
|
|
998
|
+
"Gradient is not implemented for input of type Samples."
|
|
999
|
+
)
|
|
1000
|
+
|
|
463
1001
|
# Raise an error if range_geometry is not in the list returned by
|
|
464
|
-
# `_get_identity_geometries()`. i.e. The Jacobian of its
|
|
465
|
-
# par2fun map is not identity.
|
|
466
|
-
#TODO: Add range geometry gradient to the chain rule
|
|
1002
|
+
# `_get_identity_geometries()`. i.e. The Jacobian of its
|
|
1003
|
+
# par2fun map is not identity.
|
|
1004
|
+
# TODO: Add range geometry gradient to the chain rule
|
|
467
1005
|
if not type(self.range_geometry) in _get_identity_geometries():
|
|
468
|
-
raise NotImplementedError(
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
#
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
1006
|
+
raise NotImplementedError(
|
|
1007
|
+
"Gradient is not implemented for model {} with range geometry {}. You can use one of the geometries in the list {}.".format(
|
|
1008
|
+
self,
|
|
1009
|
+
self.range_geometry,
|
|
1010
|
+
[i_g.__name__ for i_g in _get_identity_geometries()],
|
|
1011
|
+
)
|
|
1012
|
+
)
|
|
1013
|
+
|
|
1014
|
+
# Raise an error if domain_geometry (or its components in case of
|
|
1015
|
+
# _ProductGeometry) does not have gradient attribute and is not in the
|
|
1016
|
+
# list returned by `_get_identity_geometries()`. i.e. The Jacobian of its
|
|
1017
|
+
# par2fun map is not identity.
|
|
1018
|
+
domain_geometries = (
|
|
1019
|
+
self.domain_geometry.geometries
|
|
1020
|
+
if isinstance(
|
|
1021
|
+
self.domain_geometry, cuqi.experimental.geometry._ProductGeometry
|
|
1022
|
+
)
|
|
1023
|
+
else [self.domain_geometry]
|
|
1024
|
+
)
|
|
1025
|
+
for domain_geometry in domain_geometries:
|
|
1026
|
+
if (
|
|
1027
|
+
not hasattr(domain_geometry, "gradient")
|
|
1028
|
+
and not type(domain_geometry) in _get_identity_geometries()
|
|
1029
|
+
):
|
|
1030
|
+
raise NotImplementedError(
|
|
1031
|
+
"Gradient is not implemented for model \n{}\nwith domain geometry (or domain geometry component) {}. The domain geometries should have gradient method or be from the geometries in the list {}.".format(
|
|
1032
|
+
self,
|
|
1033
|
+
domain_geometry,
|
|
1034
|
+
[i_g.__name__ for i_g in _get_identity_geometries()],
|
|
1035
|
+
)
|
|
1036
|
+
)
|
|
1037
|
+
|
|
1038
|
+
def _apply_chain_rule_to_account_for_domain_geometry_gradient(self,
|
|
1039
|
+
kwargs_par,
|
|
1040
|
+
grad,
|
|
1041
|
+
grad_is_par,
|
|
1042
|
+
to_CUQIarray):
|
|
1043
|
+
""" Private function that applies the chain rule to account for the
|
|
1044
|
+
gradient of the domain geometry. That is, it computes the gradient of
|
|
1045
|
+
the function values with respect to the parameters values."""
|
|
1046
|
+
# Create list of domain geometries
|
|
1047
|
+
geometries = (
|
|
1048
|
+
self.domain_geometry.geometries
|
|
1049
|
+
if isinstance(self.domain_geometry, cuqi.experimental.geometry._ProductGeometry)
|
|
1050
|
+
else [self.domain_geometry]
|
|
1051
|
+
)
|
|
1052
|
+
|
|
1053
|
+
# turn grad_is_par to a tuple of bools if it is not already
|
|
1054
|
+
if isinstance(grad_is_par, bool):
|
|
1055
|
+
grad_is_par = tuple([grad_is_par]*len(grad))
|
|
1056
|
+
|
|
1057
|
+
# If the domain geometry is a _ProductGeometry and the gradient is
|
|
1058
|
+
# stacked, split it
|
|
1059
|
+
if (
|
|
1060
|
+
isinstance(
|
|
1061
|
+
self.domain_geometry, cuqi.experimental.geometry._ProductGeometry
|
|
1062
|
+
)
|
|
1063
|
+
and not isinstance(grad, (list, tuple))
|
|
1064
|
+
and isinstance(grad, np.ndarray)
|
|
1065
|
+
):
|
|
1066
|
+
grad = np.split(grad, self.domain_geometry.stacked_par_split_indices)
|
|
1067
|
+
|
|
1068
|
+
# If the domain geometry is not a _ProductGeometry, turn grad into a
|
|
1069
|
+
# list of length 1, so that we can iterate over it
|
|
1070
|
+
if not isinstance(self.domain_geometry, cuqi.experimental.geometry._ProductGeometry):
|
|
1071
|
+
grad = [grad]
|
|
1072
|
+
|
|
1073
|
+
# apply the gradient of each geometry component
|
|
1074
|
+
grad_kwargs = {}
|
|
1075
|
+
for i, (k, v_par) in enumerate(kwargs_par.items()):
|
|
1076
|
+
if hasattr(geometries[i], 'gradient') and grad[i] is not None:
|
|
1077
|
+
grad_kwargs[k] = geometries[i].gradient(grad[i], v_par)
|
|
1078
|
+
# update the ith component of grad_is_par to True
|
|
1079
|
+
grad_is_par = grad_is_par[:i] + (True,) + grad_is_par[i+1:]
|
|
1080
|
+
else:
|
|
1081
|
+
grad_kwargs[k] = grad[i]
|
|
1082
|
+
|
|
1083
|
+
# convert the computed gradient to parameters
|
|
1084
|
+
grad = self._2par(geometry=self.domain_geometry,
|
|
1085
|
+
to_CUQIarray=to_CUQIarray,
|
|
1086
|
+
is_par=grad_is_par,
|
|
1087
|
+
**grad_kwargs)
|
|
1088
|
+
|
|
1089
|
+
return grad
|
|
1090
|
+
|
|
1091
|
+
def __call__(self, *args, **kwargs):
|
|
1092
|
+
return self.forward(*args, **kwargs)
|
|
491
1093
|
|
|
492
1094
|
def __len__(self):
|
|
493
1095
|
return self.range_dim
|
|
494
1096
|
|
|
495
1097
|
def __repr__(self) -> str:
|
|
496
|
-
|
|
497
|
-
|
|
1098
|
+
kwargs = {}
|
|
1099
|
+
if self.number_of_inputs > 1:
|
|
1100
|
+
pad = " " * len("CUQI {}: ".format(self.__class__.__name__))
|
|
1101
|
+
kwargs["pad"]=pad
|
|
1102
|
+
return "CUQI {}: {} -> {}.\n Forward parameters: {}.".format(self.__class__.__name__,self.domain_geometry.__repr__(**kwargs),self.range_geometry,self._non_default_args)
|
|
498
1103
|
|
|
499
1104
|
class AffineModel(Model):
|
|
500
1105
|
""" Model class representing an affine model, i.e. a linear operator with a fixed shift. For linear models, represented by a linear operator only, see :class:`~cuqi.model.LinearModel`.
|
|
@@ -533,7 +1138,7 @@ class AffineModel(Model):
|
|
|
533
1138
|
if hasattr(linear_operator, '__matmul__') and hasattr(linear_operator, 'T'):
|
|
534
1139
|
if linear_operator_adjoint is not None:
|
|
535
1140
|
raise ValueError("Adjoint of linear operator should not be provided when linear operator is a matrix. If you want to provide an adjoint, use a callable function for the linear operator.")
|
|
536
|
-
|
|
1141
|
+
|
|
537
1142
|
matrix = linear_operator
|
|
538
1143
|
|
|
539
1144
|
linear_operator = lambda x: matrix@x
|
|
@@ -559,11 +1164,50 @@ class AffineModel(Model):
|
|
|
559
1164
|
if linear_operator_adjoint is not None and not callable(linear_operator_adjoint):
|
|
560
1165
|
raise TypeError("Linear operator adjoint must be defined as a callable function of some kind")
|
|
561
1166
|
|
|
1167
|
+
# If linear operator is of type Model, it needs to be a LinearModel
|
|
1168
|
+
if isinstance(linear_operator, Model) and not isinstance(
|
|
1169
|
+
linear_operator, LinearModel
|
|
1170
|
+
):
|
|
1171
|
+
raise TypeError(
|
|
1172
|
+
"The linear operator should be a LinearModel object, a callable function or a matrix."
|
|
1173
|
+
)
|
|
1174
|
+
|
|
1175
|
+
# If the adjoint operator is of type Model, it needs to be a LinearModel
|
|
1176
|
+
if isinstance(linear_operator_adjoint, Model) and not isinstance(
|
|
1177
|
+
linear_operator_adjoint, LinearModel
|
|
1178
|
+
):
|
|
1179
|
+
raise TypeError(
|
|
1180
|
+
"The adjoint linear operator should be a LinearModel object, a callable function or a matrix."
|
|
1181
|
+
)
|
|
1182
|
+
|
|
1183
|
+
# Additional checks if the linear_operator is not a LinearModel:
|
|
1184
|
+
if not isinstance(linear_operator, LinearModel):
|
|
1185
|
+
# Ensure the linear operator has exactly one input argument
|
|
1186
|
+
if len(cuqi.utilities.get_non_default_args(linear_operator)) != 1:
|
|
1187
|
+
raise ValueError(
|
|
1188
|
+
"The linear operator should have exactly one input argument."
|
|
1189
|
+
)
|
|
1190
|
+
# Ensure the adjoint linear operator has exactly one input argument
|
|
1191
|
+
if (
|
|
1192
|
+
linear_operator_adjoint is not None
|
|
1193
|
+
and len(cuqi.utilities.get_non_default_args(linear_operator_adjoint))
|
|
1194
|
+
!= 1
|
|
1195
|
+
):
|
|
1196
|
+
raise ValueError(
|
|
1197
|
+
"The adjoint linear operator should have exactly one input argument."
|
|
1198
|
+
)
|
|
1199
|
+
|
|
562
1200
|
# Check size of shift and match against range_geometry
|
|
563
1201
|
if not np.isscalar(shift):
|
|
564
1202
|
if len(shift) != range_geometry.par_dim:
|
|
565
1203
|
raise ValueError("The shift should have the same dimension as the range geometry.")
|
|
566
1204
|
|
|
1205
|
+
# Store linear operator privately
|
|
1206
|
+
# Note: we need to set the _linear_operator before calling the
|
|
1207
|
+
# super().__init__() because it is needed when calling the property
|
|
1208
|
+
# _non_default_args within the super().__init__()
|
|
1209
|
+
self._linear_operator = linear_operator
|
|
1210
|
+
|
|
567
1211
|
# Initialize Model class
|
|
568
1212
|
super().__init__(linear_operator, range_geometry, domain_geometry)
|
|
569
1213
|
|
|
@@ -573,20 +1217,27 @@ class AffineModel(Model):
|
|
|
573
1217
|
# Store shift as private attribute
|
|
574
1218
|
self._shift = shift
|
|
575
1219
|
|
|
576
|
-
# Store linear operator privately
|
|
577
|
-
self._linear_operator = linear_operator
|
|
578
1220
|
|
|
579
1221
|
# Store adjoint function
|
|
580
1222
|
self._linear_operator_adjoint = linear_operator_adjoint
|
|
581
1223
|
|
|
582
1224
|
# Define gradient
|
|
583
|
-
self._gradient_func = lambda direction,
|
|
1225
|
+
self._gradient_func = lambda direction, *args, **kwargs: linear_operator_adjoint(direction)
|
|
584
1226
|
|
|
585
1227
|
# Update forward function to include shift (overwriting the one from Model class)
|
|
586
1228
|
self._forward_func = lambda *args, **kwargs: linear_operator(*args, **kwargs) + shift
|
|
587
1229
|
|
|
588
|
-
#
|
|
589
|
-
self.
|
|
1230
|
+
# Set stored_non_default_args to None
|
|
1231
|
+
self._stored_non_default_args = None
|
|
1232
|
+
|
|
1233
|
+
@property
|
|
1234
|
+
def _non_default_args(self):
|
|
1235
|
+
if self._stored_non_default_args is None:
|
|
1236
|
+
# Use arguments from user's callable linear operator
|
|
1237
|
+
self._stored_non_default_args = cuqi.utilities.get_non_default_args(
|
|
1238
|
+
self._linear_operator
|
|
1239
|
+
)
|
|
1240
|
+
return self._stored_non_default_args
|
|
590
1241
|
|
|
591
1242
|
@property
|
|
592
1243
|
def shift(self):
|
|
@@ -599,19 +1250,35 @@ class AffineModel(Model):
|
|
|
599
1250
|
self._shift = value
|
|
600
1251
|
self._forward_func = lambda *args, **kwargs: self._linear_operator(*args, **kwargs) + value
|
|
601
1252
|
|
|
602
|
-
def _forward_func_no_shift(self,
|
|
603
|
-
"""
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
1253
|
+
def _forward_func_no_shift(self, *args, is_par=True, **kwargs):
|
|
1254
|
+
"""Helper function for computing the forward operator without the shift."""
|
|
1255
|
+
# convert args to kwargs
|
|
1256
|
+
kwargs = self._parse_args_add_to_kwargs(
|
|
1257
|
+
*args, **kwargs, map_name="model", is_par=is_par
|
|
1258
|
+
)
|
|
1259
|
+
args = list(kwargs.values())
|
|
1260
|
+
# if model has _original_non_default_args, we use it to replace the
|
|
1261
|
+
# kwargs keys so that it matches self._linear_operator signature
|
|
1262
|
+
if hasattr(self, '_original_non_default_args'):
|
|
1263
|
+
kwargs = {k:v for k,v in zip(self._original_non_default_args, args)}
|
|
1264
|
+
return self._apply_func(self._linear_operator, **kwargs, is_par=is_par)
|
|
1265
|
+
|
|
1266
|
+
def _adjoint_func_no_shift(self, *args, is_par=True, **kwargs):
|
|
1267
|
+
"""Helper function for computing the adjoint operator without the shift."""
|
|
1268
|
+
# convert args to kwargs
|
|
1269
|
+
kwargs = self._parse_args_add_to_kwargs(
|
|
1270
|
+
*args,
|
|
1271
|
+
**kwargs,
|
|
1272
|
+
map_name='adjoint',
|
|
1273
|
+
is_par=is_par,
|
|
1274
|
+
non_default_args=cuqi.utilities.get_non_default_args(
|
|
1275
|
+
self._linear_operator_adjoint
|
|
1276
|
+
),
|
|
1277
|
+
)
|
|
1278
|
+
return self._apply_func(
|
|
1279
|
+
self._linear_operator_adjoint, **kwargs, is_par=is_par, fwd=False
|
|
1280
|
+
)
|
|
608
1281
|
|
|
609
|
-
def _adjoint_func_no_shift(self, y, is_par=True):
|
|
610
|
-
""" Helper function for computing the adjoint operator without the shift. """
|
|
611
|
-
return self._apply_func(self._linear_operator_adjoint,
|
|
612
|
-
self.domain_geometry,
|
|
613
|
-
self.range_geometry,
|
|
614
|
-
y, is_par)
|
|
615
1282
|
|
|
616
1283
|
class LinearModel(AffineModel):
|
|
617
1284
|
"""Model based on a Linear forward operator.
|
|
@@ -677,50 +1344,67 @@ class LinearModel(AffineModel):
|
|
|
677
1344
|
Note that you would need to specify the range and domain geometries in this
|
|
678
1345
|
case as they cannot be inferred from the forward and adjoint functions.
|
|
679
1346
|
"""
|
|
680
|
-
|
|
1347
|
+
|
|
681
1348
|
def __init__(self, forward, adjoint=None, range_geometry=None, domain_geometry=None):
|
|
682
1349
|
|
|
683
|
-
#Initialize as AffineModel with shift=0
|
|
1350
|
+
# Initialize as AffineModel with shift=0
|
|
684
1351
|
super().__init__(forward, 0, adjoint, range_geometry, domain_geometry)
|
|
685
1352
|
|
|
686
|
-
def adjoint(self,
|
|
1353
|
+
def adjoint(self, *args, is_par=True, **kwargs):
|
|
687
1354
|
""" Adjoint of the model.
|
|
688
1355
|
|
|
689
|
-
Adjoint converts the input to function values (if needed) using the range geometry of the model.
|
|
690
|
-
Adjoint converts the output function values to parameters using the range geometry of the model.
|
|
1356
|
+
Adjoint converts the input to function values (if needed) using the range geometry of the model then applies the adjoint operator to the function values and converts the output function values to parameters using the domain geometry of the model.
|
|
691
1357
|
|
|
692
1358
|
Parameters
|
|
693
1359
|
----------
|
|
694
|
-
|
|
695
|
-
The adjoint
|
|
1360
|
+
*args : ndarrays or cuqi.array.CUQIarray object
|
|
1361
|
+
Positional arguments for the adjoint operator ( maximum one argument). The adjoint operator input can be specified as either positional arguments or keyword arguments but not both.
|
|
1362
|
+
|
|
1363
|
+
**kwargs : keyword arguments
|
|
1364
|
+
keyword arguments for the adjoint operator (maximum one argument). The adjoint operator input can be specified as either positional arguments or keyword arguments but not both.
|
|
1365
|
+
|
|
1366
|
+
If the input is specified as keyword arguments, the keys should match the non_default_args of the model.
|
|
696
1367
|
|
|
697
1368
|
Returns
|
|
698
1369
|
-------
|
|
699
1370
|
ndarray or cuqi.array.CUQIarray
|
|
700
1371
|
The adjoint model output. Always returned as parameters.
|
|
701
1372
|
"""
|
|
1373
|
+
kwargs = self._parse_args_add_to_kwargs(
|
|
1374
|
+
*args,
|
|
1375
|
+
**kwargs,
|
|
1376
|
+
map_name='adjoint',
|
|
1377
|
+
is_par=is_par,
|
|
1378
|
+
non_default_args=cuqi.utilities.get_non_default_args(
|
|
1379
|
+
self._linear_operator_adjoint
|
|
1380
|
+
),
|
|
1381
|
+
)
|
|
1382
|
+
|
|
1383
|
+
# length of kwargs should be 1
|
|
1384
|
+
if len(kwargs) > 1:
|
|
1385
|
+
raise ValueError(
|
|
1386
|
+
"The adjoint operator input is specified by more than one argument. This is not supported."
|
|
1387
|
+
)
|
|
702
1388
|
if self._linear_operator_adjoint is None:
|
|
703
1389
|
raise ValueError("No adjoint operator was provided for this model.")
|
|
704
|
-
return self._apply_func(
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
1390
|
+
return self._apply_func(
|
|
1391
|
+
self._linear_operator_adjoint, **kwargs, is_par=is_par, fwd=False
|
|
1392
|
+
)
|
|
1393
|
+
|
|
1394
|
+
def __matmul__(self, *args, **kwargs):
|
|
1395
|
+
return self.forward(*args, **kwargs)
|
|
708
1396
|
|
|
709
|
-
def __matmul__(self, x):
|
|
710
|
-
return self.forward(x)
|
|
711
|
-
|
|
712
1397
|
def get_matrix(self):
|
|
713
1398
|
"""
|
|
714
1399
|
Returns an ndarray with the matrix representing the forward operator.
|
|
715
1400
|
"""
|
|
716
|
-
|
|
717
1401
|
if self._matrix is not None: #Matrix exists so return it
|
|
718
1402
|
return self._matrix
|
|
719
1403
|
else:
|
|
720
|
-
#TODO: Can we compute this faster while still in sparse format?
|
|
1404
|
+
# TODO: Can we compute this faster while still in sparse format?
|
|
721
1405
|
mat = csc_matrix((self.range_dim,0)) #Sparse (m x 1 matrix)
|
|
722
1406
|
e = np.zeros(self.domain_dim)
|
|
723
|
-
|
|
1407
|
+
|
|
724
1408
|
# Stacks sparse matrices on csc matrix
|
|
725
1409
|
for i in range(self.domain_dim):
|
|
726
1410
|
e[i] = 1
|
|
@@ -728,7 +1412,7 @@ class LinearModel(AffineModel):
|
|
|
728
1412
|
mat = hstack((mat,col_vec[:,None])) #mat[:,i] = self.forward(e)
|
|
729
1413
|
e[i] = 0
|
|
730
1414
|
|
|
731
|
-
#Store matrix for future use
|
|
1415
|
+
# Store matrix for future use
|
|
732
1416
|
self._matrix = mat
|
|
733
1417
|
|
|
734
1418
|
return self._matrix
|
|
@@ -736,26 +1420,31 @@ class LinearModel(AffineModel):
|
|
|
736
1420
|
@property
|
|
737
1421
|
def T(self):
|
|
738
1422
|
"""Transpose of linear model. Returns a new linear model acting as the transpose."""
|
|
739
|
-
transpose = LinearModel(
|
|
1423
|
+
transpose = LinearModel(
|
|
1424
|
+
self._linear_operator_adjoint,
|
|
1425
|
+
self._linear_operator,
|
|
1426
|
+
self.domain_geometry,
|
|
1427
|
+
self.range_geometry,
|
|
1428
|
+
)
|
|
740
1429
|
if self._matrix is not None:
|
|
741
1430
|
transpose._matrix = self._matrix.T
|
|
742
1431
|
return transpose
|
|
743
|
-
|
|
1432
|
+
|
|
744
1433
|
|
|
745
1434
|
class PDEModel(Model):
|
|
746
1435
|
"""
|
|
747
1436
|
Model based on an underlying cuqi.pde.PDE.
|
|
748
|
-
In the forward
|
|
1437
|
+
In the forward method the PDE is assembled, solved and observed.
|
|
749
1438
|
|
|
750
1439
|
Parameters
|
|
751
1440
|
-----------
|
|
752
|
-
|
|
753
|
-
|
|
1441
|
+
PDE : cuqi.pde.PDE
|
|
1442
|
+
The PDE that specifies the forward operator.
|
|
754
1443
|
|
|
755
|
-
range_geometry : integer or cuqi.geometry.Geometry
|
|
1444
|
+
range_geometry : integer or cuqi.geometry.Geometry, optional
|
|
756
1445
|
If integer is given, a cuqi.geometry._DefaultGeometry is created with dimension of the integer.
|
|
757
1446
|
|
|
758
|
-
domain_geometry : integer or cuqi.geometry.Geometry
|
|
1447
|
+
domain_geometry : integer or cuqi.geometry.Geometry, optional
|
|
759
1448
|
If integer is given, a cuqi.geometry._DefaultGeometry is created with dimension of the integer.
|
|
760
1449
|
|
|
761
1450
|
|
|
@@ -766,21 +1455,37 @@ class PDEModel(Model):
|
|
|
766
1455
|
|
|
767
1456
|
if not isinstance(PDE, cuqi.pde.PDE):
|
|
768
1457
|
raise ValueError("PDE needs to be a cuqi PDE.")
|
|
1458
|
+
# PDE needs to be set before calling super().__init__
|
|
1459
|
+
# for the property _non_default_args to work
|
|
1460
|
+
self.pde = PDE
|
|
1461
|
+
self._stored_non_default_args = None
|
|
769
1462
|
|
|
770
|
-
super().__init__(self._forward_func, range_geometry, domain_geometry
|
|
1463
|
+
super().__init__(self._forward_func, range_geometry, domain_geometry)
|
|
771
1464
|
|
|
772
|
-
|
|
1465
|
+
@property
|
|
1466
|
+
def _non_default_args(self):
|
|
1467
|
+
if self._stored_non_default_args is None:
|
|
1468
|
+
# extract the non-default arguments of the PDE
|
|
1469
|
+
self._stored_non_default_args = cuqi.utilities.get_non_default_args(
|
|
1470
|
+
self.pde.PDE_form
|
|
1471
|
+
)
|
|
1472
|
+
# remove t from the non-default arguments
|
|
1473
|
+
self._stored_non_default_args = self._non_default_args
|
|
1474
|
+
if "t" in self._non_default_args:
|
|
1475
|
+
self._stored_non_default_args.remove("t")
|
|
773
1476
|
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
1477
|
+
return self._stored_non_default_args
|
|
1478
|
+
|
|
1479
|
+
def _forward_func(self, **kwargs):
|
|
1480
|
+
|
|
1481
|
+
self.pde.assemble(**kwargs)
|
|
777
1482
|
|
|
778
1483
|
sol, info = self.pde.solve()
|
|
779
1484
|
|
|
780
1485
|
obs = self.pde.observe(sol)
|
|
781
1486
|
|
|
782
1487
|
return obs
|
|
783
|
-
|
|
1488
|
+
|
|
784
1489
|
def _gradient_func(self, direction, wrt):
|
|
785
1490
|
""" Compute direction-Jacobian product (gradient) of the model. """
|
|
786
1491
|
if hasattr(self.pde, "gradient_wrt_parameter"):
|
|
@@ -793,4 +1498,3 @@ class PDEModel(Model):
|
|
|
793
1498
|
# Add the underlying PDE class name to the repr.
|
|
794
1499
|
def __repr__(self) -> str:
|
|
795
1500
|
return super().__repr__()+"\n PDE: {}.".format(self.pde.__class__.__name__)
|
|
796
|
-
|