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.
Files changed (98) hide show
  1. feectools/__init__.py +0 -0
  2. feectools/accelerate/__init__.py +0 -0
  3. feectools/accelerate/accelerate.py +220 -0
  4. feectools/accelerate/compile_psydac.mk +52 -0
  5. feectools/api/__init__.py +0 -0
  6. feectools/api/essential_bc.py +122 -0
  7. feectools/api/fem_bilinear_form.py +2226 -0
  8. feectools/api/fem_common.py +286 -0
  9. feectools/api/fem_sum_form.py +123 -0
  10. feectools/api/settings.py +82 -0
  11. feectools/core/__init__.py +11 -0
  12. feectools/core/bsplines.py +1107 -0
  13. feectools/core/bsplines_kernels.py +1349 -0
  14. feectools/core/field_evaluation_kernels.py +5015 -0
  15. feectools/core/tests/__init__.py +0 -0
  16. feectools/core/tests/test_bsplines.py +263 -0
  17. feectools/core/tests/test_bsplines_kernel.py +40 -0
  18. feectools/core/tests/test_bsplines_pyccel.py +752 -0
  19. feectools/ddm/__init__.py +3 -0
  20. feectools/ddm/basic.py +78 -0
  21. feectools/ddm/blocking_data_exchanger.py +348 -0
  22. feectools/ddm/cart.py +1835 -0
  23. feectools/ddm/interface_data_exchanger.py +122 -0
  24. feectools/ddm/mpi.py +109 -0
  25. feectools/ddm/nonblocking_data_exchanger.py +331 -0
  26. feectools/ddm/partition.py +207 -0
  27. feectools/ddm/petsc.py +112 -0
  28. feectools/ddm/tests/__init__.py +0 -0
  29. feectools/ddm/tests/test_cart_1d.py +138 -0
  30. feectools/ddm/tests/test_cart_2d.py +164 -0
  31. feectools/ddm/tests/test_cart_3d.py +158 -0
  32. feectools/ddm/tests/test_multicart_2d.py +173 -0
  33. feectools/ddm/tests/test_partition.py +124 -0
  34. feectools/ddm/utilities.py +24 -0
  35. feectools/feec/__init__.py +0 -0
  36. feectools/feec/derivatives.py +780 -0
  37. feectools/feec/dof_kernels.py +210 -0
  38. feectools/feec/global_geometric_projectors.py +1073 -0
  39. feectools/feec/hodge.py +148 -0
  40. feectools/fem/__init__.py +0 -0
  41. feectools/fem/basic.py +465 -0
  42. feectools/fem/grid.py +181 -0
  43. feectools/fem/partitioning.py +344 -0
  44. feectools/fem/projectors.py +160 -0
  45. feectools/fem/splines.py +559 -0
  46. feectools/fem/tensor.py +1393 -0
  47. feectools/fem/tests/__init__.py +0 -0
  48. feectools/fem/tests/analytical_profiles_1d.py +100 -0
  49. feectools/fem/tests/analytical_profiles_base.py +34 -0
  50. feectools/fem/tests/splines_error_bounds.py +155 -0
  51. feectools/fem/tests/test_spline_histopolation.py +120 -0
  52. feectools/fem/tests/test_spline_interpolation.py +182 -0
  53. feectools/fem/tests/test_splines.py +184 -0
  54. feectools/fem/tests/test_splines_par.py +46 -0
  55. feectools/fem/tests/test_vector_spaces.py +150 -0
  56. feectools/fem/tests/utilities.py +47 -0
  57. feectools/fem/vector.py +729 -0
  58. feectools/linalg/__init__.py +0 -0
  59. feectools/linalg/basic.py +1386 -0
  60. feectools/linalg/block.py +1451 -0
  61. feectools/linalg/direct_solvers.py +201 -0
  62. feectools/linalg/fft.py +258 -0
  63. feectools/linalg/kernels/__init__.py +0 -0
  64. feectools/linalg/kernels/axpy_kernels.py +57 -0
  65. feectools/linalg/kernels/inner_kernels.py +100 -0
  66. feectools/linalg/kernels/matvec_kernels.py +206 -0
  67. feectools/linalg/kernels/stencil2IJV_kernels.py +227 -0
  68. feectools/linalg/kernels/stencil2coo_kernels.py +179 -0
  69. feectools/linalg/kernels/transpose_kernels.py +263 -0
  70. feectools/linalg/kron.py +911 -0
  71. feectools/linalg/solvers.py +1914 -0
  72. feectools/linalg/sparse.py +114 -0
  73. feectools/linalg/stencil.py +2923 -0
  74. feectools/linalg/stencil_dot_kernels.py +317 -0
  75. feectools/linalg/stencil_transpose_kernels.py +372 -0
  76. feectools/linalg/tests/__init__.py +0 -0
  77. feectools/linalg/tests/test_block.py +1588 -0
  78. feectools/linalg/tests/test_fft.py +106 -0
  79. feectools/linalg/tests/test_kron_stencil_matrix.py +114 -0
  80. feectools/linalg/tests/test_linalg.py +1065 -0
  81. feectools/linalg/tests/test_matrix_free.py +128 -0
  82. feectools/linalg/tests/test_solvers.py +213 -0
  83. feectools/linalg/tests/test_stencil_interface_matrix.py +379 -0
  84. feectools/linalg/tests/test_stencil_vector.py +1036 -0
  85. feectools/linalg/tests/test_stencil_vector_space.py +440 -0
  86. feectools/linalg/topetsc.py +522 -0
  87. feectools/linalg/utilities.py +200 -0
  88. feectools/utilities/__init__.py +0 -0
  89. feectools/utilities/quadratures.py +113 -0
  90. feectools/utilities/utils.py +166 -0
  91. feectools/version.py +1 -0
  92. feectools-0.1.0.dist-info/METADATA +66 -0
  93. feectools-0.1.0.dist-info/RECORD +98 -0
  94. feectools-0.1.0.dist-info/WHEEL +5 -0
  95. feectools-0.1.0.dist-info/entry_points.txt +3 -0
  96. feectools-0.1.0.dist-info/licenses/AUTHORS +22 -0
  97. feectools-0.1.0.dist-info/licenses/LICENSE +21 -0
  98. 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)