feectools 0.1.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.
- feectools/__init__.py +0 -0
- feectools/accelerate/__init__.py +0 -0
- feectools/accelerate/accelerate.py +220 -0
- feectools/accelerate/compile_psydac.mk +52 -0
- feectools/api/__init__.py +0 -0
- feectools/api/essential_bc.py +122 -0
- feectools/api/fem_bilinear_form.py +2226 -0
- feectools/api/fem_common.py +286 -0
- feectools/api/fem_sum_form.py +123 -0
- feectools/api/settings.py +82 -0
- feectools/core/__init__.py +11 -0
- feectools/core/bsplines.py +1107 -0
- feectools/core/bsplines_kernels.py +1349 -0
- feectools/core/field_evaluation_kernels.py +5015 -0
- feectools/core/tests/__init__.py +0 -0
- feectools/core/tests/test_bsplines.py +263 -0
- feectools/core/tests/test_bsplines_kernel.py +40 -0
- feectools/core/tests/test_bsplines_pyccel.py +752 -0
- feectools/ddm/__init__.py +3 -0
- feectools/ddm/basic.py +78 -0
- feectools/ddm/blocking_data_exchanger.py +348 -0
- feectools/ddm/cart.py +1835 -0
- feectools/ddm/interface_data_exchanger.py +122 -0
- feectools/ddm/mpi.py +109 -0
- feectools/ddm/nonblocking_data_exchanger.py +331 -0
- feectools/ddm/partition.py +207 -0
- feectools/ddm/petsc.py +112 -0
- feectools/ddm/tests/__init__.py +0 -0
- feectools/ddm/tests/test_cart_1d.py +138 -0
- feectools/ddm/tests/test_cart_2d.py +164 -0
- feectools/ddm/tests/test_cart_3d.py +158 -0
- feectools/ddm/tests/test_multicart_2d.py +173 -0
- feectools/ddm/tests/test_partition.py +124 -0
- feectools/ddm/utilities.py +24 -0
- feectools/feec/__init__.py +0 -0
- feectools/feec/derivatives.py +780 -0
- feectools/feec/dof_kernels.py +210 -0
- feectools/feec/global_geometric_projectors.py +1073 -0
- feectools/feec/hodge.py +148 -0
- feectools/fem/__init__.py +0 -0
- feectools/fem/basic.py +465 -0
- feectools/fem/grid.py +181 -0
- feectools/fem/partitioning.py +344 -0
- feectools/fem/projectors.py +160 -0
- feectools/fem/splines.py +559 -0
- feectools/fem/tensor.py +1393 -0
- feectools/fem/tests/__init__.py +0 -0
- feectools/fem/tests/analytical_profiles_1d.py +100 -0
- feectools/fem/tests/analytical_profiles_base.py +34 -0
- feectools/fem/tests/splines_error_bounds.py +155 -0
- feectools/fem/tests/test_spline_histopolation.py +120 -0
- feectools/fem/tests/test_spline_interpolation.py +182 -0
- feectools/fem/tests/test_splines.py +184 -0
- feectools/fem/tests/test_splines_par.py +46 -0
- feectools/fem/tests/test_vector_spaces.py +150 -0
- feectools/fem/tests/utilities.py +47 -0
- feectools/fem/vector.py +729 -0
- feectools/linalg/__init__.py +0 -0
- feectools/linalg/basic.py +1386 -0
- feectools/linalg/block.py +1451 -0
- feectools/linalg/direct_solvers.py +201 -0
- feectools/linalg/fft.py +258 -0
- feectools/linalg/kernels/__init__.py +0 -0
- feectools/linalg/kernels/axpy_kernels.py +57 -0
- feectools/linalg/kernels/inner_kernels.py +100 -0
- feectools/linalg/kernels/matvec_kernels.py +206 -0
- feectools/linalg/kernels/stencil2IJV_kernels.py +227 -0
- feectools/linalg/kernels/stencil2coo_kernels.py +179 -0
- feectools/linalg/kernels/transpose_kernels.py +263 -0
- feectools/linalg/kron.py +911 -0
- feectools/linalg/solvers.py +1914 -0
- feectools/linalg/sparse.py +114 -0
- feectools/linalg/stencil.py +2923 -0
- feectools/linalg/stencil_dot_kernels.py +317 -0
- feectools/linalg/stencil_transpose_kernels.py +372 -0
- feectools/linalg/tests/__init__.py +0 -0
- feectools/linalg/tests/test_block.py +1588 -0
- feectools/linalg/tests/test_fft.py +106 -0
- feectools/linalg/tests/test_kron_stencil_matrix.py +114 -0
- feectools/linalg/tests/test_linalg.py +1065 -0
- feectools/linalg/tests/test_matrix_free.py +128 -0
- feectools/linalg/tests/test_solvers.py +213 -0
- feectools/linalg/tests/test_stencil_interface_matrix.py +379 -0
- feectools/linalg/tests/test_stencil_vector.py +1036 -0
- feectools/linalg/tests/test_stencil_vector_space.py +440 -0
- feectools/linalg/topetsc.py +522 -0
- feectools/linalg/utilities.py +200 -0
- feectools/utilities/__init__.py +0 -0
- feectools/utilities/quadratures.py +113 -0
- feectools/utilities/utils.py +166 -0
- feectools/version.py +1 -0
- feectools-0.1.0.dist-info/METADATA +66 -0
- feectools-0.1.0.dist-info/RECORD +98 -0
- feectools-0.1.0.dist-info/WHEEL +5 -0
- feectools-0.1.0.dist-info/entry_points.txt +3 -0
- feectools-0.1.0.dist-info/licenses/AUTHORS +22 -0
- feectools-0.1.0.dist-info/licenses/LICENSE +21 -0
- feectools-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1386 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2018 Yaman Güçlü, Jalal Lakhlili
|
|
4
|
+
# Copyright 2022 Yaman Güçlü, Said Hadjout, Julian Owezarek
|
|
5
|
+
"""
|
|
6
|
+
provides the fundamental classes for linear algebra operations.
|
|
7
|
+
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from abc import ABC, abstractmethod
|
|
11
|
+
from types import LambdaType
|
|
12
|
+
from inspect import signature
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
from scipy.sparse import coo_matrix
|
|
16
|
+
|
|
17
|
+
from feectools.utilities.utils import is_real
|
|
18
|
+
|
|
19
|
+
__all__ = (
|
|
20
|
+
'VectorSpace',
|
|
21
|
+
'Vector',
|
|
22
|
+
'LinearOperator',
|
|
23
|
+
'ZeroOperator',
|
|
24
|
+
'IdentityOperator',
|
|
25
|
+
'ScaledLinearOperator',
|
|
26
|
+
'SumLinearOperator',
|
|
27
|
+
'ComposedLinearOperator',
|
|
28
|
+
'PowerLinearOperator',
|
|
29
|
+
'InverseLinearOperator',
|
|
30
|
+
'LinearSolver',
|
|
31
|
+
'MatrixFreeLinearOperator'
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
#===============================================================================
|
|
35
|
+
class VectorSpace(ABC):
|
|
36
|
+
"""
|
|
37
|
+
Finite-dimensional vector space V with a scalar (inner) product.
|
|
38
|
+
|
|
39
|
+
"""
|
|
40
|
+
@property
|
|
41
|
+
@abstractmethod
|
|
42
|
+
def dimension(self):
|
|
43
|
+
"""
|
|
44
|
+
The dimension of a vector space V is the cardinality
|
|
45
|
+
(i.e. the number of vectors) of a basis of V over its base field.
|
|
46
|
+
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
@abstractmethod
|
|
51
|
+
def dtype(self):
|
|
52
|
+
"""
|
|
53
|
+
The data type of the field over which the space is built.
|
|
54
|
+
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
@abstractmethod
|
|
58
|
+
def zeros(self):
|
|
59
|
+
"""
|
|
60
|
+
Get a copy of the null element of the vector space V.
|
|
61
|
+
|
|
62
|
+
Returns
|
|
63
|
+
-------
|
|
64
|
+
null : Vector
|
|
65
|
+
A new vector object with all components equal to zero.
|
|
66
|
+
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
@abstractmethod
|
|
70
|
+
def inner(self, x, y):
|
|
71
|
+
"""
|
|
72
|
+
Evaluate the inner vector product between two vectors of this space V.
|
|
73
|
+
|
|
74
|
+
If the field of V is real, compute the classical scalar product.
|
|
75
|
+
If the field of V is complex, compute the classical sesquilinear
|
|
76
|
+
product with linearity on the second vector.
|
|
77
|
+
|
|
78
|
+
TODO [YG 01.05.2025]: Currently, the first vector is conjugated. We
|
|
79
|
+
want to reverse this behavior in order to align with the convention
|
|
80
|
+
of FEniCS.
|
|
81
|
+
|
|
82
|
+
Parameters
|
|
83
|
+
----------
|
|
84
|
+
x : Vector
|
|
85
|
+
The first vector in the scalar product. In the case of a complex
|
|
86
|
+
field, the inner product is antilinear w.r.t. this vector (hence
|
|
87
|
+
this vector is conjugated).
|
|
88
|
+
|
|
89
|
+
y : Vector
|
|
90
|
+
The second vector in the scalar product. The inner product is
|
|
91
|
+
linear w.r.t. this vector.
|
|
92
|
+
|
|
93
|
+
Returns
|
|
94
|
+
-------
|
|
95
|
+
float | complex
|
|
96
|
+
The scalar product of the two vectors. Note that inner(x, x) is
|
|
97
|
+
a non-negative real number which is zero if and only if x = 0.
|
|
98
|
+
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
@abstractmethod
|
|
102
|
+
def axpy(self, a, x, y):
|
|
103
|
+
"""
|
|
104
|
+
Increment the vector y with the a-scaled vector x, i.e. y = a * x + y,
|
|
105
|
+
provided that x and y belong to the same vector space V (self).
|
|
106
|
+
The scalar value a may be real or complex, depending on the field of V.
|
|
107
|
+
|
|
108
|
+
Parameters
|
|
109
|
+
----------
|
|
110
|
+
a : scalar
|
|
111
|
+
The scaling coefficient needed for the operation.
|
|
112
|
+
|
|
113
|
+
x : Vector
|
|
114
|
+
The vector which is not modified by this function.
|
|
115
|
+
|
|
116
|
+
y : Vector
|
|
117
|
+
The vector modified by this function (incremented by a * x).
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
#===============================================================================
|
|
121
|
+
class Vector(ABC):
|
|
122
|
+
"""
|
|
123
|
+
Element of a vector space V.
|
|
124
|
+
|
|
125
|
+
"""
|
|
126
|
+
@property
|
|
127
|
+
def shape(self):
|
|
128
|
+
""" A tuple containing the dimension of the space. """
|
|
129
|
+
return (self.space.dimension, )
|
|
130
|
+
|
|
131
|
+
@property
|
|
132
|
+
def dtype(self):
|
|
133
|
+
""" The data type of the vector field V this vector belongs to. """
|
|
134
|
+
return self.space.dtype
|
|
135
|
+
|
|
136
|
+
def inner(self, v):
|
|
137
|
+
"""
|
|
138
|
+
Evaluate the scalar product with the vector v of the same space.
|
|
139
|
+
|
|
140
|
+
Parameters
|
|
141
|
+
----------
|
|
142
|
+
v : Vector
|
|
143
|
+
Vector belonging to the same space as self.
|
|
144
|
+
|
|
145
|
+
"""
|
|
146
|
+
assert isinstance(v, Vector)
|
|
147
|
+
assert self.space is v.space
|
|
148
|
+
return self.space.inner(self, v)
|
|
149
|
+
|
|
150
|
+
def mul_iadd(self, a, v):
|
|
151
|
+
"""
|
|
152
|
+
Compute self += a * v, where v is another vector of the same space.
|
|
153
|
+
|
|
154
|
+
Parameters
|
|
155
|
+
----------
|
|
156
|
+
a : scalar
|
|
157
|
+
Rescaling coefficient, which can be cast to the correct dtype.
|
|
158
|
+
|
|
159
|
+
v : Vector
|
|
160
|
+
Vector belonging to the same space as self.
|
|
161
|
+
"""
|
|
162
|
+
self.space.axpy(a, v, self)
|
|
163
|
+
|
|
164
|
+
#-------------------------------------
|
|
165
|
+
# Deferred methods
|
|
166
|
+
#-------------------------------------
|
|
167
|
+
@property
|
|
168
|
+
@abstractmethod
|
|
169
|
+
def space(self):
|
|
170
|
+
""" Vector space to which this vector belongs. """
|
|
171
|
+
|
|
172
|
+
@abstractmethod
|
|
173
|
+
def toarray(self, **kwargs):
|
|
174
|
+
""" Convert to Numpy 1D array. """
|
|
175
|
+
|
|
176
|
+
@abstractmethod
|
|
177
|
+
def copy(self, out=None):
|
|
178
|
+
"""
|
|
179
|
+
Return an identical copy of this vector.
|
|
180
|
+
|
|
181
|
+
Subclasses must ensure that x.copy(out=x) returns x and not a new
|
|
182
|
+
object.
|
|
183
|
+
"""
|
|
184
|
+
|
|
185
|
+
@abstractmethod
|
|
186
|
+
def conjugate(self, out=None):
|
|
187
|
+
"""
|
|
188
|
+
Compute the complex conjugate vector.
|
|
189
|
+
|
|
190
|
+
Please note that x.conjugate(out=x) modifies x in place and returns x.
|
|
191
|
+
|
|
192
|
+
If the field is real (i.e. `self.dtype in (np.float32, np.float64)`) this method is equivalent to `copy`.
|
|
193
|
+
If the field is complex (i.e. `self.dtype in (np.complex64, np.complex128)`) this method returns
|
|
194
|
+
the complex conjugate of `self`, element-wise.
|
|
195
|
+
|
|
196
|
+
The behavior of this function is similar to `numpy.conjugate(self, out=None)`.
|
|
197
|
+
"""
|
|
198
|
+
|
|
199
|
+
@abstractmethod
|
|
200
|
+
def __neg__(self):
|
|
201
|
+
pass
|
|
202
|
+
|
|
203
|
+
@abstractmethod
|
|
204
|
+
def __mul__(self, a):
|
|
205
|
+
pass
|
|
206
|
+
|
|
207
|
+
@abstractmethod
|
|
208
|
+
def __add__(self, v):
|
|
209
|
+
pass
|
|
210
|
+
|
|
211
|
+
@abstractmethod
|
|
212
|
+
def __sub__(self, v):
|
|
213
|
+
pass
|
|
214
|
+
|
|
215
|
+
@abstractmethod
|
|
216
|
+
def __imul__(self, a):
|
|
217
|
+
pass
|
|
218
|
+
|
|
219
|
+
@abstractmethod
|
|
220
|
+
def __iadd__(self, v):
|
|
221
|
+
pass
|
|
222
|
+
|
|
223
|
+
@abstractmethod
|
|
224
|
+
def __isub__(self, v):
|
|
225
|
+
pass
|
|
226
|
+
|
|
227
|
+
#-------------------------------------
|
|
228
|
+
# Methods with default implementation
|
|
229
|
+
#-------------------------------------
|
|
230
|
+
def __rmul__(self, a):
|
|
231
|
+
return self * a
|
|
232
|
+
|
|
233
|
+
def __truediv__(self, a):
|
|
234
|
+
return self * (1.0 / a)
|
|
235
|
+
|
|
236
|
+
def __itruediv__(self, a):
|
|
237
|
+
self *= 1.0 / a
|
|
238
|
+
return self
|
|
239
|
+
|
|
240
|
+
def conj(self, out=None):
|
|
241
|
+
"""Compute the complex conjugate vector.
|
|
242
|
+
|
|
243
|
+
If the field is real (i.e. `self.dtype in (np.float32, np.float64)`) this method is equivalent to `copy`.
|
|
244
|
+
If the field is complex (i.e. `self.dtype in (np.complex64, np.complex128)`) this method returns
|
|
245
|
+
the complex conjugate of `self`, element-wise.
|
|
246
|
+
|
|
247
|
+
The behavior of this function is similar to `numpy.conj(self, out=None)`.
|
|
248
|
+
"""
|
|
249
|
+
return self.conjugate(out)
|
|
250
|
+
|
|
251
|
+
#===============================================================================
|
|
252
|
+
class LinearOperator(ABC):
|
|
253
|
+
"""
|
|
254
|
+
Abstract base class for all linear operators acting between two vector spaces V (domain)
|
|
255
|
+
and W (codomain).
|
|
256
|
+
|
|
257
|
+
"""
|
|
258
|
+
@property
|
|
259
|
+
def shape(self):
|
|
260
|
+
""" A tuple containing the dimension of the codomain and domain. """
|
|
261
|
+
return (self.codomain.dimension, self.domain.dimension)
|
|
262
|
+
|
|
263
|
+
#-------------------------------------
|
|
264
|
+
# Deferred methods
|
|
265
|
+
#-------------------------------------
|
|
266
|
+
@property
|
|
267
|
+
@abstractmethod
|
|
268
|
+
def domain(self):
|
|
269
|
+
""" The domain of the linear operator - an element of Vectorspace """
|
|
270
|
+
|
|
271
|
+
@property
|
|
272
|
+
@abstractmethod
|
|
273
|
+
def codomain(self):
|
|
274
|
+
""" The codomain of the linear operator - an element of Vectorspace """
|
|
275
|
+
|
|
276
|
+
@property
|
|
277
|
+
@abstractmethod
|
|
278
|
+
def dtype(self):
|
|
279
|
+
""" The data type of the coefficients of the linear operator,
|
|
280
|
+
upon convertion to matrix.
|
|
281
|
+
"""
|
|
282
|
+
|
|
283
|
+
@abstractmethod
|
|
284
|
+
def tosparse(self):
|
|
285
|
+
""" Convert to a sparse matrix in any of the formats supported by scipy.sparse."""
|
|
286
|
+
|
|
287
|
+
@abstractmethod
|
|
288
|
+
def toarray(self):
|
|
289
|
+
""" Convert to Numpy 2D array. """
|
|
290
|
+
|
|
291
|
+
@abstractmethod
|
|
292
|
+
def dot(self, v, out=None):
|
|
293
|
+
""" Apply the LinearOperator self to the Vector v.
|
|
294
|
+
|
|
295
|
+
The result is written to the Vector out, if provided.
|
|
296
|
+
|
|
297
|
+
Parameters
|
|
298
|
+
----------
|
|
299
|
+
v : Vector
|
|
300
|
+
The vector to which the linear operator (self) is applied. It must
|
|
301
|
+
belong to the domain of self.
|
|
302
|
+
|
|
303
|
+
out : Vector
|
|
304
|
+
The vector in which the result of the operation is stored. It must
|
|
305
|
+
belong to the codomain of self. If out is None, a new vector is
|
|
306
|
+
created and returned.
|
|
307
|
+
|
|
308
|
+
Returns
|
|
309
|
+
-------
|
|
310
|
+
Vector
|
|
311
|
+
The result of the operation. If out is None, a new vector is
|
|
312
|
+
returned. Otherwise, the result is stored in out and out is
|
|
313
|
+
returned.
|
|
314
|
+
"""
|
|
315
|
+
|
|
316
|
+
@abstractmethod
|
|
317
|
+
def transpose(self, conjugate=False):
|
|
318
|
+
"""
|
|
319
|
+
Transpose the LinearOperator .
|
|
320
|
+
|
|
321
|
+
If conjugate is True, return the Hermitian transpose.
|
|
322
|
+
"""
|
|
323
|
+
|
|
324
|
+
# TODO: check if we should add a copy method!!!
|
|
325
|
+
|
|
326
|
+
#-------------------------------------
|
|
327
|
+
# Magic methods
|
|
328
|
+
#-------------------------------------
|
|
329
|
+
def __neg__(self):
|
|
330
|
+
"""
|
|
331
|
+
Scales itself by -1 and thus returns the addititive inverse as
|
|
332
|
+
a new object of the class ScaledLinearOperator.
|
|
333
|
+
|
|
334
|
+
"""
|
|
335
|
+
return ScaledLinearOperator(self.domain, self.codomain, -1.0, self)
|
|
336
|
+
|
|
337
|
+
def __mul__(self, c):
|
|
338
|
+
"""
|
|
339
|
+
Scales a linear operator by a real scalar c by creating an object of the class ScaledLinearOperator,
|
|
340
|
+
unless c = 0 or c = 1, in which case either a ZeroOperator or self is returned.
|
|
341
|
+
|
|
342
|
+
"""
|
|
343
|
+
assert np.isscalar(c)
|
|
344
|
+
if c==0:
|
|
345
|
+
return ZeroOperator(self.domain, self.codomain)
|
|
346
|
+
elif c == 1:
|
|
347
|
+
return self
|
|
348
|
+
else:
|
|
349
|
+
return ScaledLinearOperator(self.domain, self.codomain, c, self)
|
|
350
|
+
|
|
351
|
+
def __rmul__(self, c):
|
|
352
|
+
""" Calls __mul__ instead. """
|
|
353
|
+
return self * c
|
|
354
|
+
|
|
355
|
+
def __matmul__(self, B):
|
|
356
|
+
"""
|
|
357
|
+
Matrix multiplication using the @ operator.
|
|
358
|
+
|
|
359
|
+
If B is a LinearOperator, create a ComposedLinearOperator object.
|
|
360
|
+
This is simplified to self if B is an IdentityOperator, and to a
|
|
361
|
+
ZeroOperator if B is a ZeroOperator.
|
|
362
|
+
|
|
363
|
+
If B is a Vector, the @ operator is treated as a matrix-vector
|
|
364
|
+
multiplication and returns the result of self.dot(B).
|
|
365
|
+
|
|
366
|
+
Parameters
|
|
367
|
+
----------
|
|
368
|
+
B : LinearOperator | Vector
|
|
369
|
+
The object to be multiplied with self. If B is a LinearOperator,
|
|
370
|
+
its codomain must be equal to the domain of self. If B is a Vector,
|
|
371
|
+
it must belong to the domain of self.
|
|
372
|
+
|
|
373
|
+
Returns
|
|
374
|
+
-------
|
|
375
|
+
LinearOperator | Vector
|
|
376
|
+
If B is a LinearOperator, return a ComposedLinearOperator object,
|
|
377
|
+
or a simplification to self or a ZeroOperator. In all cases the
|
|
378
|
+
resulting LinearOperator has the same domain as self and the same
|
|
379
|
+
codomain as B. If B is a Vector, return the result of self.dot(B),
|
|
380
|
+
which is a Vector belonging to the codomain of self.
|
|
381
|
+
"""
|
|
382
|
+
assert isinstance(B, (LinearOperator, Vector))
|
|
383
|
+
if isinstance(B, LinearOperator):
|
|
384
|
+
assert self.domain == B.codomain
|
|
385
|
+
if isinstance(B, ZeroOperator):
|
|
386
|
+
return ZeroOperator(B.domain, self.codomain)
|
|
387
|
+
elif isinstance(B, IdentityOperator):
|
|
388
|
+
return self
|
|
389
|
+
else:
|
|
390
|
+
return ComposedLinearOperator(B.domain, self.codomain, self, B)
|
|
391
|
+
else:
|
|
392
|
+
return self.dot(B)
|
|
393
|
+
|
|
394
|
+
def __add__(self, B):
|
|
395
|
+
""" Creates an object of the class SumLinearOperator unless B is a ZeroOperator in which case self is returned. """
|
|
396
|
+
assert isinstance(B, LinearOperator)
|
|
397
|
+
if isinstance(B, ZeroOperator):
|
|
398
|
+
return self
|
|
399
|
+
else:
|
|
400
|
+
return SumLinearOperator(self.domain, self.codomain, self, B)
|
|
401
|
+
|
|
402
|
+
def __sub__(self, B):
|
|
403
|
+
""" Creates an object of the class SumLinearOperator unless B is a ZeroOperator in which case self is returned. """
|
|
404
|
+
assert isinstance(B, LinearOperator)
|
|
405
|
+
if isinstance(B, ZeroOperator):
|
|
406
|
+
return self
|
|
407
|
+
else:
|
|
408
|
+
return SumLinearOperator(self.domain, self.codomain, self, -B)
|
|
409
|
+
|
|
410
|
+
def __pow__(self, n):
|
|
411
|
+
""" Creates an object of class :ref:`PowerLinearOperator <powerlinearoperator>`. """
|
|
412
|
+
return PowerLinearOperator(self.domain, self.codomain, self, n)
|
|
413
|
+
|
|
414
|
+
def __truediv__(self, c):
|
|
415
|
+
""" Divide by scalar. """
|
|
416
|
+
return self * (1.0 / c)
|
|
417
|
+
|
|
418
|
+
def __itruediv__(self, c):
|
|
419
|
+
""" Divide by scalar, in place. """
|
|
420
|
+
self *= 1.0 / c
|
|
421
|
+
return self
|
|
422
|
+
|
|
423
|
+
#-------------------------------------
|
|
424
|
+
# Methods with default implementation
|
|
425
|
+
#-------------------------------------
|
|
426
|
+
@property
|
|
427
|
+
def T(self):
|
|
428
|
+
""" Calls transpose method to return the transpose of self. """
|
|
429
|
+
return self.transpose()
|
|
430
|
+
|
|
431
|
+
@property
|
|
432
|
+
def H(self):
|
|
433
|
+
""" Calls transpose method with `conjugate=True` flag to return the Hermitian transpose of self. """
|
|
434
|
+
return self.transpose(conjugate=True)
|
|
435
|
+
|
|
436
|
+
def idot(self, v, out):
|
|
437
|
+
"""
|
|
438
|
+
Implements `out += self @ v` without a temporary, using a work array.
|
|
439
|
+
|
|
440
|
+
This default implementation uses a local work array to store the result
|
|
441
|
+
of `self @ v`, and then sums it to the vector `out`. This doubles the
|
|
442
|
+
amount of read/write operations from/to local memory. If possible,
|
|
443
|
+
subclasses should provide a more efficient implementation which does
|
|
444
|
+
not use work arrays.
|
|
445
|
+
|
|
446
|
+
Parameters
|
|
447
|
+
----------
|
|
448
|
+
v : Vector
|
|
449
|
+
The vector to which the linear operator `self` is applied. It must
|
|
450
|
+
belong to the domain of `self`.
|
|
451
|
+
|
|
452
|
+
out : Vector
|
|
453
|
+
The vector to be incremented by `self @ v`. It must belong to the
|
|
454
|
+
codomain of `self`.
|
|
455
|
+
|
|
456
|
+
"""
|
|
457
|
+
assert isinstance( v, Vector)
|
|
458
|
+
assert isinstance(out, Vector)
|
|
459
|
+
assert v.space is self.domain
|
|
460
|
+
assert out.space is self.codomain
|
|
461
|
+
|
|
462
|
+
if not hasattr(self, '_work'):
|
|
463
|
+
self._work = self.codomain.zeros()
|
|
464
|
+
|
|
465
|
+
self.dot(v, out=self._work)
|
|
466
|
+
out += self._work
|
|
467
|
+
|
|
468
|
+
def dot_inner(self, v, w):
|
|
469
|
+
"""
|
|
470
|
+
Compute the inner product of (self @ v) with w, without a temporary.
|
|
471
|
+
|
|
472
|
+
This is equivalent to self.dot(v).inner(w), but avoids the creation of
|
|
473
|
+
a temporary vector because the result of self.dot(v) is stored in a
|
|
474
|
+
local work array. If self is a positive-definite operator, this
|
|
475
|
+
operation is a (weighted) inner product.
|
|
476
|
+
|
|
477
|
+
Parameters
|
|
478
|
+
----------
|
|
479
|
+
v : Vector
|
|
480
|
+
The vector to which the linear operator (self) is applied. It must
|
|
481
|
+
belong to the domain of self.
|
|
482
|
+
|
|
483
|
+
w : Vector
|
|
484
|
+
The second vector in the inner product. It must belong to the
|
|
485
|
+
codomain of self.
|
|
486
|
+
|
|
487
|
+
Returns
|
|
488
|
+
-------
|
|
489
|
+
float | complex
|
|
490
|
+
The result of the inner product between (self @ v) and w. If the
|
|
491
|
+
field of self is real, this is a real number. If the field of self
|
|
492
|
+
is complex, this is a complex number.
|
|
493
|
+
"""
|
|
494
|
+
assert isinstance(v, Vector)
|
|
495
|
+
assert isinstance(w, Vector)
|
|
496
|
+
assert v.space is self.domain
|
|
497
|
+
assert w.space is self.codomain
|
|
498
|
+
|
|
499
|
+
if not hasattr(self, '_work'):
|
|
500
|
+
self._work = self.codomain.zeros()
|
|
501
|
+
|
|
502
|
+
return self.dot(v, out=self._work).inner(w)
|
|
503
|
+
|
|
504
|
+
def dot_inner(self, v, w):
|
|
505
|
+
"""
|
|
506
|
+
Compute the inner product of (self @ v) with w, without a temporary.
|
|
507
|
+
|
|
508
|
+
This is equivalent to self.dot(v).inner(w), but avoids the creation of
|
|
509
|
+
a temporary vector because the result of self.dot(v) is stored in a
|
|
510
|
+
local work array. If self is a positive-definite operator, this
|
|
511
|
+
operation is a (weighted) inner product.
|
|
512
|
+
|
|
513
|
+
Parameters
|
|
514
|
+
----------
|
|
515
|
+
v : Vector
|
|
516
|
+
The vector to which the linear operator (self) is applied. It must
|
|
517
|
+
belong to the domain of self.
|
|
518
|
+
|
|
519
|
+
w : Vector
|
|
520
|
+
The second vector in the inner product. It must belong to the
|
|
521
|
+
codomain of self.
|
|
522
|
+
|
|
523
|
+
Returns
|
|
524
|
+
-------
|
|
525
|
+
float | complex
|
|
526
|
+
The result of the inner product between (self @ v) and w. If the
|
|
527
|
+
field of self is real, this is a real number. If the field of self
|
|
528
|
+
is complex, this is a complex number.
|
|
529
|
+
"""
|
|
530
|
+
assert isinstance(v, Vector)
|
|
531
|
+
assert isinstance(w, Vector)
|
|
532
|
+
assert v.space is self.domain
|
|
533
|
+
assert w.space is self.codomain
|
|
534
|
+
|
|
535
|
+
if not hasattr(self, '_work'):
|
|
536
|
+
self._work = self.codomain.zeros()
|
|
537
|
+
|
|
538
|
+
return self.dot(v, out=self._work).inner(w)
|
|
539
|
+
|
|
540
|
+
#===============================================================================
|
|
541
|
+
class ZeroOperator(LinearOperator):
|
|
542
|
+
"""
|
|
543
|
+
Zero operator mapping any vector from its domain V to the zero vector of its codomain W.
|
|
544
|
+
|
|
545
|
+
"""
|
|
546
|
+
|
|
547
|
+
def __new__(cls, domain, codomain=None):
|
|
548
|
+
|
|
549
|
+
assert isinstance(domain, VectorSpace)
|
|
550
|
+
assert isinstance(codomain, VectorSpace)
|
|
551
|
+
|
|
552
|
+
from feectools.linalg.block import BlockVectorSpace, BlockLinearOperator
|
|
553
|
+
if isinstance(domain, BlockVectorSpace) or isinstance(codomain, BlockVectorSpace):
|
|
554
|
+
if isinstance(domain, BlockVectorSpace):
|
|
555
|
+
domain_spaces = domain.spaces
|
|
556
|
+
else:
|
|
557
|
+
domain_spaces = (domain,)
|
|
558
|
+
if isinstance(codomain, BlockVectorSpace):
|
|
559
|
+
codomain_spaces = codomain.spaces
|
|
560
|
+
else:
|
|
561
|
+
codomain_spaces = (codomain,)
|
|
562
|
+
blocks = {}
|
|
563
|
+
for i, D in enumerate(domain_spaces):
|
|
564
|
+
for j, C in enumerate(codomain_spaces):
|
|
565
|
+
blocks[j,i] = ZeroOperator(D,C)
|
|
566
|
+
return BlockLinearOperator(domain, codomain, blocks)
|
|
567
|
+
else:
|
|
568
|
+
return super().__new__(cls)
|
|
569
|
+
|
|
570
|
+
def __init__(self, domain, codomain):
|
|
571
|
+
|
|
572
|
+
self._domain = domain
|
|
573
|
+
self._codomain = codomain
|
|
574
|
+
|
|
575
|
+
@property
|
|
576
|
+
def domain(self):
|
|
577
|
+
return self._domain
|
|
578
|
+
|
|
579
|
+
@property
|
|
580
|
+
def codomain(self):
|
|
581
|
+
return self._codomain
|
|
582
|
+
|
|
583
|
+
@property
|
|
584
|
+
def dtype(self):
|
|
585
|
+
return None
|
|
586
|
+
|
|
587
|
+
def copy(self):
|
|
588
|
+
return ZeroOperator(self.domain, self.codomain)
|
|
589
|
+
|
|
590
|
+
def toarray(self):
|
|
591
|
+
return np.zeros(self.shape, dtype=self.dtype)
|
|
592
|
+
|
|
593
|
+
def tosparse(self):
|
|
594
|
+
from scipy.sparse import csr_matrix
|
|
595
|
+
return csr_matrix(self.shape, dtype=self.dtype)
|
|
596
|
+
|
|
597
|
+
def transpose(self, conjugate=False):
|
|
598
|
+
return ZeroOperator(domain=self.codomain, codomain=self.domain)
|
|
599
|
+
|
|
600
|
+
def dot(self, v, out=None):
|
|
601
|
+
assert isinstance(v, Vector)
|
|
602
|
+
assert v.space == self.domain
|
|
603
|
+
if out is not None:
|
|
604
|
+
assert isinstance(out, Vector)
|
|
605
|
+
assert out.space == self.codomain
|
|
606
|
+
out *= 0
|
|
607
|
+
else:
|
|
608
|
+
out = self.codomain.zeros()
|
|
609
|
+
return out
|
|
610
|
+
|
|
611
|
+
def __neg__(self):
|
|
612
|
+
return self
|
|
613
|
+
|
|
614
|
+
def __add__(self, B):
|
|
615
|
+
assert isinstance(B, LinearOperator)
|
|
616
|
+
assert self.domain == B.domain
|
|
617
|
+
assert self.codomain == B.codomain
|
|
618
|
+
return B
|
|
619
|
+
|
|
620
|
+
def __sub__(self, B):
|
|
621
|
+
assert isinstance(B, LinearOperator)
|
|
622
|
+
assert self.domain == B.domain
|
|
623
|
+
assert self.codomain == B.codomain
|
|
624
|
+
return -B
|
|
625
|
+
|
|
626
|
+
def __mul__(self, c):
|
|
627
|
+
assert np.isscalar(c)
|
|
628
|
+
return self
|
|
629
|
+
|
|
630
|
+
def __matmul__(self, B):
|
|
631
|
+
assert isinstance(B, (LinearOperator, Vector))
|
|
632
|
+
if isinstance(B, LinearOperator):
|
|
633
|
+
assert self.domain == B.codomain
|
|
634
|
+
return ZeroOperator(domain=B.domain, codomain=self.codomain)
|
|
635
|
+
else:
|
|
636
|
+
return self.dot(B)
|
|
637
|
+
|
|
638
|
+
#===============================================================================
|
|
639
|
+
class IdentityOperator(LinearOperator):
|
|
640
|
+
"""
|
|
641
|
+
Identity operator acting between a vector space V and itself.
|
|
642
|
+
Useful for example in custom linear operator classes together with the apply_essential_bc method to create projection operators.
|
|
643
|
+
|
|
644
|
+
"""
|
|
645
|
+
|
|
646
|
+
def __init__(self, domain, codomain=None):
|
|
647
|
+
|
|
648
|
+
assert isinstance(domain, VectorSpace)
|
|
649
|
+
if codomain:
|
|
650
|
+
assert isinstance(codomain, VectorSpace)
|
|
651
|
+
assert domain == codomain
|
|
652
|
+
|
|
653
|
+
self._domain = domain
|
|
654
|
+
self._codomain = domain
|
|
655
|
+
|
|
656
|
+
@property
|
|
657
|
+
def domain(self):
|
|
658
|
+
return self._domain
|
|
659
|
+
|
|
660
|
+
@property
|
|
661
|
+
def codomain(self):
|
|
662
|
+
return self._codomain
|
|
663
|
+
|
|
664
|
+
@property
|
|
665
|
+
def dtype(self):
|
|
666
|
+
return None
|
|
667
|
+
|
|
668
|
+
def copy(self):
|
|
669
|
+
""" Returns a new IdentityOperator object acting between the same vector spaces."""
|
|
670
|
+
return IdentityOperator(self.domain, self.codomain)
|
|
671
|
+
|
|
672
|
+
def toarray(self):
|
|
673
|
+
return np.diag(np.ones(self.domain.dimension , dtype=self.dtype))
|
|
674
|
+
|
|
675
|
+
def tosparse(self):
|
|
676
|
+
from scipy.sparse import identity
|
|
677
|
+
return identity(self.domain.dimension, dtype=self.dtype, format="csr")
|
|
678
|
+
|
|
679
|
+
def transpose(self, conjugate=False):
|
|
680
|
+
""" Could return self, but by convention returns new object. """
|
|
681
|
+
return IdentityOperator(self.domain, self.codomain)
|
|
682
|
+
|
|
683
|
+
def dot(self, v, out=None):
|
|
684
|
+
assert isinstance(v, Vector)
|
|
685
|
+
assert v.space == self.domain
|
|
686
|
+
if out is not None:
|
|
687
|
+
assert isinstance(out, Vector)
|
|
688
|
+
assert out.space == self.codomain
|
|
689
|
+
out *= 0
|
|
690
|
+
out += v
|
|
691
|
+
return out
|
|
692
|
+
else:
|
|
693
|
+
return v.copy()
|
|
694
|
+
|
|
695
|
+
def __matmul__(self, B):
|
|
696
|
+
assert isinstance(B, (LinearOperator, Vector))
|
|
697
|
+
if isinstance(B, LinearOperator):
|
|
698
|
+
assert self.domain == B.codomain
|
|
699
|
+
return B
|
|
700
|
+
else:
|
|
701
|
+
return self.dot(B)
|
|
702
|
+
|
|
703
|
+
#===============================================================================
|
|
704
|
+
class ScaledLinearOperator(LinearOperator):
|
|
705
|
+
"""
|
|
706
|
+
A linear operator $A$ scalar multiplied by a constant $c$.
|
|
707
|
+
|
|
708
|
+
"""
|
|
709
|
+
|
|
710
|
+
def __init__(self, domain, codomain, c, A):
|
|
711
|
+
|
|
712
|
+
assert isinstance(domain, VectorSpace)
|
|
713
|
+
assert isinstance(codomain, VectorSpace)
|
|
714
|
+
assert np.isscalar(c)
|
|
715
|
+
assert np.iscomplexobj(c) == (codomain._dtype == complex)
|
|
716
|
+
assert isinstance(A, LinearOperator)
|
|
717
|
+
assert domain == A.domain
|
|
718
|
+
assert codomain == A.codomain
|
|
719
|
+
|
|
720
|
+
if isinstance(A, ScaledLinearOperator):
|
|
721
|
+
scalar = A.scalar * c
|
|
722
|
+
operator = A.operator
|
|
723
|
+
else:
|
|
724
|
+
scalar = c
|
|
725
|
+
operator = A
|
|
726
|
+
|
|
727
|
+
self._operator = operator
|
|
728
|
+
self._scalar = scalar
|
|
729
|
+
self._domain = domain
|
|
730
|
+
self._codomain = codomain
|
|
731
|
+
|
|
732
|
+
@property
|
|
733
|
+
def domain(self):
|
|
734
|
+
return self._domain
|
|
735
|
+
|
|
736
|
+
@property
|
|
737
|
+
def codomain(self):
|
|
738
|
+
return self._codomain
|
|
739
|
+
|
|
740
|
+
@property
|
|
741
|
+
def scalar(self):
|
|
742
|
+
""" Returns the scalar value by which the operator is multiplied."""
|
|
743
|
+
return self._scalar
|
|
744
|
+
|
|
745
|
+
@property
|
|
746
|
+
def operator(self):
|
|
747
|
+
""" Returns the operator that is multiplied by the scalar."""
|
|
748
|
+
return self._operator
|
|
749
|
+
|
|
750
|
+
@property
|
|
751
|
+
def dtype(self):
|
|
752
|
+
return None
|
|
753
|
+
|
|
754
|
+
def set_scalar(self, c):
|
|
755
|
+
""" Modifies the scalar with which this LinearOperator is multiplied. E.g. for updating the stepsize."""
|
|
756
|
+
self._scalar = c
|
|
757
|
+
|
|
758
|
+
def toarray(self):
|
|
759
|
+
return self._scalar * self._operator.toarray()
|
|
760
|
+
|
|
761
|
+
def tosparse(self):
|
|
762
|
+
return self._scalar * self._operator.tosparse().tocsr()
|
|
763
|
+
|
|
764
|
+
def transpose(self, conjugate=False):
|
|
765
|
+
return ScaledLinearOperator(domain=self.codomain, codomain=self.domain, c=self._scalar if not conjugate else np.conjugate(self._scalar), A=self._operator.transpose(conjugate=conjugate))
|
|
766
|
+
|
|
767
|
+
def __neg__(self):
|
|
768
|
+
return ScaledLinearOperator(domain=self.domain, codomain=self.codomain, c=-1*self._scalar, A=self._operator)
|
|
769
|
+
|
|
770
|
+
def dot(self, v, out=None):
|
|
771
|
+
assert isinstance(v, Vector)
|
|
772
|
+
assert v.space == self.domain
|
|
773
|
+
if out is not None:
|
|
774
|
+
assert isinstance(out, Vector)
|
|
775
|
+
assert out.space == self.codomain
|
|
776
|
+
self._operator.dot(v, out = out)
|
|
777
|
+
out *= self._scalar
|
|
778
|
+
return out
|
|
779
|
+
else:
|
|
780
|
+
out = self._operator.dot(v)
|
|
781
|
+
out *= self._scalar
|
|
782
|
+
return out
|
|
783
|
+
|
|
784
|
+
#===============================================================================
|
|
785
|
+
class SumLinearOperator(LinearOperator):
|
|
786
|
+
r"""
|
|
787
|
+
Sum $\sum_{i=1}^n A_i$ of linear operators $A_1,\dots,A_n$ acting between the same vector spaces V (domain) and W (codomain).
|
|
788
|
+
|
|
789
|
+
"""
|
|
790
|
+
def __new__(cls, domain, codomain, *args):
|
|
791
|
+
|
|
792
|
+
if len(args) == 0:
|
|
793
|
+
return ZeroOperator(domain,codomain)
|
|
794
|
+
elif len(args) == 1:
|
|
795
|
+
return args[0]
|
|
796
|
+
else:
|
|
797
|
+
return super().__new__(cls)
|
|
798
|
+
|
|
799
|
+
def __init__(self, domain, codomain, *args):
|
|
800
|
+
|
|
801
|
+
assert isinstance(domain, VectorSpace)
|
|
802
|
+
assert isinstance(codomain, VectorSpace)
|
|
803
|
+
for a in args:
|
|
804
|
+
assert isinstance(a, LinearOperator)
|
|
805
|
+
assert a.domain == domain
|
|
806
|
+
assert a.codomain == codomain
|
|
807
|
+
|
|
808
|
+
addends = ()
|
|
809
|
+
for a in args:
|
|
810
|
+
if isinstance(a, SumLinearOperator):
|
|
811
|
+
addends = (*addends, *a.addends)
|
|
812
|
+
else:
|
|
813
|
+
addends = (*addends, a)
|
|
814
|
+
|
|
815
|
+
addends = SumLinearOperator.simplify(addends)
|
|
816
|
+
|
|
817
|
+
self._domain = domain
|
|
818
|
+
self._codomain = codomain
|
|
819
|
+
self._addends = addends
|
|
820
|
+
self._out = codomain.zeros()
|
|
821
|
+
|
|
822
|
+
#-------------------------------------
|
|
823
|
+
# Abstract interface
|
|
824
|
+
#-------------------------------------
|
|
825
|
+
@property
|
|
826
|
+
def domain(self):
|
|
827
|
+
""" The domain of the linear operator, element of class ``VectorSpace``. """
|
|
828
|
+
return self._domain
|
|
829
|
+
|
|
830
|
+
@property
|
|
831
|
+
def codomain(self):
|
|
832
|
+
""" The codomain of the linear operator, element of class ``VectorSpace``. """
|
|
833
|
+
return self._codomain
|
|
834
|
+
|
|
835
|
+
@property
|
|
836
|
+
def dtype(self):
|
|
837
|
+
return None
|
|
838
|
+
|
|
839
|
+
def tosparse(self):
|
|
840
|
+
from scipy.sparse import csr_matrix
|
|
841
|
+
out = csr_matrix(self.shape, dtype=self.dtype)
|
|
842
|
+
for a in self._addends:
|
|
843
|
+
out += a.tosparse()
|
|
844
|
+
return out
|
|
845
|
+
|
|
846
|
+
def toarray(self):
|
|
847
|
+
out = np.zeros(self.shape, dtype=self.dtype)
|
|
848
|
+
for a in self._addends:
|
|
849
|
+
out += a.toarray()
|
|
850
|
+
return out
|
|
851
|
+
|
|
852
|
+
def dot(self, v, out=None):
|
|
853
|
+
""" Evaluates SumLinearOperator object at a vector v element of domain. """
|
|
854
|
+
|
|
855
|
+
assert isinstance(v, Vector)
|
|
856
|
+
assert v.space is self.domain
|
|
857
|
+
|
|
858
|
+
if out is not None:
|
|
859
|
+
assert isinstance(out, Vector)
|
|
860
|
+
assert out.space is self.codomain
|
|
861
|
+
out *= 0
|
|
862
|
+
else:
|
|
863
|
+
out = self.codomain.zeros()
|
|
864
|
+
|
|
865
|
+
for A in self._addends:
|
|
866
|
+
A.idot(v, out)
|
|
867
|
+
|
|
868
|
+
return out
|
|
869
|
+
|
|
870
|
+
def transpose(self, conjugate=False):
|
|
871
|
+
t_addends = ()
|
|
872
|
+
for a in self._addends:
|
|
873
|
+
t_addends = (*t_addends, a.transpose(conjugate=conjugate))
|
|
874
|
+
return SumLinearOperator(self.codomain, self.domain, *t_addends)
|
|
875
|
+
|
|
876
|
+
#--------------------------------------
|
|
877
|
+
# Other properties/methods
|
|
878
|
+
#--------------------------------------
|
|
879
|
+
@property
|
|
880
|
+
def addends(self):
|
|
881
|
+
""" A tuple containing the addends of the linear operator, elements of class ``LinearOperator``. """
|
|
882
|
+
return self._addends
|
|
883
|
+
|
|
884
|
+
@staticmethod
|
|
885
|
+
def simplify(addends):
|
|
886
|
+
""" Simplifies a sum of linear operators by combining addends of the same class. """
|
|
887
|
+
class_list = [a.__class__ for a in addends]
|
|
888
|
+
unique_list = [*{c: a for c, a in zip(class_list, addends)}]
|
|
889
|
+
if len(unique_list) == 1:
|
|
890
|
+
return addends
|
|
891
|
+
out = ()
|
|
892
|
+
for j in unique_list:
|
|
893
|
+
indices = [k for k, l in enumerate(class_list) if l == j]
|
|
894
|
+
if len(indices) == 1:
|
|
895
|
+
out = (*out, addends[indices[0]])
|
|
896
|
+
else:
|
|
897
|
+
A = addends[indices[0]] + addends[indices[1]]
|
|
898
|
+
for n in range(len(indices)-2):
|
|
899
|
+
A += addends[indices[n+2]]
|
|
900
|
+
if isinstance(A, SumLinearOperator):
|
|
901
|
+
out = (*out, *A.addends)
|
|
902
|
+
else:
|
|
903
|
+
out = (*out, A)
|
|
904
|
+
return out
|
|
905
|
+
|
|
906
|
+
#===============================================================================
|
|
907
|
+
class ComposedLinearOperator(LinearOperator):
|
|
908
|
+
r"""
|
|
909
|
+
Composition $A_n\circ\dots\circ A_1$ of two or more linear operators $A_1,\dots,A_n$.
|
|
910
|
+
|
|
911
|
+
"""
|
|
912
|
+
|
|
913
|
+
def __init__(self, domain, codomain, *args):
|
|
914
|
+
|
|
915
|
+
assert isinstance(domain, VectorSpace)
|
|
916
|
+
assert isinstance(codomain, VectorSpace)
|
|
917
|
+
|
|
918
|
+
for a in args:
|
|
919
|
+
assert isinstance(a, LinearOperator)
|
|
920
|
+
assert args[0].codomain == codomain
|
|
921
|
+
assert args[-1].domain == domain
|
|
922
|
+
|
|
923
|
+
for i in range(len(args)-1):
|
|
924
|
+
assert args[i].domain == args[i+1].codomain
|
|
925
|
+
|
|
926
|
+
multiplicants = ()
|
|
927
|
+
tmp_vectors = []
|
|
928
|
+
for a in args[:-1]:
|
|
929
|
+
if isinstance(a, ComposedLinearOperator):
|
|
930
|
+
multiplicants = (*multiplicants, *a.multiplicants)
|
|
931
|
+
tmp_vectors.extend(a.tmp_vectors)
|
|
932
|
+
tmp_vectors.append(a.domain.zeros())
|
|
933
|
+
else:
|
|
934
|
+
multiplicants = (*multiplicants, a)
|
|
935
|
+
tmp_vectors.append(a.domain.zeros())
|
|
936
|
+
|
|
937
|
+
last = args[-1]
|
|
938
|
+
if isinstance(last, ComposedLinearOperator):
|
|
939
|
+
multiplicants = (*multiplicants, *last.multiplicants)
|
|
940
|
+
tmp_vectors.extend(last.tmp_vectors)
|
|
941
|
+
else:
|
|
942
|
+
multiplicants = (*multiplicants, last)
|
|
943
|
+
|
|
944
|
+
self._domain = domain
|
|
945
|
+
self._codomain = codomain
|
|
946
|
+
self._multiplicants = multiplicants
|
|
947
|
+
self._tmp_vectors = tuple(tmp_vectors)
|
|
948
|
+
|
|
949
|
+
@property
|
|
950
|
+
def tmp_vectors(self):
|
|
951
|
+
"""
|
|
952
|
+
A tuple containing the storage vectors that are repeatedly being used upon calling the `dot` method.
|
|
953
|
+
This avoids the creation of new vectors at each call of the `dot` method.
|
|
954
|
+
|
|
955
|
+
"""
|
|
956
|
+
return self._tmp_vectors
|
|
957
|
+
|
|
958
|
+
@property
|
|
959
|
+
def domain(self):
|
|
960
|
+
return self._domain
|
|
961
|
+
|
|
962
|
+
@property
|
|
963
|
+
def codomain(self):
|
|
964
|
+
return self._codomain
|
|
965
|
+
|
|
966
|
+
@property
|
|
967
|
+
def multiplicants(self):
|
|
968
|
+
r"""
|
|
969
|
+
A tuple $(A_1,\dots,A_n)$ containing the multiplicants of the linear operator
|
|
970
|
+
$self = A_n\circ\dots\circ A_1$.
|
|
971
|
+
|
|
972
|
+
"""
|
|
973
|
+
return self._multiplicants
|
|
974
|
+
|
|
975
|
+
@property
|
|
976
|
+
def dtype(self):
|
|
977
|
+
return None
|
|
978
|
+
|
|
979
|
+
def toarray(self):
|
|
980
|
+
raise NotImplementedError('toarray() is not defined for ComposedLinearOperators.')
|
|
981
|
+
|
|
982
|
+
def tosparse(self):
|
|
983
|
+
mats = [M.tosparse() for M in self._multiplicants]
|
|
984
|
+
M = mats[0]
|
|
985
|
+
for Mi in mats[1:]:
|
|
986
|
+
M = M @ Mi
|
|
987
|
+
return coo_matrix(M)
|
|
988
|
+
|
|
989
|
+
def transpose(self, conjugate=False):
|
|
990
|
+
t_multiplicants = ()
|
|
991
|
+
for a in self._multiplicants:
|
|
992
|
+
t_multiplicants = (a.transpose(conjugate=conjugate), *t_multiplicants)
|
|
993
|
+
new_dom = self.codomain
|
|
994
|
+
new_cod = self.domain
|
|
995
|
+
assert isinstance(new_dom, VectorSpace)
|
|
996
|
+
assert isinstance(new_cod, VectorSpace)
|
|
997
|
+
return ComposedLinearOperator(self.codomain, self.domain, *t_multiplicants)
|
|
998
|
+
|
|
999
|
+
def dot(self, v, out=None):
|
|
1000
|
+
assert isinstance(v, Vector)
|
|
1001
|
+
assert v.space == self.domain
|
|
1002
|
+
if out is not None:
|
|
1003
|
+
assert isinstance(out, Vector)
|
|
1004
|
+
assert out.space == self.codomain
|
|
1005
|
+
|
|
1006
|
+
x = v
|
|
1007
|
+
for i in range(len(self._tmp_vectors)):
|
|
1008
|
+
y = self._tmp_vectors[-1-i]
|
|
1009
|
+
A = self._multiplicants[-1-i]
|
|
1010
|
+
A.dot(x, out=y)
|
|
1011
|
+
x = y
|
|
1012
|
+
|
|
1013
|
+
A = self._multiplicants[0]
|
|
1014
|
+
if out is not None:
|
|
1015
|
+
|
|
1016
|
+
A.dot(x, out=out)
|
|
1017
|
+
else:
|
|
1018
|
+
out = A.dot(x)
|
|
1019
|
+
return out
|
|
1020
|
+
|
|
1021
|
+
def exchange_assembly_data(self):
|
|
1022
|
+
for op in self._multiplicants:
|
|
1023
|
+
op.exchange_assembly_data()
|
|
1024
|
+
|
|
1025
|
+
def set_backend(self, backend, precompiled=False):
|
|
1026
|
+
for op in self._multiplicants:
|
|
1027
|
+
op.set_backend(backend)
|
|
1028
|
+
|
|
1029
|
+
#===============================================================================
|
|
1030
|
+
class PowerLinearOperator(LinearOperator):
|
|
1031
|
+
r"""
|
|
1032
|
+
Power $A^n$ of a linear operator $A$ for some integer $n\geq 0$.
|
|
1033
|
+
|
|
1034
|
+
"""
|
|
1035
|
+
|
|
1036
|
+
def __new__(cls, domain, codomain, A, n):
|
|
1037
|
+
|
|
1038
|
+
assert isinstance(n, int)
|
|
1039
|
+
assert n >= 0
|
|
1040
|
+
|
|
1041
|
+
assert isinstance(A, LinearOperator)
|
|
1042
|
+
assert A.domain == domain
|
|
1043
|
+
assert A.codomain == codomain
|
|
1044
|
+
assert domain == codomain
|
|
1045
|
+
|
|
1046
|
+
if n == 0:
|
|
1047
|
+
return IdentityOperator(domain, codomain)
|
|
1048
|
+
elif n == 1:
|
|
1049
|
+
return A
|
|
1050
|
+
else:
|
|
1051
|
+
return super().__new__(cls)
|
|
1052
|
+
|
|
1053
|
+
def __init__(self, domain, codomain, A, n):
|
|
1054
|
+
|
|
1055
|
+
if isinstance(A, PowerLinearOperator):
|
|
1056
|
+
self._operator = A.operator
|
|
1057
|
+
self._factorial = A.factorial*n
|
|
1058
|
+
else:
|
|
1059
|
+
self._operator = A
|
|
1060
|
+
self._factorial = n
|
|
1061
|
+
self._domain = domain
|
|
1062
|
+
self._codomain = codomain
|
|
1063
|
+
|
|
1064
|
+
@property
|
|
1065
|
+
def domain(self):
|
|
1066
|
+
return self._domain
|
|
1067
|
+
|
|
1068
|
+
@property
|
|
1069
|
+
def codomain(self):
|
|
1070
|
+
return self._codomain
|
|
1071
|
+
|
|
1072
|
+
@property
|
|
1073
|
+
def dtype(self):
|
|
1074
|
+
return None
|
|
1075
|
+
|
|
1076
|
+
@property
|
|
1077
|
+
def operator(self):
|
|
1078
|
+
""" Returns the operator that is raised to the power. """
|
|
1079
|
+
return self._operator
|
|
1080
|
+
|
|
1081
|
+
@property
|
|
1082
|
+
def factorial(self):
|
|
1083
|
+
""" Returns the power to which the operator is raised. """
|
|
1084
|
+
return self._factorial
|
|
1085
|
+
|
|
1086
|
+
def toarray(self):
|
|
1087
|
+
raise NotImplementedError('toarray() is not defined for PowerLinearOperators.')
|
|
1088
|
+
|
|
1089
|
+
def tosparse(self):
|
|
1090
|
+
raise NotImplementedError('tosparse() is not defined for PowerLinearOperators.')
|
|
1091
|
+
|
|
1092
|
+
def transpose(self, conjugate=False):
|
|
1093
|
+
return PowerLinearOperator(domain=self.codomain, codomain=self.domain, A=self._operator.transpose(conjugate=conjugate), n=self._factorial)
|
|
1094
|
+
|
|
1095
|
+
def dot(self, v, out=None):
|
|
1096
|
+
assert isinstance(v, Vector)
|
|
1097
|
+
assert v.space == self.domain
|
|
1098
|
+
if out is not None:
|
|
1099
|
+
assert isinstance(out, Vector)
|
|
1100
|
+
assert out.space == self.codomain
|
|
1101
|
+
for i in range(self._factorial):
|
|
1102
|
+
self._operator.dot(v, out=out)
|
|
1103
|
+
v = out.copy()
|
|
1104
|
+
else:
|
|
1105
|
+
out = v.copy()
|
|
1106
|
+
for i in range(self._factorial):
|
|
1107
|
+
out = self._operator.dot(out)
|
|
1108
|
+
return out
|
|
1109
|
+
|
|
1110
|
+
#===============================================================================
|
|
1111
|
+
class InverseLinearOperator(LinearOperator):
|
|
1112
|
+
"""
|
|
1113
|
+
Abstract base class for the (approximate) inverse $A^{-1}$ of a
|
|
1114
|
+
square matrix $A$. The result of A_inv.dot(b) is the (approximate) solution x
|
|
1115
|
+
of the linear system A x = b, where x and b belong to the same vector space V.
|
|
1116
|
+
|
|
1117
|
+
We assume that the linear system is solved by an iterative method, which
|
|
1118
|
+
needs a first guess `x0` and an exit condition based on `tol` and `maxiter`.
|
|
1119
|
+
|
|
1120
|
+
Concrete subclasses of this class must implement the `dot` method and take
|
|
1121
|
+
care of any internal storage which might be necessary.
|
|
1122
|
+
|
|
1123
|
+
Parameters
|
|
1124
|
+
----------
|
|
1125
|
+
A : feectools.linalg.basic.LinearOperator
|
|
1126
|
+
Left-hand-side matrix A of linear system.
|
|
1127
|
+
|
|
1128
|
+
x0 : feectools.linalg.basic.Vector
|
|
1129
|
+
First guess of solution for iterative solver (optional).
|
|
1130
|
+
|
|
1131
|
+
tol : float
|
|
1132
|
+
Absolute tolerance for L2-norm of residual r = A*x - b.
|
|
1133
|
+
|
|
1134
|
+
maxiter: int
|
|
1135
|
+
Maximum number of iterations.
|
|
1136
|
+
|
|
1137
|
+
verbose : bool
|
|
1138
|
+
If True, L2-norm of residual r is printed at each iteration.
|
|
1139
|
+
"""
|
|
1140
|
+
|
|
1141
|
+
def __init__(self, A, **kwargs):
|
|
1142
|
+
|
|
1143
|
+
assert isinstance(A, LinearOperator)
|
|
1144
|
+
assert A.domain.dimension == A.codomain.dimension
|
|
1145
|
+
domain = A.codomain
|
|
1146
|
+
codomain = A.domain
|
|
1147
|
+
|
|
1148
|
+
if kwargs['x0'] is None:
|
|
1149
|
+
kwargs['x0'] = codomain.zeros()
|
|
1150
|
+
|
|
1151
|
+
self._A = A
|
|
1152
|
+
self._domain = domain
|
|
1153
|
+
self._codomain = codomain
|
|
1154
|
+
|
|
1155
|
+
self._check_options(**kwargs)
|
|
1156
|
+
self._options = kwargs
|
|
1157
|
+
|
|
1158
|
+
@property
|
|
1159
|
+
def domain(self):
|
|
1160
|
+
return self._domain
|
|
1161
|
+
|
|
1162
|
+
@property
|
|
1163
|
+
def codomain(self):
|
|
1164
|
+
return self._codomain
|
|
1165
|
+
|
|
1166
|
+
@property
|
|
1167
|
+
def dtype(self):
|
|
1168
|
+
return None
|
|
1169
|
+
|
|
1170
|
+
@property
|
|
1171
|
+
def linop(self):
|
|
1172
|
+
"""
|
|
1173
|
+
The linear operator $A$ of which this object is the inverse $A^{-1}$.
|
|
1174
|
+
|
|
1175
|
+
The linear operator $A$ can be modified in place, or replaced entirely
|
|
1176
|
+
through the setter. A substitution should only be made in cases where
|
|
1177
|
+
no other options are viable, as it breaks the one-to-one map between
|
|
1178
|
+
the original linear operator $A$ (passed to the constructor) and the
|
|
1179
|
+
current `InverseLinearOperator` object $A^{-1}$. Use with extreme care!
|
|
1180
|
+
|
|
1181
|
+
"""
|
|
1182
|
+
return self._A
|
|
1183
|
+
|
|
1184
|
+
@linop.setter
|
|
1185
|
+
def linop(self, a):
|
|
1186
|
+
""" Set the linear operator $A$ of which this object is the inverse $A^{-1}$. """
|
|
1187
|
+
assert isinstance(a, LinearOperator)
|
|
1188
|
+
assert a.domain is self.domain
|
|
1189
|
+
assert a.codomain is self.codomain
|
|
1190
|
+
self._A = a
|
|
1191
|
+
|
|
1192
|
+
def _check_options(self, **kwargs):
|
|
1193
|
+
""" Check whether the options passed to the solver class are valid. """
|
|
1194
|
+
for key, value in kwargs.items():
|
|
1195
|
+
|
|
1196
|
+
if key == 'x0':
|
|
1197
|
+
if value is not None:
|
|
1198
|
+
assert isinstance(value, Vector), "x0 must be a Vector or None"
|
|
1199
|
+
assert value.space == self.codomain, "x0 belongs to the wrong VectorSpace"
|
|
1200
|
+
elif key == 'tol':
|
|
1201
|
+
assert is_real(value), "tol must be a real number"
|
|
1202
|
+
assert value > 0, "tol must be positive"
|
|
1203
|
+
elif key == 'maxiter':
|
|
1204
|
+
assert isinstance(value, int), "maxiter must be an int"
|
|
1205
|
+
assert value > 0, "maxiter must be positive"
|
|
1206
|
+
elif key == 'verbose':
|
|
1207
|
+
assert isinstance(value, bool), "verbose must be a bool"
|
|
1208
|
+
|
|
1209
|
+
def toarray(self):
|
|
1210
|
+
raise NotImplementedError('toarray() is not defined for InverseLinearOperators.')
|
|
1211
|
+
|
|
1212
|
+
def tosparse(self):
|
|
1213
|
+
raise NotImplementedError('tosparse() is not defined for InverseLinearOperators.')
|
|
1214
|
+
|
|
1215
|
+
def get_info(self):
|
|
1216
|
+
""" Returns the previous convergence information. """
|
|
1217
|
+
return self._info
|
|
1218
|
+
|
|
1219
|
+
def get_options(self, key=None):
|
|
1220
|
+
"""Get a copy of all the solver options, or a specific value of interest.
|
|
1221
|
+
|
|
1222
|
+
Parameters
|
|
1223
|
+
----------
|
|
1224
|
+
key : str | None
|
|
1225
|
+
Name of the specific option of interest (default: None).
|
|
1226
|
+
|
|
1227
|
+
Returns
|
|
1228
|
+
-------
|
|
1229
|
+
dict | type(self._options['key']) | None
|
|
1230
|
+
If `key` is given, get the specific option of interest. If there is
|
|
1231
|
+
no such option, `None` is returned instead. If `key` is not given,
|
|
1232
|
+
get a copy of all the solver options in a dictionary.
|
|
1233
|
+
|
|
1234
|
+
"""
|
|
1235
|
+
if key is None:
|
|
1236
|
+
return self._options.copy()
|
|
1237
|
+
else:
|
|
1238
|
+
return self._options.get(key)
|
|
1239
|
+
|
|
1240
|
+
def set_options(self, **kwargs):
|
|
1241
|
+
""" Set the solver options by passing keyword arguments. """
|
|
1242
|
+
self._check_options(**kwargs)
|
|
1243
|
+
self._options.update(kwargs)
|
|
1244
|
+
|
|
1245
|
+
def transpose(self, conjugate=False):
|
|
1246
|
+
cls = type(self)
|
|
1247
|
+
At = self.linop.transpose(conjugate=conjugate)
|
|
1248
|
+
options = self._options
|
|
1249
|
+
return cls(At, **options)
|
|
1250
|
+
|
|
1251
|
+
#===============================================================================
|
|
1252
|
+
class LinearSolver(ABC):
|
|
1253
|
+
"""
|
|
1254
|
+
Solver for the square linear system Ax=b, where x and b belong to the same vector space V.
|
|
1255
|
+
|
|
1256
|
+
"""
|
|
1257
|
+
@property
|
|
1258
|
+
def shape(self):
|
|
1259
|
+
return (self.space.dimension, self.space.dimension)
|
|
1260
|
+
|
|
1261
|
+
#-------------------------------------
|
|
1262
|
+
# Deferred methods
|
|
1263
|
+
#-------------------------------------
|
|
1264
|
+
@property
|
|
1265
|
+
@abstractmethod
|
|
1266
|
+
def space(self):
|
|
1267
|
+
pass
|
|
1268
|
+
|
|
1269
|
+
@abstractmethod
|
|
1270
|
+
def transpose(self):
|
|
1271
|
+
"""Return the transpose of the LinearSolver."""
|
|
1272
|
+
pass
|
|
1273
|
+
|
|
1274
|
+
@abstractmethod
|
|
1275
|
+
def solve(self, rhs, out=None):
|
|
1276
|
+
pass
|
|
1277
|
+
|
|
1278
|
+
@property
|
|
1279
|
+
def T(self):
|
|
1280
|
+
return self.transpose()
|
|
1281
|
+
|
|
1282
|
+
#===============================================================================
|
|
1283
|
+
class MatrixFreeLinearOperator(LinearOperator):
|
|
1284
|
+
"""
|
|
1285
|
+
General linear operator represented by a callable dot method.
|
|
1286
|
+
|
|
1287
|
+
Parameters
|
|
1288
|
+
----------
|
|
1289
|
+
domain : VectorSpace
|
|
1290
|
+
The domain of the linear operator.
|
|
1291
|
+
|
|
1292
|
+
codomain : VectorSpace
|
|
1293
|
+
The codomain of the linear operator.
|
|
1294
|
+
|
|
1295
|
+
dot : Callable
|
|
1296
|
+
The method of the linear operator, assumed to map from domain to codomain.
|
|
1297
|
+
This method can take out as an optional argument but this is not mandatory.
|
|
1298
|
+
The callable can take other keyword arguments as for instance function parameters.
|
|
1299
|
+
|
|
1300
|
+
dot_transpose: Callable
|
|
1301
|
+
The method of the transpose of the linear operator, assumed to map from codomain to domain.
|
|
1302
|
+
This method can take out as an optional argument but this is not mandatory.
|
|
1303
|
+
|
|
1304
|
+
Examples
|
|
1305
|
+
--------
|
|
1306
|
+
# example 1: a matrix encapsulated as a (fake) matrix-free linear operator
|
|
1307
|
+
A_SM = StencilMatrix(V, W)
|
|
1308
|
+
AT_SM = A_SM.transpose()
|
|
1309
|
+
A = MatrixFreeLinearOperator(domain=V, codomain=W, dot=lambda v: A_SM @ v, dot_transpose=lambda v: AT_SM @ v)
|
|
1310
|
+
|
|
1311
|
+
# example 2: a truly matrix-free linear operator
|
|
1312
|
+
A = MatrixFreeLinearOperator(domain=V, codomain=V, dot=lambda v: 2*v, dot_transpose=lambda v: 2*v)
|
|
1313
|
+
|
|
1314
|
+
"""
|
|
1315
|
+
|
|
1316
|
+
def __init__(self, domain, codomain, dot, dot_transpose=None):
|
|
1317
|
+
|
|
1318
|
+
assert isinstance(domain, VectorSpace)
|
|
1319
|
+
assert isinstance(codomain, VectorSpace)
|
|
1320
|
+
assert isinstance(dot, LambdaType)
|
|
1321
|
+
|
|
1322
|
+
self._domain = domain
|
|
1323
|
+
self._codomain = codomain
|
|
1324
|
+
self._dot = dot
|
|
1325
|
+
|
|
1326
|
+
sig = signature(dot)
|
|
1327
|
+
self._dot_takes_out_arg = ('out' in [p.name for p in sig.parameters.values() if p.kind == p.KEYWORD_ONLY])
|
|
1328
|
+
|
|
1329
|
+
if dot_transpose is not None:
|
|
1330
|
+
assert isinstance(dot_transpose, LambdaType)
|
|
1331
|
+
self._dot_transpose = dot_transpose
|
|
1332
|
+
sig = signature(dot_transpose)
|
|
1333
|
+
self._dot_transpose_takes_out_arg = ('out' in [p.name for p in sig.parameters.values() if p.kind == p.KEYWORD_ONLY])
|
|
1334
|
+
else:
|
|
1335
|
+
self._dot_transpose = None
|
|
1336
|
+
self._dot_transpose_takes_out_arg = False
|
|
1337
|
+
|
|
1338
|
+
@property
|
|
1339
|
+
def domain(self):
|
|
1340
|
+
return self._domain
|
|
1341
|
+
|
|
1342
|
+
@property
|
|
1343
|
+
def codomain(self):
|
|
1344
|
+
return self._codomain
|
|
1345
|
+
|
|
1346
|
+
@property
|
|
1347
|
+
def dtype(self):
|
|
1348
|
+
return None
|
|
1349
|
+
|
|
1350
|
+
def dot(self, v, out=None, **kwargs):
|
|
1351
|
+
assert isinstance(v, Vector)
|
|
1352
|
+
assert v.space == self.domain
|
|
1353
|
+
|
|
1354
|
+
if out is not None:
|
|
1355
|
+
assert isinstance(out, Vector)
|
|
1356
|
+
assert out.space == self.codomain
|
|
1357
|
+
else:
|
|
1358
|
+
out = self.codomain.zeros()
|
|
1359
|
+
|
|
1360
|
+
if self._dot_takes_out_arg:
|
|
1361
|
+
self._dot(v, out=out, **kwargs)
|
|
1362
|
+
else:
|
|
1363
|
+
# provided dot product does not take an out argument: we simply copy the result into out
|
|
1364
|
+
self._dot(v, **kwargs).copy(out=out)
|
|
1365
|
+
|
|
1366
|
+
return out
|
|
1367
|
+
|
|
1368
|
+
def toarray(self):
|
|
1369
|
+
raise NotImplementedError('toarray() is not defined for MatrixFreeLinearOperator.')
|
|
1370
|
+
|
|
1371
|
+
def tosparse(self):
|
|
1372
|
+
raise NotImplementedError('tosparse() is not defined for MatrixFreeLinearOperator.')
|
|
1373
|
+
|
|
1374
|
+
def transpose(self, conjugate=False):
|
|
1375
|
+
if self._dot_transpose is None:
|
|
1376
|
+
raise NotImplementedError('no transpose dot method was given -- cannot create the transpose operator')
|
|
1377
|
+
|
|
1378
|
+
if conjugate:
|
|
1379
|
+
if self._dot_transpose_takes_out_arg:
|
|
1380
|
+
new_dot = lambda v, out=None: self._dot_transpose(v, out=out).conjugate()
|
|
1381
|
+
else:
|
|
1382
|
+
new_dot = lambda v: self._dot_transpose(v).conjugate()
|
|
1383
|
+
else:
|
|
1384
|
+
new_dot = self._dot_transpose
|
|
1385
|
+
|
|
1386
|
+
return MatrixFreeLinearOperator(domain=self.codomain, codomain=self.domain, dot=new_dot, dot_transpose=self._dot)
|