feectools 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- feectools/__init__.py +0 -0
- feectools/accelerate/__init__.py +0 -0
- feectools/accelerate/accelerate.py +220 -0
- feectools/accelerate/compile_psydac.mk +52 -0
- feectools/api/__init__.py +0 -0
- feectools/api/essential_bc.py +122 -0
- feectools/api/fem_bilinear_form.py +2226 -0
- feectools/api/fem_common.py +286 -0
- feectools/api/fem_sum_form.py +123 -0
- feectools/api/settings.py +82 -0
- feectools/core/__init__.py +11 -0
- feectools/core/bsplines.py +1107 -0
- feectools/core/bsplines_kernels.py +1349 -0
- feectools/core/field_evaluation_kernels.py +5015 -0
- feectools/core/tests/__init__.py +0 -0
- feectools/core/tests/test_bsplines.py +263 -0
- feectools/core/tests/test_bsplines_kernel.py +40 -0
- feectools/core/tests/test_bsplines_pyccel.py +752 -0
- feectools/ddm/__init__.py +3 -0
- feectools/ddm/basic.py +78 -0
- feectools/ddm/blocking_data_exchanger.py +348 -0
- feectools/ddm/cart.py +1835 -0
- feectools/ddm/interface_data_exchanger.py +122 -0
- feectools/ddm/mpi.py +109 -0
- feectools/ddm/nonblocking_data_exchanger.py +331 -0
- feectools/ddm/partition.py +207 -0
- feectools/ddm/petsc.py +112 -0
- feectools/ddm/tests/__init__.py +0 -0
- feectools/ddm/tests/test_cart_1d.py +138 -0
- feectools/ddm/tests/test_cart_2d.py +164 -0
- feectools/ddm/tests/test_cart_3d.py +158 -0
- feectools/ddm/tests/test_multicart_2d.py +173 -0
- feectools/ddm/tests/test_partition.py +124 -0
- feectools/ddm/utilities.py +24 -0
- feectools/feec/__init__.py +0 -0
- feectools/feec/derivatives.py +780 -0
- feectools/feec/dof_kernels.py +210 -0
- feectools/feec/global_geometric_projectors.py +1073 -0
- feectools/feec/hodge.py +148 -0
- feectools/fem/__init__.py +0 -0
- feectools/fem/basic.py +465 -0
- feectools/fem/grid.py +181 -0
- feectools/fem/partitioning.py +344 -0
- feectools/fem/projectors.py +160 -0
- feectools/fem/splines.py +559 -0
- feectools/fem/tensor.py +1393 -0
- feectools/fem/tests/__init__.py +0 -0
- feectools/fem/tests/analytical_profiles_1d.py +100 -0
- feectools/fem/tests/analytical_profiles_base.py +34 -0
- feectools/fem/tests/splines_error_bounds.py +155 -0
- feectools/fem/tests/test_spline_histopolation.py +120 -0
- feectools/fem/tests/test_spline_interpolation.py +182 -0
- feectools/fem/tests/test_splines.py +184 -0
- feectools/fem/tests/test_splines_par.py +46 -0
- feectools/fem/tests/test_vector_spaces.py +150 -0
- feectools/fem/tests/utilities.py +47 -0
- feectools/fem/vector.py +729 -0
- feectools/linalg/__init__.py +0 -0
- feectools/linalg/basic.py +1386 -0
- feectools/linalg/block.py +1451 -0
- feectools/linalg/direct_solvers.py +201 -0
- feectools/linalg/fft.py +258 -0
- feectools/linalg/kernels/__init__.py +0 -0
- feectools/linalg/kernels/axpy_kernels.py +57 -0
- feectools/linalg/kernels/inner_kernels.py +100 -0
- feectools/linalg/kernels/matvec_kernels.py +206 -0
- feectools/linalg/kernels/stencil2IJV_kernels.py +227 -0
- feectools/linalg/kernels/stencil2coo_kernels.py +179 -0
- feectools/linalg/kernels/transpose_kernels.py +263 -0
- feectools/linalg/kron.py +911 -0
- feectools/linalg/solvers.py +1914 -0
- feectools/linalg/sparse.py +114 -0
- feectools/linalg/stencil.py +2923 -0
- feectools/linalg/stencil_dot_kernels.py +317 -0
- feectools/linalg/stencil_transpose_kernels.py +372 -0
- feectools/linalg/tests/__init__.py +0 -0
- feectools/linalg/tests/test_block.py +1588 -0
- feectools/linalg/tests/test_fft.py +106 -0
- feectools/linalg/tests/test_kron_stencil_matrix.py +114 -0
- feectools/linalg/tests/test_linalg.py +1065 -0
- feectools/linalg/tests/test_matrix_free.py +128 -0
- feectools/linalg/tests/test_solvers.py +213 -0
- feectools/linalg/tests/test_stencil_interface_matrix.py +379 -0
- feectools/linalg/tests/test_stencil_vector.py +1036 -0
- feectools/linalg/tests/test_stencil_vector_space.py +440 -0
- feectools/linalg/topetsc.py +522 -0
- feectools/linalg/utilities.py +200 -0
- feectools/utilities/__init__.py +0 -0
- feectools/utilities/quadratures.py +113 -0
- feectools/utilities/utils.py +166 -0
- feectools/version.py +1 -0
- feectools-0.1.0.dist-info/METADATA +66 -0
- feectools-0.1.0.dist-info/RECORD +98 -0
- feectools-0.1.0.dist-info/WHEEL +5 -0
- feectools-0.1.0.dist-info/entry_points.txt +3 -0
- feectools-0.1.0.dist-info/licenses/AUTHORS +22 -0
- feectools-0.1.0.dist-info/licenses/LICENSE +21 -0
- feectools-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -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
|