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,2226 @@
1
+ import sys
2
+ import os
3
+ import importlib
4
+
5
+ import numpy as np
6
+
7
+ from sympy import ImmutableDenseMatrix, Matrix, Symbol, sympify
8
+ from sympy.tensor.indexed import Indexed, IndexedBase
9
+ from sympy.simplify import cse_main
10
+
11
+ from pyccel import epyccel
12
+
13
+ from sympde.topology.basic import Boundary, Interface
14
+ from sympde.topology.mapping import Mapping, SymbolicExpr
15
+ from sympde.topology.space import ScalarFunction, VectorFunction, IndexedVectorFunction
16
+ from sympde.topology.derivatives import get_atom_logical_derivatives
17
+ from sympde.topology.derivatives import _logical_partial_derivatives
18
+ from sympde.topology.derivatives import get_index_logical_derivatives
19
+ from sympde.topology.derivatives import get_max_logical_partial_derivatives # NOTE [YG 31.07.2025]: Maybe use the one in ast.utilities
20
+ from sympde.expr.expr import BilinearForm
21
+ from sympde.expr.evaluation import KernelExpression, TerminalExpr
22
+ from sympde.calculus.core import PlusInterfaceOperator
23
+
24
+ from feectools.cad.geometry import Geometry
25
+ from feectools.mapping.discrete import SplineMapping, NurbsMapping
26
+ from feectools.fem.basic import FemSpace, FemField
27
+ from feectools.fem.vector import VectorFemSpace
28
+ from feectools.linalg.stencil import StencilMatrix
29
+ from feectools.linalg.block import BlockVectorSpace, BlockLinearOperator
30
+ from feectools.api.grid import QuadratureGrid, BasisValues
31
+ from feectools.api.settings import PSYDAC_BACKENDS
32
+ from feectools.api.utilities import flatten, random_string
33
+ from feectools.api.fem_common import (
34
+ compute_imports,
35
+ compute_max_nderiv,
36
+ compute_free_arguments,
37
+ construct_test_space_arguments,
38
+ construct_trial_space_arguments,
39
+ construct_quad_grids_arguments,
40
+ reset_arrays,
41
+ do_nothing,
42
+ extract_stencil_mats,
43
+ )
44
+
45
+ # TODO [YG 01.08.2025]: Avoid importing anything from feectools.pyccel
46
+ from feectools.pyccel.ast.core import _atomic, Assign
47
+
48
+ __all__ = ('DiscreteBilinearForm',)
49
+
50
+ NoneType = type(None)
51
+
52
+ #==============================================================================
53
+ class DiscreteBilinearForm:
54
+ """
55
+ Discrete bilinear form ready to be assembled into a matrix.
56
+
57
+ This class represents the concept of a discrete bilinear form in feectools.
58
+ Instances of this class generate an appropriate matrix assembly kernel,
59
+ allocate the matrix if not provided, and prepare a list of arguments for
60
+ the kernel.
61
+
62
+ An implementation of the sum factorization algorithm is used to assemble
63
+ the matrix.
64
+
65
+ Parameters
66
+ ----------
67
+
68
+ expr : sympde.expr.expr.BilinearForm
69
+ The symbolic bilinear form.
70
+
71
+ kernel_expr : list or tuple of sympde.expr.evaluation.KernelExpression
72
+ The atomic representation of the bilinear form.
73
+
74
+ domain_h : feectools.cad.geometry.Geometry
75
+ The discretized domain.
76
+
77
+ spaces : list of feectools.fem.basic.FemSpace
78
+ The discrete trial and test spaces.
79
+
80
+ nquads : list or tuple of int
81
+ The number of quadrature points used in the assembly kernel along each
82
+ direction.
83
+
84
+ matrix : feectools.linalg.stencil.StencilMatrix or feectools.linalg.block.BlockLinearOperator, optional
85
+ The matrix that we assemble into. If not provided, a new matrix is
86
+ created with the appropriate domain and codomain (default: None).
87
+
88
+ update_ghost_regions : bool, default=True
89
+ Accumulate the contributions of the neighbouring processes.
90
+
91
+ backend : dict, optional
92
+ The backend used to accelerate the computing kernels.
93
+ The backend dictionaries are defined in the file psydac/api/settings.py
94
+
95
+ assembly_backend : dict, optional
96
+ The backend used to accelerate the assembly kernel.
97
+ The backend dictionaries are defined in the file psydac/api/settings.py
98
+
99
+ linalg_backend : dict, optional
100
+ The backend used to accelerate the computing kernels of the linear operator.
101
+ The backend dictionaries are defined in the file psydac/api/settings.py
102
+
103
+ symbolic_mapping : sympde.topology.mapping.Mapping, optional
104
+ The symbolic mapping which defines the physical domain of the bilinear form.
105
+
106
+ See Also
107
+ --------
108
+ DiscreteLinearForm
109
+ DiscreteFunctional
110
+ DiscreteSumForm
111
+
112
+ """
113
+ def __init__(self, expr, kernel_expr, domain_h, spaces, *, nquads,
114
+ matrix=None, update_ghost_regions=True, backend=None,
115
+ linalg_backend=None, assembly_backend=None,
116
+ symbolic_mapping=None):
117
+
118
+ #... Sanity checks
119
+ assert isinstance(expr, BilinearForm)
120
+ assert isinstance(domain_h, Geometry)
121
+ for space in spaces:
122
+ assert isinstance(space, FemSpace)
123
+ for nquad in nquads:
124
+ assert isinstance(nquad, int)
125
+ assert nquad > 0
126
+ assert isinstance(matrix, (NoneType, StencilMatrix, BlockLinearOperator))
127
+ assert isinstance(update_ghost_regions, bool)
128
+ assert isinstance( backend, (NoneType, dict))
129
+ assert isinstance( linalg_backend, (NoneType, dict))
130
+ assert isinstance(assembly_backend, (NoneType, dict))
131
+ assert isinstance(symbolic_mapping, (NoneType, Mapping))
132
+ #...
133
+
134
+ if isinstance(kernel_expr, (tuple, list)):
135
+ if len(kernel_expr) == 1:
136
+ kernel_expr = kernel_expr[0]
137
+ else:
138
+ raise ValueError('> Expecting only one kernel')
139
+ assert isinstance(kernel_expr, KernelExpression)
140
+
141
+ self._kernel_expr = kernel_expr
142
+ self._expr = expr
143
+ self._target = kernel_expr.target
144
+ self._domain = domain_h.domain
145
+ self._spaces = spaces
146
+ self._matrix = matrix
147
+
148
+ domain = self.domain
149
+ target = self.target
150
+
151
+ # ...
152
+ if len(domain) > 1:
153
+ i, j = self.get_space_indices_from_target(domain, target)
154
+ test_space = self.spaces[1].spaces[i]
155
+ trial_space = self.spaces[0].spaces[j]
156
+ if isinstance(target, Interface):
157
+ m,_ = self.get_space_indices_from_target(domain, target.minus)
158
+ p,_ = self.get_space_indices_from_target(domain, target.plus)
159
+ mapping_m = list(domain_h.mappings.values())[m]
160
+ mapping_p = list(domain_h.mappings.values())[p]
161
+ mapping = (mapping_m, mapping_p) if mapping_m else None
162
+ else:
163
+ mapping = list(domain_h.mappings.values())[i]
164
+ else:
165
+ trial_space = self.spaces[0]
166
+ test_space = self.spaces[1]
167
+ mapping = list(domain_h.mappings.values())[0]
168
+
169
+ self._mapping = mapping
170
+
171
+ is_rational_mapping = False
172
+ mapping_space = None
173
+ if (mapping is not None) and not isinstance(target, Interface):
174
+ is_rational_mapping = isinstance(mapping, NurbsMapping)
175
+ mapping_space = mapping.space
176
+ elif (mapping is not None) and isinstance(target, Interface):
177
+ is_rational_mapping = (isinstance(mapping[0], NurbsMapping), isinstance(mapping[1], NurbsMapping))
178
+ mapping_space = (mapping[0].space, mapping[1].space)
179
+
180
+ self._is_rational_mapping = is_rational_mapping
181
+ # ...
182
+
183
+ if isinstance(test_space.coeff_space, BlockVectorSpace):
184
+ coeff_space = test_space.coeff_space.spaces[0]
185
+ else:
186
+ coeff_space = test_space.coeff_space
187
+
188
+ self._coeff_space = coeff_space
189
+ self._num_threads = 1
190
+ if coeff_space.parallel and coeff_space.cart.num_threads > 1:
191
+ self._num_threads = coeff_space.cart.num_threads
192
+
193
+ self._update_ghost_regions = update_ghost_regions
194
+
195
+ # In case of multiple patches, if the communicator is MPI_COMM_NULL, we do not generate the assembly code
196
+ # because the patch is not owned by the MPI rank.
197
+ if coeff_space.parallel and coeff_space.cart.is_comm_null:
198
+ self._free_args = ()
199
+ self._func = do_nothing
200
+ self._args = ()
201
+ self._threads_args = ()
202
+ self._global_matrices = ()
203
+ self._update_ghost_regions = False
204
+ return
205
+
206
+ # ...
207
+ test_ext = None
208
+ trial_ext = None
209
+ if isinstance(target, Boundary):
210
+ axis = target.axis
211
+ test_ext = target.ext
212
+ trial_ext = target.ext
213
+ elif isinstance(target, Interface):
214
+ # this part treats the cases of:
215
+ # integral(v_minus * u_plus)
216
+ # integral(v_plus * u_minus)
217
+ # the other cases, integral(v_minus * u_minus) and integral(v_plus * u_plus)
218
+ # are converted to boundary integrals by Sympde
219
+ axis = target.axis
220
+ test = self.kernel_expr.test
221
+ trial = self.kernel_expr.trial
222
+ test_target = target.plus if isinstance( test, PlusInterfaceOperator) else target.minus
223
+ trial_target = target.plus if isinstance(trial, PlusInterfaceOperator) else target.minus
224
+ test_ext = test_target.ext
225
+ trial_ext = trial_target.ext
226
+ ncells = tuple(max(i, j) for i, j in zip(test_space.ncells, trial_space.ncells))
227
+ if isinstance(trial_space, VectorFemSpace):
228
+ spaces = []
229
+ for sp in trial_space.spaces:
230
+ if (trial_target.axis, trial_target.ext) in sp.interfaces:
231
+ spaces.append(sp.get_refined_space(ncells).interfaces[trial_target.axis, trial_target.ext])
232
+
233
+ if len(spaces) == len(trial_space.spaces):
234
+ sym_space = trial_space.symbolic_space
235
+ trial_space = VectorFemSpace(*spaces)
236
+ trial_space.symbolic_space = sym_space
237
+
238
+ elif (trial_target.axis, trial_target.ext) in trial_space.interfaces:
239
+ sym_space = trial_space.symbolic_space
240
+ trial_space = trial_space.get_refined_space(ncells).interfaces[trial_target.axis, trial_target.ext]
241
+ trial_space.symbolic_space = sym_space
242
+
243
+ test_space = test_space.get_refined_space(ncells)
244
+ self._test_ext = test_target.ext
245
+ self._trial_ext = trial_target.ext
246
+
247
+ #...
248
+
249
+ # Assuming that all vector spaces (and their Cartesian decomposition,
250
+ # if any) are compatible with each other, extract the first available
251
+ # vector space from which (starts, ends, npts) will be read:
252
+ starts = coeff_space.starts
253
+ ends = coeff_space.ends
254
+ npts = coeff_space.npts
255
+
256
+ # MPI communicator
257
+ comm = coeff_space.cart.comm if coeff_space.parallel else None
258
+
259
+ # Store the MPI communicator (or None)
260
+ self._comm = comm
261
+
262
+ #...
263
+ # Get default backend from environment, or use 'python'.
264
+ default_backend = PSYDAC_BACKENDS.get(os.environ.get('PSYDAC_BACKEND'))\
265
+ or PSYDAC_BACKENDS['python']
266
+
267
+ # Backends for code generation
268
+ assembly_backend = backend or assembly_backend
269
+ linalg_backend = backend or linalg_backend
270
+
271
+ # Store backend dictionary
272
+ self._backend = assembly_backend or default_backend
273
+ #...
274
+
275
+ # TODO: remove
276
+ # BasicDiscrete generates the assembly code and sets the following attributes that are used afterwards:
277
+ # self._func, self._free_args, self._max_nderiv and self._backend
278
+ # BasicDiscrete.__init__(self, expr, kernel_expr, comm=comm, root=0, discrete_space=discrete_space,
279
+ # nquads=nquads, is_rational_mapping=is_rational_mapping, mapping=symbolic_mapping,
280
+ # mapping_space=mapping_space, num_threads=self._num_threads, backend=assembly_backend)
281
+
282
+
283
+ #... Compute the string with all the imports
284
+ texpr = kernel_expr
285
+ sym_expr = SymbolicExpr(texpr.expr)
286
+ imports = compute_imports(sym_expr, spaces=(trial_space, test_space), openmp=False)
287
+ indent = 4
288
+ glue = '\n' + ' '* indent
289
+ imports_str = glue.join([f"from {m} import {', '.join(vars)}"
290
+ for m, vars in imports.items()])
291
+
292
+ # Broadcast the import information (sqrt, sin, pi, ...) to all processes
293
+ if (comm is not None) and (comm.size > 1):
294
+ imports_str = comm.bcast(imports_str, root=0)
295
+
296
+ # Store the imports string as it will be used by make_file()
297
+ self._imports_string = imports_str
298
+ #...
299
+
300
+ # Compute the highest order of derivation in the kernel expression
301
+ self._max_nderiv = compute_max_nderiv(kernel_expr)
302
+
303
+ # TODO [YG 31.07.2025]: Implement this
304
+ self._free_args = compute_free_arguments(expr, kernel_expr)
305
+
306
+ #... Handle the special case where the current MPI process does not need to do anything
307
+ if isinstance(target, (Boundary, Interface)):
308
+
309
+ # If process does not own the boundary or interface, do not assemble anything
310
+ if test_ext == -1:
311
+ if starts[axis] != 0:
312
+ self._func = do_nothing
313
+
314
+ elif test_ext == 1:
315
+ if ends[axis] != npts[axis]-1:
316
+ self._func = do_nothing
317
+
318
+ # In case of target==Interface, we only use the MPI ranks that are on the interface to assemble the BilinearForm
319
+ if self._func == do_nothing and isinstance(target, Interface):
320
+ self._free_args = ()
321
+ self._args = ()
322
+ self._global_matrices = ()
323
+ self._threads_args = ()
324
+ return
325
+ #...
326
+
327
+ #... Build the quadrature grids
328
+ if isinstance(target, Boundary):
329
+ test_grid = QuadratureGrid( test_space, axis=axis, ext= test_ext, nquads=nquads)
330
+ trial_grid = QuadratureGrid(trial_space, axis=axis, ext=trial_ext, nquads=nquads)
331
+ self._grid = (test_grid,)
332
+ elif isinstance(target, Interface):
333
+ # this part treats the cases of:
334
+ # integral(v_minus * u_plus)
335
+ # integral(v_plus * u_minus)
336
+ # the other cases, integral(v_minus * u_minus) and integral(v_plus * u_plus)
337
+ # are converted to boundary integrals by Sympde
338
+ test_grid = QuadratureGrid( test_space, axis=axis, ext= test_ext, nquads=nquads)
339
+ trial_grid = QuadratureGrid(trial_space, axis=axis, ext=trial_ext, nquads=nquads)
340
+ self._grid = (test_grid, trial_grid) if test_target == target.minus else (trial_grid, test_grid)
341
+ self._test_ext = test_target.ext
342
+ self._trial_ext = trial_target.ext
343
+ else:
344
+ test_grid = QuadratureGrid( test_space, nquads=nquads)
345
+ trial_grid = QuadratureGrid(trial_space, nquads=nquads)
346
+ self._grid = (test_grid,)
347
+ #...
348
+
349
+ # Extract the basis function values on the quadrature grids
350
+ self._test_basis = BasisValues(
351
+ test_space,
352
+ nderiv = self.max_nderiv,
353
+ nquads = nquads,
354
+ trial = False,
355
+ grid = test_grid
356
+ )
357
+ self._trial_basis = BasisValues(
358
+ trial_space,
359
+ nderiv = self.max_nderiv,
360
+ nquads = nquads,
361
+ trial = True ,
362
+ grid = trial_grid
363
+ )
364
+
365
+ # Allocate the output matrix, if needed
366
+ self.allocate_matrices(linalg_backend)
367
+
368
+ # Determine whether OpenMP instructions were generated
369
+ self._with_openmp = (assembly_backend['name'] == 'pyccel' and assembly_backend['openmp']) if assembly_backend else False
370
+
371
+ # Construct the arguments to be passed to the assemble() function, which is stored in self._func
372
+ # First we generate the assembly file
373
+
374
+ # pyccelize process of computing the test_trial arrays
375
+ # currently set to False, as a Python 3.9 test fails, and due to the "speed up" not being significant
376
+ self._pyccelize_test_trial_computation = False
377
+
378
+ # no openmp support yet: with_openmp is not passed
379
+ self._args, self._threads_args = self.construct_arguments_generate_assembly_file()
380
+
381
+ #--------------------------------------------------------------------------
382
+ @property
383
+ def comm(self):
384
+ return self._comm
385
+
386
+ @property
387
+ def expr(self):
388
+ return self._expr
389
+
390
+ @property
391
+ def kernel_expr(self):
392
+ return self._kernel_expr
393
+
394
+ @property
395
+ def domain(self):
396
+ return self._domain
397
+
398
+ @property
399
+ def mapping(self):
400
+ return self._mapping
401
+
402
+ @property
403
+ def is_rational_mapping(self):
404
+ return self._is_rational_mapping
405
+
406
+ @property
407
+ def target(self):
408
+ return self._target
409
+
410
+ @property
411
+ def spaces(self):
412
+ return self._spaces
413
+
414
+ @property
415
+ def test_basis(self):
416
+ return self._test_basis
417
+
418
+ @property
419
+ def trial_basis(self):
420
+ return self._trial_basis
421
+
422
+ @property
423
+ def grid(self):
424
+ return self._grid
425
+
426
+ @property
427
+ def nquads(self):
428
+ return self._grid[0].nquads
429
+
430
+ @property
431
+ def free_args(self):
432
+ return self._free_args
433
+
434
+ @property
435
+ def max_nderiv(self):
436
+ # TODO: compute with read_BilinearForm and store
437
+ return self._max_nderiv
438
+
439
+ @property
440
+ def backend(self):
441
+ return self._backend
442
+
443
+ @property
444
+ def args(self):
445
+ return self._args
446
+
447
+ @property
448
+ def global_matrices(self):
449
+ return self._global_matrices
450
+
451
+ #--------------------------------------------------------------------------
452
+ def allocate_matrices(self, backend=None):
453
+ """
454
+ Allocate the global matrices used in the assembly method.
455
+ In this method we allocate only the matrices that are computed in the self._target domain,
456
+ we also avoid double allocation if we have many DiscreteLinearForm that are defined on the same self._target domain.
457
+
458
+ Parameters
459
+ ----------
460
+ backend : dict
461
+ The backend used to accelerate the computing kernels.
462
+
463
+ """
464
+ global_mats = {}
465
+
466
+ expr = self.kernel_expr.expr
467
+ target = self.kernel_expr.target
468
+ test_degree = np.array(self.test_basis.space.degree)
469
+ trial_degree = np.array(self.trial_basis.space.degree)
470
+ test_space = self.spaces[1].coeff_space
471
+ trial_space = self.spaces[0].coeff_space
472
+ test_fem_space = self.spaces[1]
473
+ trial_fem_space = self.spaces[0]
474
+ domain = self.domain
475
+ is_broken = len(domain) > 1
476
+ is_conformal = True
477
+
478
+ if isinstance(expr, (ImmutableDenseMatrix, Matrix)):
479
+ if not isinstance(test_degree[0],(list, tuple, np.ndarray)):
480
+ test_degree = [test_degree]
481
+
482
+ if not isinstance(trial_degree[0],(list, tuple, np.ndarray)):
483
+ trial_degree = [trial_degree]
484
+
485
+ pads = np.empty((len(test_degree),len(trial_degree),len(test_degree[0])), dtype=int)
486
+ for i in range(len(test_degree)):
487
+ for j in range(len(trial_degree)):
488
+ td = test_degree[i]
489
+ trd = trial_degree[j]
490
+ pads[i,j][:] = np.array([td, trd]).max(axis=0)
491
+ else:
492
+ pads = np.maximum(test_degree, trial_degree)
493
+
494
+ if self._matrix is None and (is_broken or isinstance(expr, (ImmutableDenseMatrix, Matrix))):
495
+ self._matrix = BlockLinearOperator(trial_space, test_space)
496
+
497
+ if is_broken:
498
+ i, j = self.get_space_indices_from_target(domain, target)
499
+ test_fem_space = self.spaces[1].spaces[i]
500
+ trial_fem_space = self.spaces[0].spaces[j]
501
+ test_space = test_space.spaces[i]
502
+ trial_space = trial_space.spaces[j]
503
+ ncells = tuple(max(i,j) for i,j in zip(test_fem_space.ncells, trial_fem_space.ncells))
504
+ is_conformal = tuple(test_fem_space.ncells) == ncells and tuple(trial_fem_space.ncells) == ncells
505
+ if is_broken and not is_conformal and not i==j:
506
+ use_restriction = all(trn>=tn for trn,tn in zip(trial_fem_space.ncells, test_fem_space.ncells))
507
+ use_prolongation = not use_restriction
508
+
509
+ else:
510
+ ncells = tuple(max(i,j) for i,j in zip(test_fem_space.ncells, trial_fem_space.ncells))
511
+ i=0
512
+ j=0
513
+ #else so initialisation causing bug on line 682
514
+
515
+ if isinstance(expr, (ImmutableDenseMatrix, Matrix)): # case of system of equations
516
+
517
+ if is_broken: #multi patch
518
+ if not self._matrix[i,j]:
519
+ mat = BlockLinearOperator(trial_fem_space.get_refined_space(ncells).coeff_space, test_fem_space.get_refined_space(ncells).coeff_space)
520
+ if not is_conformal and not i==j:
521
+ if use_restriction:
522
+ Ps = [knot_insertion_projection_operator(ts.get_refined_space(ncells), ts) for ts in test_fem_space.spaces]
523
+ P = BlockLinearOperator(test_fem_space.get_refined_space(ncells).coeff_space, test_fem_space.coeff_space)
524
+ for ni,Pi in enumerate(Ps):
525
+ P[ni,ni] = Pi
526
+
527
+ mat = ComposedLinearOperator(trial_space, test_space, P, mat)
528
+
529
+ elif use_prolongation:
530
+ Ps = [knot_insertion_projection_operator(trs, trs.get_refined_space(ncells)) for trs in trial_fem_space.spaces]
531
+ P = BlockLinearOperator(trial_fem_space.coeff_space, trial_fem_space.get_refined_space(ncells).coeff_space)
532
+ for ni,Pi in enumerate(Ps):
533
+ P[ni,ni] = Pi
534
+
535
+ mat = ComposedLinearOperator(trial_space, test_space, mat, P)
536
+
537
+ self._matrix[i,j] = mat
538
+
539
+ matrix = self._matrix[i,j]
540
+ else: # single patch
541
+ matrix = self._matrix
542
+
543
+ shape = expr.shape
544
+ for k1 in range(shape[0]):
545
+ for k2 in range(shape[1]):
546
+ if expr[k1,k2].is_zero:
547
+ continue
548
+
549
+ if isinstance(test_fem_space, VectorFemSpace):
550
+ ts_space = test_fem_space.get_refined_space(ncells).coeff_space.spaces[k1]
551
+ else:
552
+ ts_space = test_fem_space.get_refined_space(ncells).coeff_space
553
+
554
+ if isinstance(trial_fem_space, VectorFemSpace):
555
+ tr_space = trial_fem_space.get_refined_space(ncells).coeff_space.spaces[k2]
556
+ else:
557
+ tr_space = trial_fem_space.get_refined_space(ncells).coeff_space
558
+
559
+ if is_conformal and matrix[k1, k2]:
560
+ global_mats[k1, k2] = matrix[k1, k2]
561
+ elif not i == j: # assembling in an interface (type(target) == Interface)
562
+ axis = target.axis
563
+ ext_d = self._trial_ext
564
+ ext_c = self._test_ext
565
+ test_n = self. test_basis.space.spaces[k1].spaces[axis].nbasis
566
+ test_s = self. test_basis.space.spaces[k1].coeff_space.starts[axis]
567
+ trial_n = self.trial_basis.space.spaces[k2].spaces[axis].nbasis
568
+ cart = self.trial_basis.space.spaces[k2].coeff_space.cart
569
+ trial_s = cart.global_starts[axis][cart._coords[axis]]
570
+
571
+ s_d = trial_n - trial_s - trial_degree[k2][axis] - 1 if ext_d == 1 else 0
572
+ s_c = test_n - trial_s - test_degree[k1][axis] - 1 if ext_c == 1 else 0
573
+
574
+ # We only handle the case where direction = 1
575
+ direction = target.ornt
576
+ if domain.dim == 2:
577
+ assert direction == 1
578
+ elif domain.dim == 3:
579
+ assert all(d==1 for d in direction)
580
+
581
+ direction = 1
582
+ flip = [direction]*domain.dim
583
+ flip[axis] = 1
584
+ if self._func != do_nothing:
585
+ global_mats[k1, k2] = StencilInterfaceMatrix(tr_space, ts_space,
586
+ s_d, s_c,
587
+ axis, axis,
588
+ ext_d, ext_c,
589
+ pads=tuple(pads[k1, k2]),
590
+ flip=flip)
591
+ else:
592
+ global_mats[k1, k2] = StencilMatrix(tr_space, ts_space, pads = tuple(pads[k1, k2]))
593
+
594
+ if is_conformal:
595
+ matrix[k1, k2] = global_mats[k1, k2]
596
+ elif use_restriction:
597
+ matrix.multiplicants[-1][k1, k2] = global_mats[k1, k2]
598
+ elif use_prolongation:
599
+ matrix.multiplicants[0][k1, k2] = global_mats[k1, k2]
600
+
601
+ else: # case of scalar equation
602
+ if is_broken: # multi-patch
603
+ if self._matrix[i, j]:
604
+ global_mats[i, j] = self._matrix[i, j]
605
+
606
+ elif not i == j: # assembling in an interface (type(target) == Interface)
607
+ axis = target.axis
608
+ ext_d = self._trial_ext
609
+ ext_c = self._test_ext
610
+ test_n = self.test_basis.space.spaces[axis].nbasis
611
+ test_s = self.test_basis.space.coeff_space.starts[axis]
612
+ trial_n = self.trial_basis.space.spaces[axis].nbasis
613
+ cart = self.trial_basis.space.coeff_space.cart
614
+ trial_s = cart.global_starts[axis][cart._coords[axis]]
615
+
616
+ s_d = trial_n - trial_s - trial_degree[axis] - 1 if ext_d == 1 else 0
617
+ s_c = test_n - trial_s - test_degree[axis] - 1 if ext_c == 1 else 0
618
+
619
+ # We only handle the case where direction = 1
620
+ direction = target.ornt
621
+ if domain.dim == 2:
622
+ assert direction == 1
623
+ elif domain.dim == 3:
624
+ assert all(d==1 for d in direction)
625
+
626
+ direction = 1
627
+ flip = [direction]*domain.dim
628
+ flip[axis] = 1
629
+
630
+ if self._func != do_nothing:
631
+ mat = StencilInterfaceMatrix(trial_fem_space.get_refined_space(ncells).coeff_space,
632
+ test_fem_space.get_refined_space(ncells).coeff_space,
633
+ s_d, s_c,
634
+ axis, axis,
635
+ ext_d, ext_c,
636
+ flip=flip)
637
+ if not is_conformal:
638
+ if use_restriction:
639
+ P = knot_insertion_projection_operator(test_fem_space.get_refined_space(ncells), test_fem_space)
640
+ mat = ComposedLinearOperator(trial_space, test_space, P, mat)
641
+ elif use_prolongation:
642
+ P = knot_insertion_projection_operator(trial_fem_space, trial_fem_space.get_refined_space(ncells))
643
+ mat = ComposedLinearOperator(trial_space, test_space, mat, P)
644
+
645
+ global_mats[i, j] = mat
646
+
647
+ # define part of the global matrix as a StencilMatrix
648
+ else:
649
+ global_mats[i, j] = StencilMatrix(trial_space, test_space, pads=tuple(pads))
650
+
651
+ if (i, j) in global_mats:
652
+ self._matrix[i, j] = global_mats[i, j]
653
+
654
+
655
+ # in single patch case, we define the matrices needed for the patch
656
+ else:
657
+ if self._matrix:
658
+ global_mats[0, 0] = self._matrix
659
+ else:
660
+ global_mats[0, 0] = StencilMatrix(trial_space, test_space, pads=tuple(pads))
661
+
662
+ self._matrix = global_mats[0, 0]
663
+
664
+ # Set the backend of our matrices if given
665
+ if backend is not None and is_broken:
666
+ for mat in global_mats.values():
667
+ mat.set_backend(backend)
668
+ elif backend is not None:
669
+ self._matrix.set_backend(backend)
670
+
671
+ self._global_matrices = [M._data for M in extract_stencil_mats(global_mats.values())]
672
+
673
+ #--------------------------------------------------------------------------
674
+ def assemble(self, *, reset=True, **kwargs):
675
+ """
676
+ This method assembles the left hand side Matrix by calling the private method `self._func` with proper arguments.
677
+
678
+ In the complex case, this function returns the matrix conjugate. This comes from the fact that the
679
+ problem `a(u,v)=b(v)` is discretized as `A @ conj(U) = B` due to the antilinearity of `a` in the first variable.
680
+ Thus, to obtain `U`, the assemble function returns `conj(A)`.
681
+
682
+ TODO: remove these lines when the dot product is changed for complex.
683
+ For now, since the dot product does not compute the conjugate in the complex case. We do not use the conjugate in the assemble function.
684
+ It should work if the complex only comes from the `rhs` in the linear form.
685
+ """
686
+
687
+ if self._free_args:
688
+ basis = []
689
+ spans = []
690
+ degrees = []
691
+ pads = []
692
+ coeffs = []
693
+ consts = []
694
+
695
+ for key in self._free_args:
696
+ v = kwargs[key]
697
+
698
+ if len(self.domain) > 1 and isinstance(v, FemField) and (v.space.is_multipatch or v.space.is_vector_valued):
699
+ assert v.space.is_multipatch ## [MCP 27.03.2025] should hold since len(domain) > 1. If Ok we can simplify above if
700
+ i, j = self.get_space_indices_from_target(self.domain, self.target)
701
+ assert i == j
702
+ v = v[i]
703
+ if isinstance(v, FemField):
704
+ assert len(self.grid) == 1
705
+ if not v.coeffs.ghost_regions_in_sync:
706
+ v.coeffs.update_ghost_regions()
707
+ basis_v = BasisValues(
708
+ v.space,
709
+ nderiv = self.max_nderiv,
710
+ nquads = self.nquads,
711
+ trial = True,
712
+ grid = self.grid[0]
713
+ )
714
+ bs, d, s, p, mult = construct_test_space_arguments(basis_v)
715
+ basis += bs
716
+ spans += s
717
+ degrees += [np.int64(a) for a in d]
718
+ pads += [np.int64(a) for a in p]
719
+ if v.space.is_multipatch or v.space.is_vector_valued:
720
+ coeffs += (e._data for e in v.coeffs)
721
+ else:
722
+ coeffs += (v.coeffs._data, )
723
+ else:
724
+ consts += (v, )
725
+
726
+ args = (*self.args, *basis, *spans, *degrees, *pads, *coeffs, *consts)
727
+
728
+ else:
729
+ args = self._args
730
+
731
+ if reset:
732
+ reset_arrays(*self.global_matrices)
733
+
734
+ self._func(*args, *self._threads_args)
735
+ if self._matrix and self._update_ghost_regions:
736
+ self._matrix.exchange_assembly_data()
737
+
738
+ # TODO : uncomment this line when the conjugate is applied on the dot product in the complex case
739
+ #self._matrix.conjugate(out=self._matrix)
740
+
741
+ if self._matrix:
742
+ self._matrix.ghost_regions_in_sync = False
743
+
744
+ return self._matrix
745
+
746
+ #--------------------------------------------------------------------------
747
+ @property
748
+ def _assembly_template_head(self):
749
+ """A template for the 'head' of the assembly function. Only used with the sum factorization algorithm."""
750
+ code = '''def assemble_matrix_{FILE_ID}({MAPPING_PART_1}
751
+ {SPAN} {MAPPING_PART_2}
752
+ global_x1 : "float64[:,:]", global_x2 : "float64[:,:]", global_x3 : "float64[:,:]",
753
+ {MAPPING_PART_3}
754
+ n_element_1 : "int64", n_element_2 : "int64", n_element_3 : "int64",
755
+ nq1 : "int64", nq2 : "int64", nq3 : "int64",
756
+ pad1 : "int64", pad2 : "int64", pad3 : "int64",
757
+ {MAPPING_PART_4}
758
+ {G_MAT}{NEW_ARGS}{FIELD_ARGS}):
759
+
760
+ from numpy import abs as Abs
761
+ {imports}
762
+ '''
763
+ return code
764
+
765
+ #--------------------------------------------------------------------------
766
+ @property
767
+ def _assembly_template_body_bspline(self):
768
+ """A template for the 'body' of the assembly function (when using a spline mapping). Only used with the sum factorization algorithm."""
769
+ code = '''
770
+ arr_coeffs_x = zeros((1 + test_mapping_p1, 1 + test_mapping_p2, 1 + test_mapping_p3), dtype='float64')
771
+ arr_coeffs_y = zeros((1 + test_mapping_p1, 1 + test_mapping_p2, 1 + test_mapping_p3), dtype='float64')
772
+ arr_coeffs_z = zeros((1 + test_mapping_p1, 1 + test_mapping_p2, 1 + test_mapping_p3), dtype='float64')
773
+
774
+ {F_COEFFS_ZEROS}
775
+
776
+ {KEYS}
777
+ for k_1 in range(n_element_1):
778
+ span_mapping_1 = global_span_mapping_1[k_1]
779
+ {LOCAL_SPAN}{F_SPAN_1}{A1}
780
+ for q_1 in range(nq1):
781
+ for k_2 in range(n_element_2):
782
+ span_mapping_2 = global_span_mapping_2[k_2]
783
+ {F_SPAN_2}
784
+ for q_2 in range(nq2):
785
+ for k_3 in range(n_element_3):
786
+ span_mapping_3 = global_span_mapping_3[k_3]
787
+ {F_SPAN_3}{F_COEFFS}
788
+ arr_coeffs_x[:,:,:] = global_arr_coeffs_x[test_mapping_p1 + span_mapping_1 - test_mapping_p1:test_mapping_p1 + 1 + span_mapping_1,test_mapping_p2 + span_mapping_2 - test_mapping_p2:test_mapping_p2 + 1 + span_mapping_2,test_mapping_p3 + span_mapping_3 - test_mapping_p3:test_mapping_p3 + 1 + span_mapping_3]
789
+ arr_coeffs_y[:,:,:] = global_arr_coeffs_y[test_mapping_p1 + span_mapping_1 - test_mapping_p1:test_mapping_p1 + 1 + span_mapping_1,test_mapping_p2 + span_mapping_2 - test_mapping_p2:test_mapping_p2 + 1 + span_mapping_2,test_mapping_p3 + span_mapping_3 - test_mapping_p3:test_mapping_p3 + 1 + span_mapping_3]
790
+ arr_coeffs_z[:,:,:] = global_arr_coeffs_z[test_mapping_p1 + span_mapping_1 - test_mapping_p1:test_mapping_p1 + 1 + span_mapping_1,test_mapping_p2 + span_mapping_2 - test_mapping_p2:test_mapping_p2 + 1 + span_mapping_2,test_mapping_p3 + span_mapping_3 - test_mapping_p3:test_mapping_p3 + 1 + span_mapping_3]
791
+ for q_3 in range(nq3):
792
+ x = 0.0
793
+ y = 0.0
794
+ z = 0.0
795
+
796
+ x_x1 = 0.0
797
+ x_x2 = 0.0
798
+ x_x3 = 0.0
799
+ y_x1 = 0.0
800
+ y_x2 = 0.0
801
+ y_x3 = 0.0
802
+ z_x1 = 0.0
803
+ z_x2 = 0.0
804
+ z_x3 = 0.0
805
+ {D2_1}
806
+
807
+ {F_INIT}
808
+
809
+ {F_ASSIGN_LOOP}
810
+
811
+ for i_1 in range(test_mapping_p1+1):
812
+ mapping_1 = global_basis_mapping_1[k_1, i_1, 0, q_1]
813
+ mapping_1_x1 = global_basis_mapping_1[k_1, i_1, 1, q_1]
814
+ {D2_2}
815
+ for i_2 in range(test_mapping_p2+1):
816
+ mapping_2 = global_basis_mapping_2[k_2, i_2, 0, q_2]
817
+ mapping_2_x2 = global_basis_mapping_2[k_2, i_2, 1, q_2]
818
+ {D2_3}
819
+ for i_3 in range(test_mapping_p3+1):
820
+ mapping_3 = global_basis_mapping_3[k_3, i_3, 0, q_3]
821
+ mapping_3_x3 = global_basis_mapping_3[k_3, i_3, 1, q_3]
822
+ {D2_4}
823
+
824
+ coeff_x = arr_coeffs_x[i_1,i_2,i_3]
825
+ coeff_y = arr_coeffs_y[i_1,i_2,i_3]
826
+ coeff_z = arr_coeffs_z[i_1,i_2,i_3]
827
+
828
+ mapping = mapping_1*mapping_2*mapping_3
829
+ mapping_x1 = mapping_1_x1*mapping_2*mapping_3
830
+ mapping_x2 = mapping_1*mapping_2_x2*mapping_3
831
+ mapping_x3 = mapping_1*mapping_2*mapping_3_x3
832
+
833
+ {D2_5}
834
+
835
+ x += mapping*coeff_x
836
+ y += mapping*coeff_y
837
+ z += mapping*coeff_z
838
+
839
+ x_x1 += mapping_x1*coeff_x
840
+ x_x2 += mapping_x2*coeff_x
841
+ x_x3 += mapping_x3*coeff_x
842
+ y_x1 += mapping_x1*coeff_y
843
+ y_x2 += mapping_x2*coeff_y
844
+ y_x3 += mapping_x3*coeff_y
845
+ z_x1 += mapping_x1*coeff_z
846
+ z_x2 += mapping_x2*coeff_z
847
+ z_x3 += mapping_x3*coeff_z
848
+
849
+ {D2_6}
850
+
851
+ {TEMPS}
852
+ {COUPLING_TERMS}
853
+ '''
854
+ return code
855
+
856
+ #--------------------------------------------------------------------------
857
+ @property
858
+ def _assembly_template_body_analytic(self):
859
+ """A template for the 'body' of the assembly function (when using an analytic or no mapping). Only used with the sum factorization algorithm."""
860
+ code = '''
861
+ local_x1 = zeros_like(global_x1[0,:])
862
+ local_x2 = zeros_like(global_x2[0,:])
863
+ local_x3 = zeros_like(global_x3[0,:])
864
+
865
+ {F_COEFFS_ZEROS}
866
+
867
+ {KEYS}
868
+ for k_1 in range(n_element_1):
869
+ local_x1[:] = global_x1[k_1,:]
870
+ {LOCAL_SPAN}{F_SPAN_1}{A1}
871
+ for q_1 in range(nq1):
872
+ x1 = local_x1[q_1]
873
+ for k_2 in range(n_element_2):
874
+ local_x2[:] = global_x2[k_2,:]
875
+ {F_SPAN_2}
876
+ for q_2 in range(nq2):
877
+ x2 = local_x2[q_2]
878
+ for k_3 in range(n_element_3):
879
+ local_x3[:] = global_x3[k_3,:]
880
+ {F_SPAN_3}{F_COEFFS}
881
+ for q_3 in range(nq3):
882
+ x3 = local_x3[q_3]
883
+
884
+ {F_INIT}
885
+
886
+ {F_ASSIGN_LOOP}
887
+
888
+ {TEMPS}
889
+ {COUPLING_TERMS}
890
+ '''
891
+ return code
892
+
893
+ #--------------------------------------------------------------------------
894
+ @property
895
+ def _assembly_template_loop(self):
896
+ """A template for the 'loop' of the assembly function. Only used with the sum factorization algorithm."""
897
+ code = '''
898
+ {A2}[:] = 0.0
899
+ for k_2 in range(n_element_2):
900
+ {SPAN_2} = {GLOBAL_SPAN_2}[k_2]
901
+ for q_2 in range(nq2):
902
+ {A3}[:] = 0.0
903
+ for k_3 in range(n_element_3):
904
+ {SPAN_3} = {GLOBAL_SPAN_3}[k_3]
905
+ for q_3 in range(nq3):
906
+ a4 = {COUPLING_TERMS}[k_2, q_2, k_3, q_3, :]
907
+ for i_3 in range({TEST_V_P3} + 1):
908
+ for j_3 in range({TRIAL_U_P3} + 1):
909
+ for e in range({NEXPR}):
910
+ {A3}[e, {SPAN_3} - {TEST_V_P3} + i_3, {MAX_P3} - {I_3} + j_3] += {TEST_TRIAL_3}[k_3, q_3, i_3, j_3, {KEYS_3}[2*e], {KEYS_3}[2*e+1]] * a4[e]
911
+ for i_2 in range({TEST_V_P2} + 1):
912
+ for j_2 in range({TRIAL_U_P2} + 1):
913
+ for e in range({NEXPR}):
914
+ {A2}[e, {SPAN_2} - {TEST_V_P2} + i_2, :, {MAX_P2} - {I_2} + j_2, :] += {TEST_TRIAL_2}[k_2, q_2, i_2, j_2, {KEYS_2}[2*e], {KEYS_2}[2*e+1]] * {A3}[e,:,:]
915
+ for i_1 in range({TEST_V_P1} + 1):
916
+ for j_1 in range({TRIAL_U_P1} + 1):
917
+ {A1}[i_1, :, :, {MAX_P1} - {I_1} + j_1, :, :] += {A2_TEMP}
918
+ '''
919
+ return code
920
+
921
+ #--------------------------------------------------------------------------
922
+ def make_file(self, temps, ordered_stmts, field_derivatives, max_logical_derivative, test_mult, trial_mult, test_v_p, trial_u_p, keys_1, keys_2, keys_3, mapping_option):
923
+ """
924
+ Part of the sum factorization algorithm implementation.
925
+ Generates the correct assembly file.
926
+ Used at the end of construct_arguments_generate_assembly_file, before eventually pyccelizing that file.
927
+
928
+ Parameters
929
+ ----------
930
+ temps : tuple
931
+ Tuple of Assign statements defining temporary values.
932
+ Arithmetic combinations of these make up the coupling terms.
933
+
934
+ ordered_stmts : dict
935
+ Dictionary defining the coupling terms. Keys are combinations of
936
+ test and trial function components, values are Assign statements
937
+ in terms of temporaries appearing in temps.
938
+
939
+ field_derivatives : dict
940
+ Dictionary containing information on the derivatives of free FemFields.
941
+ Keys are components of free FemFields. Values are dictionaries again.
942
+ Their keys are names, as appearing in the assembly file, of partial derivatives of the
943
+ corresponding FemField component, and their values are dictionaries again.
944
+ Example: {F1_0_x3 : {'x1': 0, 'x2': 0, 'x3': 1}, F1_0_x2 : ...}
945
+ Meaning: There exists a free FemField named F1. Among other, the partial derivative w.r.t. x3
946
+ of its first component F1_0 appears.
947
+
948
+ max_logical_derivative : int
949
+ The largest appearing derivative order.
950
+
951
+ test_mult : list
952
+ List of length 3(scalar test function) or 9(vector test function) including multiplicity information.
953
+
954
+ trial_mult : list
955
+ List of length 3(scalar trial function) or 9(vector trial function) including multiplicity information.
956
+
957
+ test_v_p : dict
958
+ Dictionary of length 1(scalar test function) or length 3(vector test function).
959
+ Each key corresponds to a component of the funciton (space), and each corresponding value
960
+ is a list of Bspline degrees of length 3. Example: Discretizing a de de Rham sequence using
961
+ a degree vector [2, 3, 4] means that test_v_p for a test function belonging to H(curl) will be
962
+ {0: [1, 3, 4], 1: [2, 2, 4], 2: [2, 3, 3]}
963
+
964
+ trial_u_p : dict
965
+ Dictionary of length 1(scalar trial function) or length 3(vector trial function).
966
+ Each key corresponds to a component of the funciton (space), and each corresponding value
967
+ is a list of Bspline degrees of length 3. Example: Discretizing a de de Rham sequence using
968
+ a degree vector [2, 3, 4] means that trial_u_p for a trial function belonging to H^1 will be
969
+ {0: [2, 3, 4]}
970
+
971
+ keys_1 : dict
972
+ Dictionary relating subexpressions to x1-derivative combinations.
973
+ Keys are combinations of test and trial function components.
974
+ Values are lists, each entry corresponding to one appearing partial derivative
975
+ combination of these components.
976
+ Example: keys_1[(u[0], v[1])][3] = [1,0] means that the fourth ([3])
977
+ sub-expression (partial derivative combination) corresponding to the trial-test-function-component-product
978
+ u[0] * v[1] involves a first derivative in x1 direction of the trial function
979
+ and no derivative in x1 direction of the test function.
980
+ Information on appearing partial derivatives in x2 and x3 direction is stored in keys_2 and keys_3.
981
+
982
+ keys_2 : dict
983
+ See keys_1.
984
+
985
+ keys_3 : dict
986
+ See keys_1.
987
+
988
+ mapping_option : None | 'Bspline'
989
+ None in case of no mapping or an analytical mapping, 'Bspline' in case of a Bspline mapping.
990
+
991
+ Returns
992
+ -------
993
+
994
+ file_id : str
995
+ random string of length 8, corresponding to the assembly file name located in __psydac__/
996
+
997
+ """
998
+
999
+ #------------------------- FILE_ID -------------------------
1000
+ comm = self.comm
1001
+
1002
+ # Root process generates a random string to be used as file_id
1003
+ if comm is None or comm.rank == 0:
1004
+ file_id = random_string(size=8)
1005
+ else:
1006
+ file_id = None
1007
+
1008
+ # Parallel case: root process broadcasts file_id to all processes
1009
+ if comm is not None and comm.size > 1:
1010
+ file_id = comm.bcast(file_id, root=0)
1011
+
1012
+ # ----- free FemField related strings -----
1013
+
1014
+ # used as {FIELD_ARGS} in _assembly_template_head
1015
+ # adding the right arguments for free FemFields to the assembly function header
1016
+ basis_args_block = [f'global_test_basis_'+'{field}'+f'_{i+1} : "float64[:,:,:,:]"' for i in range(3)]
1017
+ basis_args_block = ", ".join(basis_args_block) + ","
1018
+ basis_args_block = [basis_args_block.format(field=field) for field in field_derivatives]
1019
+ basis_args = " " + "\n ".join(basis_args_block) + "\n"
1020
+ span_args_block = [f'global_span_'+'{field}'+f'_{i+1} : "int64[:]"' for i in range(3)]
1021
+ span_args_block = ", ".join(span_args_block) + ","
1022
+ span_args_block = [span_args_block.format(field=field) for field in field_derivatives]
1023
+ span_args = " " + "\n ".join(span_args_block) + "\n"
1024
+ degree_args_block = [f'test_'+'{field}'+f'_p{i+1} : "int64"' for i in range(3)]
1025
+ degree_args_block = ", ".join(degree_args_block) + ","
1026
+ degree_args_block = [degree_args_block.format(field=field) for field in field_derivatives]
1027
+ degree_args = " " + "\n ".join(degree_args_block) + "\n"
1028
+ pad_args_block = [f'pad_'+'{field}'+f'_{i+1} : "int64"' for i in range(3)]
1029
+ pad_args_block = ", ".join(pad_args_block) + ","
1030
+ pad_args_block = [pad_args_block.format(field=field) for field in field_derivatives]
1031
+ pad_args = " " + "\n ".join(pad_args_block) + "\n"
1032
+ coeff_args_block = [f'global_arr_coeffs_{field} : "float64[:,:,:]"' for field in field_derivatives]
1033
+ coeff_args = " " + ", ".join(coeff_args_block)
1034
+ FIELD_ARGS = basis_args+span_args+degree_args+pad_args+coeff_args
1035
+
1036
+ # {F_COEFFS_ZEROS} in both _assembly_template_body_bspline & _analytic
1037
+ F_COEFFS_ZEROS = "\n".join([f" arr_coeffs_{field} = zeros((1 + test_{field}_p1, 1 + test_{field}_p2, 1 + test_{field}_p3), dtype='float64')" for field in field_derivatives])
1038
+
1039
+ # {F_SPAN_1}, {F_SPAN_2}, {F_SPAN_3} in both _assembly_template_body_bspline & _analytic
1040
+ F_SPAN_1 = "\n".join([f" span_{field}_1 = global_span_{field}_1[k_1]" for field in field_derivatives]) + "\n"
1041
+ F_SPAN_2 = "\n".join([f" span_{field}_2 = global_span_{field}_2[k_2]" for field in field_derivatives]) + "\n"
1042
+ F_SPAN_3 = "\n".join([f" span_{field}_3 = global_span_{field}_3[k_3]" for field in field_derivatives]) + "\n"
1043
+
1044
+ # {F_COEFFS} in both _assembly_template_body_bspline & _analytic
1045
+ coeff_ranges = ", ".join([f"pad_"+"{field}"+f"_{i+1} + span_"+"{field}"+f"_{i+1} - test_"+"{field}"+f"_p{i+1}:1 + pad_"+"{field}"+f"_{i+1} + span_"+"{field}"+f"_{i+1}" for i in range(3)])
1046
+ F_COEFFS = "\n".join([f" arr_coeffs_{field}[:,:,:] = global_arr_coeffs_{field}[{coeff_ranges.format(field=field)}]" for i, field in enumerate(field_derivatives)])
1047
+
1048
+ # {F_INIT}
1049
+ F_INIT = "\n".join([f" {derivative} = 0.0" for field in field_derivatives for derivative in field_derivatives[field]])
1050
+
1051
+ #
1052
+ # field_init assigns 0 to appearing free FemField derivatives (F_x1 = 0.0 \n F_x2 = 0.0 \n ...)
1053
+ # In the following, we assemble loops that correctly compute those free FemField derivatives at
1054
+ # a specific quadrature point (q_1, q_2, q_3). Those values will then be used in the computation
1055
+ # of the temps or directly in the computation of the coupling terms
1056
+ #
1057
+ assign_loop_contents = {'1':{}, '2':{}, '3':{}}
1058
+ multiplication_info = {}
1059
+
1060
+ for field, derivatives in field_derivatives.items():
1061
+ multiplication_info[field] = {}
1062
+ assign_statements = {'1':[], '2':[], '3':[]}
1063
+ for derivative, dxs in derivatives.items():
1064
+ multiplication_info[field][derivative] = []
1065
+ dx1 = dxs['x1']
1066
+ dx2 = dxs['x2']
1067
+ dx3 = dxs['x3']
1068
+ for i, dx in enumerate([dx1, dx2, dx3]):
1069
+ name = f"{field}_{i+1}" if dx == 0 else f"{field}_{i+1}_{dx*f'x{i+1}'}"
1070
+ multiplication_info[field][derivative].append(name)
1071
+ if dx == 0:
1072
+ assign_statement = f"{name} = global_test_basis_{field}_{i+1}[k_{i+1}, i_{i+1}, 0, q_{i+1}]"
1073
+ else:
1074
+ assign_statement = f"{name} = global_test_basis_{field}_{i+1}[k_{i+1}, i_{i+1}, {dx}, q_{i+1}]"
1075
+ if assign_statement not in assign_statements[f"{i+1}"]:
1076
+ assign_statements[f"{i+1}"].append(assign_statement)
1077
+ for i in range(3):
1078
+ content = ("\n"+(8+i)*" ").join(assign_statements[f"{i+1}"])
1079
+ assign_loop_contents[f"{i+1}"][field] = content
1080
+ tab = 7*" "
1081
+ assign = []
1082
+ for field in field_derivatives:
1083
+ txt = f"{tab}for i_1 in range(1 + test_{field}_p1):\n" + \
1084
+ f"{tab} {assign_loop_contents['1'][field]}\n" + \
1085
+ f"{tab} for i_2 in range(1 + test_{field}_p2):\n" + \
1086
+ f"{tab} {assign_loop_contents['2'][field]}\n" + \
1087
+ f"{tab} for i_3 in range(1 + test_{field}_p3):\n" + \
1088
+ f"{tab} {assign_loop_contents['3'][field]}\n" + \
1089
+ f"{tab} coeff_{field} = arr_coeffs_{field}[i_1, i_2, i_3]\n"
1090
+ for derivative in multiplication_info[field]:
1091
+ factors = " * ".join(multiplication_info[field][derivative])
1092
+ txt += f"{tab} {derivative} += {factors} * coeff_{field}\n"
1093
+ txt += "\n"
1094
+ assign.append(txt)
1095
+
1096
+ # {F_ASSIGN_LOOP} in both _assembly_template_body_bspline & _analytic
1097
+ F_ASSIGN_LOOP = "\n".join(assign)
1098
+
1099
+ # -----------------------------------------
1100
+
1101
+ # ----- load the templates -----
1102
+ #
1103
+ # head for the function header and imports
1104
+ # body for the computation of coupling terms
1105
+ # loop (part of function body): one loop per block ( e.g. (u[0], v[1]) ), each loop effectively
1106
+ # assembles one StencilMatrix per sub expression ( e.g. (dx1(u[0]), dx3(v[1])) )
1107
+ code_head = self._assembly_template_head
1108
+ code_loop = self._assembly_template_loop
1109
+ if mapping_option == 'Bspline':
1110
+ code_body = self._assembly_template_body_bspline
1111
+ else:
1112
+ code_body = self._assembly_template_body_analytic
1113
+ # ------------------------------
1114
+
1115
+ # ---- obtain basic information not explicitely passed in the args -----
1116
+ blocks = ordered_stmts.keys()
1117
+ block_list = list(blocks)
1118
+ trial_components = [block[0] for block in block_list]
1119
+ test_components = [block[1] for block in block_list]
1120
+ nu = len(set(trial_components))
1121
+ nv = len(set(test_components))
1122
+ d = 3
1123
+ assert d == 3
1124
+ # ----------------------------------------------------------------------
1125
+
1126
+ # Prepare strings and string templates depending on whether the trial and test function are vector-valued or not (nu, nv > 1 or == 1)
1127
+
1128
+ # ------------------------- STRINGS HEAD -------------------------
1129
+
1130
+ global_span_v_str = 'global_span_v_{v_j}_' if nv > 1 else 'global_span_v_'
1131
+
1132
+ if mapping_option == 'Bspline':
1133
+ MAPPING_PART_1 = 'global_basis_mapping_1 : "float64[:,:,:,:]", global_basis_mapping_2 : "float64[:,:,:,:]", global_basis_mapping_3 : "float64[:,:,:,:]", '
1134
+ MAPPING_PART_2 = 'global_span_mapping_1 : "int64[:]", global_span_mapping_2 : "int64[:]", global_span_mapping_3 : "int64[:]", '
1135
+ MAPPING_PART_3 = 'test_mapping_p1 : "int64", test_mapping_p2 : "int64", test_mapping_p3 : "int64", '
1136
+ MAPPING_PART_4 = 'global_arr_coeffs_x : "float64[:,:,:]", global_arr_coeffs_y : "float64[:,:,:]", global_arr_coeffs_z : "float64[:,:,:]", '
1137
+ else:
1138
+ MAPPING_PART_1 = ''
1139
+ MAPPING_PART_2 = ''
1140
+ MAPPING_PART_3 = ''
1141
+ MAPPING_PART_4 = ''
1142
+
1143
+ if nv > 1:
1144
+ tt1_str = 'test_trial_1_u_{u_i}_v_{v_j}' if nu > 1 else 'test_trial_1_u_v_{v_j}'
1145
+ tt2_str = 'test_trial_2_u_{u_i}_v_{v_j}' if nu > 1 else 'test_trial_2_u_v_{v_j}'
1146
+ tt3_str = 'test_trial_3_u_{u_i}_v_{v_j}' if nu > 1 else 'test_trial_3_u_v_{v_j}'
1147
+ a3_str = 'a3_u_{u_i}_v_{v_j}' if nu > 1 else 'a3_u_v_{v_j}'
1148
+ a2_str = 'a2_u_{u_i}_v_{v_j}' if nu > 1 else 'a2_u_v_{v_j}'
1149
+ ct_str = 'coupling_terms_u_{u_i}_v_{v_j}' if nu > 1 else 'coupling_terms_u_v_{v_j}'
1150
+ g_mat_str = 'g_mat_u_{u_i}_v_{v_j}' if nu > 1 else 'g_mat_u_v_{v_j}'
1151
+ else:
1152
+ tt1_str = 'test_trial_1_u_{u_i}_v' if nu > 1 else 'test_trial_1_u_v'
1153
+ tt2_str = 'test_trial_2_u_{u_i}_v' if nu > 1 else 'test_trial_2_u_v'
1154
+ tt3_str = 'test_trial_3_u_{u_i}_v' if nu > 1 else 'test_trial_3_u_v'
1155
+ a3_str = 'a3_u_{u_i}_v' if nu > 1 else 'a3_u_v'
1156
+ a2_str = 'a2_u_{u_i}_v' if nu > 1 else 'a2_u_v'
1157
+ ct_str = 'coupling_terms_u_{u_i}_v' if nu > 1 else 'coupling_terms_u_v'
1158
+ g_mat_str = 'g_mat_u_{u_i}_v' if nu > 1 else 'g_mat_u_v'
1159
+
1160
+ # ----------------------------------------------------------------
1161
+
1162
+ # ------------------------- STRINGS BODY -------------------------
1163
+
1164
+ span_v_1_str = 'span_v_{v_j}_1' if nv > 1 else 'span_v_1'
1165
+ test_v_p1_str = 'test_v_{v_j}_p1' if nv > 1 else 'test_v_p1'
1166
+
1167
+ if nv > 1:
1168
+ keys_2_str = 'keys_2_u_{u_i}_v_{v_j}' if nu > 1 else 'keys_2_u_v_{v_j}'
1169
+ keys_3_str = 'keys_3_u_{u_i}_v_{v_j}' if nu > 1 else 'keys_3_u_v_{v_j}'
1170
+ a1_str = 'a1_u_{u_i}_v_{v_j}' if nu > 1 else 'a1_u_v_{v_j}'
1171
+ else:
1172
+ keys_2_str = 'keys_2_u_{u_i}_v' if nu > 1 else 'keys_2_u_v'
1173
+ keys_3_str = 'keys_3_u_{u_i}_v' if nu > 1 else 'keys_3_u_v'
1174
+ a1_str = 'a1_u_{u_i}_v' if nu > 1 else 'a1_u_v'
1175
+
1176
+ # ----------------------------------------------------------------
1177
+
1178
+ # ------------------------- STRINGS LOOP -------------------------
1179
+
1180
+ span_2_str = 'span_v_{v_j}_2' if nv > 1 else 'span_v_2'
1181
+ span_3_str = 'span_v_{v_j}_3' if nv > 1 else 'span_v_3'
1182
+ global_span_2_str = 'global_span_v_{v_j}_2' if nv > 1 else 'global_span_v_2'
1183
+ global_span_3_str = 'global_span_v_{v_j}_3' if nv > 1 else 'global_span_v_3'
1184
+
1185
+ # ----------------------------------------------------------------
1186
+
1187
+ #------------------------- MAKE HEAD -------------------------
1188
+ SPAN = ''
1189
+ G_MAT = ''
1190
+
1191
+ TT1 = ' '
1192
+ TT2 = ' '
1193
+ TT3 = ' '
1194
+ A3 = ' '
1195
+ A2 = ' '
1196
+ CT = ' '
1197
+
1198
+ for v_j in range(nv):
1199
+ global_span_v = global_span_v_str.format(v_j=v_j)
1200
+ SPAN += ' '
1201
+ for di in range(d):
1202
+ SPAN += f'{global_span_v}{di+1} : "int64[:]", '
1203
+ SPAN = SPAN[:-1] + '\n'
1204
+
1205
+ for block in blocks:
1206
+ u_i = block[0].indices[0] if nu > 1 else 0
1207
+ v_j = block[1].indices[0] if nv > 1 else 0
1208
+
1209
+ # reverse order intended
1210
+ if ((nu > 1) and (nv > 1)):
1211
+ g_mat = g_mat_str.format(u_i=v_j, v_j=u_i)
1212
+ else:
1213
+ g_mat = g_mat_str.format(u_i=u_i, v_j=v_j)
1214
+ G_MAT += f' {g_mat} : "float64[:,:,:,:,:,:]",\n'
1215
+
1216
+ TT1 += tt1_str.format(u_i=u_i, v_j=v_j) + ' : "float64[:,:,:,:,:,:]", '
1217
+ TT2 += tt2_str.format(u_i=u_i, v_j=v_j) + ' : "float64[:,:,:,:,:,:]", '
1218
+ TT3 += tt3_str.format(u_i=u_i, v_j=v_j) + ' : "float64[:,:,:,:,:,:]", '
1219
+ A3 += a3_str.format(u_i=u_i, v_j=v_j) + ' : "float64[:,:,:]", '
1220
+ A2 += a2_str.format(u_i=u_i, v_j=v_j) + ' : "float64[:,:,:,:,:]", '
1221
+ CT += ct_str.format(u_i=u_i, v_j=v_j) + ' : "float64[:,:,:,:,:]", '
1222
+
1223
+ TT1 += '\n'
1224
+ TT2 += '\n'
1225
+ TT3 += '\n'
1226
+ A3 += '\n'
1227
+ A2 += '\n'
1228
+ CT += '\n'
1229
+ NEW_ARGS = TT1 + TT2 + TT3 + A3 + A2 + CT
1230
+ IMPORTS = self._imports_string
1231
+
1232
+ head = code_head.format(FILE_ID = file_id,
1233
+ SPAN = SPAN,
1234
+ G_MAT = G_MAT,
1235
+ NEW_ARGS = NEW_ARGS,
1236
+ MAPPING_PART_1 = MAPPING_PART_1,
1237
+ MAPPING_PART_2 = MAPPING_PART_2,
1238
+ MAPPING_PART_3 = MAPPING_PART_3,
1239
+ MAPPING_PART_4 = MAPPING_PART_4,
1240
+ FIELD_ARGS = FIELD_ARGS,
1241
+ imports = IMPORTS)
1242
+
1243
+ #------------------------- MAKE BODY -------------------------
1244
+ A1 = ''
1245
+ KEYS_2 = ''
1246
+ KEYS_3 = ''
1247
+ LOCAL_SPAN = ''
1248
+ TEMPS = ''
1249
+ COUPLING_TERMS = ''
1250
+
1251
+ for block in blocks:
1252
+ u_i = block[0].indices[0] if nu > 1 else 0
1253
+ v_j = block[1].indices[0] if nv > 1 else 0
1254
+
1255
+ keys2 = keys_2[block].copy()
1256
+ keys3 = keys_3[block].copy()
1257
+ keys2 = ','.join(str(i) for i in keys2.flatten())
1258
+ keys3 = ','.join(str(i) for i in keys3.flatten())
1259
+ KEYS2 = keys_2_str.format(u_i=u_i, v_j=v_j)
1260
+ KEYS3 = keys_3_str.format(u_i=u_i, v_j=v_j)
1261
+ KEYS_2 += f' {KEYS2} = array([{keys2}])\n'
1262
+ KEYS_3 += f' {KEYS3} = array([{keys3}])\n'
1263
+
1264
+ test_v_p1, test_v_p2, test_v_p3 = test_v_p[v_j]
1265
+ a1 = a1_str.format(u_i=u_i, v_j=v_j)
1266
+ g_mat = g_mat_str.format(u_i=u_i, v_j=v_j)
1267
+ TEST_V_P1 = test_v_p1_str.format(v_j=v_j)
1268
+ SPAN_V_1 = span_v_1_str.format(v_j=v_j)
1269
+
1270
+ A1_1 = f'{test_mult[0]}*pad1 + {SPAN_V_1} - {test_v_p1} : {test_mult[0]}*pad1 + {SPAN_V_1} + 1' if test_mult[0] > 1 else f'pad1 + {SPAN_V_1} - {test_v_p1} : pad1 + {SPAN_V_1} + 1'
1271
+ A1_2 = f'{test_mult[1]}*pad2 : {test_mult[1]}*pad2 + n_element_2 + {test_v_p2} + ({test_mult[1]}-1)*(n_element_2-1)' if test_mult[1] > 1 else f'pad2 : pad2 + n_element_2 + {test_v_p2}'
1272
+ A1_3 = f'{test_mult[2]}*pad3 : {test_mult[2]}*pad3 + n_element_3 + {test_v_p3} + ({test_mult[2]}-1)*(n_element_3-1)' if test_mult[2] > 1 else f'pad3 : pad3 + n_element_3 + {test_v_p3}'
1273
+ A1 += f' {a1} = {g_mat}[{A1_1}, {A1_2}, {A1_3}, :, :, :]\n'
1274
+
1275
+ for v_j in range(nv):
1276
+ local_span_v_1 = span_v_1_str.format(v_j=v_j)
1277
+ global_span_v = global_span_v_str.format(v_j=v_j)
1278
+ LOCAL_SPAN += f' {local_span_v_1} = {global_span_v}1[k_1]\n'
1279
+
1280
+ for temp in temps:
1281
+ TEMPS += f' {temp.lhs} = {temp.rhs}\n'
1282
+ for block in blocks:
1283
+ for stmt in ordered_stmts[block]:
1284
+ COUPLING_TERMS += f' {stmt.lhs} = {stmt.rhs}\n'
1285
+
1286
+ KEYS = KEYS_2 + KEYS_3
1287
+
1288
+ # This part is interesting. Right now, below you find hardcoded rules regarding lines of code
1289
+ # that need to be included when max_logical_derivative == 2 ( and mapping_option == 'Bspline').
1290
+ # E.g., the bilinear form corresponding to a bilaplacian problem ( laplace(laplace(u)) = f ) satisfies this assumption.
1291
+ # This hardcoded set of rules could be generalized to n-th max derivatives - if needed!
1292
+ # But for now, higher than second order derivatives on either trial or test function are not supported.
1293
+ #
1294
+ # Additional note: Given a Bspline mapping, the code computing the first order derivatives of mapping related terms is always required!
1295
+ # Even in the case of a trivial bilinear form without derivatives. But only when there are second order partial derivatives involved
1296
+ # do we need to compute second derivatives of the mapping (chain rule).
1297
+ if (mapping_option == 'Bspline') and (max_logical_derivative == 2):
1298
+ D2_1 = '\n'
1299
+ spaces1 = ' '
1300
+ spaces2 = spaces1 + ' '
1301
+ for symbol in ('x', 'y', 'z'):
1302
+ for d1 in range(1, 4):
1303
+ for d2 in range(1, 4):
1304
+ if d2 >= d1:
1305
+ D2_1 += spaces1 + f'{symbol}_x{d1}x{d2} = 0.0\n'
1306
+ D2_1 += '\n'
1307
+ D2_2 = 'mapping_1_x1x1 = global_basis_mapping_1[k_1, i_1, 2, q_1]'
1308
+ D2_3 = 'mapping_2_x2x2 = global_basis_mapping_2[k_2, i_2, 2, q_2]'
1309
+ D2_4 = 'mapping_3_x3x3 = global_basis_mapping_3[k_3, i_3, 2, q_3]'
1310
+ D2_5 = spaces2+'mapping_x1x1 = mapping_1_x1x1 * mapping_2 * mapping_3\n'+spaces2
1311
+ D2_5 += 'mapping_x1x2 = mapping_1_x1 * mapping_2_x2 * mapping_3\n'+spaces2
1312
+ D2_5 += 'mapping_x1x3 = mapping_1_x1 * mapping_2 * mapping_3_x3\n'+spaces2
1313
+ D2_5 += 'mapping_x2x2 = mapping_1 * mapping_2_x2x2 * mapping_3\n'+spaces2
1314
+ D2_5 += 'mapping_x2x3 = mapping_1 * mapping_2_x2 * mapping_3_x3\n'+spaces2
1315
+ D2_5 += 'mapping_x3x3 = mapping_1 * mapping_2 * mapping_3_x3x3\n'
1316
+ D2_6 = ''
1317
+ for symbol in ('x', 'y', 'z'):
1318
+ for d1 in range(1, 4):
1319
+ for d2 in range(1, 4):
1320
+ if d2 >= d1:
1321
+ D2_6 += f'{spaces2}{symbol}_x{d1}x{d2} += mapping_x{d1}x{d2} * coeff_{symbol}\n'
1322
+ D2_6 += '\n'
1323
+ else:
1324
+ D2_1 = ''
1325
+ D2_2 = ''
1326
+ D2_3 = ''
1327
+ D2_4 = ''
1328
+ D2_5 = ''
1329
+ D2_6 = ''
1330
+
1331
+ body = code_body.format(LOCAL_SPAN = LOCAL_SPAN,
1332
+ KEYS = KEYS,
1333
+ A1 = A1,
1334
+ TEMPS = TEMPS,
1335
+ COUPLING_TERMS = COUPLING_TERMS,
1336
+ F_COEFFS_ZEROS = F_COEFFS_ZEROS,
1337
+ F_SPAN_1 = F_SPAN_1,
1338
+ F_SPAN_2 = F_SPAN_2,
1339
+ F_SPAN_3 = F_SPAN_3,
1340
+ F_COEFFS = F_COEFFS,
1341
+ F_INIT = F_INIT,
1342
+ F_ASSIGN_LOOP = F_ASSIGN_LOOP,
1343
+ D2_1 = D2_1,
1344
+ D2_2 = D2_2,
1345
+ D2_3 = D2_3,
1346
+ D2_4 = D2_4,
1347
+ D2_5 = D2_5,
1348
+ D2_6 = D2_6)
1349
+
1350
+ #------------------------- MAKE LOOP -------------------------
1351
+ assembly_code = head + body
1352
+ loop_str = ''
1353
+
1354
+ for block in blocks:
1355
+ u_i = block[0].indices[0] if nu > 1 else 0
1356
+ v_j = block[1].indices[0] if nv > 1 else 0
1357
+
1358
+ A1 = a1_str.format(u_i=u_i, v_j=v_j)
1359
+ A2 = a2_str.format(u_i=u_i, v_j=v_j)
1360
+ A3 = a3_str.format(u_i=u_i, v_j=v_j)
1361
+ TEST_TRIAL_2 = tt2_str.format(u_i=u_i, v_j=v_j)
1362
+ TEST_TRIAL_3 = tt3_str.format(u_i=u_i, v_j=v_j)
1363
+ SPAN_2 = span_2_str.format(u_i=u_i, v_j=v_j)
1364
+ SPAN_3 = span_3_str.format(u_i=u_i, v_j=v_j)
1365
+ GLOBAL_SPAN_2 = global_span_2_str.format(u_i=u_i, v_j=v_j)
1366
+ GLOBAL_SPAN_3 = global_span_3_str.format(u_i=u_i, v_j=v_j)
1367
+ KEYS_3 = keys_3_str.format(u_i=u_i, v_j=v_j)
1368
+ KEYS_2 = keys_2_str.format(u_i=u_i, v_j=v_j)
1369
+ COUPLING_TERMS = ct_str.format(u_i=u_i, v_j=v_j)
1370
+
1371
+ TEST_V_P1, TEST_V_P2, TEST_V_P3 = test_v_p[v_j]
1372
+ TRIAL_U_P1, TRIAL_U_P2, TRIAL_U_P3 = trial_u_p[u_i]
1373
+ MAX_P1 = max(TEST_V_P1, TRIAL_U_P1)
1374
+ MAX_P2 = max(TEST_V_P2, TRIAL_U_P2)
1375
+ MAX_P3 = max(TEST_V_P3, TRIAL_U_P3)
1376
+ NEXPR = len(ordered_stmts[block])
1377
+
1378
+ keys1 = keys_1[block]
1379
+ TEST_TRIAL_1 = tt1_str.format(u_i=u_i, v_j=v_j)
1380
+ A2_TEMP = " + ".join([f"{TEST_TRIAL_1}[k_1, q_1, i_1, j_1, {keys1[e][0]}, {keys1[e][1]}] * {A2}[{e},:,:,:,:]" for e in range(NEXPR)])
1381
+
1382
+ I_1 = f'int(floor(i_1/{test_mult[0]})*{trial_mult[0]})' if max(test_mult[0], trial_mult[0]) > 1 else 'i_1'
1383
+ I_2 = f'int(floor(i_2/{test_mult[1]})*{trial_mult[1]})' if max(test_mult[1], trial_mult[1]) > 1 else 'i_2'
1384
+ I_3 = f'int(floor(i_3/{test_mult[2]})*{trial_mult[2]})' if max(test_mult[2], trial_mult[2]) > 1 else 'i_3'
1385
+ #MAX_P1 = max(int( ( MAX_P1 + np.floor(MAX_P1 / test_mult[0]) * trial_mult[0] ) / 2 ), MAX_P1) if max(test_mult[0], trial_mult[0]) > 1 else MAX_P1
1386
+ #MAX_P2 = max(int( ( MAX_P2 + np.floor(MAX_P2 / test_mult[1]) * trial_mult[1] ) / 2 ), MAX_P2) if max(test_mult[1], trial_mult[1]) > 1 else MAX_P2
1387
+ #MAX_P3 = max(int( ( MAX_P3 + np.floor(MAX_P3 / test_mult[2]) * trial_mult[2] ) / 2 ), MAX_P3) if max(test_mult[2], trial_mult[2]) > 1 else MAX_P3
1388
+ n_cols_x1 = max( int(MAX_P1 + 1 + np.floor(MAX_P1 / test_mult[0]) * trial_mult[0]), 2*MAX_P1+1 )
1389
+ n_cols_x2 = max( int(MAX_P2 + 1 + np.floor(MAX_P2 / test_mult[1]) * trial_mult[1]), 2*MAX_P2+1 )
1390
+ n_cols_x3 = max( int(MAX_P3 + 1 + np.floor(MAX_P3 / test_mult[2]) * trial_mult[2]), 2*MAX_P3+1 )
1391
+ MAX_P1 = n_cols_x1 - MAX_P1 - 1
1392
+ MAX_P2 = n_cols_x2 - MAX_P2 - 1
1393
+ MAX_P3 = n_cols_x3 - MAX_P3 - 1
1394
+
1395
+ loop = code_loop.format(A1 = A1,
1396
+ A2 = A2,
1397
+ A3 = A3,
1398
+ TEST_TRIAL_2 = TEST_TRIAL_2,
1399
+ TEST_TRIAL_3 = TEST_TRIAL_3,
1400
+ SPAN_2 = SPAN_2,
1401
+ SPAN_3 = SPAN_3,
1402
+ GLOBAL_SPAN_2 = GLOBAL_SPAN_2,
1403
+ GLOBAL_SPAN_3 = GLOBAL_SPAN_3,
1404
+ KEYS_2 = KEYS_2,
1405
+ KEYS_3 = KEYS_3,
1406
+ COUPLING_TERMS = COUPLING_TERMS,
1407
+ TEST_V_P1 = TEST_V_P1,
1408
+ TEST_V_P2 = TEST_V_P2,
1409
+ TEST_V_P3 = TEST_V_P3,
1410
+ TRIAL_U_P1 = TRIAL_U_P1,
1411
+ TRIAL_U_P2 = TRIAL_U_P2,
1412
+ TRIAL_U_P3 = TRIAL_U_P3,
1413
+ MAX_P1 = MAX_P1,
1414
+ MAX_P2 = MAX_P2,
1415
+ MAX_P3 = MAX_P3,
1416
+ NEXPR = NEXPR,
1417
+ A2_TEMP = A2_TEMP,
1418
+ I_1 = I_1,
1419
+ I_2 = I_2,
1420
+ I_3 = I_3)
1421
+
1422
+ loop_str += loop
1423
+
1424
+ assembly_code += loop_str
1425
+ assembly_code += '\n return\n'
1426
+
1427
+ #------------------------- MAKE FILE -------------------------
1428
+ import os
1429
+ if not os.path.isdir('__psydac__'):
1430
+ os.makedirs('__psydac__')
1431
+
1432
+ # Root process writes the assembly code to a file
1433
+ if comm is None or comm.rank == 0:
1434
+ filename = f'__psydac__/assemble_{file_id}.py'
1435
+ f = open(filename, 'w')
1436
+ f.writelines(assembly_code)
1437
+ f.close()
1438
+
1439
+ # Parallel case: wait for the file to be closed before proceeding
1440
+ if comm is not None and comm.size > 1:
1441
+ _ = comm.bcast(None, root=0)
1442
+
1443
+ return file_id
1444
+
1445
+ #--------------------------------------------------------------------------
1446
+ def read_BilinearForm(self):
1447
+ """
1448
+ Part of the sum factorization algorithm implementation.
1449
+ Used at the beginning of construct_arguments_generate_assembly_file().
1450
+ It's output determines both the design of the assembly function, and the arguments passed to it.
1451
+
1452
+ Returns
1453
+ -------
1454
+
1455
+ temps : tuple
1456
+ tuple of Assign objects. Often times usable building blocks of complicated coupling terms.
1457
+
1458
+ ordered_stmts : dict
1459
+ assigns each block (trial&test component combination) a list of coupling term assignment
1460
+
1461
+ ordered_sub_exprs_keys : dict
1462
+ relates each coupling term assignment of ordered_stmts a partial derivative combination
1463
+
1464
+ mapping_option : str | None
1465
+ 'Bspline' if a spline mapping is involved, None if an analytical or no mapping is involved
1466
+
1467
+ field_derivatives : dict
1468
+ contains information regarding appearing free FemFields and appearing partial derivatives of those
1469
+
1470
+ g_mat_information_false : list
1471
+ possibly wrong list of non-zero blocks
1472
+
1473
+ g_mat_information_true : list
1474
+ correct list of non-zero blocks
1475
+
1476
+ max_logical_derivative : int
1477
+ maximum appearing partial derivative (in any fixed direction)
1478
+
1479
+ """
1480
+
1481
+ a = self.expr
1482
+ domain = a.domain
1483
+
1484
+ # Because an analytical mapping only changes the expression, only the case of a Bspline mapping has to be treated
1485
+ # entirely different
1486
+ mapping_option = 'Bspline' if isinstance(self._mapping, SplineMapping) else None
1487
+
1488
+ # The following are tuples consisting of test, trial and free FemField functions appearing, e.g.
1489
+ # u, v, F1, F2 = elements_of(V, names='u, v, F1, F2)
1490
+ # a = BilinearForm((u, v), integral(domain, dot(u, F1) * dot(v, F2)))
1491
+ # tests = (v, ), trials = (u, ) fields = (F1, F2) - Note: The order of F1 & F2 is apparently random and changes from time to time!
1492
+ # tuple entries are either sympde.topology.space.ScalarFunction or sympde.topology.space.VectorFunction objects
1493
+ tests = a.test_functions
1494
+ trials = a.trial_functions
1495
+ fields = a.fields
1496
+
1497
+ # A sympde.expr.evaluation.DomainExpression object
1498
+ # TODO [YG 31.07.2025]: Why not using self.terminal_expr[0] instead?
1499
+ texpr = TerminalExpr(a, domain)[0]
1500
+
1501
+ # We extract all appearing components of test, trial and free FemFields, as well as appearing partial derivatives of these.
1502
+ # e.g. atoms = [F1[1], F2[1], v[0], u[0], F1[2], F2[2], F1[0], v[1], v[2], F2[0], u[1], u[2]]
1503
+ # for a bilinear form, without derivatives, involving two vector valued Fem fields F1 & F2 and vector valued test & trial functions v and u
1504
+ atoms_types = (ScalarFunction, VectorFunction, IndexedVectorFunction)
1505
+ atoms = _atomic(texpr, cls=atoms_types+_logical_partial_derivatives)
1506
+
1507
+ # Preparing to sort all atoms into test_, trial_ and field_atoms
1508
+ test_atoms = {}
1509
+ for v in tests:
1510
+ if isinstance(v, VectorFunction):
1511
+ for i in range(domain.dim):
1512
+ test_atoms[v[i]] = []
1513
+ else:
1514
+ test_atoms[v] = []
1515
+
1516
+ trial_atoms = {}
1517
+ for u in trials:
1518
+ if isinstance(u, VectorFunction):
1519
+ for i in range(domain.dim):
1520
+ trial_atoms[u[i]] = []
1521
+ else:
1522
+ trial_atoms[u] = []
1523
+
1524
+ field_atoms = {}
1525
+ for f in fields:
1526
+ if isinstance(f, VectorFunction):
1527
+ for i in range(domain.dim):
1528
+ field_atoms[f[i]] = []
1529
+ else:
1530
+ field_atoms[f] = []
1531
+
1532
+ # atoms can consist of scalar functions (u, v), partial derivatives of scalar functions (dx1(u), dx3(v), ...),
1533
+ # components of vector valued functions (u[0], v[1], ...), partial derivatives of components of vector valued functions
1534
+ # (dx1(u[0]), dx3(v[1]), ...), and the same thing but for free FemFields.
1535
+ # With
1536
+ # get_atom_logical_derivatives(atom)
1537
+ # we obtain the component without partial derivatives (u -> u ; dx1(u) -> u ; dx2(v[2]) -> v[2] ; ...)
1538
+ # This way we can gather subexpressions belonging to the same block
1539
+ for atom in atoms:
1540
+ a = get_atom_logical_derivatives(atom)
1541
+ # IF: NOT Indexed Mapping AND NOT VectorFunction
1542
+ # I guess: <=> IF ScalarFunction
1543
+ if not ((isinstance(a, Indexed) and isinstance(a.base, Mapping)) or (isinstance(a, IndexedVectorFunction))):
1544
+ if a in tests:
1545
+ # tests is a tuple, e.g. (v, ), hence tests[0] = v
1546
+ test_atoms[tests[0]].append(atom)
1547
+ elif a in trials:
1548
+ trial_atoms[trials[0]].append(atom)
1549
+ elif a in fields:
1550
+ # while there can only be one trial and one test function, there can be multiple free FemFields.
1551
+ for f in field_atoms:
1552
+ if f == a:
1553
+ field_atoms[f].append(atom)
1554
+ else:
1555
+ raise NotImplementedError(f"atoms of type {str(atom)} are not supported")
1556
+ # IF VectorFunction
1557
+ elif isinstance(a, IndexedVectorFunction):
1558
+ # .base returns ... the base of a VectorFunction! E.g., u[2] -> u, v[0] -> v
1559
+ if a.base in tests:
1560
+ for vi in test_atoms:
1561
+ if vi == a:
1562
+ test_atoms[vi].append(atom)
1563
+ break
1564
+ elif a.base in trials:
1565
+ for ui in trial_atoms:
1566
+ if ui == a:
1567
+ trial_atoms[ui].append(atom)
1568
+ break
1569
+ elif a.base in fields:
1570
+ for fi in field_atoms:
1571
+ if fi == a:
1572
+ field_atoms[fi].append(atom)
1573
+ break
1574
+ else:
1575
+ raise NotImplementedError(f"atoms of type {str(atom)} are not supported")
1576
+
1577
+ # ----- Julian O. 11.06.25 -----
1578
+ # Regarding the code that follows:
1579
+ # When dealing with a DiscreteBilinearForm depending on two or more free FemFields,
1580
+ # the order of the dictionary `field_derivatives` must be the same as the order
1581
+ # of the free FemFields in `self._free_args`.
1582
+ # For some reason, the order of all appearing "atoms" in a BilinearForm (trial function, test function, free fields, .?.)
1583
+ # as obtained in the __init__ of AST
1584
+ # atoms = terminal_expr.expr.atoms(ScalarFunction, VectorFunction)
1585
+ # is random and changes from code execution to code execution.
1586
+ # This order of atoms however determines the order of the free FemFields appearing in `self._free_args`.
1587
+ # In particular, this order only sometimes matches the order of `field_derivatives`, which results in wrong matrices.
1588
+ #
1589
+ # Below is the old version of the code that follows:
1590
+ #field_derivatives = {}
1591
+ #for key in field_atoms:
1592
+ # sym_key = SymbolicExpr(key)
1593
+ # field_derivatives[sym_key] = {}
1594
+ # for f in field_atoms[key]:
1595
+ # field_derivatives[sym_key][SymbolicExpr(f)] = get_index_logical_derivatives(f)
1596
+ # ------------------------------
1597
+
1598
+ # For the computation of the coupling terms, among other we need to organize information
1599
+ # related to free FemFields. For now, we have the dictionary field_atoms, whose keys are
1600
+ # components of appearing fields, and whose values are appearing partial derivatives of these, e.g.,
1601
+ # field_atoms = {'F1[0]':[dx1(F1[0]), ], 'F1[1]':[dx2(F1[1]), ], 'F1[2]':[dx3(F1[2]), ], 'F2':[F2, ]}
1602
+ #
1603
+ # We now create the dictionary field_derivatives.
1604
+ # It's keys are SymbolicExpr of the previous keys (F1[0] -> F1_0, F1[1] -> F1_1, F1[2] -> F1_2, F2 -> F2)
1605
+ # and its values are again dictionaries, whose keys are symbolic expressions of the appearing partial derivatives, e.g.
1606
+ # dx1(F1[0]) -> F1_0_x1, dx2(F1[1]) -> F1_1_x2, dx3(F1[2]) -> F1_2_x3, F2 -> F2,
1607
+ # and whose values are dictionaries that store the respective derivative information.
1608
+ # Consider for example the BilinearForm (u, v) \mapsto integral(domain, dot(u, grad(Fs)) * dot(v, grad(Fs2)):
1609
+ # The corresponding field_derivatives dict will be
1610
+ # {Fs: {Fs_x3: {'x1': 0, 'x2': 0, 'x3': 1}, Fs_x2: {'x1': 0, 'x2': 1, 'x3': 0}, Fs_x1: {'x1': 1, 'x2': 0, 'x3': 0}}, Fs2: {Fs2_x3: {'x1': 0, 'x2': 0, 'x3': 1}, Fs2_x2: {'x1': 0, 'x2': 1, 'x3': 0}, Fs2_x1: {'x1': 1, 'x2': 0, 'x3': 0}}}
1611
+
1612
+ # Amount of free FemFields (NOT counting each component individually)
1613
+ n_free_fields = len(self._free_args)
1614
+ field_derivatives = {}
1615
+ # The keys in field_derivatives will be in the same order as the fields appearing in self._free_args
1616
+ for n in range(n_free_fields):
1617
+ # The key might be F1[0], but we want to check whether F1 == self._free_args[0], and ...
1618
+ for key in field_atoms:
1619
+ # ... field_name does exactly that
1620
+ field_name = str(key.base) if hasattr(key, 'base') else str(key)
1621
+ if field_name == self._free_args[n]:
1622
+ # SymbolicExpr transforms something like F1[0] into F1_0 (part of the name of a variable in the assembly code later)
1623
+ sym_key = SymbolicExpr(key)
1624
+ field_derivatives[sym_key] = {}
1625
+ for f in field_atoms[key]:
1626
+ # And similarly f, which might look like dx1(F1[0]), will be transformed to F1_0_x2
1627
+ # while get_index_logical_derivatives(dx1(F1[0])) = {'x1': 1, 'x2': 0, 'x3': 0}
1628
+ field_derivatives[sym_key][SymbolicExpr(f)] = get_index_logical_derivatives(f)
1629
+
1630
+ # This part was proposed by Said at some point
1631
+ #syme = False
1632
+ #if syme:
1633
+ # from symengine import sympify as syme_sympify
1634
+ # sym_test_atoms = {k:[syme_sympify(SymbolicExpr(ai)) for ai in a] for k,a in test_atoms.items()}
1635
+ # sym_trial_atoms = {k:[syme_sympify(SymbolicExpr(ai)) for ai in a] for k,a in trial_atoms.items()}
1636
+ # sym_expr = syme_sympify(SymbolicExpr(texpr.expr))
1637
+ #else:
1638
+ # sym_test_atoms = {k:[SymbolicExpr(ai) for ai in a] for k,a in test_atoms.items()}
1639
+ # sym_trial_atoms = {k:[SymbolicExpr(ai) for ai in a] for k,a in trial_atoms.items()}
1640
+ # sym_expr = SymbolicExpr(texpr.expr)
1641
+
1642
+ # test_atoms is a dict whose values are components of the test function and whose values
1643
+ # are arrays with appearing partial derivatives of those components.
1644
+ # sym_test_atoms has the same structure, but replaces the appearing partial derivatives with
1645
+ # symbolic expressions of those partial derivatives. E.g.,
1646
+ # test_atoms: {v2[0]: [dx3(v2[0]), dx2(v2[0])], v2[1]: [dx1(v2[1]), dx3(v2[1])], v2[2]: [dx2(v2[2]), dx1(v2[2])]}
1647
+ # sym_test_atoms: {v2[0]: [v2_0_x3, v2_0_x2], v2[1]: [v2_1_x1, v2_1_x3], v2[2]: [v2_2_x2, v2_2_x1]}
1648
+ # In the following, we will gather all (coupling) terms of a specific combination of a sym_test_atom with a sym_trial_atom in sym_expr
1649
+ sym_test_atoms = {k:[SymbolicExpr(ai) for ai in a] for k,a in test_atoms.items()}
1650
+ sym_trial_atoms = {k:[SymbolicExpr(ai) for ai in a] for k,a in trial_atoms.items()}
1651
+ sym_expr = SymbolicExpr(texpr.expr)
1652
+
1653
+ # ----- temps, rhs -----
1654
+
1655
+ trials_subs = {ui:0 for u in sym_trial_atoms for ui in sym_trial_atoms[u]}
1656
+ tests_subs = {vi:0 for v in sym_test_atoms for vi in sym_test_atoms[v]}
1657
+ sub_exprs = {}
1658
+
1659
+ # This is where the real magic happens: The at times extremely long and complicated SymbolicExpr sym_expr
1660
+ # 0. is brought into a more readable form (sub_exprs) &
1661
+ # 1. gets split into many small parts (temps), that often times appear in multiple sub_exprs,
1662
+ # but now only have to be computed once, e.g. (temp_0, -F2_1*F1_1) &
1663
+ # 2. those temporaries get assigned to coupling terms (rhs), i.e.:
1664
+ # The coupling term corresponding to the sub-expr dx1(u[0])*dx3(v[1]) might be -temp_7*(temp_22*temp_27 + temp_33*temp_35 + temp_36*temp_37)
1665
+ for u in sym_trial_atoms:
1666
+ for v in sym_test_atoms:
1667
+ if isinstance(u, IndexedVectorFunction) and isinstance(v, IndexedVectorFunction):
1668
+ sub_expr = sym_expr[v.indices[0], u.indices[0]]
1669
+ elif isinstance(u, ScalarFunction) and isinstance(v, ScalarFunction):
1670
+ sub_expr = sym_expr
1671
+ elif isinstance(u, ScalarFunction) and isinstance(v, IndexedVectorFunction):
1672
+ sub_expr = sym_expr[v.indices[0]]
1673
+ elif isinstance(u, IndexedVectorFunction) and isinstance(v, ScalarFunction):
1674
+ sub_expr = sym_expr[u.indices[0]]
1675
+ for ui,sui in zip(trial_atoms[u], sym_trial_atoms[u]):
1676
+ trcp = trials_subs.copy()
1677
+ trcp[sui] = 1
1678
+ newsub_expr = sub_expr.subs(trcp)
1679
+ for vi,svi in zip(test_atoms[v],sym_test_atoms[v]):
1680
+ tcp = tests_subs.copy()
1681
+ tcp[svi] = 1
1682
+ expr = newsub_expr.subs(tcp)
1683
+ if not expr.is_zero:
1684
+ sub_exprs[ui,vi] = sympify(expr)
1685
+
1686
+ temps, rhs = cse_main.cse(sub_exprs.values(), symbols=cse_main.numbered_symbols(prefix=f'temp_'))
1687
+
1688
+ # ----------------------
1689
+
1690
+ # Finally, temps and rhs must be brought into a form that can be included in the assembly code, e.g.
1691
+ # temp_0 = x_x1*y_x2
1692
+ # temp_1 = x_x2*z_x1
1693
+ # temp_2 = y_x1*z_x2
1694
+ # ...
1695
+ # coupling_terms_u_v[k_2, q_2, k_3, q_3, 0] = temp_7*(temp_10**2*temp_9 + temp_11**2*temp_9 + temp_8**2*temp_9)
1696
+ # coupling_terms_u_v[k_2, q_2, k_3, q_3, 1] = temp_18
1697
+ # coupling_terms_u_v[k_2, q_2, k_3, q_3, 2] = temp_22
1698
+ # ...
1699
+
1700
+ # See above example: In our implementation of the sum factorization algorithm, we precompute arrays
1701
+ # for each quadrature point in x1 direction, meaning that those arrays contain values depending on
1702
+ # elements and quadrature points in x2 and x3 direction (k_2, k_3 & q_2 & q_3)
1703
+ element_indices = [Symbol('k_{}'.format(i)) for i in range(2,4)]
1704
+ quadrature_indices = [Symbol('q_{}'.format(i)) for i in range(2,4)]
1705
+ # indices = (k_2, q_2, k_3, q_3)
1706
+ indices = tuple(j for i in zip(element_indices, quadrature_indices) for j in i)
1707
+
1708
+ # From the sub_exprs dictionary, we read all the appearing trial and test component combinations (blocks) that
1709
+ # add a non-zero contribution to the matrix
1710
+ ordered_stmts = {}
1711
+ ordered_sub_exprs_keys = {}
1712
+ for key in sub_exprs.keys():
1713
+ u_i, v_j = [get_atom_logical_derivatives(atom) for atom in key]
1714
+ ordered_stmts[u_i, v_j] = []
1715
+ ordered_sub_exprs_keys[u_i, v_j] = []
1716
+ blocks = ordered_stmts.keys()
1717
+
1718
+ block_list = list(blocks)
1719
+ trial_components = [block[0] for block in block_list]
1720
+ test_components = [block[1] for block in block_list]
1721
+ nu = len(set(trial_components))
1722
+ nv = len(set(test_components))
1723
+
1724
+ expr = self.kernel_expr.expr
1725
+
1726
+ # We store the maximum partial derivative (for a fixed direction), not including pertial derivatives
1727
+ # appearing in mapping related terms (i.e., a BilinearForm on a mapped domain will have max_logical_derivative = 0
1728
+ # even though derivatives of the (spline) mapping appear in the coupling terms).
1729
+ if isinstance(expr, (ImmutableDenseMatrix, Matrix)):
1730
+ shape = expr.shape
1731
+ logical_max_derivatives = []
1732
+ for k1 in range(shape[0]):
1733
+ for k2 in range(shape[1]):
1734
+ logical_max_derivatives.append(get_max_logical_partial_derivatives(expr[k1,k2]))
1735
+ max_logical_derivative = max([max([value for value in dic.values()]) for dic in logical_max_derivatives])
1736
+ else:
1737
+ max_logical_derivative = max([value for value in get_max_logical_partial_derivatives(expr).values()])
1738
+
1739
+ # See comment underneath this code block for more details.
1740
+ # There was a test case, in which the amount of generated StencilMatrices (one for each appearing block,
1741
+ # i.e., one for each trial&test component combination for which a non-zero coupling term exists)
1742
+ # was larger than the amount true amount of needed StencilMatrices.
1743
+ # That discrepancy appears when expr[block].is_zero wrongly does not detect that a block is zero,
1744
+ # whereas the corresponding block does rightfully not appear in block_list!
1745
+ if isinstance(expr, (ImmutableDenseMatrix, Matrix)): # only relevenat if either trial or test function is vector valued
1746
+ g_mat_information_false = []
1747
+ shape = expr.shape
1748
+ for k1 in range(shape[0]):
1749
+ for k2 in range(shape[1]):
1750
+ if not expr[k1,k2].is_zero: # although it might actually be zero!
1751
+ if (nu == 1) and (nv > 1):
1752
+ g_mat_information_false.append((k2,k1))
1753
+ else:
1754
+ g_mat_information_false.append((k1,k2))
1755
+ if nu == 1:
1756
+ g_mat_information_true = [(0, get_atom_logical_derivatives(block[1]).indices[0]) for block in block_list]
1757
+ elif nv == 1:
1758
+ g_mat_information_true = [(get_atom_logical_derivatives(block[0]).indices[0], 0) for block in block_list]
1759
+ else:
1760
+ g_mat_information_true = [(get_atom_logical_derivatives(block[0]).indices[0], get_atom_logical_derivatives(block[1]).indices[0]) for block in block_list]
1761
+ else:
1762
+ g_mat_information_false = []
1763
+ g_mat_information_true = []
1764
+
1765
+ # Julian O. 17.06.25: Back when I added this unreadable comment below I forgot to write a test for this problem.
1766
+ # Eventually it might be interesting to remove everything related to `g_mat_information_false/true`
1767
+ # and see where errors occur.
1768
+ #
1769
+ #1, 1: expr[1,1] = F0*sqrt(x1**2*(x1*cos(2*pi*x3) + 2)**2*(sin(pi*x2)**2 + cos(pi*x2)**2)**2*(sin(2*pi*x3)**2 + cos(2*pi*x3)**2)**2)*(pi*(x1*cos(2*pi*x3) + 2)*
1770
+ # (-2*pi*x1*sin(pi*x2)*sin(2*pi*x3)*dx1(v1[1]) - sin(pi*x2)*cos(2*pi*x3)*dx3(v1[1]))*cos(pi*x2)*w2[1] - pi*(x1*cos(2*pi*x3) + 2)*(-2*pi*x1*sin(2*pi*x3)*cos(pi*x2)*dx1(v1[1]) -
1771
+ # cos(pi*x2)*cos(2*pi*x3)*dx3(v1[1]))*sin(pi*x2)*w2[1])/(2*pi**2*x1**2*(x1*cos(2*pi*x3) + 2)**2*(sin(pi*x2)**2 + cos(pi*x2)**2)**2*(sin(2*pi*x3)**2 + cos(2*pi*x3)**2)**2)
1772
+ # = 0 - but is not yet detected as 0! Hence a matrix is generated, that later is not required!
1773
+ #
1774
+
1775
+ # Here we create a template for the names of the coupling terms arrays,
1776
+ # depending on whether or not trial and test function are scalar or vector valued
1777
+ if nv > 1:
1778
+ ct_str = 'coupling_terms_u_{u_i}_v_{v_j}' if nu > 1 else 'coupling_terms_u_v_{v_j}'
1779
+ else:
1780
+ ct_str = 'coupling_terms_u_{u_i}_v' if nu > 1 else 'coupling_terms_u_v'
1781
+
1782
+ # Now we format this template based on the appearing blocks (combinations of trial and test function components)
1783
+ # and transform those formatted strings into IndexedBase objects
1784
+ lhs = {}
1785
+ for block in blocks:
1786
+ u_i = get_atom_logical_derivatives(block[0]).indices[0] if nu > 1 else 0
1787
+ v_j = get_atom_logical_derivatives(block[1]).indices[0] if nv > 1 else 0
1788
+ ct = ct_str.format(u_i=u_i, v_j=v_j)
1789
+ lhs[block] = IndexedBase(f'{ct}')
1790
+
1791
+ # lhs[block] will look w.g. like this coupling_terms_u_v (u, v scalar).
1792
+ # Now, we add to that [k_2, q_2, k_3, q_3, count], where count enumerates the sub expressions belonging to the same block
1793
+ # sub expressions corresponding to the block (u[0], v[1]) might be: (u[0], v[1]), (dx1(u[0]), v[1]), (dx2(u[0]), v[1]), ...
1794
+ # and then assign the corresponding rhs, e.g. temp_7*(temp_10**2*temp_9 + temp_11**2*temp_9 + temp_8**2*temp_9), to obtain:
1795
+ # coupling_terms_u_v[k_2, q_2, k_3, q_3, 4] = temp_7*(temp_10**2*temp_9 + temp_11**2*temp_9 + temp_8**2*temp_9)
1796
+ counts = {block:0 for block in blocks}
1797
+ for r,key in zip(rhs, sub_exprs.keys()):
1798
+ u_i, v_j = [get_atom_logical_derivatives(atom) for atom in key]
1799
+ count = counts[u_i, v_j]
1800
+ counts[u_i, v_j] += 1
1801
+ ordered_stmts[u_i, v_j].append(Assign(lhs[u_i, v_j][(*indices, count)], r))
1802
+ ordered_sub_exprs_keys[u_i, v_j].append(key)
1803
+ # ordered_stmts is a dict whose keys are combinations of trial and test functions components (e.g. u[0], v[1]),
1804
+ # and whose values are a list of coupling term assignments corresponding to this block, e.g.
1805
+ # (v1[0], v2[0]): [coupling_terms_u_0_v_0[k_2, q_2, k_3, q_3, 0] := -1, coupling_terms_u_0_v_0[k_2, q_2, k_3, q_3, 1] := 1]
1806
+ #
1807
+ # The information regarding which partial derivative combination belongs to which coupling term is stored in ordered_sub_exprs_keys.
1808
+ # This dict has the same keys, but instead of coupling term assignments as values, list of tuples of partial derivative combinations are stored.
1809
+
1810
+ # temps, which previously consisted of tuples like this one: (temp_0, -F2_1*F1_1),
1811
+ # will now be a tuple consisting of assignments, e.g. (temp_0 := -F2_1*F1_1, ...)
1812
+ temps = tuple(Assign(a,b) for a,b in temps)
1813
+
1814
+ return temps, ordered_stmts, ordered_sub_exprs_keys, mapping_option, field_derivatives, g_mat_information_false, g_mat_information_true, max_logical_derivative
1815
+
1816
+ #--------------------------------------------------------------------------
1817
+ def construct_arguments_generate_assembly_file(self):
1818
+ """
1819
+ Collect the arguments used in the assembly method, and generate and possibly pyccelize the assembly function.
1820
+
1821
+ Used only when sum factorization is enabled, else the method construct_arguments is called.
1822
+
1823
+ Returns
1824
+ -------
1825
+ args: tuple
1826
+ The arguments passed to the assembly method.
1827
+
1828
+ threads_args: None
1829
+ None as openMP parallelization is not supported by this implementation.
1830
+
1831
+ """
1832
+ temps, ordered_stmts, ordered_sub_exprs_keys, mapping_option, field_derivatives, g_mat_information_false, g_mat_information_true, max_logical_derivative = self.read_BilinearForm()
1833
+
1834
+ # Each block corresponds to a combination of trial and test function components, and thus indeed to a "block" in the matrix.
1835
+ # Not all possible combination have to exist, e.g.,
1836
+ # given a function space of vector valued functions V (3d) and a bilinear form a: VxV -> R, a(u, v) = (u, v)_L^2(Omega)
1837
+ # there will be only 3 blocks on a logical domain (u[0]&v[0], u[1]&v[1], u[2]&v[2]),
1838
+ # but up to 9 blocks on a mapped domain (e.g. u[0]&v[1], ...)
1839
+ blocks = ordered_stmts.keys()
1840
+ block_list = list(blocks)
1841
+ trial_components = [block[0] for block in block_list]
1842
+ test_components = [block[1] for block in block_list]
1843
+ # dim = 1 corresponds to a scalar valued function, dim = 3 to a vector valued function
1844
+ trial_dim = len(set(trial_components))
1845
+ test_dim = len(set(test_components))
1846
+
1847
+ # A reminder that this implementation only supports bilinear forms on 3d domains.
1848
+ d = 3
1849
+ assert d == 3
1850
+
1851
+ # Rename - also: establish that throughout "u" corresponds to the trial function, whereas "v" corresponds to the test function
1852
+ nu = trial_dim # dim of trial function; 1 (scalar) or 3 (vector)
1853
+ nv = test_dim # dim of test function ; 1 (scalar) or 3 (vector)
1854
+
1855
+ # Obtain the most basic information: function values, degrees, spans, ...
1856
+ test_basis, test_degrees, spans, pads, test_mult = construct_test_space_arguments(self.test_basis)
1857
+ trial_basis, trial_degrees, pads, trial_mult = construct_trial_space_arguments(self.trial_basis)
1858
+ n_elements, quads, quad_degrees = construct_quad_grids_arguments(self.grid[0], use_weights=False)
1859
+
1860
+ #! pads is being overwritten. That is because already somewhere else (__init__ of StencilMatrix via self.allocate_matrices)
1861
+ # do we assert that domain and codomain (trial and test) pads coincide!
1862
+ # That is not strictly necessary as Valentin at some point proved in one of his branches, but currently not implemented as
1863
+ # not required.
1864
+
1865
+ #! the above pads variable is multiplied by the multiplicity vector! For the remaining implementation, we need
1866
+ # the pads vector un-multiplied, as obtained by :
1867
+ pads = self.test_basis.space.coeff_space.pads
1868
+
1869
+ # quad_degrees is the amount of quadrature points per element in each direction
1870
+ # Clearly, this amount must coincide with the amount of basis function values stored per element in test_basis and trial_basis
1871
+ n_element_1, n_element_2, n_element_3 = n_elements
1872
+ k1, k2, k3 = quad_degrees
1873
+
1874
+ # We store component wise degree and function values for trial and test function in the dictionaries
1875
+ # trial_u_p, global_basis_u, test_v_p, global_basis_v
1876
+ if (nu == 3) and (len(trial_basis) == 3):
1877
+ # Edge Case: If the trial function space V is a VectorFunctionSpace
1878
+ # but neither an Hdiv nor an Hcurl space, i.e.,
1879
+ # V = VectorFunctionSpace('V', domain) and not +, kind='hcurl') or +, kind='hdiv')
1880
+ # then the function values in each of the three directions are identical for each of the three components.
1881
+ # Hence len(trial_basis) == 3 instead of 9.
1882
+ #
1883
+ # global_basis_u is a dict whose values are arrays of function values of one particular trial function component,
1884
+ # hence for this edge case we simply assign the same array trial_basis to each component
1885
+ # Same function degree in each direction for each component -> do the same thing with trial_u_p
1886
+ trial_u_p = {u:trial_degrees for u in range(nu)}
1887
+ global_basis_u = {u:trial_basis for u in range(nu)}
1888
+ else:
1889
+ trial_u_p = {u:trial_degrees[d*u:d*(u+1)] for u in range(nu)}
1890
+ global_basis_u = {u:trial_basis[d*u:d*(u+1)] for u in range(nu)}
1891
+ if (nv == 3) and (len(test_basis) == 3):
1892
+ # See above explanation, which also applies for the spans variable
1893
+ test_v_p = {v:test_degrees for v in range(nv)}
1894
+ global_basis_v = {v:test_basis for v in range(nv)}
1895
+ spans = [*spans, *spans, *spans]
1896
+ else:
1897
+ test_v_p = {v:test_degrees[d*v:d*(v+1)] for v in range(nv)}
1898
+ global_basis_v = {v:test_basis[d*v:d*(v+1)] for v in range(nv)}
1899
+
1900
+ # See other method construct_arguments:
1901
+ # When self._target is an Interface domain len(self._grid) == 2
1902
+ # where grid contains the QuadratureGrid of both sides of the interface
1903
+ assert len(self.grid) == 1
1904
+ if self.mapping:
1905
+ # We gather mapping related information in the case of a Bspline mapping
1906
+ # self.mapping == False if either no or an analytical mapping
1907
+
1908
+ map_coeffs = [[e._coeffs._data for e in self.mapping._fields]]
1909
+ spaces = [self.mapping._fields[0].space]
1910
+ map_degree = [sp.degree for sp in spaces]
1911
+ map_span = [[q.spans - s for q,s in zip(sp.get_assembly_grids(*self.nquads), sp.coeff_space.starts)] for sp in spaces]
1912
+ map_basis = [[q.basis for q in sp.get_assembly_grids(*self.nquads)] for sp in spaces]
1913
+ points = [g.points for g in self.grid]
1914
+ weights = [self.mapping.weights_field.coeffs._data] if self.is_rational_mapping else []
1915
+
1916
+ for i in range(len(self.grid)):
1917
+ axis = self.grid[i].axis
1918
+ # See construct_arguments - have not come across an example of when axis was not None!
1919
+ assert axis is None
1920
+
1921
+ map_degree = flatten(map_degree)
1922
+ map_span = flatten(map_span)
1923
+ map_basis = flatten(map_basis)
1924
+ points = flatten(points)
1925
+ mapping = [*map_coeffs[0], *weights]
1926
+ else:
1927
+
1928
+ mapping = []
1929
+ map_degree = []
1930
+ map_span = []
1931
+ map_basis = []
1932
+
1933
+ #---------- The following part is entirely different from the old construct_arguments method ----------
1934
+
1935
+ # Each block, say u[0]&v[1],
1936
+ # consists of possibly many derivative combinations (sub-expressions) of these two components, e.g.
1937
+ # dx1(u[0])&dx1(v[1]) or dx1(u[0])&dx2(v[1]) (dx1, dx2, dx3 representing respective partial derivatives).
1938
+ #
1939
+ # For each block, here still e.g. u[0]&v[1],
1940
+ # and for each sub-expression, we store corresponding derivative information:
1941
+ # get_index_logical_derivatives(dx1(u[0])) = {'x1': 1, 'x2': 0, 'x3': 0}
1942
+ # get_index_logical_derivatives(dx2(v[1])) = {'x1': 0, 'x2': 1, 'x3': 0}
1943
+ # Each of these 6 dicts has for each block an array of length #sub-expressions (appearing derivative combination) stored
1944
+ # x2_test_keys[(u[0], v[1])][3] = 2 means, that the fourth sub-expression of block (u[0], v[1])
1945
+ # involves a second partial derivative of the test function in x2 direction
1946
+ x1_trial_keys = {block:[] for block in blocks}
1947
+ x1_test_keys = {block:[] for block in blocks}
1948
+ x2_trial_keys = {block:[] for block in blocks}
1949
+ x2_test_keys = {block:[] for block in blocks}
1950
+ x3_trial_keys = {block:[] for block in blocks}
1951
+ x3_test_keys = {block:[] for block in blocks}
1952
+
1953
+ for block in blocks:
1954
+ # alpha, beta for example being dx1(u[0]), dx2(v[1])
1955
+ for alpha, beta in ordered_sub_exprs_keys[block]:
1956
+ x1_trial_keys[block].append(get_index_logical_derivatives(alpha)['x1'])
1957
+ x1_test_keys [block].append(get_index_logical_derivatives(beta) ['x1'])
1958
+ x2_trial_keys[block].append(get_index_logical_derivatives(alpha)['x2'])
1959
+ x2_test_keys [block].append(get_index_logical_derivatives(beta) ['x2'])
1960
+ x3_trial_keys[block].append(get_index_logical_derivatives(alpha)['x3'])
1961
+ x3_test_keys [block].append(get_index_logical_derivatives(beta) ['x3'])
1962
+
1963
+ # See sum factorization paper by Bressan & Takacs:
1964
+ # coupling_terms, a3 and a2 correspond to A^{>=4}_{x1,x2,x3}, A^{>=3}_{x1,x2} and A^{>=2}_{x1}
1965
+ # Here, for each block we assign a zero-array of the correct size.
1966
+ coupling_terms = {}
1967
+ a3 = {}
1968
+ a2 = {}
1969
+
1970
+ # For each block, we precompute ~enough~ products of partial derivatives of trial and basis functions in each direction
1971
+ # These precomputed values will then be read rather than computed in the assembly
1972
+ test_trial_1s = {}
1973
+ test_trial_2s = {}
1974
+ test_trial_3s = {}
1975
+
1976
+ # keys_1/2/3 is a restructuring of the 6 dictionaries created above
1977
+ keys_1 = {}
1978
+ keys_2 = {}
1979
+ keys_3 = {}
1980
+
1981
+ assembly_backend = self.backend
1982
+ if self._pyccelize_test_trial_computation and assembly_backend['name'] == 'pyccel':
1983
+
1984
+ import os
1985
+ if not os.path.isdir('__psydac__'):
1986
+ os.makedirs('__psydac__')
1987
+
1988
+ comm = self.comm
1989
+
1990
+ if comm is not None and comm.size > 1:
1991
+ if comm.rank == 0:
1992
+ filename = '__psydac__/test_trial_computation.py'
1993
+ code = self.test_trial_template
1994
+ f = open(filename, 'w')
1995
+ f.writelines(code)
1996
+ f.close()
1997
+ else:
1998
+ filename = '__psydac__/test_trial_computation.py'
1999
+ code = self.test_trial_template
2000
+ f = open(filename, 'w')
2001
+ f.writelines(code)
2002
+ f.close()
2003
+
2004
+ base_dirpath = os.getcwd()
2005
+ sys.path.insert(0, base_dirpath)
2006
+
2007
+ package = importlib.import_module(f'__psydac__.test_trial_computation')
2008
+ kwargs = {
2009
+ 'language' : 'fortran',
2010
+ 'compiler_family' : assembly_backend['compiler_family'],
2011
+ 'flags' : assembly_backend['flags'],
2012
+ 'openmp' : True if assembly_backend['openmp'] else False,
2013
+ 'verbose' : False,
2014
+ 'comm' : self.comm,
2015
+ }
2016
+
2017
+ test_trial_func = epyccel(package.test_trial_array, **kwargs)
2018
+
2019
+ for block in blocks:
2020
+ # We translate a block, e.g. (u[0], v[1]) into two integers u_i=0, v_j=1.
2021
+ # In the case of a scalar function (u, v instead of u[0], u[1], u[2], v[0], v[1], v[2]), store 0.
2022
+ u_i = block[0].indices[0] if nu > 1 else 0
2023
+ v_j = block[1].indices[0] if nv > 1 else 0
2024
+
2025
+ # keys_2[(u[0], v[1])][3] = (1,2) means that the fourth sub-expression corresponding to the trial-test-function-component-product
2026
+ # u[0] * v[1] involves a first derivative in x2 direction of the trial function and a second derivative in x2 direction of the test function
2027
+ keys_1[block] = np.array([(alpha_1, beta_1) for alpha_1, beta_1 in zip(x1_trial_keys[block], x1_test_keys[block])])
2028
+ keys_2[block] = np.array([(alpha_2, beta_2) for alpha_2, beta_2 in zip(x2_trial_keys[block], x2_test_keys[block])])
2029
+ keys_3[block] = np.array([(alpha_3, beta_3) for alpha_3, beta_3 in zip(x3_trial_keys[block], x3_test_keys[block])])
2030
+
2031
+ # Those are the function values in each direction of a particular component of the trial/test function
2032
+ global_basis_u_1, global_basis_u_2, global_basis_u_3 = global_basis_u[u_i]
2033
+ global_basis_v_1, global_basis_v_2, global_basis_v_3 = global_basis_v[v_j]
2034
+
2035
+ # Those are the Bspline degrees in each direction of a particular component of the trial/test function
2036
+ trial_u_p1, trial_u_p2, trial_u_p3 = trial_u_p[u_i]
2037
+ test_v_p1, test_v_p2, test_v_p3 = test_v_p [v_j]
2038
+
2039
+ max_p_2 = max(test_v_p2, trial_u_p2)
2040
+ max_p_3 = max(test_v_p3, trial_u_p3)
2041
+
2042
+ # That's the amount of subexpressions, i.e., combinations of partial derivatives appearing for a specific combination of
2043
+ # trial and test function components
2044
+ n_expr = len(ordered_stmts[block])
2045
+
2046
+ # To compute enough (possibly too many, but never too few) products of trial and test functions, we read the maximum
2047
+ # appearing partial derivative (for this specific block, in each direction, for both trial and test function)
2048
+ max_block_trial_x1_derivative = max(x1_trial_keys[block])
2049
+ max_block_trial_x2_derivative = max(x2_trial_keys[block])
2050
+ max_block_trial_x3_derivative = max(x3_trial_keys[block])
2051
+ max_block_test_x1_derivative = max(x1_test_keys[block])
2052
+ max_block_test_x2_derivative = max(x2_test_keys[block])
2053
+ max_block_test_x3_derivative = max(x3_test_keys[block])
2054
+
2055
+ # On each Bspline cell (element / subdomain), there are (test_degree+1)*(trial_degree+1) test & trial function pairs
2056
+ # of non-zero product.
2057
+ # Hence, we assign zeros for each element, each quadrature point on the element, each test and trial function combination,
2058
+ # and each (or even more than required) appearing partial derivative combination of these functions - in each direction
2059
+ test_trial_1 = np.zeros((n_element_1, k1, test_v_p1 + 1, trial_u_p1 + 1, max_block_trial_x1_derivative+1, max_block_test_x1_derivative+1), dtype='float64')
2060
+ test_trial_2 = np.zeros((n_element_2, k2, test_v_p2 + 1, trial_u_p2 + 1, max_block_trial_x2_derivative+1, max_block_test_x2_derivative+1), dtype='float64')
2061
+ test_trial_3 = np.zeros((n_element_3, k3, test_v_p3 + 1, trial_u_p3 + 1, max_block_trial_x3_derivative+1, max_block_test_x3_derivative+1), dtype='float64')
2062
+
2063
+ # And that's how we fill the test_trial arrays
2064
+ if self._pyccelize_test_trial_computation and assembly_backend['name'] == 'pyccel':
2065
+ for args in zip(n_elements,
2066
+ quad_degrees, [test_v_p1, test_v_p2, test_v_p3], [trial_u_p1, trial_u_p2, trial_u_p3],
2067
+ [global_basis_u_1, global_basis_u_2, global_basis_u_3], [global_basis_v_1, global_basis_v_2, global_basis_v_3],
2068
+ [max_block_trial_x1_derivative, max_block_trial_x2_derivative, max_block_trial_x3_derivative], [max_block_test_x1_derivative, max_block_test_x2_derivative, max_block_test_x3_derivative],
2069
+ [test_trial_1, test_trial_2, test_trial_3]):
2070
+
2071
+ args = tuple(np.int64(a) if isinstance(a, int) else a for a in args)
2072
+
2073
+ test_trial_func(*args)
2074
+ else:
2075
+ for k_1 in range(n_element_1):
2076
+ for q_1 in range(k1):
2077
+ for i_1 in range(test_v_p1 + 1):
2078
+ for j_1 in range(trial_u_p1 + 1):
2079
+ trial = global_basis_u_1[k_1, j_1, :, q_1]
2080
+ test = global_basis_v_1[k_1, i_1, :, q_1]
2081
+ for alpha_1 in range(max_block_trial_x1_derivative+1):
2082
+ for beta_1 in range(max_block_test_x1_derivative+1):
2083
+ test_trial_1[k_1, q_1, i_1, j_1, alpha_1, beta_1] = trial[alpha_1] * test[beta_1]
2084
+
2085
+ for k_2 in range(n_element_2):
2086
+ for q_2 in range(k2):
2087
+ for i_2 in range(test_v_p2 + 1):
2088
+ for j_2 in range(trial_u_p2 + 1):
2089
+ trial = global_basis_u_2[k_2, j_2, :, q_2]
2090
+ test = global_basis_v_2[k_2, i_2, :, q_2]
2091
+ for alpha_2 in range(max_block_trial_x2_derivative+1):
2092
+ for beta_2 in range(max_block_test_x2_derivative+1):
2093
+ test_trial_2[k_2, q_2, i_2, j_2, alpha_2, beta_2] = trial[alpha_2] * test[beta_2]
2094
+
2095
+ for k_3 in range(n_element_3):
2096
+ for q_3 in range(k3):
2097
+ for i_3 in range(test_v_p3 + 1):
2098
+ for j_3 in range(trial_u_p3 + 1):
2099
+ trial = global_basis_u_3[k_3, j_3, :, q_3]
2100
+ test = global_basis_v_3[k_3, i_3, :, q_3]
2101
+ for alpha_3 in range(max_block_trial_x3_derivative+1):
2102
+ for beta_3 in range(max_block_test_x3_derivative+1):
2103
+ test_trial_3[k_3, q_3, i_3, j_3, alpha_3, beta_3] = trial[alpha_3] * test[beta_3]
2104
+
2105
+ test_trial_1s[block] = test_trial_1
2106
+ test_trial_2s[block] = test_trial_2
2107
+ test_trial_3s[block] = test_trial_3
2108
+
2109
+ # Instead of having a different a3, a2 & coupling term array for each sub-expression, we choose to have only one
2110
+ # such array per block.
2111
+ # a3 will store line integral values for all combinations of test and trial functions in x3 direction, hence the dimension
2112
+ # (n_element_3 + test_v_p3 + (mult[2]-1)*(n_element_3-1), 2 * max_p_3 + 1)
2113
+ # a2 will store surface integral values for all combinations of test and trial functions in x2 and x3 direction, hence the dimension ...
2114
+ # coupling_terms stores point values of the coupling terms at all quadrature points
2115
+ # but only in x2 and x3 direction, because we only "precompute" this array for a fixed quadrature point in x1 direction
2116
+
2117
+ # a3[block] size explained: #sub expressions ; #test functions depending on x3 ; #complicated expression for the minimum columns needed
2118
+ # to store local information correctly. 2*degree+1 in the simplest case.
2119
+ n_funs_x2 = n_element_2 + test_v_p2 + (test_mult[1]-1)*(n_element_2-1)
2120
+ n_funs_x3 = n_element_3 + test_v_p3 + (test_mult[2]-1)*(n_element_3-1)
2121
+ n_cols_x2 = max( int(max_p_2 + 1 + np.floor(max_p_2 / test_mult[1]) * trial_mult[1]), 2*max_p_2+1 )
2122
+ n_cols_x3 = max( int(max_p_3 + 1 + np.floor(max_p_3 / test_mult[2]) * trial_mult[2]), 2*max_p_3+1 )
2123
+
2124
+ a3[block] = np.zeros((n_expr, n_funs_x3, n_cols_x3), dtype='float64')
2125
+ a2[block] = np.zeros((n_expr, n_funs_x2, n_funs_x3, n_cols_x2, n_cols_x3), dtype='float64')
2126
+
2127
+ coupling_terms[block] = np.zeros((n_element_2, k2, n_element_3, k3, n_expr), dtype='float64')
2128
+
2129
+ # We gather the socalled new args - all other args are being obtained in a similar way using the old assembly implementation
2130
+ new_args = (*list(test_trial_1s.values()),
2131
+ *list(test_trial_2s.values()),
2132
+ *list(test_trial_3s.values()),
2133
+ *list(a3.values()),
2134
+ *list(a2.values()),
2135
+ *list(coupling_terms.values()))
2136
+
2137
+ # This part is a bit shady.
2138
+ # There has been a case, where my code wasn't running, because one instance of deep-(Psydac/Sympde/Sympy)-code
2139
+ # correctly understood that a possibly complicated expression (corresponding to a block) in fact evaluates to 0,
2140
+ # and hence no StencilMatrix for that particular block ever needs to be created - but a different part of
2141
+ # deep-(Psydac/Sympde/Sympy)-code did not get that simplification right (yet?), and decided that the assembly code
2142
+ # needs a StencilMatrix as input for this particular block.
2143
+ # See readBilinearForm for additional information.
2144
+ # This part of the code filters out unnecessary StencilMatrices, such that only the relevant StencilMatrices
2145
+ # are being passed to the assembly function
2146
+ expr = self.kernel_expr.expr
2147
+ if isinstance(expr, (ImmutableDenseMatrix, Matrix)):
2148
+ matrices = []
2149
+ for i, block in enumerate(g_mat_information_false):
2150
+ if block in g_mat_information_true:
2151
+ matrices.append(self._global_matrices[i])
2152
+ else:
2153
+ matrices = self._global_matrices
2154
+
2155
+ # We have gathered all args!
2156
+ args = (*map_basis, *spans, *map_span, *quads, *map_degree, *n_elements, *quad_degrees, *pads, *mapping, *matrices,
2157
+ *new_args)
2158
+
2159
+ threads_args = ()
2160
+
2161
+ args = tuple(np.int64(a) if isinstance(a, int) else a for a in args)
2162
+ threads_args = tuple(np.int64(a) if isinstance(a, int) else a for a in threads_args)
2163
+
2164
+ #---------- We now generate the assembly file ----------
2165
+
2166
+ # file_id is a random string that has been used to name the assembly file
2167
+ file_id = self.make_file(temps, ordered_stmts, field_derivatives, max_logical_derivative, test_mult, trial_mult, test_v_p, trial_u_p, keys_1, keys_2, keys_3, mapping_option)
2168
+
2169
+ # Store the current directory and add it to the variable `sys.path`
2170
+ # to imitate Python's import behavior
2171
+ import os
2172
+ base_dirpath = os.getcwd()
2173
+ sys.path.insert(0, base_dirpath)
2174
+
2175
+ # Import the generated assembly function
2176
+ package = importlib.import_module(f'__psydac__.assemble_{file_id}')
2177
+
2178
+ # The assembly function is the one that has been generated in the make_file method
2179
+ assembly_function_name = f'assemble_matrix_{file_id}'
2180
+ assembly_function = getattr(package, assembly_function_name)
2181
+
2182
+ # If the backend is pyccel, we compile the new assembly function
2183
+ assembly_backend = self.backend
2184
+ if assembly_backend['name'] == 'pyccel':
2185
+ kwargs = {
2186
+ 'language' : 'fortran', # hardcoded for now
2187
+ 'compiler_family' : assembly_backend['compiler_family'],
2188
+ 'flags' : assembly_backend['flags'],
2189
+ 'openmp' : True if assembly_backend['openmp'] else False,
2190
+ 'verbose' : False,
2191
+ # 'folder': assembly_backend['folder'],
2192
+ 'comm' : self.comm,
2193
+ # 'time_execution': verbose,
2194
+ # 'verbose': verbose
2195
+ }
2196
+ new_func = epyccel(assembly_function, **kwargs)
2197
+ else:
2198
+ new_func = assembly_function
2199
+
2200
+ # Use the new assembly function (either compiled or not)
2201
+ self._func = new_func
2202
+
2203
+ return args, threads_args
2204
+
2205
+ #--------------------------------------------------------------------------
2206
+ @property
2207
+ def test_trial_template(self):
2208
+ code = '''def test_trial_array(n_element : "int64",
2209
+ quad_degree : "int64", test_degree : "int64", trial_degree : "int64",
2210
+ trial_basis : "float64[:,:,:,:]", test_basis : "float64[:,:,:,:]",
2211
+ max_trial_derivative : "int64", max_test_derivative : "int64",
2212
+ test_trial : "float64[:,:,:,:,:,:]"):
2213
+
2214
+ for k in range(n_element):
2215
+ for q in range(quad_degree):
2216
+ for i in range(test_degree + 1):
2217
+ for j in range(trial_degree + 1):
2218
+ trial = trial_basis[k, j, :, q]
2219
+ test = test_basis [k, i, :, q]
2220
+ for alpha in range(max_trial_derivative + 1):
2221
+ for beta in range(max_test_derivative + 1):
2222
+ test_trial[k, q, i, j, alpha, beta] = trial[alpha] * test[beta]
2223
+
2224
+ return
2225
+ '''
2226
+ return code