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
feectools/feec/hodge.py
ADDED
|
@@ -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')
|