Coverage for dragonfly_sim/core/wall_distance.py: 90%
105 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-17 03:37 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-17 03:37 +0000
1"""
2Distance to the wall, as a CG1 DerivedField with an exact mesh derivative.
4d at every CG1 node is the distance to the nearest wall facet: the exact
5point-to-segment distance for the straight 2D wall facets, the distance to
6the nearest wall vertex in 3D (an over-estimate between vertices). The
7nearest facets are found with a KD-tree over the facet midpoints (2D) or
8the wall vertices (3D).
10mesh_jacobian() is the analytic derivative of every nodal distance with
11respect to the coordinates of the node itself and of the wall vertices of
12its nearest facet. With P the nearest point on segment AB, t its parameter
13and r = (x - P)/d,
15 dd/dx = r, dd/dA = -(1 - t) r, dd/dB = -t r
17(t is stationary at an interior foot point, so it does not move to first
18order; at a clipped end, t = 0 or 1, the same formula holds). On the wall
19itself, d = 0 and the node moves with the wall, so the row is zero. The
20derivative is exact wherever the nearest facet does not switch, i.e.
21almost everywhere.
22"""
23import numpy as np
24import scipy.spatial
25from petsc4py import PETSc
26import dolfinx
28from dragonfly_sim.utils.derived_fields import DerivedField
31def _nearest_segments(x, A, B, k=16):
32 """Index of the nearest segment, foot parameter t and foot point, per point."""
33 tree = scipy.spatial.cKDTree(0.5 * (A + B))
34 k = min(k, len(A))
35 _, idx = tree.query(x, k=k)
36 idx = idx.reshape(len(x), k)
37 Ak, Bk = A[idx], B[idx]
38 AB = Bk - Ak
39 t = np.clip(np.einsum("nkj,nkj->nk", x[:, None, :] - Ak, AB)
40 / np.maximum(np.einsum("nkj,nkj->nk", AB, AB), 1e-300), 0.0, 1.0)
41 P = Ak + t[..., None] * AB
42 dist = np.linalg.norm(x[:, None, :] - P, axis=2)
43 j = np.argmin(dist, axis=1)
44 rows = np.arange(len(x))
45 return idx[rows, j], t[rows, j], P[rows, j], dist[rows, j]
48class WallDistance(DerivedField):
50 def __init__(self, mesh_obj, wall_tags):
51 self.mesh_obj = mesh_obj
52 self.wall_tags = list(wall_tags)
53 msh = mesh_obj.mesh
54 V = dolfinx.fem.functionspace(msh, ("Lagrange", 1))
55 Vx = mesh_obj.coordinate_space
56 # The derivative maps d's dofs onto coordinate_space's: they must be
57 # the same nodes in the same numbering.
58 if not (np.array_equal(V.dofmap.list, Vx.dofmap.list)
59 and V.dofmap.index_map.size_local == Vx.dofmap.index_map.size_local):
60 raise RuntimeError("wall distance: CG1 dofmap differs from the coordinate space's")
61 self.function = dolfinx.fem.Function(V, name="wall_distance")
62 self.update_geometry()
64 def _wall_geometry(self):
65 """Wall segment endpoints (2D) or vertices (3D) on every rank, with the
66 global coordinate_space block index of each endpoint."""
67 msh = self.mesh_obj.mesh
68 comm = msh.comm
69 gdim = msh.geometry.dim
70 fdim = msh.topology.dim - 1
71 tags = self.mesh_obj.meshtags
72 facets = tags.indices[np.isin(tags.values, self.wall_tags)]
73 facets = facets[facets < msh.topology.index_map(fdim).size_local]
74 Vx = self.mesh_obj.coordinate_space
75 imap = Vx.dofmap.index_map
76 # vertex -> coordinate_space block: through the cells' local numbering
77 geom = dolfinx.mesh.entities_to_geometry(msh, fdim, facets, False)
78 node_to_dof = self._geometry_node_to_dof()
79 coords = msh.geometry.x[geom][:, :, :gdim] if len(facets) else np.zeros((0, 2, gdim))
80 gids = (imap.local_to_global(node_to_dof[geom.ravel()].astype(np.int32)).reshape(geom.shape)
81 if len(facets) else np.zeros((0, 2), dtype=np.int64))
82 coords = np.concatenate([c for c in comm.allgather(coords) if c.size] or [np.zeros((0, 2, gdim))])
83 gids = np.concatenate([g for g in comm.allgather(gids) if g.size] or [np.zeros((0, 2), np.int64)])
84 if coords.shape[1] > 2 and msh.topology.dim == 2:
85 raise NotImplementedError("wall distance for facets with {} geometry nodes".format(coords.shape[1]))
86 return coords, gids
88 def _geometry_node_to_dof(self):
89 """Local geometry node -> local coordinate_space block (both ghosted)."""
90 msh = self.mesh_obj.mesh
91 Vx = self.mesh_obj.coordinate_space
92 g = msh.geometry.dofmaps[0]
93 d = Vx.dofmap.list
94 out = np.full(msh.geometry.x.shape[0], -1, dtype=np.int64)
95 out[g.ravel()] = d.ravel()
96 return out
98 def update_geometry(self):
99 msh = self.mesh_obj.mesh
100 gdim = msh.geometry.dim
101 x = self.function.function_space.tabulate_dof_coordinates()[:, :gdim]
102 segs, gids = self._wall_geometry()
103 self._segs, self._gids = segs, gids
104 if gdim == 3:
105 verts = segs.reshape(-1, 3)
106 vgids = gids.reshape(-1)
107 tree = scipy.spatial.cKDTree(verts)
108 dist, j = tree.query(x)
109 self._nearest = ("vertex", j, vgids, verts)
110 else:
111 A, B = segs[:, 0, :], segs[:, -1, :]
112 j, t, P, dist = _nearest_segments(x, A, B)
113 self._nearest = ("segment", j, t, P)
114 self._x = x
115 self._dist = dist
116 self.function.x.array[:] = dist
117 self.function.x.scatter_forward()
119 def mesh_jacobian(self):
120 msh = self.mesh_obj.mesh
121 gdim = msh.geometry.dim
122 V = self.function.function_space
123 Vx = self.mesh_obj.coordinate_space
124 imap = V.dofmap.index_map
125 n_own = imap.size_local
126 rows_g = imap.local_to_global(np.arange(n_own, dtype=np.int32))
127 D = PETSc.Mat().createAIJ(
128 ((n_own, imap.size_global), (n_own * gdim, imap.size_global * gdim)), comm=msh.comm)
129 D.setPreallocationNNZ((3 * gdim, 3 * gdim))
130 D.setOption(PETSc.Mat.Option.NEW_NONZERO_ALLOCATION_ERR, False)
131 x, dist = self._x[:n_own], self._dist[:n_own]
132 if self._nearest[0] == "segment":
133 _, j, t, P = self._nearest
134 j, t, P = j[:n_own], t[:n_own], P[:n_own]
135 else:
136 _, j, vgids, verts = self._nearest
137 j = j[:n_own]
138 for i in range(n_own):
139 if dist[i] <= 1e-14:
140 continue
141 if self._nearest[0] == "segment":
142 r = (x[i] - P[i]) / dist[i]
143 ga, gb = self._gids[j[i], 0], self._gids[j[i], -1]
144 entries = [(rows_g[i], r), (ga, -(1.0 - t[i]) * r), (gb, -t[i] * r)]
145 else:
146 r = (x[i] - verts[j[i]]) / dist[i]
147 entries = [(rows_g[i], r), (vgids[j[i]], -r)]
148 for block, val in entries:
149 D.setValues(np.array([rows_g[i]], dtype=PETSc.IntType),
150 np.array([block * gdim + k for k in range(gdim)], dtype=PETSc.IntType),
151 val, addv=True)
152 D.assemble()
153 return D