Coverage for python/src/dolfinx_mpc/utils/mpc_utils.py: 87%

271 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-07 09:58 +0000

1# Copyright (C) 2020-2021 Jørgen S. Dokken 

2# 

3# This file is part of DOLFINX_MPC 

4# 

5# SPDX-License-Identifier: MIT 

6from __future__ import annotations 

7 

8from mpi4py import MPI 

9from petsc4py import PETSc 

10 

11import dolfinx.common as _common 

12import dolfinx.cpp as _cpp 

13import dolfinx.fem as _fem 

14import dolfinx.geometry as _geometry 

15import dolfinx.la as _la 

16import dolfinx.log as _log 

17import dolfinx.mesh as _mesh 

18import numpy as np 

19import ufl 

20from dolfinx import default_scalar_type as _dt 

21 

22import dolfinx_mpc.cpp 

23 

24__all__ = [ 

25 "rotation_matrix", 

26 "facet_normal_approximation", 

27 "log_info", 

28 "rigid_motions_nullspace", 

29 "determine_closest_block", 

30 "create_normal_approximation", 

31 "create_point_to_point_constraint", 

32] 

33 

34 

35def rotation_matrix(axis, angle): 

36 # See https://en.wikipedia.org/wiki/Rotation_matrix, 

37 # Subsection: Rotation_matrix_from_axis_and_angle. 

38 if np.isclose(np.inner(axis, axis), 1): 

39 n_axis = axis 

40 else: 

41 # Normalize axis 

42 n_axis = axis / np.sqrt(np.inner(axis, axis)) 

43 

44 # Define cross product matrix of axis 

45 axis_x = np.array([[0, -n_axis[2], n_axis[1]], [n_axis[2], 0, -n_axis[0]], [-n_axis[1], n_axis[0], 0]]) 

46 identity = np.cos(angle) * np.eye(3) 

47 outer = (1 - np.cos(angle)) * np.outer(n_axis, n_axis) 

48 return np.sin(angle) * axis_x + identity + outer 

49 

50 

51def facet_normal_approximation( 

52 V, 

53 mt: _mesh.MeshTags, 

54 mt_id: int, 

55 tangent=False, 

56 jit_options: dict = {}, 

57 form_compiler_options: dict = {}, 

58): 

59 """ 

60 Approximate the facet normal by projecting it into the function space for a set of facets 

61 

62 Args: 

63 V: The function space to project into 

64 mt: The `dolfinx.mesh.MeshTagsMetaClass` containing facet markers 

65 mt_id: The id for the facets in `mt` we want to represent the normal at 

66 tangent: To approximate the tangent to the facet set this flag to `True` 

67 jit_options: Parameters used in CFFI JIT compilation of C code generated by FFCx. 

68 See https://github.com/FEniCS/dolfinx/blob/main/python/dolfinx/jit.py#L22-L37 

69 for all available parameters. Takes priority over all other parameter values. 

70 form_compiler_options: Parameters used in FFCx compilation of this form. Run `ffcx - -help` at 

71 the commandline to see all available options. Takes priority over all 

72 other parameter values, except for `scalar_type` which is determined by 

73 DOLFINx. 

74 """ 

75 timer = _common.Timer("~MPC: Facet normal projection") 

76 comm = V.mesh.comm 

77 n = ufl.FacetNormal(V.mesh) 

78 nh = _fem.Function(V) 

79 u, v = ufl.TrialFunction(V), ufl.TestFunction(V) 

80 ds = ufl.ds(domain=V.mesh, subdomain_data=mt, subdomain_id=mt_id) 

81 if tangent: 

82 if V.mesh.geometry.dim == 1: 

83 raise ValueError("Tangent not defined for 1D problem") 

84 elif V.mesh.geometry.dim == 2: 

85 a = ufl.inner(u, v) * ds 

86 L = ufl.inner(ufl.as_vector([-n[1], n[0]]), v) * ds 

87 else: 

88 

89 def tangential_proj(u, n): 

90 """ 

91 See for instance: 

92 https://link.springer.com/content/pdf/10.1023/A:1022235512626.pdf 

93 """ 

94 return (ufl.Identity(u.ufl_shape[0]) - ufl.outer(n, n)) * u 

95 

96 c = _fem.Constant(V.mesh, [1, 1, 1]) 

97 a = ufl.inner(u, v) * ds 

98 L = ufl.inner(tangential_proj(c, n), v) * ds 

99 else: 

100 a = ufl.inner(u, v) * ds 

101 L = ufl.inner(n, v) * ds 

102 

103 # Find all dofs that are not boundary dofs 

104 imap = V.dofmap.index_map 

105 all_blocks = np.arange(imap.size_local, dtype=np.int32) 

106 top_blocks = _fem.locate_dofs_topological(V, V.mesh.topology.dim - 1, mt.find(mt_id)) 

107 deac_blocks = all_blocks[np.isin(all_blocks, top_blocks, invert=True)] 

108 

109 # Note there should be a better way to do this 

110 # Create sparsity pattern only for constraint + bc 

111 bilinear_form = _fem.form(a, jit_options=jit_options, form_compiler_options=form_compiler_options) 

112 pattern = _fem.create_sparsity_pattern(bilinear_form) 

113 pattern.insert_diagonal(deac_blocks) 

114 pattern.finalize() 

115 u_0 = _fem.Function(V) 

116 u_0.x.petsc_vec.set(0) 

117 

118 bc_deac = _fem.dirichletbc(u_0, deac_blocks) 

119 A = _cpp.la.petsc.create_matrix(comm, pattern._cpp_object, None) 

120 A.zeroEntries() 

121 

122 # Assemble the matrix with all entries 

123 form_coeffs = _cpp.fem.pack_coefficients(bilinear_form._cpp_object) 

124 form_consts = _cpp.fem.pack_constants(bilinear_form._cpp_object) 

125 markers = _fem.petsc._matrix_bc_markers(bilinear_form, [bc_deac]) 

126 _cpp.fem.petsc.assemble_matrix(A, bilinear_form._cpp_object, form_consts, form_coeffs, *markers, False) 

127 if bilinear_form.function_spaces[0] is bilinear_form.function_spaces[1]: 

128 A.assemblyBegin(PETSc.Mat.AssemblyType.FLUSH) # type: ignore 

129 A.assemblyEnd(PETSc.Mat.AssemblyType.FLUSH) # type: ignore 

130 dofs = np.empty(0, dtype=np.int32) 

131 if bilinear_form.function_spaces[0].contains(bc_deac.function_space): 

132 dofs, owned = bc_deac.dof_indices() 

133 rows = np.array(dofs, dtype=np.int32) 

134 _cpp.fem.petsc.set_diagonal(A, rows, 1.0, PETSc.InsertMode.INSERT_VALUES) # type: ignore 

135 A.assemble() 

136 linear_form = _fem.form(L, jit_options=jit_options, form_compiler_options=form_compiler_options) 

137 b = _fem.petsc.assemble_vector(linear_form) 

138 

139 _fem.petsc.apply_lifting(b, [bilinear_form], [[bc_deac]]) 

140 b.ghostUpdate(addv=PETSc.InsertMode.ADD_VALUES, mode=PETSc.ScatterMode.REVERSE) # type: ignore 

141 _fem.petsc.set_bc(b, [bc_deac]) 

142 

143 # Solve Linear problem 

144 solver = PETSc.KSP().create(V.mesh.comm) # type: ignore 

145 solver.setType("cg") 

146 solver.rtol = 1e-8 

147 solver.setOperators(A) 

148 solver.solve(b, nh.x.petsc_vec) 

149 nh.x.petsc_vec.ghostUpdate(addv=PETSc.InsertMode.INSERT, mode=PETSc.ScatterMode.FORWARD) # type: ignore 

150 timer.stop() 

151 solver.destroy() 

152 b.destroy() 

153 return nh 

154 

155 

156def log_info(message): 

157 """ 

158 Wrapper for logging a simple string on the zeroth communicator 

159 Reverting the log level 

160 """ 

161 old_level = _log.get_log_level() 

162 if MPI.COMM_WORLD.rank == 0: 

163 _log.set_log_level(_log.LogLevel.INFO) 

164 _log.log(_log.LogLevel.INFO, message) 

165 _log.set_log_level(old_level) 

166 

167 

168def rigid_motions_nullspace(V: _fem.FunctionSpace): 

169 """ 

170 Function to build nullspace for 2D/3D elasticity. 

171 

172 Args: 

173 V: The function space 

174 """ 

175 _x = _fem.Function(V) 

176 # Get geometric dim 

177 gdim = V.mesh.geometry.dim 

178 assert gdim == 2 or gdim == 3 

179 

180 # Set dimension of nullspace 

181 dim = 3 if gdim == 2 else 6 

182 

183 # Create list of vectors for null space 

184 nullspace_basis = [ 

185 _la.vector(V.dofmap.index_map, bs=V.dofmap.index_map_bs, dtype=PETSc.ScalarType) # type: ignore 

186 for i in range(dim) 

187 ] 

188 

189 basis = [b.array for b in nullspace_basis] 

190 dofs = [V.sub(i).dofmap.list.reshape(-1) for i in range(gdim)] 

191 

192 # Build translational null space basis 

193 for i in range(gdim): 

194 basis[i][dofs[i]] = 1.0 

195 # Build rotational null space basis 

196 x = V.tabulate_dof_coordinates() 

197 dofs_block = V.dofmap.list.reshape(-1) 

198 x0, x1, x2 = x[dofs_block, 0], x[dofs_block, 1], x[dofs_block, 2] 

199 if gdim == 2: 

200 basis[2][dofs[0]] = -x1 

201 basis[2][dofs[1]] = x0 

202 elif gdim == 3: 

203 basis[3][dofs[0]] = -x1 

204 basis[3][dofs[1]] = x0 

205 

206 basis[4][dofs[0]] = x2 

207 basis[4][dofs[2]] = -x0 

208 basis[5][dofs[2]] = x1 

209 basis[5][dofs[1]] = -x2 

210 for b in nullspace_basis: 

211 b.scatter_forward() 

212 

213 _la.orthonormalize(nullspace_basis) 

214 assert _la.is_orthonormal(nullspace_basis, float(np.finfo(_x.x.array.dtype).eps)) 

215 local_size = V.dofmap.index_map.size_local * V.dofmap.index_map_bs 

216 basis_petsc = [ 

217 PETSc.Vec().createWithArray(x[:local_size], bsize=gdim, comm=V.mesh.comm) # type: ignore 

218 for x in basis 

219 ] 

220 return PETSc.NullSpace().create(comm=V.mesh.comm, vectors=basis_petsc) # type: ignore 

221 

222 

223def determine_closest_block(V, point): 

224 """ 

225 Determine the closest dofs (in a single block) to a point and the distance 

226 """ 

227 # Create boundingboxtree of cells connected to boundary facets 

228 tdim = V.mesh.topology.dim 

229 boundary_facets = _mesh.exterior_facet_indices(V.mesh.topology) 

230 V.mesh.topology.create_connectivity(tdim - 1, tdim) 

231 f_to_c = V.mesh.topology.connectivity(tdim - 1, tdim) 

232 boundary_cells = [] 

233 for facet in boundary_facets: 

234 boundary_cells.extend(f_to_c.links(facet)) 

235 cell_imap = V.mesh.topology.index_map(tdim) 

236 boundary_cells = np.array(np.unique(boundary_cells), dtype=np.int32) 

237 boundary_cells = boundary_cells[boundary_cells < cell_imap.size_local] 

238 bb_tree = _geometry.bb_tree(V.mesh, tdim, entities=boundary_cells, padding=0.0) 

239 midpoint_tree = _geometry.create_midpoint_tree(V.mesh, tdim, boundary_cells) 

240 

241 # Find facet closest 

242 point = np.reshape(point, (1, 3)).astype(V.mesh.geometry.x.dtype) 

243 closest_cell = _geometry.compute_closest_entity(bb_tree, midpoint_tree, V.mesh, point)[0] 

244 

245 # Set distance high if cell is not owned 

246 if cell_imap.size_local < closest_cell or closest_cell == -1: 

247 R = 1e5 

248 else: 

249 # Get cell geometry 

250 p = V.mesh.geometry.x 

251 V.mesh.topology.create_connectivity(tdim, tdim) 

252 entities = _mesh.entities_to_geometry(V.mesh, tdim, np.array([closest_cell], dtype=np.int32), False) 

253 R = np.linalg.norm(_geometry.compute_distance_gjk(point, p[entities[0]])) 

254 

255 # Find processor with cell closest to point 

256 global_distances = MPI.COMM_WORLD.allgather(R) 

257 owning_processor = np.argmin(global_distances) 

258 

259 dofmap = V.dofmap 

260 imap = dofmap.index_map 

261 ghost_owner = imap.owners 

262 local_max = imap.size_local 

263 # Determine which block of dofs is closest 

264 min_distance = max(R, 1e5) 

265 minimal_distance_block = None 

266 min_dof_owner = owning_processor 

267 if MPI.COMM_WORLD.rank == owning_processor: 

268 x = V.tabulate_dof_coordinates() 

269 cell_blocks = dofmap.cell_dofs(closest_cell) 

270 for block in cell_blocks: 

271 distance = np.linalg.norm(_geometry.compute_distance_gjk(point, x[block])) 

272 if distance < min_distance: 

273 # If cell owned by processor, but not the closest dof 

274 if block < local_max: 

275 min_dof_owner = MPI.COMM_WORLD.rank 

276 else: 

277 min_dof_owner = ghost_owner[block - local_max] 

278 minimal_distance_block = block 

279 min_distance = distance 

280 min_dof_owner = MPI.COMM_WORLD.bcast(min_dof_owner, root=owning_processor) 

281 # If dofs not owned by cell 

282 if owning_processor != min_dof_owner: 

283 owning_processor = min_dof_owner 

284 

285 if MPI.COMM_WORLD.rank == min_dof_owner: 

286 # Re-search using the closest cell 

287 x = V.tabulate_dof_coordinates() 

288 cell_blocks = dofmap.cell_dofs(closest_cell) 

289 for block in cell_blocks: 

290 distance = np.linalg.norm(_geometry.compute_distance_gjk(point, x[block])) 

291 if distance < min_distance: 

292 # If cell owned by processor, but not the closest dof 

293 if block < local_max: 

294 min_dof_owner = MPI.COMM_WORLD.rank 

295 else: 

296 min_dof_owner = ghost_owner[block - local_max] 

297 minimal_distance_block = block 

298 min_distance = distance 

299 assert min_dof_owner == owning_processor 

300 return owning_processor, [minimal_distance_block] 

301 else: 

302 return owning_processor, [] 

303 

304 

305def create_point_to_point_constraint(V, slave_point, master_point, vector=None): 

306 # Determine which processor owns the dof closest to the slave and master point 

307 slave_proc, slave_block = determine_closest_block(V, slave_point) 

308 master_proc, master_block = determine_closest_block(V, master_point) 

309 is_master_proc = MPI.COMM_WORLD.rank == master_proc 

310 is_slave_proc = MPI.COMM_WORLD.rank == slave_proc 

311 

312 block_size = V.dofmap.index_map_bs 

313 imap = V.dofmap.index_map 

314 # Output structures 

315 slaves, masters, coeffs, owners, offsets = [], [], [], [], [] 

316 # Information required to handle vector as input 

317 zero_indices, slave_index = None, None 

318 if vector is not None: 

319 zero_indices = np.argwhere(np.isclose(vector, 0)).T[0] 

320 slave_index = np.argmax(np.abs(vector)) 

321 if is_slave_proc: 

322 assert len(slave_block) == 1 

323 slave_block_g = imap.local_to_global(np.asarray(slave_block, dtype=np.int32))[0] 

324 if vector is None: 

325 slaves = np.arange( 

326 slave_block[0] * block_size, 

327 slave_block[0] * block_size + block_size, 

328 dtype=np.int32, 

329 ) 

330 else: 

331 assert len(vector) == block_size 

332 # Check for input vector (Should be of same length as number of slaves) 

333 # All entries should not be zero 

334 assert not np.isin(slave_index, zero_indices) 

335 # Check vector for zero contributions 

336 slaves = np.array([slave_block[0] * block_size + slave_index], dtype=np.int32) 

337 for i in range(block_size): 

338 if i != slave_index and not np.isin(i, zero_indices): 

339 masters.append(slave_block_g * block_size + i) 

340 owners.append(slave_proc) 

341 coeffs.append(-vector[i] / vector[slave_index]) 

342 

343 global_masters = None 

344 if is_master_proc: 

345 assert len(master_block) == 1 

346 master_block_g = imap.local_to_global(np.asarray(master_block, dtype=np.int32))[0] 

347 masters_as_glob = np.arange( 

348 master_block_g * block_size, master_block_g * block_size + block_size, dtype=np.int64 

349 ) 

350 else: 

351 masters_as_glob = np.array([], dtype=np.int64) 

352 

353 ghost_processors = [] 

354 shared_indices = dolfinx_mpc.cpp.mpc.compute_shared_indices(V._cpp_object) 

355 

356 if is_master_proc and is_slave_proc: 

357 # If slaves and masters are on the same processor finalize local work 

358 if vector is None: 

359 masters = masters_as_glob 

360 owners = np.full(len(masters), master_proc, dtype=np.int32) 

361 coeffs = np.ones(len(masters), dtype=_dt) 

362 offsets = np.arange(0, len(masters) + 1, dtype=np.int32) 

363 else: 

364 for i in range(len(masters_as_glob)): 

365 if not np.isin(i, zero_indices): 

366 masters.append(masters_as_glob[i]) 

367 owners.append(master_proc) 

368 coeffs.append(vector[i] / vector[slave_index]) 

369 offsets = [0, len(masters)] 

370 else: 

371 # Send/Recv masters from other processor 

372 if is_master_proc: 

373 MPI.COMM_WORLD.send(masters_as_glob, dest=slave_proc, tag=10) 

374 if is_slave_proc: 

375 global_masters = MPI.COMM_WORLD.recv(source=master_proc, tag=10) 

376 for i, master in enumerate(global_masters): 

377 if not np.isin(i, zero_indices): 

378 masters.append(master) 

379 owners.append(master_proc) 

380 if vector is None: 

381 coeffs.append(1) 

382 else: 

383 coeffs.append(vector[i] / vector[slave_index]) 

384 if vector is None: 

385 offsets = np.arange(0, len(slaves) + 1, dtype=np.int32) 

386 else: 

387 offsets = np.array([0, len(masters)], dtype=np.int32) 

388 ghost_processors = shared_indices.links(slave_block[0]) 

389 

390 # Broadcast processors containg slave 

391 ghost_processors = MPI.COMM_WORLD.bcast(ghost_processors, root=slave_proc) 

392 if is_slave_proc: 

393 for proc in ghost_processors: 

394 MPI.COMM_WORLD.send(slave_block_g * block_size + slaves % block_size, dest=proc, tag=20 + proc) 

395 MPI.COMM_WORLD.send(coeffs, dest=proc, tag=30 + proc) 

396 MPI.COMM_WORLD.send(owners, dest=proc, tag=40 + proc) 

397 MPI.COMM_WORLD.send(masters, dest=proc, tag=50 + proc) 

398 MPI.COMM_WORLD.send(offsets, dest=proc, tag=60 + proc) 

399 

400 # Receive data for ghost slaves 

401 ghost_slaves, ghost_masters, ghost_coeffs, ghost_owners, ghost_offsets = [], [], [], [], [] 

402 if np.isin(MPI.COMM_WORLD.rank, ghost_processors): 

403 # Convert recieved slaves to the corresponding ghost index 

404 recv_slaves = MPI.COMM_WORLD.recv(source=slave_proc, tag=20 + MPI.COMM_WORLD.rank) 

405 ghost_coeffs = MPI.COMM_WORLD.recv(source=slave_proc, tag=30 + MPI.COMM_WORLD.rank) 

406 ghost_owners = MPI.COMM_WORLD.recv(source=slave_proc, tag=40 + MPI.COMM_WORLD.rank) 

407 ghost_masters = MPI.COMM_WORLD.recv(source=slave_proc, tag=50 + MPI.COMM_WORLD.rank) 

408 ghost_offsets = MPI.COMM_WORLD.recv(source=slave_proc, tag=60 + MPI.COMM_WORLD.rank) 

409 

410 # Unroll ghost blocks 

411 ghosts = imap.ghosts 

412 ghost_dofs = [g * block_size + i for g in ghosts for i in range(block_size)] 

413 

414 ghost_slaves = np.zeros(len(recv_slaves), dtype=np.int32) 

415 local_size = imap.size_local 

416 for i, slave in enumerate(recv_slaves): 

417 idx = np.argwhere(ghost_dofs == slave)[0, 0] 

418 ghost_slaves[i] = local_size * block_size + idx 

419 slaves = np.asarray(np.append(slaves, ghost_slaves), dtype=np.int32) 

420 masters = np.asarray(np.append(masters, ghost_masters), dtype=np.int64) 

421 coeffs = np.asarray(np.append(coeffs, ghost_coeffs), dtype=_dt) 

422 owners = np.asarray(np.append(owners, ghost_owners), dtype=np.int32) 

423 offsets = np.asarray(np.append(offsets, ghost_offsets), dtype=np.int32) 

424 return slaves, masters, coeffs, owners, offsets 

425 

426 

427def create_normal_approximation(V: _fem.FunctionSpace, mt: _cpp.mesh.MeshTags_int32, value: int): 

428 """ 

429 Creates a normal approximation for the dofs in the closure of the attached entities. 

430 Where a dof is attached to multiple entities, an average is computed. 

431 

432 Args: 

433 V: The function space 

434 mt: The meshtag containing the indices 

435 value: Value for the entities in the mesh tag to compute normal on 

436 

437 Returns: 

438 nh: The normal vector 

439 """ 

440 nh = _fem.Function(V) 

441 n_cpp = dolfinx_mpc.cpp.mpc.create_normal_approximation(V._cpp_object, mt.dim, mt.find(value)) 

442 nh._cpp_object = n_cpp 

443 return nh