fenicsx-ii 0.2.0__tar.gz

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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Scientific Computing at Simula Research Laboratory
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,84 @@
1
+ Metadata-Version: 2.4
2
+ Name: fenicsx_ii
3
+ Version: 0.2.0
4
+ Summary: Mixed-dimensional coupling in FEniCSx
5
+ Author-email: "Jørgen S. Dokken" <dokken@simula.no>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2025 Scientific Computing at Simula Research Laboratory
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Description-Content-Type: text/markdown
29
+ License-File: LICENSE
30
+ Requires-Dist: fenics-dolfinx>0.9.0
31
+ Provides-Extra: test
32
+ Requires-Dist: pytest; extra == "test"
33
+ Requires-Dist: sympy; extra == "test"
34
+ Provides-Extra: dev
35
+ Requires-Dist: pdbpp; extra == "dev"
36
+ Requires-Dist: ipython; extra == "dev"
37
+ Requires-Dist: mypy; extra == "dev"
38
+ Requires-Dist: ruff; extra == "dev"
39
+ Provides-Extra: docs
40
+ Requires-Dist: jupyter-book; extra == "docs"
41
+ Requires-Dist: jupytext; extra == "docs"
42
+ Requires-Dist: sphinx-codeautolink; extra == "docs"
43
+ Provides-Extra: jupyter
44
+ Requires-Dist: trame_jupyter_extension; extra == "jupyter"
45
+ Requires-Dist: trame; extra == "jupyter"
46
+ Requires-Dist: pyvista[all]>=0.45.0; extra == "jupyter"
47
+ Provides-Extra: all
48
+ Requires-Dist: fenicsx_ii[test]; extra == "all"
49
+ Requires-Dist: fenicsx_ii[dev]; extra == "all"
50
+ Requires-Dist: fenicsx_ii[docs]; extra == "all"
51
+ Requires-Dist: fenicsx_ii[jupyter]; extra == "all"
52
+ Dynamic: license-file
53
+
54
+ # FEniCSx_ii (FEniCSx trace)
55
+
56
+ FEniCSx_ii is an extension of FEniCSx that allows users to work with non-conforming 3D-1D meshes.
57
+
58
+ Given a 3D domain with a function $u\in V(\Omega)$ and a $1D$ domain $\Gamma$.
59
+ We define a restriction operator $\Pi:V\mapsto L^2(\Gamma)$,
60
+ which becomes a central part of the variational formulation.
61
+
62
+ See for instance
63
+ [D'Angelo & Quarteroni, 2008](https://doi.org/10.1142/S0218202508003108) {cite}`intro-dangelo20083d1d`,
64
+ [Kutcha 2021](https://doi.org/10.1007/978-3-030-55874-1_63) {cite}`intro-kutcha2021trace` or
65
+ [Masri, Kutcha & Riviere, 2024](https://doi.org/10.1137/23M1627390) {cite}`intro-masri2024coupled3d1d`.
66
+
67
+ The restrictions implemented in {py:mod}`fenicsx_ii` is:
68
+ - {py:class}`PointwiseTrace<fenicsx_ii.PointwiseTrace>`, the operator: $\Pi(u)(\hat x)=u(\hat x)$, $\hat x \in \Gamma$ .
69
+ - {py:class}`Circle<fenicsx_ii.Circle>`, the operator $\Pi(u)(\hat x)=\frac{1}{\vert P_R \vert}\int_{P_{R}(\Gamma(\hat x))}u~\mathrm{d}s$, where $P_R(\Gamma(\hat x))$ is the perimeter of a disk with radius $R$, normal aligning with $\Gamma(\hat x)$ and origin at $\hat x$.
70
+ - {py:class}`Disk<fenicsx_ii.Disk>`, the operator $\Pi(u)(\hat x)=\frac{1}{\vert D_R \vert}\int_{D_R(\Gamma(\hat x))} u~\mathrm{d}x$, where $D_R(\Gamma(\hat x))$ is the disk with radius $R$, normal aligining with $\Gamma(\hat x)$ and origin at $\hat x$.
71
+
72
+ Any other operator can be implenmented by following the {py:class}`ReductionOperator<fenicsx_ii.ReductionOperator>`-protocol.
73
+
74
+ ## Funding
75
+ The development of FEniCSx_ii has been funded by the Wellcome Trust,
76
+ grant number: 313298/Z/24/Z
77
+
78
+ ## References
79
+
80
+ ```{bibliography}
81
+ :filter: cited
82
+ :labelprefix:
83
+ :keyprefix: intro-
84
+ ```
@@ -0,0 +1,31 @@
1
+ # FEniCSx_ii (FEniCSx trace)
2
+
3
+ FEniCSx_ii is an extension of FEniCSx that allows users to work with non-conforming 3D-1D meshes.
4
+
5
+ Given a 3D domain with a function $u\in V(\Omega)$ and a $1D$ domain $\Gamma$.
6
+ We define a restriction operator $\Pi:V\mapsto L^2(\Gamma)$,
7
+ which becomes a central part of the variational formulation.
8
+
9
+ See for instance
10
+ [D'Angelo & Quarteroni, 2008](https://doi.org/10.1142/S0218202508003108) {cite}`intro-dangelo20083d1d`,
11
+ [Kutcha 2021](https://doi.org/10.1007/978-3-030-55874-1_63) {cite}`intro-kutcha2021trace` or
12
+ [Masri, Kutcha & Riviere, 2024](https://doi.org/10.1137/23M1627390) {cite}`intro-masri2024coupled3d1d`.
13
+
14
+ The restrictions implemented in {py:mod}`fenicsx_ii` is:
15
+ - {py:class}`PointwiseTrace<fenicsx_ii.PointwiseTrace>`, the operator: $\Pi(u)(\hat x)=u(\hat x)$, $\hat x \in \Gamma$ .
16
+ - {py:class}`Circle<fenicsx_ii.Circle>`, the operator $\Pi(u)(\hat x)=\frac{1}{\vert P_R \vert}\int_{P_{R}(\Gamma(\hat x))}u~\mathrm{d}s$, where $P_R(\Gamma(\hat x))$ is the perimeter of a disk with radius $R$, normal aligning with $\Gamma(\hat x)$ and origin at $\hat x$.
17
+ - {py:class}`Disk<fenicsx_ii.Disk>`, the operator $\Pi(u)(\hat x)=\frac{1}{\vert D_R \vert}\int_{D_R(\Gamma(\hat x))} u~\mathrm{d}x$, where $D_R(\Gamma(\hat x))$ is the disk with radius $R$, normal aligining with $\Gamma(\hat x)$ and origin at $\hat x$.
18
+
19
+ Any other operator can be implenmented by following the {py:class}`ReductionOperator<fenicsx_ii.ReductionOperator>`-protocol.
20
+
21
+ ## Funding
22
+ The development of FEniCSx_ii has been funded by the Wellcome Trust,
23
+ grant number: 313298/Z/24/Z
24
+
25
+ ## References
26
+
27
+ ```{bibliography}
28
+ :filter: cited
29
+ :labelprefix:
30
+ :keyprefix: intro-
31
+ ```
@@ -0,0 +1,75 @@
1
+ [build-system] # Require setuptool version due to https://github.com/pypa/setuptools/issues/2938
2
+ requires = ["setuptools>=61.0.0", "wheel"]
3
+
4
+ [project]
5
+ name = "fenicsx_ii"
6
+ version = "0.2.0"
7
+ description = "Mixed-dimensional coupling in FEniCSx"
8
+ authors = [{ name = "Jørgen S. Dokken", email = "dokken@simula.no" }]
9
+ license = { file = "LICENSE" }
10
+ readme = "README.md"
11
+ dependencies = ["fenics-dolfinx>0.9.0"]
12
+
13
+
14
+ [project.optional-dependencies]
15
+ test = ["pytest", "sympy"]
16
+ dev = ["pdbpp", "ipython", "mypy", "ruff"]
17
+ docs = ["jupyter-book", "jupytext", "sphinx-codeautolink"]
18
+ jupyter = ["trame_jupyter_extension", "trame", "pyvista[all]>=0.45.0"]
19
+ all = [
20
+ "fenicsx_ii[test]",
21
+ "fenicsx_ii[dev]",
22
+ "fenicsx_ii[docs]",
23
+ "fenicsx_ii[jupyter]",
24
+ ]
25
+
26
+ [tool.pytest.ini_options]
27
+ addopts = ["--import-mode=importlib"]
28
+ testpaths = ["tests"]
29
+
30
+ [tool.mypy]
31
+ ignore_missing_imports = true
32
+ # Folders to exclude
33
+ exclude = ["docs/", "build/"]
34
+ # Folder to check with mypy
35
+ files = ["src", "tests"]
36
+
37
+ [tool.ruff]
38
+ src = ["src", "docs", "tests", "examples"]
39
+ indent-width = 4
40
+
41
+ [tool.ruff.lint]
42
+ select = [
43
+ # Pyflakes
44
+ "F",
45
+ # Pycodestyle
46
+ "E",
47
+ "W",
48
+ # isort
49
+ "I001",
50
+ ]
51
+
52
+
53
+ [tool.ruff.lint.isort]
54
+ known-first-party = ["fenicsx_iis"]
55
+ known-third-party = [
56
+ "basix",
57
+ "dolfinx",
58
+ "ffcx",
59
+ "ufl",
60
+ "gmsh",
61
+ "numpy",
62
+ "pytest",
63
+ "pyadjoint",
64
+ ]
65
+ section-order = [
66
+ "future",
67
+ "standard-library",
68
+ "mpi",
69
+ "third-party",
70
+ "first-party",
71
+ "local-folder",
72
+ ]
73
+
74
+ [tool.ruff.lint.isort.sections]
75
+ "mpi" = ["mpi4py", "petsc4py"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,25 @@
1
+ from .assembly import assemble_scalar, average_coefficients
2
+ from .interpolation import create_interpolation_matrix
3
+ from .matrix_assembler import assemble_matrix, create_matrix
4
+ from .quadrature import Quadrature
5
+ from .restriction_operators import Circle, Disk, PointwiseTrace, ReductionOperator
6
+ from .solver import LinearProblem
7
+ from .ufl_operations import Average
8
+ from .vector_assembler import assemble_vector, create_vector
9
+
10
+ __all__ = [
11
+ "Circle",
12
+ "Disk",
13
+ "PointwiseTrace",
14
+ "average_coefficients",
15
+ "create_interpolation_matrix",
16
+ "Average",
17
+ "LinearProblem",
18
+ "assemble_vector",
19
+ "create_vector",
20
+ "assemble_matrix",
21
+ "create_matrix",
22
+ "assemble_scalar",
23
+ "ReductionOperator",
24
+ "Quadrature",
25
+ ]
@@ -0,0 +1,103 @@
1
+ import typing
2
+
3
+ from mpi4py import MPI
4
+ from petsc4py import PETSc
5
+
6
+ import dolfinx
7
+ import numpy as np
8
+ import ufl
9
+
10
+ from .interpolation import create_interpolation_matrix
11
+ from .ufl_operations import (
12
+ AveragedCoefficient,
13
+ apply_replacer,
14
+ )
15
+
16
+ __all__ = ["assemble_scalar", "assign_LG_map", "average_coefficients"]
17
+
18
+
19
+ def assign_LG_map(
20
+ C: PETSc.Mat, # type: ignore
21
+ row_map: dolfinx.common.IndexMap,
22
+ col_map: dolfinx.common.IndexMap,
23
+ row_bs: int,
24
+ col_bs: int,
25
+ ):
26
+ """
27
+ Assign a Local to Global map (LG-map) to a PETSc matrix.
28
+
29
+ Args:
30
+ C: The PETSc matrix to which the LG-map is assigned.
31
+ row_map: The row index map.
32
+ col_map: The column index map.
33
+ row_bs: The row block size.
34
+ col_bs: The column block size.
35
+ """
36
+ global_row_map = row_map.local_to_global(
37
+ np.arange(row_map.size_local + row_map.num_ghosts, dtype=np.int32)
38
+ ).astype(PETSc.IntType) # type: ignore
39
+ global_col_map = col_map.local_to_global(
40
+ np.arange(col_map.size_local + col_map.num_ghosts, dtype=np.int32)
41
+ ).astype(PETSc.IntType) # type: ignore
42
+ row_map = PETSc.LGMap().create(global_row_map, bsize=row_bs, comm=row_map.comm) # type: ignore
43
+ col_map = PETSc.LGMap().create(global_col_map, bsize=col_bs, comm=col_map.comm) # type: ignore
44
+ C.setLGMap(row_map, col_map) # type: ignore
45
+
46
+
47
+ def average_coefficients(form: ufl.Form):
48
+ """Perform the average operation on all coefficients of a given form.
49
+
50
+ Note:
51
+ This updates the underlying vector of each coefficient.
52
+
53
+ Args:
54
+ form: The form whose coefficients are averaged.
55
+ """
56
+ for coeff in form.coefficients():
57
+ if isinstance(coeff, AveragedCoefficient):
58
+ u = coeff.parent_coefficient
59
+ res_op = coeff.restriction_operator
60
+ # Replace rows, i.e. apply interpolation matrix on the left
61
+ K, _, _ = create_interpolation_matrix(
62
+ u.function_space,
63
+ coeff.function_space,
64
+ res_op,
65
+ use_petsc=True,
66
+ )
67
+ K.mult(u.x.petsc_vec, coeff.x.petsc_vec)
68
+ coeff.x.scatter_forward()
69
+
70
+
71
+ def assemble_scalar(
72
+ form: ufl.Form,
73
+ form_compiler_options: dict | None = None,
74
+ jit_options: dict | None = None,
75
+ entity_maps: typing.Sequence[dolfinx.mesh.EntityMap] | None = None,
76
+ op: MPI.Op = MPI.SUM,
77
+ ) -> np.inexact | float | complex:
78
+ """
79
+ Assemble a rank-0 form into a scalar value. Perform necessary MPI communication.
80
+
81
+ Args:
82
+ form: The rank-0 form to assemble.
83
+ form_compiler_options: Options for the form compiler.
84
+ jit_options: Options for just-in-time compilation.
85
+ entity_maps: Entity maps for the mesh.
86
+ op: The MPI operation to use for communication (default is
87
+ :py:obj:`mpi4py.MPI.SUM`).
88
+ Returns
89
+ The assembled scalar value.
90
+ """
91
+ new_forms = apply_replacer(form)
92
+ loc_val: float | complex = 0.0
93
+ for avg_form in new_forms:
94
+ L_c = dolfinx.fem.form(
95
+ avg_form,
96
+ form_compiler_options=form_compiler_options,
97
+ jit_options=jit_options,
98
+ entity_maps=entity_maps,
99
+ )
100
+ average_coefficients(avg_form)
101
+ loc_val += dolfinx.fem.assemble_scalar(L_c)
102
+ val = L_c.mesh.comm.allreduce(loc_val, op=op)
103
+ return val
@@ -0,0 +1,230 @@
1
+ from __future__ import annotations
2
+
3
+ from mpi4py import MPI as _MPI
4
+
5
+ import dolfinx
6
+ import numpy as np
7
+ from dolfinx.common import IndexMap as _im
8
+
9
+ from .interpolation_utils import create_extended_indexmap, evaluate_basis_function
10
+ from .restriction_operators import ReductionOperator
11
+ from .utils import send_dofs_to_other_process, unroll_dofmap
12
+
13
+
14
+ def create_interpolation_matrix(
15
+ V: dolfinx.fem.FunctionSpace,
16
+ K: dolfinx.fem.FunctionSpace,
17
+ red_op: ReductionOperator,
18
+ tol: float = 1.0e-8,
19
+ use_petsc: bool = False,
20
+ ) -> tuple["PETSc.Mat" | dolfinx.la.MatrixCSR, _im, _im]: # type: ignore[name-defined] # noqa: F821
21
+ """
22
+ Create an interpolation matrix from `V` to `K` with a specific reduction operator
23
+ applied to the interpolation points of `K`.
24
+
25
+ Args:
26
+ V: The function space to interpolate from.
27
+ K: The function space to interpolate to.
28
+ red_op: Reduction operator for each interpolation point on `K`.
29
+ tol: Tolerance for determining point ownership across processes.
30
+ use_petsc: Flag to indicate whether to use PETSc for the matrix.
31
+
32
+ Returns:
33
+ The interpolation matrix, as a DOLFINx built in matrix or a PETSc matrix.
34
+ The accumulation of contributions from all processes has been accounted before
35
+ returning the matrix.
36
+ """
37
+ mesh_to = K.mesh
38
+ mesh_from = V.mesh
39
+ num_line_cells = mesh_to.topology.index_map(mesh_to.topology.dim).size_local
40
+ reference_interpolation_points = K.element.interpolation_points
41
+
42
+ num_ip_per_cell = reference_interpolation_points.shape[0]
43
+ line_cells = np.arange(num_line_cells, dtype=np.int32)
44
+
45
+ quadrature_rule = red_op.compute_quadrature(
46
+ line_cells, reference_interpolation_points
47
+ )
48
+ quad_points = quadrature_rule.points
49
+
50
+ interpolation_coordinates = quad_points.reshape(-1, mesh_to.geometry.dim)
51
+ assert mesh_to.geometry.dim == 3
52
+
53
+ num_average_qp = red_op.num_points
54
+
55
+ point_ownership = dolfinx.geometry.determine_point_ownership(
56
+ mesh_from, interpolation_coordinates, padding=tol
57
+ )
58
+ cells_on_proc = (
59
+ point_ownership.dest_cells
60
+ ) # Cells in 3D domain that has interpolation points
61
+ points_on_proc = (
62
+ point_ownership.dest_points
63
+ ) # Interpolation points relating to `cells_on_proc`
64
+ ip_sender = (
65
+ point_ownership.src_owner
66
+ ) # For IP in 1D grid, what process has taken ownership
67
+ assert (ip_sender >= 0).all()
68
+ ip_owner = point_ownership.dest_owner # For received data, who sent it
69
+
70
+ num_dofs_per_cell_K = K.dofmap.list.shape[1]
71
+ incoming_K_dofs, incoming_K_owners = send_dofs_to_other_process(
72
+ K,
73
+ ip_owner,
74
+ ip_sender,
75
+ np.repeat(np.arange(num_line_cells), num_ip_per_cell * num_average_qp),
76
+ )
77
+ # Create extended index map
78
+ new_imap_K = create_extended_indexmap(
79
+ K.mesh.comm,
80
+ K.dofmap.index_map,
81
+ incoming_K_dofs.flatten(),
82
+ incoming_K_owners,
83
+ 123,
84
+ )
85
+ assert (new_imap_K.global_to_local(incoming_K_dofs.flatten()) >= 0).all()
86
+
87
+ incoming_V_dofs, incoming_V_owners = send_dofs_to_other_process(
88
+ V, ip_sender, ip_owner, cells_on_proc
89
+ )
90
+
91
+ # Create extended index map
92
+ new_imap_V = create_extended_indexmap(
93
+ V.mesh.comm,
94
+ V.dofmap.index_map,
95
+ incoming_V_dofs.flatten(),
96
+ incoming_V_owners,
97
+ 321,
98
+ )
99
+ num_dofs_per_cell_V = V.dofmap.list.shape[1]
100
+ new_local_V_dofs = new_imap_V.global_to_local(incoming_V_dofs.flatten())
101
+ assert (new_local_V_dofs >= 0).all()
102
+ new_local_V_dofs = new_local_V_dofs.reshape(-1, num_dofs_per_cell_V)
103
+
104
+ # Evaluate basis functions in 3D space
105
+ basis_values_on_V = evaluate_basis_function(V, points_on_proc, cells_on_proc)
106
+ second_dimension = max(V.dofmap.bs, np.prod(V.element.basix_element.value_shape))
107
+ recv_basis_functions = np.empty(
108
+ (len(ip_sender), basis_values_on_V.shape[1], basis_values_on_V.shape[2]),
109
+ dtype=basis_values_on_V.dtype,
110
+ )
111
+ volume_send_to, send_counts_V = np.unique(ip_owner, return_counts=True)
112
+ line_recv_from, recv_counts_V = np.unique(ip_sender, return_counts=True)
113
+ basis_send_counts = (
114
+ send_counts_V * num_dofs_per_cell_V * V.dofmap.bs * second_dimension
115
+ )
116
+ basis_recv_counts = (
117
+ recv_counts_V * num_dofs_per_cell_V * V.dofmap.bs * second_dimension
118
+ )
119
+ send_message = [basis_values_on_V.flatten(), basis_send_counts, _MPI.DOUBLE]
120
+ recv_message = [recv_basis_functions, basis_recv_counts, _MPI.DOUBLE]
121
+ volume_to_line_comm = mesh_from.comm.Create_dist_graph_adjacent(
122
+ line_recv_from.tolist(), volume_send_to.tolist(), reorder=False
123
+ )
124
+ volume_to_line_comm.Neighbor_alltoallv(send_message, recv_message)
125
+ # Free communicators post communication
126
+ volume_to_line_comm.Free()
127
+
128
+ # Create sparsity pattern for the interpolation matrix
129
+ sp = dolfinx.cpp.la.SparsityPattern(
130
+ K.mesh.comm,
131
+ [new_imap_K, new_imap_V],
132
+ [K.dofmap.index_map_bs, V.dofmap.index_map_bs],
133
+ )
134
+ insert_position = np.argsort(ip_sender, stable=True)
135
+ V_in_Q_order = np.argsort(insert_position, stable=True)
136
+
137
+ assert K.element.interpolation_ident
138
+ assert not K.element.needs_dof_transformations
139
+ for i in range(num_line_cells):
140
+ local_k_dofs = K.dofmap.list[i]
141
+ local_v_dofs = new_local_V_dofs[
142
+ V_in_Q_order[
143
+ num_dofs_per_cell_K * num_average_qp * i : num_dofs_per_cell_K
144
+ * num_average_qp
145
+ * (i + 1)
146
+ ]
147
+ ]
148
+ for j in range(num_dofs_per_cell_K):
149
+ for k in range(num_average_qp):
150
+ ldofs = local_v_dofs[j * num_average_qp + k]
151
+ sp.insert(local_k_dofs[j : j + 1], ldofs)
152
+ sp.finalize()
153
+
154
+ # Create distributed petsc matrix and insert basis function values
155
+ weights = quadrature_rule.weights
156
+ scales = quadrature_rule.scales
157
+
158
+ if use_petsc:
159
+ assert dolfinx.has_petsc, (
160
+ "DOLFINx has to be installed with PETSc support to use PETSc matrices"
161
+ )
162
+ from petsc4py import PETSc
163
+
164
+ A = dolfinx.cpp.la.petsc.create_matrix(K.mesh.comm, sp)
165
+
166
+ def insert_function(A, rows, columns, values):
167
+ A.setValuesLocal(rows, columns, values, addv=PETSc.InsertMode.ADD)
168
+
169
+ def finalize(A):
170
+ A.assemble()
171
+ else:
172
+ A = dolfinx.la.matrix_csr(
173
+ sp,
174
+ block_mode=dolfinx.la.BlockMode.compact,
175
+ dtype=dolfinx.default_scalar_type,
176
+ )
177
+
178
+ def insert_function(A, rows, columns, values):
179
+ A.add(values, rows, columns)
180
+
181
+ def finalize(A):
182
+ A.scatter_reverse()
183
+
184
+ # Keep track of dofs that are local to process, to ensure that we only insert for
185
+ # - Once per degree of freedom
186
+ # - Only on the process that owns the degree of freedom
187
+ dofs_visited = np.full(
188
+ (K.dofmap.index_map.size_local + K.dofmap.index_map.num_ghosts)
189
+ * K.dofmap.index_map_bs,
190
+ False,
191
+ dtype=np.bool_,
192
+ )
193
+ K_bs = K.dofmap.bs
194
+ dofs_visited[K.dofmap.index_map.size_local * K.dofmap.index_map_bs :] = True
195
+ padded_K_dm = unroll_dofmap(K.dofmap.list, K_bs)
196
+ local_visit = np.full(num_average_qp * K_bs, False, dtype=np.bool_)
197
+ for i in range(num_line_cells):
198
+ local_k_dofs = padded_K_dm[i]
199
+ V_slice = V_in_Q_order[
200
+ num_average_qp * num_dofs_per_cell_K * i : num_average_qp
201
+ * num_dofs_per_cell_K
202
+ * (i + 1)
203
+ ]
204
+ local_v_dofs = unroll_dofmap(new_local_V_dofs[V_slice], V.dofmap.index_map_bs)
205
+ local_v_values = recv_basis_functions[V_slice]
206
+ for j in range(num_dofs_per_cell_K):
207
+ local_dofs = local_v_dofs[j * num_average_qp : (j + 1) * num_average_qp]
208
+ local_values = local_v_values[j * num_average_qp : (j + 1) * num_average_qp]
209
+ average_weights = weights[i * num_dofs_per_cell_K + j]
210
+ # Get visited dofs from previous run
211
+ for b in range(K_bs):
212
+ local_visit[:] = dofs_visited[local_k_dofs[j * K_bs + b]]
213
+ lv = (
214
+ local_values[:, :, b]
215
+ * average_weights[:, None]
216
+ / scales[i * num_dofs_per_cell_K + j]
217
+ )
218
+ for k in range(num_average_qp):
219
+ # We insert for all average nodes, thus local visit
220
+ # is only updated next time we pass through the `j` loop
221
+ lv[k][:] = 0 if local_visit[b * num_average_qp + k] else lv[k]
222
+ insert_function(
223
+ A,
224
+ local_k_dofs[j * K_bs + b : j * K_bs + b + 1],
225
+ local_dofs[k],
226
+ lv[k],
227
+ )
228
+ dofs_visited[local_k_dofs[j * K_bs + b]] = True
229
+ finalize(A)
230
+ return A, new_imap_K, new_imap_V
@@ -0,0 +1,96 @@
1
+ from mpi4py import MPI as _MPI
2
+
3
+ import dolfinx
4
+ import numpy as np
5
+ import numpy.typing as npt
6
+ import ufl
7
+
8
+
9
+ def evaluate_basis_function(
10
+ V: dolfinx.fem.FunctionSpace,
11
+ points: npt.NDArray[np.inexact],
12
+ cells: npt.NDArray[np.int32],
13
+ batch_size: int = 200,
14
+ ):
15
+ """Evaluate basis functions in `V` at a set of points.
16
+
17
+ Args:
18
+ V: The function space
19
+ points: The points to evaluate at (in physical space)
20
+ cells: The cells. The ith cell corresponds to the ith point.
21
+ batch_size: The number of points/cells to evaluate at the time,
22
+ to find a balance between flooding the expression cache and
23
+ the memory usage.
24
+
25
+ Returns:
26
+ The evaluated basis functions at the given points.
27
+ """
28
+ # Pull owning points back to reference cell
29
+ mesh = V.mesh
30
+ mesh_nodes = mesh.geometry.x
31
+ cmap = mesh.geometry.cmap
32
+
33
+ ref_x = np.zeros((len(cells), mesh.topology.dim), dtype=mesh.geometry.x.dtype)
34
+ for i, (point, cell) in enumerate(zip(points, cells)):
35
+ geom_dofs = mesh.geometry.dofmap[cell]
36
+ ref_x[i] = cmap.pull_back(point.reshape(-1, 3), mesh_nodes[geom_dofs])
37
+
38
+ # Create expression evaluating a trial function (i.e. just the basis function)
39
+ u = ufl.TestFunction(V)
40
+ bs = V.dofmap.bs
41
+ num_dofs = V.dofmap.dof_layout.num_dofs
42
+ value_size = np.prod(V.element.basix_element.value_shape)
43
+ if bs > 1 and value_size > 1:
44
+ raise ValueError(
45
+ f"A function space cant have both {value_size=} and {bs=} bigger than 1."
46
+ )
47
+ # Get basis values as (num_cells, num_basis_functions*bs, max(bs, value_size))
48
+ basis_values = np.zeros(
49
+ (len(cells), num_dofs * bs, max(bs, value_size)),
50
+ dtype=dolfinx.default_scalar_type,
51
+ )
52
+ if len(cells) > 0:
53
+ # NOTE: Expression lives on only this communicator rank
54
+ # Expression is evaluated for every point in every cell, which means that we
55
+ # need to discard values that are not on the diagonal.
56
+ assert ref_x.shape[0] == len(cells)
57
+ num_batches = len(cells) // batch_size + 1
58
+ for b in range(num_batches):
59
+ x_batch = ref_x[b * batch_size : (b + 1) * batch_size]
60
+ cell_batch = cells[b * batch_size : (b + 1) * batch_size]
61
+ expr = dolfinx.fem.Expression(u, x_batch, comm=_MPI.COMM_SELF)
62
+ all_values = expr.eval(mesh, cell_batch)
63
+ if bs > 1:
64
+ basis_values[b * batch_size : (b + 1) * batch_size, :, :] = np.swapaxes(
65
+ np.diagonal(all_values, axis1=0, axis2=1), 0, 2
66
+ )
67
+ else:
68
+ basis_values[b * batch_size : (b + 1) * batch_size, :, :] = np.diagonal(
69
+ all_values, axis1=0, axis2=1
70
+ ).T.reshape(-1, basis_values.shape[1], basis_values.shape[2])
71
+ return basis_values
72
+
73
+
74
+ def create_extended_indexmap(
75
+ comm: _MPI.Intracomm,
76
+ imap: dolfinx.common.IndexMap,
77
+ potential_new_dofs: npt.NDArray[np.int64],
78
+ owners: npt.NDArray[np.int32],
79
+ tag: int,
80
+ ) -> dolfinx.common.IndexMap:
81
+ """
82
+ Create an extended index map that includes new dofs and their owners.
83
+ """
84
+ local_indices = imap.global_to_local(potential_new_dofs)
85
+ new_ghosts = local_indices < 0
86
+ new_owners = owners[new_ghosts]
87
+ extended_ghosts = np.concatenate([imap.ghosts, potential_new_dofs[new_ghosts]])
88
+ extended_owners = np.concatenate([imap.owners, new_owners])
89
+
90
+ return dolfinx.common.IndexMap(
91
+ comm,
92
+ imap.size_local,
93
+ extended_ghosts,
94
+ extended_owners,
95
+ tag,
96
+ )