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,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