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,148 @@
1
+ import os
2
+ import numpy as np
3
+
4
+ from sympde.topology import elements_of
5
+ from sympde.topology.space import ScalarFunction
6
+ from sympde.calculus import dot
7
+ from sympde.expr.expr import BilinearForm
8
+ from sympde.expr.expr import integral
9
+
10
+ from feectools.api.settings import PSYDAC_BACKENDS
11
+
12
+ # ===============================================================================
13
+ class HodgeOperator:
14
+ """
15
+ Change of basis operator: dual basis -> primal basis
16
+
17
+ self._linop: matrix (LinearOperator) of the primal Hodge = this is the mass matrix !
18
+ self.dual_linop: this is the INVERSE mass matrix (LinearOperator)
19
+
20
+ Parameters
21
+ ----------
22
+ Vh: <FemSpace>
23
+ The discrete space
24
+
25
+ domain_h: <Geometry>
26
+ The discrete domain of the projector
27
+
28
+ metric : <str>
29
+ the metric of the de Rham complex
30
+
31
+ backend_language: <str>
32
+ The backend used to accelerate the code
33
+
34
+ Notes
35
+ -----
36
+ We only support the identity metric, this implies that the dual Hodge is the inverse of the primal one.
37
+ # todo: allow for non-identity metrics
38
+ """
39
+
40
+ def __init__(self, Vh, domain_h, metric='identity', backend_language='python'):
41
+
42
+ self._fem_domain = Vh
43
+ self._fem_codomain = Vh
44
+
45
+ # FemLinearOperators
46
+ self._primal_hodge = None
47
+ self._dual_hodge = None
48
+
49
+ # LinearOperators
50
+ self._linop = None
51
+ self._dual_linop = None
52
+
53
+ self._domain_h = domain_h
54
+ self._backend_language = backend_language
55
+
56
+ if not (metric == 'identity'):
57
+ raise NotImplementedError('only the identity metric is available')
58
+
59
+ self._metric = metric
60
+
61
+ def assemble_matrix(self):
62
+ """
63
+ the Hodge matrix is the patch-wise multi-patch mass matrix
64
+ it is not stored by default but assembled on demand
65
+ """
66
+ from feectools.api.discretization import discretize
67
+ from feectools.fem.basic import FemLinearOperator
68
+
69
+ if self._linop is None:
70
+ Vh = self._fem_domain
71
+ assert Vh == self._fem_codomain
72
+
73
+ V = Vh.symbolic_space
74
+ domain = V.domain
75
+ u, v = elements_of(V, names='u, v')
76
+
77
+ if isinstance(u, ScalarFunction):
78
+ expr = u * v
79
+ else:
80
+ expr = dot(u, v)
81
+
82
+ a = BilinearForm((u, v), integral(domain, expr))
83
+ ah = discretize(a, self._domain_h, [Vh, Vh], backend=PSYDAC_BACKENDS[self._backend_language])
84
+
85
+ self._linop = ah.assemble() # Mass matrix in stencil format
86
+
87
+ self._primal_hodge = FemLinearOperator(self._fem_domain, self._fem_codomain, linop=self._linop)
88
+
89
+ def assemble_dual_matrix(self, solver ='cg', **kwargs):
90
+ """
91
+ the dual Hodge matrix is the patch-wise inverse of the multi-patch mass matrix
92
+ it is not stored by default but computed on demand, by approximate local (patch-wise) inversion of the mass matrix
93
+ """
94
+ from feectools.linalg.solvers import inverse
95
+ from feectools.linalg.block import BlockLinearOperator
96
+ from feectools.fem.basic import FemLinearOperator
97
+
98
+ if self._dual_linop is None:
99
+ if not self._linop:
100
+ self.assemble_matrix()
101
+
102
+ M = self._linop # mass matrix of the (primal) basis
103
+
104
+ if self._fem_domain.is_multipatch:
105
+
106
+ nrows = M.n_block_rows
107
+ ncols = M.n_block_cols
108
+
109
+ inv_M_blocks = [list(b) for b in M.blocks]
110
+ for i in range(nrows):
111
+ Mii = M[i, i]
112
+ inv_Mii = inverse(Mii, solver=solver, **kwargs)
113
+ inv_M_blocks[i][i] = inv_Mii
114
+
115
+ self._dual_linop = BlockLinearOperator(M.codomain, M.domain, blocks=inv_M_blocks)
116
+ self._dual_hodge = FemLinearOperator(self._fem_codomain, self._fem_domain, linop=self._dual_linop)
117
+
118
+ else:
119
+ inv_M = inverse(M, solver=solver, **kwargs)
120
+ self._dual_hodge = FemLinearOperator(self._fem_codomain, self._fem_domain, linop=self._dual_linop)
121
+
122
+ @property
123
+ def linop(self):
124
+ if self._linop is None:
125
+ self.assemble_matrix()
126
+
127
+ return self._linop
128
+
129
+ @property
130
+ def dual_linop(self):
131
+ if self._dual_linop is None:
132
+ self.assemble_dual_matrix()
133
+
134
+ return self._dual_linop
135
+
136
+ @property
137
+ def hodge(self):
138
+ if self._linop is None:
139
+ self.assemble_matrix()
140
+
141
+ return self._primal_hodge
142
+
143
+ @property
144
+ def dual_hodge(self):
145
+ if self._dual_linop is None:
146
+ self.assemble_dual_matrix()
147
+
148
+ return self._dual_hodge
File without changes
feectools/fem/basic.py ADDED
@@ -0,0 +1,465 @@
1
+ # coding: utf-8
2
+
3
+ """
4
+ In order to avoid multiple inheritence, we define the base objects for Finite
5
+ Elements as abstract classes that contain a topological member. This member can
6
+ be used to specify the used data structure for example.
7
+ """
8
+
9
+ from abc import ABCMeta, abstractmethod
10
+ from feectools.linalg.basic import Vector, LinearOperator
11
+
12
+ __all__ = ('FemSpace', 'FemField', 'FemLinearOperator')
13
+
14
+ #===============================================================================
15
+ # ABSTRACT BASE CLASS: FINITE ELEMENT SPACE
16
+ #===============================================================================
17
+ class FemSpace( metaclass=ABCMeta ):
18
+ """
19
+ Generic Finite Element space V.
20
+
21
+ A unique basis is associated to a FemSpace, i.e. FemSpace = Span( basis )
22
+
23
+ """
24
+
25
+ #-----------------------------------------
26
+ # Abstract interface: read-only attributes
27
+ #-----------------------------------------
28
+ @property
29
+ @abstractmethod
30
+ def ldim( self ):
31
+ """
32
+ Number of dimensions in logical space,
33
+ i.e. number of scalar logical coordinates.
34
+ """
35
+
36
+ @property
37
+ @abstractmethod
38
+ def periodic( self ):
39
+ """
40
+ Tuple of booleans: along each logical dimension,
41
+ say if domain is periodic.
42
+ """
43
+
44
+ @property
45
+ @abstractmethod
46
+ def mapping( self ):
47
+ """
48
+ Mapping from logical coordinates 'eta' to physical coordinates 'x'.
49
+ If None, we assume identity mapping (hence x=eta).
50
+ """
51
+
52
+ @property
53
+ @abstractmethod
54
+ def coeff_space( self ):
55
+ """
56
+ Vector space of the coefficients (mapping invariant).
57
+ :rtype: feectools.linalg.basic.VectorSpace
58
+ """
59
+
60
+ @property
61
+ @abstractmethod
62
+ def is_multipatch( self ):
63
+ """
64
+ Boolean flag that describes whether the space is a multi-patch space.
65
+ :rtype: bool
66
+ """
67
+
68
+ @property
69
+ @abstractmethod
70
+ def is_vector_valued( self ):
71
+ """
72
+ Boolean flag that describes whether the space is vector-valued.
73
+ :rtype: bool
74
+ """
75
+
76
+ @property
77
+ @abstractmethod
78
+ def symbolic_space( self ):
79
+ """Symbolic space."""
80
+
81
+ @property
82
+ @abstractmethod
83
+ def patch_spaces(self):
84
+ """
85
+ Return the patch spaces (self if single-patch) as a tuple.
86
+ """
87
+
88
+ @property
89
+ @abstractmethod
90
+ def component_spaces(self):
91
+ """
92
+ Return the component spaces (self if scalar-valued) as a tuple.
93
+ """
94
+
95
+ @property
96
+ @abstractmethod
97
+ def axis_spaces(self):
98
+ """
99
+ Return the axis spaces (self if univariate) as a tuple.
100
+ """
101
+
102
+
103
+ #---------------------------------------
104
+ # Abstract interface: evaluation methods
105
+ #---------------------------------------
106
+ @abstractmethod
107
+ def eval_field( self, field, *eta, weights=None):
108
+ """
109
+ Evaluate field at location(s) eta.
110
+
111
+ Parameters
112
+ ----------
113
+ field : FemField
114
+ Field object (element of FemSpace) to be evaluated.
115
+
116
+ eta : list of float or numpy.ndarray
117
+ Evaluation point(s) in logical domain.
118
+
119
+ weights : StencilVector, optional
120
+ Weights of the basis functions, such that weights.space == field.coeffs.space.
121
+
122
+ Returns
123
+ -------
124
+ value : float or numpy.ndarray
125
+ Field value(s) at location(s) eta.
126
+
127
+ """
128
+
129
+ @abstractmethod
130
+ def eval_field_gradient( self, field, *eta , weights=None):
131
+ """
132
+ Evaluate field gradient at location(s) eta.
133
+
134
+ Parameters
135
+ ----------
136
+ field : FemField
137
+ Field object (element of FemSpace) to be evaluated.
138
+
139
+ eta : list of float or numpy.ndarray
140
+ Evaluation point(s) in logical domain.
141
+
142
+ weights : StencilVector, optional
143
+ Weights of the basis functions, such that weights.space == field.coeffs.space.
144
+
145
+ Returns
146
+ -------
147
+ value : float or numpy.ndarray
148
+ Value(s) of field gradient at location(s) eta.
149
+
150
+ """
151
+
152
+
153
+ #----------------------
154
+ # Concrete methods
155
+ #----------------------
156
+ def __mul__(self, a):
157
+ raise NotImplementedError('if this method __mul__ is used, it should not be implemented like this: TODO')
158
+ # [MCP 27.03.2025]: commented because improper implementation. must be rewritten if needed
159
+
160
+ # from feectools.fem.vector import create_product_space
161
+
162
+ # spaces = [*(self.spaces if self.is_product else [self]),
163
+ # *( a.spaces if a.is_product else [a])]
164
+
165
+ # space = create_product_space(*spaces)
166
+ # if a.symbolic_space and self.symbolic_space:
167
+ # space._symbolic_space = self.symbolic_space*a.symbolic_space
168
+ # return space
169
+
170
+ def __rmul__(self, a):
171
+ raise NotImplementedError('if this method __rmul__ is used, it should not be implemented like this: TODO')
172
+ # [MCP 27.03.2025]: commented because improper implementation. must be rewritten if needed
173
+
174
+ # from feectools.fem.vector import create_product_space
175
+
176
+ # spaces = [*( a.spaces if a.is_product else [a]),
177
+ # *(self.spaces if self.is_product else [self]),]
178
+
179
+ # space = create_product_space(*spaces)
180
+
181
+ # if a.symbolic_space and self.symbolic_space:
182
+ # space._symbolic_space = a.symbolic_space * self.symbolic_space
183
+ # return space
184
+
185
+ #---------------------------------------
186
+ # OLD STUFF
187
+ #---------------------------------------
188
+
189
+ # @abstractmethod
190
+ # def integral( self, f ):
191
+ # """
192
+ # Compute integral of scalar callable function $f(\eta)$ over logical domain
193
+ # $\Omega$, with Jacobian determinant of mapping $J(\eta)$ as weighting function:
194
+ #
195
+ # I = \integral_{\Omega} f(\eta) |J(\eta)| d\eta.
196
+ #
197
+ # Parameters
198
+ # ----------
199
+ # f : callable
200
+ # Integrand scalar function $f(\eta)$ over logical domain.
201
+ #
202
+ # Returns
203
+ # -------
204
+ # value : float
205
+ # Integral of $f(\eta) J(\eta)$ over logical domain.
206
+ #
207
+ # """
208
+ #
209
+ #
210
+ # # NOTE: why not giving the number of field components?
211
+ # @property
212
+ # @abstractmethod
213
+ # def is_scalar( self ):
214
+ # """Elements of space are scalar fields? [True|False]."""
215
+ #
216
+ # # NOTE: why does 'nbasis' have different behavior for tensor product spaces?
217
+ # @property
218
+ # @abstractmethod
219
+ # def nbasis( self ):
220
+ # """
221
+ # Number of linearly independent elements in basis.
222
+ # For a tensor product space this is a tuple of integers.
223
+ #
224
+ # """
225
+ #
226
+ # # NOTE: why is 'degree' part of abstract interface?
227
+ # # What if one were to use a global basis like Fourier?
228
+ # @property
229
+ # @abstractmethod
230
+ # def degree( self ):
231
+ # """Tuple of integers: polynomial degree along each logical dimension."""
232
+ #
233
+ # # NOTE: why is 'ncells' part of abstract interface?
234
+ # # What if one were to use a global basis like Fourier?
235
+ # @property
236
+ # @abstractmethod
237
+ # def ncells( self ):
238
+ # """Tuple of integers: number of grid cells along each logical dimension."""
239
+
240
+ #===============================================================================
241
+ # CONCRETE CLASS: ELEMENT OF A FEM SPACE
242
+ #===============================================================================
243
+ class FemField:
244
+ """
245
+ Element of a finite element space V.
246
+
247
+ Parameters
248
+ ----------
249
+ space : feectools.fem.basic.FemSpace
250
+ Finite element space to which this field belongs.
251
+
252
+ coeffs : feectools.linalg.basic.Vector (optional)
253
+ Vector of coefficients in finite element basis
254
+ (by default assume zero vector).
255
+
256
+ """
257
+ def __init__( self, space, coeffs=None ):
258
+
259
+ assert isinstance( space, FemSpace )
260
+
261
+ if coeffs is not None:
262
+ assert isinstance( coeffs, Vector )
263
+ assert space.coeff_space is coeffs.space
264
+ else:
265
+ coeffs = space.coeff_space.zeros()
266
+
267
+ # Case of vector-valued or multipatch field, element of a Product Space
268
+ if space.is_multipatch or space.is_vector_valued:
269
+ fields = tuple(FemField(V, c) for V, c in zip(space.spaces, coeffs))
270
+ else:
271
+ fields = tuple()
272
+
273
+ self._space = space
274
+ self._coeffs = coeffs
275
+ self._fields = fields
276
+
277
+ # ...
278
+ @property
279
+ def space( self ):
280
+ """Finite element space to which this field belongs."""
281
+ return self._space
282
+
283
+ # ...
284
+ @property
285
+ def coeffs( self ):
286
+ """
287
+ Coefficients that uniquely identify this field as a linear combination of
288
+ the elements of the basis of a Finite element space.
289
+
290
+ Coefficients are stored into one element of the vector space in
291
+ 'self.space.coeff_space', which is topologically associated to
292
+ the finite element space.
293
+
294
+ """
295
+ return self._coeffs
296
+
297
+ # ...
298
+ @property
299
+ def fields(self):
300
+ return self._fields
301
+
302
+ @property
303
+ def patch_fields(self):
304
+ """ Return the patch fields (only self if single-patch) as a tuple """
305
+ if self.space.is_multipatch:
306
+ return self.fields
307
+ else:
308
+ return (self,)
309
+
310
+ @property
311
+ def component_fields(self):
312
+ """ Return the component fields (only self if scalar-valued) as a tuple """
313
+ if self.space.is_vector_valued:
314
+ return self.fields
315
+ else:
316
+ return (self,)
317
+
318
+ # ...
319
+ def __getitem__(self, key):
320
+ return self._fields[key]
321
+
322
+ # ...
323
+ def __call__( self, *eta , weights=None):
324
+ """Evaluate weighted field at location identified by logical coordinates eta."""
325
+ return self._space.eval_field( self, *eta , weights=weights)
326
+
327
+ # ...
328
+ def gradient( self, *eta , weights=None):
329
+ """Evaluate gradient of weighted field at location identified by logical coordinates eta."""
330
+ return self._space.eval_field_gradient( self, *eta , weights=weights)
331
+
332
+ # ...
333
+ def divergence(self, *eta, weights=None):
334
+ """Evaluate divergence of weighted vector field at location identified by logical coordinates eta."""
335
+ return self._space.eval_field_divergence(self, *eta, weights=weights)
336
+
337
+ # ...
338
+ def copy(self):
339
+ return FemField(self._space, coeffs = self._coeffs.copy())
340
+
341
+ # ...
342
+ def __neg__(self):
343
+ return FemField(self._space, coeffs = -self._coeffs)
344
+
345
+ # ...
346
+ def __mul__(self, a):
347
+ return FemField(self._space, coeffs = self._coeffs * a)
348
+
349
+ # ...
350
+ def __rmul__(self, a):
351
+ return FemField(self._space, coeffs = a * self._coeffs)
352
+
353
+ # ...
354
+ def __add__(self, other):
355
+ assert isinstance(other, FemField)
356
+ assert self._space is other._space
357
+ return FemField(self._space, coeffs = self._coeffs + other._coeffs)
358
+
359
+ # ...
360
+ def __sub__(self, other):
361
+ assert isinstance(other, FemField)
362
+ assert self._space is other._space
363
+ return FemField(self._space, coeffs = self._coeffs - other._coeffs)
364
+
365
+ # ...
366
+ def __imul__(self, a):
367
+ self._coeffs *= a
368
+ return self
369
+
370
+ # ...
371
+ def __iadd__(self, other):
372
+ assert isinstance(other, FemField)
373
+ assert self._space is other._space
374
+ self._coeffs += other._coeffs
375
+ return self
376
+
377
+ # ...
378
+ def __isub__(self, other):
379
+ assert isinstance(other, FemField)
380
+ assert self._space is other._space
381
+ self._coeffs -= other._coeffs
382
+ return self
383
+
384
+ #===============================================================================
385
+ # CONCRETE CLASS: Linear Operator acting on a FEM field
386
+ #===============================================================================
387
+ class FemLinearOperator:
388
+ """
389
+ Linear operators with an additional FEM layer.
390
+ There is also a shorthand access to sparse matrices as they are sometimes
391
+ used in the FEEC interfaces.
392
+ Parameters
393
+ ----------
394
+ fem_domain : feectools.fem.basic.FemSpace
395
+ The discrete space of the domain
396
+
397
+ fem_codomain : feectools.fem.basic.FemSpace
398
+ The discrete space of the codomain
399
+
400
+ linop : <feectools.linalg.basic.LinearOperator>
401
+ Linear Operator.
402
+
403
+ """
404
+
405
+ def __init__(self, fem_domain, fem_codomain, *, linop=None):
406
+ assert isinstance(fem_domain, FemSpace)
407
+ assert isinstance(fem_codomain, FemSpace)
408
+ if linop is not None:
409
+ assert isinstance(linop, LinearOperator)
410
+
411
+ self._fem_domain = fem_domain
412
+ self._fem_codomain = fem_codomain
413
+
414
+ self._linop_domain = fem_domain.coeff_space
415
+ self._linop_codomain = fem_codomain.coeff_space
416
+
417
+ self._linop = linop
418
+
419
+ @property
420
+ def fem_domain(self):
421
+ return self._fem_domain
422
+
423
+ @property
424
+ def fem_codomain(self):
425
+ return self._fem_codomain
426
+
427
+ @property
428
+ def linop_domain(self):
429
+ return self._linop_domain
430
+
431
+ @property
432
+ def linop_codomain(self):
433
+ return self._linop_codomain
434
+
435
+ @property
436
+ def linop(self):
437
+ return self._linop
438
+
439
+ def toarray(self):
440
+ return self._linop.toarray()
441
+
442
+ def tosparse(self):
443
+ return self._linop.tosparse()
444
+
445
+ #--------------------------------------------------------------------------
446
+ def __call__(self, u, *, out=None):
447
+ assert isinstance(u, FemField)
448
+ assert u.space == self.fem_domain
449
+
450
+ if self._linop is not None:
451
+ coeffs = self._linop.dot(u.coeffs)
452
+ else:
453
+ raise NotImplementedError('Class does not provide a __call__ method without a linear operator')
454
+
455
+ return FemField(self.fem_codomain, coeffs=coeffs)
456
+
457
+ def dot(self, f_coeffs, *, out=None):
458
+ assert isinstance(f_coeffs, Vector)
459
+ assert f_coeffs.space is self._linop_domain
460
+
461
+ if self._linop is not None:
462
+ f = FemField(self.fem_domain, coeffs=f_coeffs)
463
+ return self(f).coeffs
464
+ else:
465
+ raise NotImplementedError('Class does not provide a dot method without a linear operator')