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.
- fenicsx_ii-0.2.0/LICENSE +21 -0
- fenicsx_ii-0.2.0/PKG-INFO +84 -0
- fenicsx_ii-0.2.0/README.md +31 -0
- fenicsx_ii-0.2.0/pyproject.toml +75 -0
- fenicsx_ii-0.2.0/setup.cfg +4 -0
- fenicsx_ii-0.2.0/src/fenicsx_ii/__init__.py +25 -0
- fenicsx_ii-0.2.0/src/fenicsx_ii/assembly.py +103 -0
- fenicsx_ii-0.2.0/src/fenicsx_ii/interpolation.py +230 -0
- fenicsx_ii-0.2.0/src/fenicsx_ii/interpolation_utils.py +96 -0
- fenicsx_ii-0.2.0/src/fenicsx_ii/matrix_assembler.py +331 -0
- fenicsx_ii-0.2.0/src/fenicsx_ii/quadrature.py +103 -0
- fenicsx_ii-0.2.0/src/fenicsx_ii/restriction_operators.py +310 -0
- fenicsx_ii-0.2.0/src/fenicsx_ii/solver.py +300 -0
- fenicsx_ii-0.2.0/src/fenicsx_ii/ufl_operations.py +409 -0
- fenicsx_ii-0.2.0/src/fenicsx_ii/utils.py +118 -0
- fenicsx_ii-0.2.0/src/fenicsx_ii/vector_assembler.py +249 -0
- fenicsx_ii-0.2.0/src/fenicsx_ii.egg-info/PKG-INFO +84 -0
- fenicsx_ii-0.2.0/src/fenicsx_ii.egg-info/SOURCES.txt +22 -0
- fenicsx_ii-0.2.0/src/fenicsx_ii.egg-info/dependency_links.txt +1 -0
- fenicsx_ii-0.2.0/src/fenicsx_ii.egg-info/requires.txt +27 -0
- fenicsx_ii-0.2.0/src/fenicsx_ii.egg-info/top_level.txt +1 -0
- fenicsx_ii-0.2.0/tests/test_average_coefficient.py +177 -0
- fenicsx_ii-0.2.0/tests/test_circle_quadrature.py +269 -0
- fenicsx_ii-0.2.0/tests/test_interpolation_matrix.py +288 -0
fenicsx_ii-0.2.0/LICENSE
ADDED
|
@@ -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,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
|
+
)
|