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/fem/vector.py
ADDED
|
@@ -0,0 +1,729 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
|
|
3
|
+
# TODO: - have a block version for VectorSpace when all component spaces are the same
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
from functools import reduce
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
from feectools.linalg.basic import Vector
|
|
10
|
+
from feectools.linalg.stencil import StencilVectorSpace
|
|
11
|
+
from feectools.linalg.block import BlockVectorSpace
|
|
12
|
+
from feectools.fem.basic import FemSpace, FemField
|
|
13
|
+
|
|
14
|
+
__all__ = ('VectorFemSpace', 'MultipatchFemSpace')
|
|
15
|
+
|
|
16
|
+
#===============================================================================
|
|
17
|
+
class VectorFemSpace(FemSpace):
|
|
18
|
+
"""
|
|
19
|
+
FEM space with a vector basis defined on a single patch.
|
|
20
|
+
This class is used to represent either spaces of vector-valued FEM fields,
|
|
21
|
+
or product spaces involved in systems of equations.
|
|
22
|
+
|
|
23
|
+
Parameters
|
|
24
|
+
----------
|
|
25
|
+
*spaces : FemSpace
|
|
26
|
+
Single-patch FEM spaces, either scalar or vector-valued.
|
|
27
|
+
"""
|
|
28
|
+
def __init__(self, *spaces):
|
|
29
|
+
|
|
30
|
+
# Check that all input spaces are of the correct type
|
|
31
|
+
assert all(isinstance(V, FemSpace) for V in spaces)
|
|
32
|
+
|
|
33
|
+
# We do not accept multipatch spaces yet
|
|
34
|
+
assert not any(V.is_multipatch for V in spaces)
|
|
35
|
+
|
|
36
|
+
# All input spaces are flattened into a tuple `new_spaces` of scalar spaces
|
|
37
|
+
new_spaces = [sp.spaces if isinstance(sp, VectorFemSpace) else [sp] for sp in spaces]
|
|
38
|
+
new_spaces = tuple(sp2 for sp1 in new_spaces for sp2 in sp1)
|
|
39
|
+
|
|
40
|
+
# Check that we indeed have scalar spaces only
|
|
41
|
+
assert not any(V.is_vector_valued for V in new_spaces)
|
|
42
|
+
|
|
43
|
+
# Check that all spaces have the same parametric dimension
|
|
44
|
+
ldims = [V.ldim for V in new_spaces]
|
|
45
|
+
assert len(set(ldims)) == 1
|
|
46
|
+
|
|
47
|
+
# Make sure that all spaces have the same periodicity along each axis
|
|
48
|
+
periodic = [V.periodic for V in new_spaces]
|
|
49
|
+
for pp in zip(*periodic):
|
|
50
|
+
assert len(set(pp)) == 1
|
|
51
|
+
|
|
52
|
+
# Make sure that all spaces have the same mapping or no mapping at all
|
|
53
|
+
# Mapping must be of type BasicCallableMapping defined in SymPDE
|
|
54
|
+
# [YG, 27.03.2025]: this class was setting its mapping to None
|
|
55
|
+
mappings = [V.mapping for V in new_spaces]
|
|
56
|
+
assert len(set(mappings)) == 1
|
|
57
|
+
assert mappings[0] is None or isinstance(mappings[0], BasicCallableMapping)
|
|
58
|
+
|
|
59
|
+
# Make sure that all spaces have the same number of cells along each axis
|
|
60
|
+
# [YG, 27.03.2025]: This is not part of the abstract interface of
|
|
61
|
+
# FemSpace and it assumes that all spaces are TensorFemSpaces
|
|
62
|
+
ncells = [V.ncells for V in new_spaces]
|
|
63
|
+
for nc in zip(*ncells):
|
|
64
|
+
assert len(set(nc)) == 1
|
|
65
|
+
|
|
66
|
+
# Compute the SymPDE symbolic space from the symbolic spaces of the input spaces
|
|
67
|
+
# symbolic_spaces = [V.symbolic_space for V in spaces]
|
|
68
|
+
# symbolic_space = reduce(lambda x, y: x * y, symbolic_spaces) if all(symbolic_spaces) else None
|
|
69
|
+
|
|
70
|
+
self._symbolic_space = None
|
|
71
|
+
# if all(s.symbolic_space for s in spaces):
|
|
72
|
+
# symbolic_spaces = [s.symbolic_space for s in spaces]
|
|
73
|
+
# self._symbolic_space = reduce(lambda x,y:x*y, symbolic_spaces)
|
|
74
|
+
# Compute the VectorSpace of the coefficients
|
|
75
|
+
coeff_space = BlockVectorSpace(*[V.coeff_space for V in new_spaces])
|
|
76
|
+
|
|
77
|
+
# Store information in private attributes
|
|
78
|
+
self._ldim : int = ldims[0]
|
|
79
|
+
self._periodic : tuple[bool, ...] = periodic[0]
|
|
80
|
+
self._spaces : tuple[FemSpace] = new_spaces
|
|
81
|
+
self._coeff_space : BlockVectorSpace = coeff_space
|
|
82
|
+
self._ncells : tuple[int, ...] = ncells[0] # not used in the abstract interface
|
|
83
|
+
self._mapping : Optional[BasicCallableMapping] = mappings[0]
|
|
84
|
+
# self._symbolic_space : Optional[BasicFunctionSpace] = symbolic_space
|
|
85
|
+
|
|
86
|
+
# ++++++++++++++ Extra operations for multigrid methods ++++++++++++++
|
|
87
|
+
# Initialize the dictionary that will store the refined VectorFemSpaces
|
|
88
|
+
self._refined_space = {}
|
|
89
|
+
|
|
90
|
+
# Compute the refined VectorFemSpaces from the refined spaces of the
|
|
91
|
+
# scalar spaces `new_spaces`. The method `set_refined_space` is used to
|
|
92
|
+
# update the dictionary, and it perfoms additional checks. We also use
|
|
93
|
+
# the property `spaces` which is not part of the abstract interface.
|
|
94
|
+
self.set_refined_space(self.ncells, self)
|
|
95
|
+
for key in self.spaces[0]._refined_space:
|
|
96
|
+
if key != tuple(self.ncells):
|
|
97
|
+
V_fine = VectorFemSpace(*[V._refined_space[key] for V in self.spaces])
|
|
98
|
+
self.set_refined_space(key, V_fine)
|
|
99
|
+
#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
100
|
+
|
|
101
|
+
#--------------------------------------------------------------------------
|
|
102
|
+
# Abstract interface: read-only attributes
|
|
103
|
+
#--------------------------------------------------------------------------
|
|
104
|
+
@property
|
|
105
|
+
def ldim(self):
|
|
106
|
+
""" Parametric dimension.
|
|
107
|
+
"""
|
|
108
|
+
return self._ldim
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def periodic(self):
|
|
112
|
+
"""
|
|
113
|
+
Tuple of booleans: along each logical dimension,
|
|
114
|
+
say if domain is periodic.
|
|
115
|
+
:rtype: tuple[bool]
|
|
116
|
+
"""
|
|
117
|
+
return self._periodic
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def mapping(self):
|
|
121
|
+
# [YG, 27.03.2025]: not clear why there should be no mapping here
|
|
122
|
+
#return None
|
|
123
|
+
return self._mapping
|
|
124
|
+
|
|
125
|
+
@property
|
|
126
|
+
def coeff_space(self):
|
|
127
|
+
"""
|
|
128
|
+
Vector space of the coefficients (mapping invariant).
|
|
129
|
+
:rtype: feectools.linalg.block.BlockVectorSpace
|
|
130
|
+
"""
|
|
131
|
+
return self._coeff_space
|
|
132
|
+
|
|
133
|
+
@property
|
|
134
|
+
def symbolic_space( self ):
|
|
135
|
+
return self._symbolic_space
|
|
136
|
+
|
|
137
|
+
@symbolic_space.setter
|
|
138
|
+
def symbolic_space( self, symbolic_space ):
|
|
139
|
+
#assert isinstance(symbolic_space, BasicFunctionSpace)
|
|
140
|
+
self._symbolic_space = symbolic_space
|
|
141
|
+
|
|
142
|
+
@property
|
|
143
|
+
def patch_spaces(self):
|
|
144
|
+
return (self,)
|
|
145
|
+
|
|
146
|
+
@property
|
|
147
|
+
def component_spaces(self):
|
|
148
|
+
return self._spaces
|
|
149
|
+
|
|
150
|
+
@property
|
|
151
|
+
def axis_spaces(self):
|
|
152
|
+
raise NotImplementedError('Vector Fem space has no list of axis spaces')
|
|
153
|
+
|
|
154
|
+
@property
|
|
155
|
+
def is_multipatch(self):
|
|
156
|
+
return False
|
|
157
|
+
|
|
158
|
+
@property
|
|
159
|
+
def is_vector_valued(self):
|
|
160
|
+
return True
|
|
161
|
+
|
|
162
|
+
#--------------------------------------------------------------------------
|
|
163
|
+
# Abstract interface: evaluation methods
|
|
164
|
+
#--------------------------------------------------------------------------
|
|
165
|
+
def eval_field( self, field, *eta, weights=None):
|
|
166
|
+
|
|
167
|
+
assert isinstance(field, FemField)
|
|
168
|
+
assert field.space is self
|
|
169
|
+
assert len(eta) == self._ldim
|
|
170
|
+
|
|
171
|
+
return self.eval_fields(eta, field, weights=weights)[0]
|
|
172
|
+
|
|
173
|
+
# ...
|
|
174
|
+
def eval_fields(self, grid, *fields, weights=None, npts_per_cell=None, overlap=0):
|
|
175
|
+
"""Evaluates one or several fields on the given location(s) grid.
|
|
176
|
+
|
|
177
|
+
Parameters
|
|
178
|
+
----------
|
|
179
|
+
grid : List of ndarray
|
|
180
|
+
Grid on which to evaluate the fields.
|
|
181
|
+
Each array in this list corresponds to one logical coordinate.
|
|
182
|
+
|
|
183
|
+
*fields : tuple of feectools.fem.basic.FemField
|
|
184
|
+
Fields to evaluate.
|
|
185
|
+
|
|
186
|
+
weights : feectools.fem.basic.FemField or None, optional
|
|
187
|
+
Weights field used to weight the basis functions thus
|
|
188
|
+
turning them into NURBS. The same weights field is used
|
|
189
|
+
for all of fields and they thus have to use the same basis functions.
|
|
190
|
+
|
|
191
|
+
npts_per_cell: int, tuple of int or None, optional
|
|
192
|
+
Number of evaluation points in each cell.
|
|
193
|
+
If an integer is given, then assume that it is the same in every direction.
|
|
194
|
+
|
|
195
|
+
overlap : int
|
|
196
|
+
How much to overlap. Only used in the distributed context.
|
|
197
|
+
|
|
198
|
+
Returns
|
|
199
|
+
-------
|
|
200
|
+
List of list of ndarray
|
|
201
|
+
List of the same length as `fields`, containing for each field
|
|
202
|
+
a list of `self.ldim` arrays, i.e. one array for each logical coordinate.
|
|
203
|
+
|
|
204
|
+
See Also
|
|
205
|
+
--------
|
|
206
|
+
feectools.fem.tensor.TensorFemSpace.eval_fields : More information about the grid parameter.
|
|
207
|
+
"""
|
|
208
|
+
result = []
|
|
209
|
+
if weights is not None:
|
|
210
|
+
for i in range(self.ldim):
|
|
211
|
+
fields_i = list(field.fields[i] for field in fields)
|
|
212
|
+
|
|
213
|
+
result.append(self._spaces[i].eval_fields(grid,
|
|
214
|
+
*fields_i,
|
|
215
|
+
npts_per_cell=npts_per_cell,
|
|
216
|
+
weights=weights.fields[i],
|
|
217
|
+
overlap=overlap))
|
|
218
|
+
else:
|
|
219
|
+
for i in range(self.ldim):
|
|
220
|
+
fields_i = list(field.fields[i] for field in fields)
|
|
221
|
+
result.append(self._spaces[i].eval_fields(grid,
|
|
222
|
+
*fields_i,
|
|
223
|
+
npts_per_cell=npts_per_cell,
|
|
224
|
+
overlap=overlap))
|
|
225
|
+
return [tuple(result[j][i] for j in range(self.ldim)) for i in range(len(fields))]
|
|
226
|
+
|
|
227
|
+
# ...
|
|
228
|
+
def eval_fields_regular_tensor_grid(self, grid, *fields, weights=None, overlap=0):
|
|
229
|
+
"""Evaluates one or several fields on a regular tensor grid.
|
|
230
|
+
|
|
231
|
+
Parameters
|
|
232
|
+
----------
|
|
233
|
+
grid : List of ndarray
|
|
234
|
+
List of 2D arrays representing each direction of the grid.
|
|
235
|
+
Each of these arrays should have shape (ne_xi, nv_xi) where ne_xi is the
|
|
236
|
+
number of cells in the domain in the direction xi and nv_xi is the number of
|
|
237
|
+
evaluation points in the same direction.
|
|
238
|
+
|
|
239
|
+
*fields : tuple of feectools.fem.basic.FemField
|
|
240
|
+
Fields to evaluate.
|
|
241
|
+
|
|
242
|
+
weights : feectools.fem.basic.FemField or None, optional
|
|
243
|
+
Weights field used to weight the basis functions thus
|
|
244
|
+
turning them into NURBS. The same weights field is used
|
|
245
|
+
for all of fields and they thus have to use the same basis functions.
|
|
246
|
+
|
|
247
|
+
overlap : int
|
|
248
|
+
How much to overlap. Only used in the distributed context.
|
|
249
|
+
|
|
250
|
+
Returns
|
|
251
|
+
-------
|
|
252
|
+
List of list of ndarray
|
|
253
|
+
List of the same length as `fields`, containing for each field
|
|
254
|
+
a list of `self.ldim` arrays, i.e. one array for each logical coordinate.
|
|
255
|
+
|
|
256
|
+
See Also
|
|
257
|
+
--------
|
|
258
|
+
feectools.fem.tensor.TensorFemSpace.eval_fields : More information about the grid parameter.
|
|
259
|
+
"""
|
|
260
|
+
for f in fields:
|
|
261
|
+
# Necessary if vector coeffs is distributed across processes
|
|
262
|
+
if not f.coeffs.ghost_regions_in_sync:
|
|
263
|
+
f.coeffs.update_ghost_regions()
|
|
264
|
+
|
|
265
|
+
result = []
|
|
266
|
+
if isinstance(overlap, int):
|
|
267
|
+
overlap = [overlap] * self.ldim
|
|
268
|
+
if weights is not None:
|
|
269
|
+
for i in range(self.ldim):
|
|
270
|
+
fields_i = list(field.fields[i] for field in fields)
|
|
271
|
+
|
|
272
|
+
result.append(self._spaces[i].eval_fields_regular_tensor_grid(grid,
|
|
273
|
+
*fields_i,
|
|
274
|
+
weights=weights.fields[i],
|
|
275
|
+
overlap=overlap[i]))
|
|
276
|
+
else:
|
|
277
|
+
for i in range(self.ldim):
|
|
278
|
+
fields_i = list(field.fields[i] for field in fields)
|
|
279
|
+
|
|
280
|
+
result.append(self._spaces[i].eval_fields_regular_tensor_grid(grid,
|
|
281
|
+
*fields_i,
|
|
282
|
+
overlap=overlap[i]))
|
|
283
|
+
|
|
284
|
+
return result
|
|
285
|
+
|
|
286
|
+
# ...
|
|
287
|
+
def eval_fields_irregular_tensor_grid(self, grid, *fields, weights=None, overlap=0):
|
|
288
|
+
"""Evaluates one or several fields on an irregular tensor grid i.e.
|
|
289
|
+
a tensor grid where the number of points per cell depends on the cell.
|
|
290
|
+
|
|
291
|
+
Parameters
|
|
292
|
+
----------
|
|
293
|
+
grid : List of ndarray
|
|
294
|
+
List of 1D arrays representing each direction of the grid.
|
|
295
|
+
|
|
296
|
+
*fields : tuple of feectools.fem.basic.FemField
|
|
297
|
+
Fields to evaluate.
|
|
298
|
+
|
|
299
|
+
weights : feectools.fem.basic.FemField or None, optional
|
|
300
|
+
Weights field used to weight the basis functions thus
|
|
301
|
+
turning them into NURBS. The same weights field is used
|
|
302
|
+
for all of fields and they thus have to use the same basis functions.
|
|
303
|
+
|
|
304
|
+
overlap : int
|
|
305
|
+
How much to overlap. Only used in the distributed context.
|
|
306
|
+
|
|
307
|
+
Returns
|
|
308
|
+
-------
|
|
309
|
+
List of list of ndarray
|
|
310
|
+
List of the same length as `fields`, containing for each field
|
|
311
|
+
a list of `self.ldim` arrays, i.e. one array for each logical coordinate.
|
|
312
|
+
|
|
313
|
+
See Also
|
|
314
|
+
--------
|
|
315
|
+
feectools.fem.tensor.TensorFemSpace.eval_fields : More information about the grid parameter.
|
|
316
|
+
"""
|
|
317
|
+
for f in fields:
|
|
318
|
+
# Necessary if vector coeffs is distributed across processes
|
|
319
|
+
if not f.coeffs.ghost_regions_in_sync:
|
|
320
|
+
f.coeffs.update_ghost_regions()
|
|
321
|
+
|
|
322
|
+
result = []
|
|
323
|
+
if isinstance(overlap, int):
|
|
324
|
+
overlap = [overlap] * self.ldim
|
|
325
|
+
if weights is not None:
|
|
326
|
+
for i in range(self.ldim):
|
|
327
|
+
fields_i = list(field.fields[i] for field in fields)
|
|
328
|
+
|
|
329
|
+
result.append(self._spaces[i].eval_fields_irregular_tensor_grid(grid,
|
|
330
|
+
*fields_i,
|
|
331
|
+
weights=weights.fields[i],
|
|
332
|
+
overlap=overlap[i]))
|
|
333
|
+
else:
|
|
334
|
+
for i in range(self.ldim):
|
|
335
|
+
fields_i = list(field.fields[i] for field in fields)
|
|
336
|
+
|
|
337
|
+
result.append(self._spaces[i].eval_fields_irregular_tensor_grid(grid,
|
|
338
|
+
*fields_i,
|
|
339
|
+
overlap=overlap[i]))
|
|
340
|
+
|
|
341
|
+
return result
|
|
342
|
+
|
|
343
|
+
# ...
|
|
344
|
+
def eval_field_gradient( self, field, *eta ):
|
|
345
|
+
|
|
346
|
+
assert isinstance( field, FemField )
|
|
347
|
+
assert field.space is self
|
|
348
|
+
assert len( eta ) == self._ldim
|
|
349
|
+
|
|
350
|
+
raise NotImplementedError( "VectorFemSpace not yet operational" )
|
|
351
|
+
|
|
352
|
+
# ...
|
|
353
|
+
def integral( self, f ):
|
|
354
|
+
|
|
355
|
+
assert hasattr( f, '__call__' )
|
|
356
|
+
|
|
357
|
+
raise NotImplementedError( "VectorFemSpace not yet operational" )
|
|
358
|
+
|
|
359
|
+
#--------------------------------------------------------------------------
|
|
360
|
+
# Other properties and methods
|
|
361
|
+
#--------------------------------------------------------------------------
|
|
362
|
+
@property
|
|
363
|
+
def nbasis(self):
|
|
364
|
+
dims = [V.nbasis for V in self.spaces]
|
|
365
|
+
return sum(dims)
|
|
366
|
+
|
|
367
|
+
@property
|
|
368
|
+
def degree(self):
|
|
369
|
+
return [V.degree for V in self.spaces]
|
|
370
|
+
|
|
371
|
+
@property
|
|
372
|
+
def multiplicity(self):
|
|
373
|
+
return [V.multiplicity for V in self.spaces]
|
|
374
|
+
|
|
375
|
+
@property
|
|
376
|
+
def pads(self):
|
|
377
|
+
return [V.pads for V in self.spaces]
|
|
378
|
+
|
|
379
|
+
@property
|
|
380
|
+
def ncells(self):
|
|
381
|
+
return self._ncells
|
|
382
|
+
|
|
383
|
+
@property
|
|
384
|
+
def spaces( self ):
|
|
385
|
+
return self._spaces
|
|
386
|
+
|
|
387
|
+
# ...
|
|
388
|
+
def get_refined_space(self, ncells):
|
|
389
|
+
return self._refined_space[tuple(ncells)]
|
|
390
|
+
|
|
391
|
+
def set_refined_space(self, ncells, new_space):
|
|
392
|
+
# [YG, 27.03.2025]: It appears that this method is assuming that the
|
|
393
|
+
# refined space has ldim=2, and that the number of cells is the same
|
|
394
|
+
# along each axis. These two conditions are very strong.
|
|
395
|
+
assert all(nc1==nc2 for nc1,nc2 in zip(ncells, new_space.ncells))
|
|
396
|
+
self._refined_space[tuple(ncells)] = new_space
|
|
397
|
+
|
|
398
|
+
def __str__(self):
|
|
399
|
+
"""Pretty printing"""
|
|
400
|
+
txt = '\n'
|
|
401
|
+
txt += '> ldim :: {ldim}\n'.format(ldim=self.ldim)
|
|
402
|
+
txt += '> total nbasis :: {dim}\n'.format(dim=self.nbasis)
|
|
403
|
+
|
|
404
|
+
dims = ', '.join(str(V.nbasis) for V in self.spaces)
|
|
405
|
+
txt += '> nbasis :: ({dims})\n'.format(dims=dims)
|
|
406
|
+
return txt
|
|
407
|
+
|
|
408
|
+
#===============================================================================
|
|
409
|
+
class MultipatchFemSpace(FemSpace):
|
|
410
|
+
"""
|
|
411
|
+
Product of single-patch FEM spaces.
|
|
412
|
+
|
|
413
|
+
Parameters
|
|
414
|
+
----------
|
|
415
|
+
*spaces : FemSpace
|
|
416
|
+
Single-patch FEM spaces, either scalar or vector-valued.
|
|
417
|
+
|
|
418
|
+
connectivity : dict, optional
|
|
419
|
+
Dictionary representing the connectivity between the patches.
|
|
420
|
+
"""
|
|
421
|
+
def __init__(self, *spaces, connectivity=None):
|
|
422
|
+
if connectivity is None:
|
|
423
|
+
connectivity = {}
|
|
424
|
+
|
|
425
|
+
# [YG, 28.03.2025]: What happens if we have only one space?
|
|
426
|
+
if len(spaces) == 1:
|
|
427
|
+
return
|
|
428
|
+
|
|
429
|
+
self._spaces = spaces
|
|
430
|
+
|
|
431
|
+
# ... make sure that all spaces have the same parametric dimension
|
|
432
|
+
ldims = [V.ldim for V in self.spaces]
|
|
433
|
+
assert len(np.unique(ldims)) == 1
|
|
434
|
+
|
|
435
|
+
self._ldim = ldims[0]
|
|
436
|
+
# ...
|
|
437
|
+
|
|
438
|
+
self._coeff_space = BlockVectorSpace(*[V.coeff_space for V in self.spaces], connectivity=connectivity)
|
|
439
|
+
self._symbolic_space = None
|
|
440
|
+
self._connectivity = connectivity.copy()
|
|
441
|
+
|
|
442
|
+
#--------------------------------------------------------------------------
|
|
443
|
+
# Abstract interface: read-only attributes
|
|
444
|
+
#--------------------------------------------------------------------------
|
|
445
|
+
@property
|
|
446
|
+
def ldim(self):
|
|
447
|
+
""" Parametric dimension.
|
|
448
|
+
"""
|
|
449
|
+
return self._ldim
|
|
450
|
+
|
|
451
|
+
@property
|
|
452
|
+
def periodic(self):
|
|
453
|
+
# [YG, 28.03.2025]: this is not consistent with the abstract interface,
|
|
454
|
+
# which requires a tuple of booleans, but the periodicity of a multipatch
|
|
455
|
+
# space is not well defined in general.
|
|
456
|
+
return [V.periodic for V in self.spaces]
|
|
457
|
+
|
|
458
|
+
@property
|
|
459
|
+
def mapping(self):
|
|
460
|
+
return None
|
|
461
|
+
|
|
462
|
+
@property
|
|
463
|
+
def coeff_space(self):
|
|
464
|
+
"""
|
|
465
|
+
Vector space of the coefficients (mapping invariant).
|
|
466
|
+
:rtype: feectools.linalg.basic.BlockVectorSpace
|
|
467
|
+
"""
|
|
468
|
+
return self._coeff_space
|
|
469
|
+
|
|
470
|
+
@property
|
|
471
|
+
def symbolic_space( self ):
|
|
472
|
+
return self._symbolic_space
|
|
473
|
+
|
|
474
|
+
@symbolic_space.setter
|
|
475
|
+
def symbolic_space( self, symbolic_space ):
|
|
476
|
+
#assert isinstance(symbolic_space, BasicFunctionSpace)
|
|
477
|
+
self._symbolic_space = symbolic_space
|
|
478
|
+
|
|
479
|
+
@property
|
|
480
|
+
def patch_spaces(self):
|
|
481
|
+
return self._spaces
|
|
482
|
+
|
|
483
|
+
@property
|
|
484
|
+
def component_spaces(self):
|
|
485
|
+
"""
|
|
486
|
+
Return the component spaces (self if scalar-valued) as a tuple.
|
|
487
|
+
"""
|
|
488
|
+
if self.is_vector_valued:
|
|
489
|
+
# should we return here the multipatch scalar-valued space?
|
|
490
|
+
raise NotImplementedError('Component spaces not implemented for multipatch spaces')
|
|
491
|
+
else:
|
|
492
|
+
return self._spaces
|
|
493
|
+
|
|
494
|
+
@property
|
|
495
|
+
def axis_spaces(self):
|
|
496
|
+
raise NotImplementedError('Multipatch space has no list of axis spaces')
|
|
497
|
+
|
|
498
|
+
@property
|
|
499
|
+
def is_multipatch(self):
|
|
500
|
+
return True
|
|
501
|
+
|
|
502
|
+
@property
|
|
503
|
+
def is_vector_valued(self):
|
|
504
|
+
return self.patch_spaces[0].is_vector_valued
|
|
505
|
+
|
|
506
|
+
#--------------------------------------------------------------------------
|
|
507
|
+
# Abstract interface: evaluation methods
|
|
508
|
+
#--------------------------------------------------------------------------
|
|
509
|
+
def eval_field( self, field, *eta, weights=None):
|
|
510
|
+
assert isinstance(field, FemField)
|
|
511
|
+
assert field.space is self
|
|
512
|
+
assert len(eta) == self._ldim
|
|
513
|
+
raise NotImplementedError()
|
|
514
|
+
# return self.eval_fields(eta, field, weights=weights)[0]
|
|
515
|
+
|
|
516
|
+
# ...
|
|
517
|
+
def eval_fields(self, grid, *fields, weights=None, npts_per_cell=None, overlap=0):
|
|
518
|
+
"""Evaluates one or several fields on the given location(s) grid.
|
|
519
|
+
|
|
520
|
+
Parameters
|
|
521
|
+
----------
|
|
522
|
+
grid : List of ndarray
|
|
523
|
+
Grid on which to evaluate the fields.
|
|
524
|
+
Each array in this list corresponds to one logical coordinate.
|
|
525
|
+
|
|
526
|
+
*fields : tuple of feectools.fem.basic.FemField
|
|
527
|
+
Fields to evaluate.
|
|
528
|
+
|
|
529
|
+
weights : feectools.fem.basic.FemField or None, optional
|
|
530
|
+
Weights field used to weight the basis functions thus
|
|
531
|
+
turning them into NURBS. The same weights field is used
|
|
532
|
+
for all of fields and they thus have to use the same basis functions.
|
|
533
|
+
|
|
534
|
+
npts_per_cell: int, tuple of int or None, optional
|
|
535
|
+
Number of evaluation points in each cell.
|
|
536
|
+
If an integer is given, then assume that it is the same in every direction.
|
|
537
|
+
|
|
538
|
+
overlap : int
|
|
539
|
+
How much to overlap. Only used in the distributed context.
|
|
540
|
+
|
|
541
|
+
Returns
|
|
542
|
+
-------
|
|
543
|
+
List of list of ndarray
|
|
544
|
+
List of the same length as `fields`, containing for each field
|
|
545
|
+
a list of `self.ldim` arrays, i.e. one array for each logical coordinate.
|
|
546
|
+
|
|
547
|
+
See Also
|
|
548
|
+
--------
|
|
549
|
+
feectools.fem.tensor.TensorFemSpace.eval_fields : More information about the grid parameter.
|
|
550
|
+
"""
|
|
551
|
+
result = []
|
|
552
|
+
if weights is not None:
|
|
553
|
+
for i in range(self.ldim):
|
|
554
|
+
fields_i = list(field.fields[i] for field in fields)
|
|
555
|
+
|
|
556
|
+
result.append(self._spaces[i].eval_fields(grid,
|
|
557
|
+
*fields_i,
|
|
558
|
+
npts_per_cell=npts_per_cell,
|
|
559
|
+
weights=weights.fields[i],
|
|
560
|
+
overlap=overlap))
|
|
561
|
+
else:
|
|
562
|
+
for i in range(self.ldim):
|
|
563
|
+
fields_i = list(field.fields[i] for field in fields)
|
|
564
|
+
result.append(self._spaces[i].eval_fields(grid,
|
|
565
|
+
*fields_i,
|
|
566
|
+
npts_per_cell=npts_per_cell,
|
|
567
|
+
overlap=overlap))
|
|
568
|
+
return [tuple(result[j][i] for j in range(self.ldim)) for i in range(len(fields))]
|
|
569
|
+
|
|
570
|
+
# ...
|
|
571
|
+
def eval_fields_regular_tensor_grid(self, grid, *fields, weights=None, overlap=0):
|
|
572
|
+
"""Evaluates one or several fields on a regular tensor grid.
|
|
573
|
+
|
|
574
|
+
Parameters
|
|
575
|
+
----------
|
|
576
|
+
grid : List of ndarray
|
|
577
|
+
List of 2D arrays representing each direction of the grid.
|
|
578
|
+
Each of these arrays should have shape (ne_xi, nv_xi) where ne_xi is the
|
|
579
|
+
number of cells in the domain in the direction xi and nv_xi is the number of
|
|
580
|
+
evaluation points in the same direction.
|
|
581
|
+
|
|
582
|
+
*fields : tuple of feectools.fem.basic.FemField
|
|
583
|
+
Fields to evaluate.
|
|
584
|
+
|
|
585
|
+
weights : feectools.fem.basic.FemField or None, optional
|
|
586
|
+
Weights field used to weight the basis functions thus
|
|
587
|
+
turning them into NURBS. The same weights field is used
|
|
588
|
+
for all of fields and they thus have to use the same basis functions.
|
|
589
|
+
|
|
590
|
+
overlap : int
|
|
591
|
+
How much to overlap. Only used in the distributed context.
|
|
592
|
+
|
|
593
|
+
Returns
|
|
594
|
+
-------
|
|
595
|
+
List of list of ndarray
|
|
596
|
+
List of the same length as `fields`, containing for each field
|
|
597
|
+
a list of `self.ldim` arrays, i.e. one array for each logical coordinate.
|
|
598
|
+
|
|
599
|
+
See Also
|
|
600
|
+
--------
|
|
601
|
+
feectools.fem.tensor.TensorFemSpace.eval_fields : More information about the grid parameter.
|
|
602
|
+
"""
|
|
603
|
+
for f in fields:
|
|
604
|
+
# Necessary if vector coeffs is distributed across processes
|
|
605
|
+
if not f.coeffs.ghost_regions_in_sync:
|
|
606
|
+
f.coeffs.update_ghost_regions()
|
|
607
|
+
|
|
608
|
+
result = []
|
|
609
|
+
if isinstance(overlap, int):
|
|
610
|
+
overlap = [overlap] * self.ldim
|
|
611
|
+
if weights is not None:
|
|
612
|
+
for i in range(self.ldim):
|
|
613
|
+
fields_i = list(field.fields[i] for field in fields)
|
|
614
|
+
|
|
615
|
+
result.append(self._spaces[i].eval_fields_regular_tensor_grid(grid,
|
|
616
|
+
*fields_i,
|
|
617
|
+
weights=weights.fields[i],
|
|
618
|
+
overlap=overlap[i]))
|
|
619
|
+
else:
|
|
620
|
+
for i in range(self.ldim):
|
|
621
|
+
fields_i = list(field.fields[i] for field in fields)
|
|
622
|
+
|
|
623
|
+
result.append(self._spaces[i].eval_fields_regular_tensor_grid(grid,
|
|
624
|
+
*fields_i,
|
|
625
|
+
overlap=overlap[i]))
|
|
626
|
+
|
|
627
|
+
return result
|
|
628
|
+
|
|
629
|
+
# ...
|
|
630
|
+
def eval_fields_irregular_tensor_grid(self, grid, *fields, weights=None, overlap=0):
|
|
631
|
+
"""Evaluates one or several fields on an irregular tensor grid i.e.
|
|
632
|
+
a tensor grid where the number of points per cell depends on the cell.
|
|
633
|
+
|
|
634
|
+
Parameters
|
|
635
|
+
----------
|
|
636
|
+
grid : List of ndarray
|
|
637
|
+
List of 1D arrays representing each direction of the grid.
|
|
638
|
+
|
|
639
|
+
*fields : tuple of feectools.fem.basic.FemField
|
|
640
|
+
Fields to evaluate.
|
|
641
|
+
|
|
642
|
+
weights : feectools.fem.basic.FemField or None, optional
|
|
643
|
+
Weights field used to weight the basis functions thus
|
|
644
|
+
turning them into NURBS. The same weights field is used
|
|
645
|
+
for all of fields and they thus have to use the same basis functions.
|
|
646
|
+
|
|
647
|
+
overlap : int
|
|
648
|
+
How much to overlap. Only used in the distributed context.
|
|
649
|
+
|
|
650
|
+
Returns
|
|
651
|
+
-------
|
|
652
|
+
List of list of ndarray
|
|
653
|
+
List of the same length as `fields`, containing for each field
|
|
654
|
+
a list of `self.ldim` arrays, i.e. one array for each logical coordinate.
|
|
655
|
+
|
|
656
|
+
See Also
|
|
657
|
+
--------
|
|
658
|
+
feectools.fem.tensor.TensorFemSpace.eval_fields : More information about the grid parameter.
|
|
659
|
+
"""
|
|
660
|
+
for f in fields:
|
|
661
|
+
# Necessary if vector coeffs is distributed across processes
|
|
662
|
+
if not f.coeffs.ghost_regions_in_sync:
|
|
663
|
+
f.coeffs.update_ghost_regions()
|
|
664
|
+
|
|
665
|
+
result = []
|
|
666
|
+
if isinstance(overlap, int):
|
|
667
|
+
overlap = [overlap] * self.ldim
|
|
668
|
+
if weights is not None:
|
|
669
|
+
for i in range(self.ldim):
|
|
670
|
+
fields_i = list(field.fields[i] for field in fields)
|
|
671
|
+
|
|
672
|
+
result.append(self._spaces[i].eval_fields_irregular_tensor_grid(grid,
|
|
673
|
+
*fields_i,
|
|
674
|
+
weights=weights.fields[i],
|
|
675
|
+
overlap=overlap[i]))
|
|
676
|
+
else:
|
|
677
|
+
for i in range(self.ldim):
|
|
678
|
+
fields_i = list(field.fields[i] for field in fields)
|
|
679
|
+
|
|
680
|
+
result.append(self._spaces[i].eval_fields_irregular_tensor_grid(grid,
|
|
681
|
+
*fields_i,
|
|
682
|
+
overlap=overlap[i]))
|
|
683
|
+
|
|
684
|
+
return result
|
|
685
|
+
|
|
686
|
+
# ...
|
|
687
|
+
def eval_field_gradient( self, field, *eta ):
|
|
688
|
+
raise NotImplementedError( "MultipatchFemSpace not yet operational" )
|
|
689
|
+
|
|
690
|
+
# ...
|
|
691
|
+
def integral( self, f ):
|
|
692
|
+
raise NotImplementedError( "MultipatchFemSpace not yet operational" )
|
|
693
|
+
|
|
694
|
+
#--------------------------------------------------------------------------
|
|
695
|
+
# Other properties and methods
|
|
696
|
+
#--------------------------------------------------------------------------
|
|
697
|
+
@property
|
|
698
|
+
def nbasis(self):
|
|
699
|
+
dims = [V.nbasis for V in self.spaces]
|
|
700
|
+
return sum(dims)
|
|
701
|
+
|
|
702
|
+
@property
|
|
703
|
+
def degree(self):
|
|
704
|
+
return [V.degree for V in self.spaces]
|
|
705
|
+
|
|
706
|
+
@property
|
|
707
|
+
def multiplicity(self):
|
|
708
|
+
return [V.multiplicity for V in self.spaces]
|
|
709
|
+
|
|
710
|
+
@property
|
|
711
|
+
def pads(self):
|
|
712
|
+
return [V.pads for V in self.spaces]
|
|
713
|
+
|
|
714
|
+
@property
|
|
715
|
+
def ncells(self):
|
|
716
|
+
return self._ncells
|
|
717
|
+
|
|
718
|
+
@property
|
|
719
|
+
def spaces( self ):
|
|
720
|
+
return self._spaces
|
|
721
|
+
|
|
722
|
+
@property
|
|
723
|
+
def n_components( self ):
|
|
724
|
+
return len(self.spaces)
|
|
725
|
+
|
|
726
|
+
# TODO improve
|
|
727
|
+
@property
|
|
728
|
+
def comm( self ):
|
|
729
|
+
return self.spaces[0].comm
|