Coverage for dragonfly_sim/utils/meshwarping_utils.py: 21%
61 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
1import numpy as np
3def inner_bdry_function_from_corner_list(ffd_block_corner_list, overset_margin=1e-4, rel_tol=1e-6):
4 """
5 Build an inner-boundary indicator function out of an FFD block corner list.
7 ffd_block_corner_list carries the same information an inner_bdry_function
8 encodes: the inner boundary facets are the ones contained in the FFD block,
9 so the block itself can serve as the containment test. The returned closure
10 has the signature Mesh expects: it takes x of shape (n_dimensions, n_points)
11 and returns a boolean mask of the points inside the block.
13 The block is reconstructed exactly as in
14 shape_parameterization.ffd_corners_from_corner_list -- two surfaces offset
15 along a single connecting direction, linearly interpolated in between -- so a swept
16 or tapered block is tested against its actual tapered extent rather than its
17 bounding box.
19 overset_margin matches the one the control point construction applies, so the
20 test region is the FFD block itself rather than the bare corner list; rel_tol
21 widens it further by that fraction of the block extent in each direction, so
22 that geometry sitting exactly on the block boundary still counts as inside.
24 NOTE the test is purely geometric: any OTHER boundary passing through the FFD
25 block (e.g. the symmetry plane at the root of a wing) is flagged as well,
26 whereas a hand-written inner_bdry_function can exclude it by construction.
27 """
28 num_surfaces = len(ffd_block_corner_list)
29 assert num_surfaces == 2, "Expected exactly 2 surface corner pairs, got {}".format(num_surfaces)
30 ndim = len(ffd_block_corner_list[0][0])
32 # Per-surface min/max in each coordinate, as in the control point construction
33 surface_mins = np.zeros((num_surfaces, ndim))
34 surface_maxs = np.zeros((num_surfaces, ndim))
35 for s, (corner_a, corner_b) in enumerate(ffd_block_corner_list):
36 corner_a, corner_b = np.array(corner_a), np.array(corner_b)
37 surface_mins[s] = np.minimum(corner_a, corner_b)
38 surface_maxs[s] = np.maximum(corner_a, corner_b)
40 surface_midpoints = 0.5 * (surface_mins + surface_maxs)
41 coord_diffs = np.abs(surface_midpoints[1] - surface_midpoints[0])
42 connecting_dir = int(np.argmax(coord_diffs))
43 span_dirs = [d for d in range(ndim) if d != connecting_dir]
45 # Same overset margin the control points are laid out with, so that the
46 # containment test covers the whole FFD block
47 for s in range(num_surfaces):
48 for d in range(ndim):
49 delta = surface_maxs[s, d] - surface_mins[s, d]
50 if delta > 0:
51 surface_mins[s, d] -= overset_margin * delta
52 surface_maxs[s, d] += overset_margin * delta
54 # Tolerances, scaled per direction by the block's bounding box extent. A
55 # direction the block is collapsed in falls back to the largest extent, so
56 # its tolerance stays a real (finite) length.
57 block_mins = np.minimum(surface_mins[0], surface_mins[1])
58 block_maxs = np.maximum(surface_maxs[0], surface_maxs[1])
59 extents = block_maxs - block_mins
60 tol = rel_tol * np.where(extents > 0., extents, extents.max())
62 connect_lo = surface_midpoints[0][connecting_dir]
63 connect_hi = surface_midpoints[1][connecting_dir]
64 connect_span = connect_hi - connect_lo
65 t_tol = tol[connecting_dir] / abs(connect_span)
67 print("inner_bdry_function_from_corner_list: FFD block used as inner boundary test")
68 print(" connecting direction: {}".format(connecting_dir))
69 print(" block min corner: {}".format(list(block_mins)))
70 print(" block max corner: {}".format(list(block_maxs)))
72 def inner_bdry_function(x):
73 """
74 Input: x is an array of size (n_dimensions, n_points)
75 """
76 coords = np.asarray(x)[:ndim, :]
78 # Parametric position along the connecting direction; points outside
79 # [0, 1] are beyond the two surfaces bounding the block
80 t = (coords[connecting_dir, :] - connect_lo) / connect_span
81 inside = (t >= -t_tol) & (t <= 1. + t_tol)
83 # Within the block, the span-direction bounds are the linear
84 # interpolation of the two surfaces' bounds
85 t_clipped = np.clip(t, 0., 1.)
86 for d in span_dirs:
87 lower = (1. - t_clipped) * surface_mins[0, d] + t_clipped * surface_mins[1, d]
88 upper = (1. - t_clipped) * surface_maxs[0, d] + t_clipped * surface_maxs[1, d]
89 inside &= (coords[d, :] >= lower - tol[d]) & (coords[d, :] <= upper + tol[d])
91 return inside
93 return inner_bdry_function
95def find_local_surface_nodes(msh, facet_tags, surface_tag: int) -> np.ndarray:
96 """Return *local* geometry indices of owned surface nodes (ghosts excluded).
98 Parameters
99 ----------
100 msh : dolfinx.mesh.Mesh
101 facet_tags : dolfinx.mesh.MeshTags | None
102 surface_tag : int
104 Returns
105 -------
106 np.ndarray, int64, shape (M_local,)
107 """
109 tdim = msh.topology.dim
110 fdim = tdim - 1
111 msh.topology.create_connectivity(fdim, 0)
112 f_to_v = msh.topology.connectivity(fdim, 0)
114 node_set: set[int] = set()
115 if facet_tags is not None:
116 for f in facet_tags.find(surface_tag):
117 node_set.update(f_to_v.links(int(f)).tolist())
118 else:
119 msh.topology.create_entities(fdim)
120 msh.topology.create_connectivity(fdim, tdim)
121 f_to_c = msh.topology.connectivity(fdim, tdim)
122 for f in range(msh.topology.index_map(fdim).size_local):
123 if len(f_to_c.links(f)) == 1:
124 node_set.update(f_to_v.links(f).tolist())
126 n_local_geom = msh.geometry.index_map().size_local # excludes ghosts
127 return np.array(
128 sorted(idx for idx in node_set if idx < n_local_geom),
129 dtype=np.int64,
130 )