Coverage for python/src/dolfinx_mpc/multipointconstraint.py: 91%

372 statements  

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

1# Copyright (C) 2020-2023 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 typing import Callable, Dict, List, Optional, Sequence, Tuple, Union 

9 

10from petsc4py import PETSc as _PETSc 

11 

12import dolfinx.cpp as _cpp 

13import dolfinx.fem as _fem 

14import dolfinx.mesh as _mesh 

15import numpy 

16import numpy.typing as npt 

17import ufl 

18 

19import dolfinx_mpc.cpp 

20 

21from .container import ( 

22 _UNSET, 

23 MPCData, 

24 _cpp_function, 

25 _deprecated, 

26 _float_array_types, 

27 _float_classes, 

28 _mpc_classes, 

29 _mpc_data_classes, 

30 _scalar_type, 

31 _tolerance, 

32 _Unset, 

33) 

34from .dictcondition import create_dictionary_constraint 

35from .integralcondition import create_integral_constraint 

36from .rbe import create_rbe2, create_rbe3 

37 

38 

39class MultiPointConstraint: 

40 """ 

41 Hold data for multi point constraint relation ships, 

42 including new index maps for local assembly of matrices and vectors. 

43 

44 The constraint is affine, :math:`x = K x_{red} + g`, where :math:`g` is 

45 supplied through `rhs_coeffs` and through the Dirichlet conditions in 

46 `bcs`. With neither, :math:`g=0` and the constraint is the usual linear 

47 one. 

48 

49 Args: 

50 V: The function space 

51 dtype: The scalar type of the coefficients, used by everything built from this 

52 constraint. Defaults to the default scalar type of DOLFINx, real or complex, at the 

53 precision of the mesh of `V`. Its precision must be that of the mesh. 

54 bcs: Dirichlet boundary conditions for the problem. A master degree of 

55 freedom that is constrained by one of these is removed from the 

56 equation of its slave, and its contribution folded into the 

57 constraint offset :math:`g`. As the offset is recomputed from the 

58 current values of the conditions by :func:`update_constants`, time 

59 dependent boundary data is supported. 

60 rhs_coeffs: Function holding an additional inhomogeneity :math:`g_s` 

61 for the slave degrees of freedom, i.e. 

62 :math:`u_s = \\sum_j c_j u_{m_j} + g_s`. 

63 """ 

64 

65 _slaves: npt.NDArray[numpy.int32] 

66 _masters: npt.NDArray[numpy.int64] 

67 _coeffs: _float_array_types 

68 _owners: npt.NDArray[numpy.int32] 

69 _offsets: npt.NDArray[numpy.int32] 

70 _master_spaces: List[tuple[npt.NDArray[numpy.int32], Optional[_fem.FunctionSpace]]] 

71 _bcs: List[_fem.DirichletBC] 

72 _rhs_coeffs: Optional[_fem.Function] 

73 _scale_function: Optional[_fem.Function] 

74 _rbe2: List[list] 

75 _rbe3: List[tuple] 

76 _rbe3_data: Optional[tuple] 

77 V: _fem.FunctionSpace 

78 _input_space: _fem.FunctionSpace 

79 finalized: bool 

80 _cpp_object: _mpc_classes 

81 _dtype: npt.DTypeLike 

82 __slots__ = tuple(__annotations__) 

83 

84 def __init__( 

85 self, 

86 V: _fem.FunctionSpace, 

87 dtype: npt.DTypeLike | None = None, 

88 bcs: Optional[List[_fem.DirichletBC]] = None, 

89 rhs_coeffs: Optional[_fem.Function] = None, 

90 ): 

91 dtype = _scalar_type(V.mesh.geometry.x.dtype, dtype) 

92 self._slaves = numpy.array([], dtype=numpy.int32) 

93 self._masters = numpy.array([], dtype=numpy.int64) 

94 self._coeffs = numpy.array([], dtype=dtype) # type: ignore 

95 self._owners = numpy.array([], dtype=numpy.int32) 

96 self._offsets = numpy.array([0], dtype=numpy.int32) 

97 self._master_spaces = [] 

98 self._bcs = [] if bcs is None else list(bcs) 

99 if rhs_coeffs is not None: 

100 if not rhs_coeffs.x.array.dtype == dtype: 

101 raise ValueError("rhs_coeffs must have the same dtype as the MPC") 

102 if rhs_coeffs.function_space != V: 

103 raise ValueError("rhs_coeffs must be a Function in the space of the constraint") 

104 self._rhs_coeffs = rhs_coeffs 

105 self._scale_function = None 

106 # Per space on a spider mesh: [W, the tied space, the block of W], the block set by finalize 

107 self._rbe2 = [] 

108 # Feet of RBE3 constraints on this space, (V, dofs, spiders, weights) per call, built by 

109 # finalize, which keeps the arrays for update_rbe3 

110 self._rbe3 = [] 

111 self._rbe3_data = None 

112 self.V = V 

113 # Kept after finalize replaces `V` by the extended space, which contains no Dirichlet condition 

114 self._input_space = V 

115 self.finalized = False 

116 self._dtype = dtype 

117 

118 def add_constraint( 

119 self, 

120 V: _fem.FunctionSpace, 

121 slaves: npt.NDArray[numpy.int32], 

122 masters: npt.NDArray[numpy.int64], 

123 coeffs: _float_array_types, 

124 owners: npt.NDArray[numpy.int32], 

125 offsets: npt.NDArray[numpy.int32], 

126 master_space: Optional[_fem.FunctionSpace] = None, 

127 master_blocks: Optional[npt.NDArray[numpy.int32]] = None, 

128 ): 

129 """ 

130 Add new constraint given by numpy arrays. 

131 

132 Args: 

133 V: The function space for the constraint 

134 slaves: List of all slave dofs (using local dof numbering) on this process 

135 masters: List of all master dofs (using global dof numbering) on this process 

136 coeffs: The coefficients corresponding to each master. 

137 owners: The process each master is owned by. 

138 offsets: Array indicating the location in the masters array for the i-th slave 

139 in the slaves arrays, i.e. 

140 

141 .. highlight:: python 

142 .. code-block:: python 

143 

144 masters_of_owned_slave[i] = masters[offsets[i]:offsets[i+1]] 

145 

146 master_space: The function space all masters belong to, if not `V`. It must be the 

147 space of another constraint finalized together with this one by 

148 :func:`finalize_multipointconstraints`, or a subspace of it, and `masters` is in 

149 the global numbering of that constraint's space. 

150 The masters of a slave may then be in another block of a blocked problem. 

151 master_blocks: The block of each master, for masters from several spaces: its position 

152 in the list of constraints given to :func:`finalize_multipointconstraints`. Each 

153 master is in the global numbering of its block. Exclusive with `master_space`. 

154 

155 Note: 

156 Collective when `master_space` or `master_blocks` is given: every process must call 

157 it with the same `master_space`, or with `master_blocks` (possibly empty). 

158 """ 

159 assert V == self.V 

160 self._raise_if_finalized() 

161 if master_space is not None and master_blocks is not None: 

162 raise ValueError("Give either master_space or master_blocks, not both") 

163 if master_blocks is not None and len(master_blocks) != len(masters): 

164 raise ValueError("master_blocks must have one entry per master") 

165 

166 # Recorded on every process, also without local slaves, so that every process resolves the 

167 # blocks of the masters the same way when the constraints are finalized 

168 if master_blocks is not None: 

169 self._master_spaces.append((numpy.asarray(master_blocks, dtype=numpy.int32), None)) 

170 elif master_space is not None: 

171 self._master_spaces.append((numpy.full(len(masters), -1, dtype=numpy.int32), master_space)) 

172 else: 

173 self._master_spaces.append((numpy.full(len(masters), -2, dtype=numpy.int32), None)) 

174 if len(slaves) > 0: 

175 self._offsets = numpy.append(self._offsets, offsets[1:] + len(self._masters)) 

176 self._slaves = numpy.append(self._slaves, slaves) 

177 self._masters = numpy.append(self._masters, masters) 

178 self._coeffs = numpy.array(numpy.append(self._coeffs, coeffs), dtype=self._dtype) 

179 self._owners = numpy.append(self._owners, owners) 

180 

181 def add_integral_constraint( 

182 self, 

183 weight_form, 

184 value, 

185 bcs: Optional[List[_fem.DirichletBC]] = None, 

186 rtol: numpy.floating | float | None = None, 

187 *, 

188 coefficient_tol: Optional[float] = None, 

189 ): 

190 r"""Constrain a scalar integral of the solution, :math:`L(u) = \gamma`. 

191 

192 The functional is given as a linear form, and turned into a constraint 

193 with a single slave by :func:`dolfinx_mpc.create_integral_constraint`; 

194 see there for the derivation and the cost. The inhomogeneity 

195 :math:`\gamma/w_s` is written into the ``rhs_coeffs`` function of this 

196 constraint, which is created here if none was supplied to the 

197 constructor. 

198 

199 Args: 

200 weight_form: A linear form in ``ufl.TestFunction(V)`` defining the 

201 functional, for instance ``v * ufl.dx``. Its test function must 

202 be in the function space of this constraint. 

203 value: The prescribed value :math:`\gamma` of the functional. 

204 bcs: Dirichlet conditions on the space. A constrained degree of 

205 freedom is never chosen as the slave. Pass the same conditions 

206 to the constructor to have a constrained *master* folded into 

207 the constraint offset. Defaults to the conditions given to the 

208 constructor. 

209 rtol: Deprecated, use `coefficient_tol`. 

210 coefficient_tol: A master whose coefficient is below 

211 `coefficient_tol` times the largest one is dropped. Defaults to 

212 `500` machine epsilon of the real type of the constraint. 

213 

214 Note: 

215 Collective. Must be called by every process. 

216 """ 

217 self._raise_if_finalized() 

218 if rtol is not None: 

219 _deprecated("rtol", "`coefficient_tol`") 

220 coefficient_tol = rtol if coefficient_tol is None else coefficient_tol 

221 slaves, masters, coeffs, owners, offsets, rhs = create_integral_constraint( 

222 self.V, 

223 weight_form, 

224 value, 

225 self._bcs if bcs is None else bcs, 

226 coefficient_tol=_tolerance(coefficient_tol, self._dtype), 

227 ) 

228 if self._rhs_coeffs is None: 

229 self._rhs_coeffs = rhs 

230 else: 

231 # Slaves of separate constraints are disjoint, so the offsets add 

232 self._rhs_coeffs.x.array[:] += rhs.x.array 

233 self.add_constraint(self.V, slaves, masters, coeffs, owners, offsets) 

234 

235 def add_constraint_from_mpc_data( 

236 self, 

237 V: _fem.FunctionSpace, 

238 mpc_data: Union[_mpc_data_classes, MPCData], 

239 master_space: Optional[_fem.FunctionSpace] = None, 

240 ): 

241 """ 

242 Add new constraint given by an `dolfinc_mpc.cpp.mpc.mpc_data`-object. See 

243 :meth:`add_constraint` for `master_space`. 

244 """ 

245 self._raise_if_finalized() 

246 self.add_constraint( 

247 V, 

248 mpc_data.slaves, 

249 mpc_data.masters, 

250 mpc_data.coeffs, 

251 mpc_data.owners, 

252 mpc_data.offsets, 

253 master_space=master_space, 

254 ) 

255 

256 def finalize(self, filter: Optional[numpy.floating] = None) -> None: 

257 """ 

258 Finializes the multi point constraint. After this function is called, no new constraints can be added 

259 to the constraint. This function creates a map from the cells (local to index) to the slave degrees of 

260 freedom and builds a new index map and function space where unghosted master dofs are added as ghosts. 

261 

262 Args: 

263 filter: If given, discard every master whose coefficient satisfies 

264 :math:`|c_{sj}| < \\mathrm{filter}\\cdot\\max_k|c_{sk}|`, the 

265 maximum being over the masters of that same slave. A negligible 

266 coefficient contributes nothing to the constraint, but still 

267 costs a ghost, a row of the sparsity pattern and an entry in 

268 every element matrix modification, so removing them can shrink 

269 :math:`K^HAK` substantially. With `None` (the default) every 

270 master supplied is kept. 

271 

272 Note: 

273 Filtering changes the constraint that is enforced, by exactly the 

274 terms that are dropped. It is local and adds no communication. 

275 

276 Note: 

277 To finalize the constraints of several function spaces, for instance the blocks of a 

278 :class:`ufl.MixedFunctionSpace`, use :func:`finalize_multipointconstraints`. 

279 """ 

280 finalize_multipointconstraints([self], filter) 

281 

282 def update_constants(self) -> None: 

283 """ 

284 Recompute the constraint offset :math:`g` from the current values of the Dirichlet 

285 conditions supplied to the constructor. 

286 

287 Call this whenever the value of one of those conditions changes, for instance between 

288 time steps, before re-assembling. :class:`LinearProblem` calls it automatically. 

289 

290 Note: 

291 Collective. Must be called by every process. 

292 """ 

293 self._raise_if_not_finalized() 

294 if self._rhs_coeffs is not None: 

295 # Pass the array natively. Zero-copy, zero-allocation. 

296 num_dofs_local = self.V.dofmap.index_map_bs * ( 

297 self.V.dofmap.index_map.size_local + self.V.dofmap.index_map.num_ghosts 

298 ) 

299 rhs_coeffs = self._rhs_coeffs.x.array[:num_dofs_local] 

300 self._cpp_object.set_rhs_coeffs(rhs_coeffs) 

301 

302 self._cpp_object.update_constants() 

303 

304 @property 

305 def constants(self) -> _float_array_types: 

306 """ 

307 The constraint offset :math:`g` for each degree of freedom local to the process, 

308 i.e. the affine term in :math:`x = K x_{red} + g`. 

309 """ 

310 self._raise_if_not_finalized() 

311 return self._cpp_object.constants 

312 

313 @property 

314 def has_inhomogeneity(self) -> bool: 

315 """ 

316 Whether any process carries a non-zero constraint offset. The value is globally 

317 reduced, so it is identical on every process. 

318 """ 

319 self._raise_if_not_finalized() 

320 return self._cpp_object.has_inhomogeneity 

321 

322 @property 

323 def master_blocks(self) -> npt.NDArray[numpy.int32]: 

324 """ 

325 The block of each master, parallel to ``masters.array``: the position, in the list given 

326 to :func:`finalize_multipointconstraints`, of the constraint whose space the master is in. 

327 The local index of a master is in the space of its block. 

328 """ 

329 self._raise_if_not_finalized() 

330 return self._cpp_object.master_blocks 

331 

332 @property 

333 def dtype(self) -> type: 

334 """The scalar type of the coefficients, also that of everything built from the constraint.""" 

335 return self._dtype 

336 

337 @property 

338 def has_cross_block_masters(self) -> bool: 

339 """Whether a master on any process is in another block than the slaves.""" 

340 self._raise_if_not_finalized() 

341 return self._cpp_object.has_cross_block_masters 

342 

343 def create_periodic_constraint_topological( 

344 self, 

345 V: _fem.FunctionSpace, 

346 meshtag: _mesh.MeshTags, 

347 tag: int, 

348 relation: Callable[[numpy.ndarray], numpy.ndarray], 

349 bcs: List[_fem.DirichletBC], 

350 scale: Union[_float_classes, float, complex] = 1.0, 

351 tol: Union[_float_classes, float, None, _Unset] = _UNSET, 

352 num_threads: Optional[int] = 1, 

353 *, 

354 distance_tol: Optional[float] = None, 

355 coefficient_tol: Optional[float] = None, 

356 ): 

357 """ 

358 Create periodic condition for all closure dofs of on all entities in `meshtag` with value `tag`. 

359 :math:`u(x_i) = scale * u(relation(x_i))` for all of :math:`x_i` on marked entities. 

360 

361 Args: 

362 V: The function space to assign the condition to. Should either be the space of the MPC or a sub space. 

363 meshtag: MeshTag for entity to apply the periodic condition on 

364 tag: Tag indicating which entities should be slaves 

365 relation: Lambda-function describing the geometrical relation 

366 bcs: Dirichlet boundary conditions for the problem (Periodic constraints will be ignored for these dofs) 

367 scale: Factor of the masters, of the scalar type of the constraint 

368 tol: Deprecated, use `distance_tol` and `coefficient_tol`: a value sets both, `None` sets 

369 `coefficient_tol=0`. 

370 num_threads: The number of threads to use for certain operations 

371 distance_tol: The largest distance from a mapped slave point to a master cell for the point to 

372 be in the cell, and the padding of the bounding boxes of the cells. Defaults to `500` 

373 machine epsilon of the coordinate type of the mesh. 

374 coefficient_tol: A master whose coefficient is below `coefficient_tol` times the largest of 

375 its slave is dropped. `0` keeps every master, so that the coefficients can later be changed 

376 with :func:`scale_coefficients` or :func:`update_coefficients`. Defaults to `500` 

377 machine epsilon of the real type of the constraint. 

378 """ 

379 bcs_ = [bc._cpp_object for bc in bcs] 

380 if isinstance(scale, numpy.generic): # nanobind conversion of numpy dtypes to general Python types 

381 scale = scale.item() # type: ignore 

382 distance_tol, coefficient_tol = self._tolerances(distance_tol, coefficient_tol, tol=tol) 

383 is_input_space = V is self.V 

384 if not (is_input_space or self.V.contains(V)): 

385 raise RuntimeError("The input space has to be a sub space (or the full space) of the MPC") 

386 mpc_data = _cpp_function("create_periodic_constraint_topological", self._dtype)( 

387 V._cpp_object, 

388 meshtag._cpp_object, 

389 tag, 

390 relation, 

391 bcs_, 

392 scale, 

393 not is_input_space, 

394 distance_tol, 

395 coefficient_tol, 

396 num_threads, 

397 ) 

398 self.add_constraint_from_mpc_data(self.V, mpc_data=mpc_data) 

399 

400 def create_periodic_constraint_geometrical( 

401 self, 

402 V: _fem.FunctionSpace, 

403 indicator: Callable[[numpy.ndarray], numpy.ndarray], 

404 relation: Callable[[numpy.ndarray], numpy.ndarray], 

405 bcs: List[_fem.DirichletBC], 

406 scale: Union[_float_classes, float, complex] = 1.0, 

407 tol: Union[_float_classes, float, None, _Unset] = _UNSET, 

408 num_threads: Optional[int] = 1, 

409 *, 

410 distance_tol: Optional[float] = None, 

411 coefficient_tol: Optional[float] = None, 

412 ): 

413 """ 

414 Create a periodic condition for all degrees of freedom whose physical location satisfies 

415 :math:`indicator(x_i)==True`, i.e. 

416 :math:`u(x_i) = scale * u(relation(x_i))` for all :math:`x_i` 

417 

418 Args: 

419 V: The function space to assign the condition to. Should either be the space of the MPC or a sub space. 

420 indicator: Lambda-function to locate degrees of freedom that should be slaves 

421 relation: Lambda-function describing the geometrical relation to master dofs 

422 bcs: Dirichlet boundary conditions for the problem 

423 (Periodic constraints will be ignored for these dofs) 

424 scale: Factor of the masters, of the scalar type of the constraint 

425 tol: Deprecated, use `distance_tol` and `coefficient_tol`: a value sets both, `None` sets 

426 `coefficient_tol=0`. 

427 num_threads: The number of threads to use for certain operations. 

428 distance_tol: The largest distance from a mapped slave point to a master cell for the point to 

429 be in the cell, and the padding of the bounding boxes of the cells. Defaults to `500` 

430 machine epsilon of the coordinate type of the mesh. 

431 coefficient_tol: A master whose coefficient is below `coefficient_tol` times the largest of 

432 its slave is dropped. `0` keeps every master, so that the coefficients can later be changed 

433 with :func:`scale_coefficients` or :func:`update_coefficients`. Defaults to `500` 

434 machine epsilon of the real type of the constraint. 

435 """ 

436 if isinstance(scale, numpy.generic): # nanobind conversion of numpy dtypes to general Python types 

437 scale = scale.item() # type: ignore 

438 distance_tol, coefficient_tol = self._tolerances(distance_tol, coefficient_tol, tol=tol) 

439 bcs = [] if bcs is None else [bc._cpp_object for bc in bcs] 

440 is_input_space = V is self.V 

441 if not (is_input_space or self.V.contains(V)): 

442 raise RuntimeError("The input space has to be a sub space (or the full space) of the MPC") 

443 mpc_data = _cpp_function("create_periodic_constraint_geometrical", self._dtype)( 

444 V._cpp_object, 

445 indicator, 

446 relation, 

447 bcs, 

448 scale, 

449 not is_input_space, 

450 distance_tol, 

451 coefficient_tol, 

452 num_threads, 

453 ) 

454 self.add_constraint_from_mpc_data(self.V, mpc_data=mpc_data) 

455 

456 def create_submesh_constraint( 

457 self, 

458 V: _fem.FunctionSpace, 

459 master_space: _fem.FunctionSpace, 

460 entity_map: _mesh.EntityMap, 

461 bcs: Optional[List[_fem.DirichletBC]] = None, 

462 scale: Union[_float_classes, float, complex] = 1.0, 

463 tol: Union[_float_classes, float, None, _Unset] = _UNSET, 

464 num_threads: int = 1, 

465 *, 

466 coefficient_tol: Optional[float] = None, 

467 ): 

468 r""" 

469 Tie the degrees of freedom of `V` to `master_space` on a related mesh: a submesh and its 

470 parent, related by `entity_map` as returned by :func:`dolfinx.mesh.create_submesh`. 

471 

472 Every degree of freedom of `V` in the closure of a cell related to a cell of 

473 `master_space` becomes a slave, :math:`u(x_i) = \mathrm{scale}\, u_m(x_i)`, with 

474 :math:`u_m` evaluated in the related cell and component `b` tied to component `b`. With `V` 

475 on the submesh this is every degree of freedom of `V`, for instance the trace 

476 :math:`\bar u = u|_\Gamma` on a submesh of facets; with `V` on the parent it is the 

477 degrees of freedom on the submesh. No search is involved: the related cell is a table 

478 lookup. For a submesh of facets the parent cell is one attached to the facet, so for a 

479 discontinuous `master_space` the side is arbitrary. 

480 

481 Args: 

482 V: The space of the constraint, or a subspace of it 

483 master_space: The space of another constraint finalized together with this one by 

484 :func:`finalize_multipointconstraints`, or a subspace of it. Its mesh and the mesh 

485 of `V` are the two meshes of `entity_map`, either way round. 

486 entity_map: Relates the cells of the submesh to entities of the parent, of 

487 codimension 0 or 1 

488 bcs: Dirichlet conditions on the space of the constraint. Their degrees of freedom 

489 are not made slaves. 

490 scale: Factor of the masters, of the scalar type of the constraint 

491 tol: Deprecated, use `coefficient_tol`: a value sets it, `None` sets `coefficient_tol=0`. 

492 num_threads: The number of threads to use 

493 coefficient_tol: A master whose coefficient is below `coefficient_tol` times the largest of 

494 its slave is dropped. `0` keeps every basis function of the related cell, so that the 

495 coefficients can later be changed with :func:`scale_coefficients` or 

496 :func:`update_coefficients`. Defaults to `500` machine epsilon of the real type of the 

497 constraint. No distance tolerance is needed, as the related cell is not searched for. 

498 

499 Raises: 

500 ValueError: If `entity_map` does not relate the two meshes, relates entities other 

501 than the cells of the submesh, is of codimension above 1, or the spaces have 

502 different numbers of components. Raised on every process. 

503 

504 Note: 

505 Collective. 

506 """ 

507 self._raise_if_finalized() 

508 if not (V is self.V or self.V.contains(V)): 

509 raise ValueError("V must be the space of the constraint or a subspace of it") 

510 if isinstance(scale, numpy.generic): # nanobind conversion of numpy dtypes to general Python types 

511 scale = scale.item() # type: ignore 

512 if not isinstance(tol, _Unset): 

513 _deprecated("tol", "`coefficient_tol`") 

514 if coefficient_tol is None: 

515 coefficient_tol = 0.0 if tol is None else tol 

516 bcs_ = [] if bcs is None else [bc._cpp_object for bc in bcs] 

517 mpc_data = _cpp_function("create_submesh_constraint", self._dtype)( 

518 V._cpp_object, 

519 master_space._cpp_object, 

520 entity_map._cpp_object, 

521 bcs_, 

522 scale, 

523 _tolerance(coefficient_tol, self._dtype), 

524 num_threads, 

525 ) 

526 self.add_constraint_from_mpc_data(self.V, mpc_data=mpc_data, master_space=master_space) 

527 

528 def _add_rbe2(self, dofs: list[npt.NDArray[numpy.int32]], W: _fem.FunctionSpace, x=None): 

529 """Tie `dofs[k]` to spider `k` of `W`, and record `W` for :meth:`update_rbe2`.""" 

530 spiders = [numpy.full(len(d), k, dtype=numpy.int64) for k, d in enumerate(dofs)] 

531 mpc_data = create_rbe2( 

532 self.V, 

533 numpy.concatenate(dofs) if dofs else numpy.zeros(0, dtype=numpy.int32), 

534 numpy.concatenate(spiders) if spiders else numpy.zeros(0, dtype=numpy.int64), 

535 W, 

536 self._dtype, 

537 x, 

538 ) 

539 self.add_constraint_from_mpc_data(self.V, mpc_data=mpc_data, master_space=W) 

540 if not any(W is entry[0] for entry in self._rbe2): 

541 self._rbe2.append([W, self.V, None]) 

542 

543 def add_rbe2_topological( 

544 self, 

545 dim: int, 

546 entities: Union[npt.NDArray[numpy.int32], Sequence[Optional[npt.NDArray[numpy.int32]]]], 

547 W: _fem.FunctionSpace, 

548 ): 

549 r""" 

550 Tie the dofs on mesh entities rigidly to a point, as the RBE2 element of other codes 

551 (a rigid "spider"). 

552 

553 Each dof of this constraint's space on the entities is a "foot" of a spider whose "body" 

554 is a point of the point mesh of `W`. Every component of a foot follows the motion of its 

555 body, 

556 

557 .. math:: 

558 

559 u(x) = t + \theta \times (x - x_c), 

560 

561 where :math:`x_c` is the coordinate of the dofs of `W` at the point, :math:`t` its 

562 translation and :math:`\theta` its rotation, the dofs of `W` at the point. Without 

563 rotations, :math:`u(x) = t`. All rotation terms are kept, also where their coefficient is 

564 zero, so :meth:`update_rbe2` can follow the motion of the meshes. 

565 

566 Args: 

567 dim: Topological dimension of the entities 

568 entities: Entities (local to the process) whose dofs are tied to spider 0, or a 

569 sequence whose entry `k` holds the entities tied to the spider with input index 

570 `k` (see :func:`dolfinx_mpc.create_spider_mesh`). An entry may be `None`. 

571 W: Space on the spider mesh (:func:`dolfinx_mpc.create_spider_mesh`). Its value size 

572 is the geometric dimension, for translations only, or 6 in 3D and 3 in 2D, for 

573 translations and rotations. `W` must be the space of another constraint 

574 finalized together with this one by :func:`finalize_multipointconstraints`. 

575 

576 Note: 

577 Collective. Must be called by every process, with the same number of entries in 

578 `entities`. 

579 """ 

580 self._raise_if_finalized() 

581 per_spider = [entities] if isinstance(entities, numpy.ndarray) else list(entities) 

582 dofs = [] 

583 # The dofs of each spider in turn, collectively, so that a dof on the entities of two 

584 # spiders is caught as constrained twice 

585 for e in per_spider: 

586 e = numpy.zeros(0, dtype=numpy.int32) if e is None else numpy.asarray(e, dtype=numpy.int32) 

587 dofs.append(_fem.locate_dofs_topological(self.V, dim, e)) 

588 self._add_rbe2(dofs, W) 

589 

590 def add_rbe2_geometrical( 

591 self, 

592 locators: Union[ 

593 Callable[[numpy.ndarray], numpy.ndarray], Sequence[Optional[Callable[[numpy.ndarray], numpy.ndarray]]] 

594 ], 

595 W: _fem.FunctionSpace, 

596 ): 

597 r""" 

598 Tie the dofs located geometrically rigidly to a point, as the RBE2 element of other codes 

599 (a rigid "spider"). See :meth:`add_rbe2_topological` for the relation. 

600 

601 Args: 

602 locators: Marks the dofs tied to spider 0, given their coordinates, shape 

603 `(3, num_points)`, or a sequence whose entry `k` marks the dofs tied to the spider 

604 with input index `k`. An entry may be `None`. 

605 W: Space on the spider mesh, see :meth:`add_rbe2_topological` 

606 

607 Note: 

608 Collective. Must be called by every process, with the same number of locators. 

609 """ 

610 self._raise_if_finalized() 

611 per_spider = [locators] if callable(locators) else list(locators) 

612 dofs = [ 

613 numpy.zeros(0, dtype=numpy.int32) 

614 if locator is None 

615 else numpy.asarray(_fem.locate_dofs_geometrical(self.V, locator), dtype=numpy.int32) 

616 for locator in per_spider 

617 ] 

618 self._add_rbe2(dofs, W, self.V.tabulate_dof_coordinates()) 

619 

620 def update_rbe2(self) -> None: 

621 """ 

622 Recompute the coefficients of every RBE2 constraint from the current coordinates. 

623 

624 The feet are at the dof coordinates of the constraint's space, the spiders at those of the 

625 space on the spider mesh, both read now. Move the meshes, for instance to the deformed 

626 configuration in an updated Lagrangian analysis, then call this to tie the feet to the 

627 rigid motion about the new positions. Assemble again afterwards. 

628 

629 The constraint must be finalized without a `filter`, which could drop a master whose 

630 coefficient becomes nonzero. 

631 

632 Note: 

633 Collective. Must be called by every process. 

634 """ 

635 self._raise_if_not_finalized() 

636 if len(self._rbe2) == 0: 

637 raise ValueError("The constraint has no RBE2 constraints") 

638 for W, V, block in self._rbe2: 

639 dolfinx_mpc.cpp.mpc.update_rbe2(self._cpp_object, V._cpp_object, W._cpp_object, block) 

640 

641 def _add_rbe3(self, V: _fem.FunctionSpace, dofs: list[npt.NDArray[numpy.int32]], weights, x): 

642 """Record `dofs[k]` as feet of spider `k`, with weights from `weights`.""" 

643 real = V.mesh.geometry.x.dtype 

644 for k, d in enumerate(dofs): 

645 if weights is None: 

646 w = numpy.ones(len(d), dtype=real) 

647 elif callable(weights): 

648 w = numpy.asarray(weights(x[d].T), dtype=real).reshape(-1) 

649 else: 

650 w = numpy.full(len(d), weights, dtype=real) 

651 self._rbe3.append((V, d, numpy.full(len(d), k, dtype=numpy.int64), w)) 

652 

653 def add_rbe3_topological( 

654 self, 

655 V: _fem.FunctionSpace, 

656 dim: int, 

657 entities: Union[npt.NDArray[numpy.int32], Sequence[Optional[npt.NDArray[numpy.int32]]]], 

658 weights: Union[None, float, Callable[[numpy.ndarray], numpy.ndarray]] = None, 

659 ): 

660 r""" 

661 Tie the dofs of spiders to the motion of the dofs of `V` on mesh entities, as the RBE3 

662 element of other codes (a flexible "spider"). 

663 

664 This constraint is on the space of the spider mesh (:func:`dolfinx_mpc.create_spider_mesh`). 

665 Each spider moves with the rigid motion that best fits its "feet", in the weighted 

666 least-squares sense, 

667 

668 .. math:: 

669 

670 \min_{t, \theta} \sum_i w_i |u_i - t - \theta \times (x_i - x_c)|^2, 

671 

672 where :math:`x_c` is the coordinate of the spider, :math:`t` and :math:`\theta` its 

673 translation and rotation, and :math:`u_i` the displacement of foot :math:`i` at 

674 :math:`x_i`. Without rotations, :math:`t` is the weighted mean of the feet. Unlike RBE2, the 

675 feet keep their stiffness: a load on the spider is spread over them without making them 

676 rigid. 

677 

678 The feet may be in several spaces, given by one call each. The constraint is built when 

679 it is finalized, by :func:`finalize_multipointconstraints` together with the constraints of 

680 the spaces of the feet. 

681 

682 Args: 

683 V: The space of the feet, with one component per dimension 

684 dim: Topological dimension of the entities 

685 entities: Entities (local to the process) whose dofs are feet of spider 0, or a 

686 sequence whose entry `k` holds those of the spider with input index `k`. An entry 

687 may be `None`. 

688 weights: The weight of each foot: `None` for one, a number, or a function of the 

689 coordinates, shape `(3, num_points)`, returning one non-negative weight per foot. 

690 Evaluated once, here: :meth:`update_rbe3` keeps the weights. 

691 

692 Note: 

693 Collective. Must be called by every process, with the same number of entries in 

694 `entities`. 

695 """ 

696 self._raise_if_finalized() 

697 per_spider = [entities] if isinstance(entities, numpy.ndarray) else list(entities) 

698 dofs = [] 

699 for e in per_spider: 

700 e = numpy.zeros(0, dtype=numpy.int32) if e is None else numpy.asarray(e, dtype=numpy.int32) 

701 dofs.append(_fem.locate_dofs_topological(V, dim, e)) 

702 self._add_rbe3(V, dofs, weights, V.tabulate_dof_coordinates() if callable(weights) else None) 

703 

704 def add_rbe3_geometrical( 

705 self, 

706 V: _fem.FunctionSpace, 

707 locators: Union[ 

708 Callable[[numpy.ndarray], numpy.ndarray], Sequence[Optional[Callable[[numpy.ndarray], numpy.ndarray]]] 

709 ], 

710 weights: Union[None, float, Callable[[numpy.ndarray], numpy.ndarray]] = None, 

711 ): 

712 """ 

713 Tie the dofs of spiders to the motion of the dofs of `V` located geometrically, as the 

714 RBE3 element of other codes. See :meth:`add_rbe3_topological` for the relation. 

715 

716 Args: 

717 V: The space of the feet, with one component per dimension 

718 locators: Marks the feet of spider 0, given their coordinates, shape 

719 `(3, num_points)`, or a sequence whose entry `k` marks the feet of the spider with 

720 input index `k`. An entry may be `None`. 

721 weights: See :meth:`add_rbe3_topological` 

722 

723 Note: 

724 Collective. Must be called by every process, with the same number of locators. 

725 """ 

726 self._raise_if_finalized() 

727 per_spider = [locators] if callable(locators) else list(locators) 

728 dofs = [ 

729 numpy.zeros(0, dtype=numpy.int32) 

730 if locator is None 

731 else numpy.asarray(_fem.locate_dofs_geometrical(V, locator), dtype=numpy.int32) 

732 for locator in per_spider 

733 ] 

734 self._add_rbe3(V, dofs, weights, V.tabulate_dof_coordinates() if callable(weights) else None) 

735 

736 def _build_rbe3(self, mpcs: Sequence[MultiPointConstraint]) -> None: 

737 """Build the RBE3 constraint from the feet recorded, with the blocks of their spaces.""" 

738 spaces: list[_fem.FunctionSpace] = [] 

739 for V, *_ in self._rbe3: 

740 if not any(V is other for other in spaces): 

741 spaces.append(V) 

742 blocks = [] 

743 for V in spaces: 

744 matches = [j for j, other in enumerate(mpcs) if other.V is V] 

745 if len(matches) != 1: 

746 raise ValueError( 

747 "The feet of an RBE3 constraint must be in the space of exactly one of the " 

748 "constraints finalized together with it" 

749 ) 

750 blocks.append(matches[0]) 

751 

752 def gather(i): 

753 return [numpy.concatenate([r[i] for r in self._rbe3 if r[0] is V]) for V in spaces] 

754 

755 data = (self.V, spaces, gather(1), gather(2), gather(3)) 

756 mpc_data, space = create_rbe3(*data, dtype=self._dtype) 

757 self.add_constraint( 

758 self.V, 

759 mpc_data.slaves, 

760 mpc_data.masters, 

761 mpc_data.coeffs, 

762 mpc_data.owners, 

763 mpc_data.offsets, 

764 master_blocks=numpy.asarray(blocks, dtype=numpy.int32)[space], 

765 ) 

766 self._rbe3_data = (*data, blocks) 

767 

768 def update_rbe3(self) -> None: 

769 """ 

770 Recompute the coefficients of the RBE3 constraint from the current coordinates. 

771 

772 The feet are at the dof coordinates of their spaces, the spiders at those of the space of 

773 this constraint, both read now. Move the meshes, then call this. Assemble again 

774 afterwards. 

775 

776 Note: 

777 Collective. Must be called by every process. 

778 """ 

779 self._raise_if_not_finalized() 

780 if self._rbe3_data is None: 

781 raise ValueError("The constraint has no RBE3 constraint") 

782 W, spaces, dofs, spiders, weights, blocks = self._rbe3_data 

783 dolfinx_mpc.cpp.mpc.update_rbe3( 

784 self._cpp_object, 

785 W._cpp_object, 

786 [V._cpp_object for V in spaces], 

787 blocks, 

788 [numpy.ascontiguousarray(d, dtype=numpy.int32) for d in dofs], 

789 [numpy.ascontiguousarray(k, dtype=numpy.int64) for k in spiders], 

790 [numpy.ascontiguousarray(w) for w in weights], 

791 ) 

792 

793 def create_slip_constraint( 

794 self, 

795 space: _fem.FunctionSpace, 

796 facet_marker: Tuple[_mesh.MeshTags, int], 

797 v: _fem.Function, 

798 bcs: List[_fem.DirichletBC] = [], 

799 ): 

800 """ 

801 Create a slip constraint :math:`u \\cdot v=0` over the entities defined in `facet_marker` with the given index. 

802 

803 Args: 

804 space: Function space (possible sub space) for the current constraint 

805 facet_marker: Tuple containomg the mesh tag and marker used to locate degrees of freedom 

806 v: Function containing the directional vector to dot your slip condition (most commonly a normal vector) 

807 bcs: List of Dirichlet BCs (slip conditions will be ignored on these dofs) 

808 

809 Examples: 

810 Create constaint :math:`u\\cdot n=0` of all indices in `mt` marked with `i` 

811 

812 .. highlight:: python 

813 .. code-block:: python 

814 

815 V = dolfinx.fem.functionspace(mesh, ("CG", 1)) 

816 mpc = MultiPointConstraint(V) 

817 n = dolfinx.fem.Function(V) 

818 mpc.create_slip_constaint(V, (mt, i), n) 

819 

820 Create slip constaint for a mixed function space: 

821 

822 .. highlight:: python 

823 .. code-block:: python 

824 

825 cellname = mesh.basix_cell() 

826 Ve = basix.ufl.element(basix.ElementFamily.P, cellname , 2, shape=(mesh.geometry.dim,)) 

827 Qe = basix.ufl.element(basix.ElementFamily.P, cellname , 1) 

828 me = basix.ufl.mixed_element([Ve, Qe]) 

829 W = dolfinx.fem.functionspace(mesh, me) 

830 mpc = MultiPointConstraint(W) 

831 n_space, _ = W.sub(0).collapse() 

832 normal = dolfinx.fem.Function(n_space) 

833 mpc.create_slip_constraint(W.sub(0), (mt, i), normal, bcs=[]) 

834 

835 A slip condition cannot be applied on the same degrees of freedom as a Dirichlet BC, and therefore 

836 any Dirichlet bc for the space of the multi point constraint should be supplied. 

837 

838 .. highlight:: python 

839 .. code-block:: python 

840 

841 cellname = mesh.basix_cell() 

842 Ve = basix.ufl.element(basix.ElementFamily.P, cellname , 2, shape=(mesh.geometry.dim,)) 

843 Qe = basix.ufl.element(basix.ElementFamily.P, cellname , 1) 

844 me = basix.ufl.mixed_element([Ve, Qe]) 

845 W = dolfinx.fem.functionspace(mesh, me) 

846 mpc = MultiPointConstraint(W) 

847 n_space, _ = W.sub(0).collapse() 

848 normal = Function(n_space) 

849 bc = dolfinx.fem.dirichletbc(inlet_velocity, dofs, W.sub(0)) 

850 mpc.create_slip_constraint(W.sub(0), (mt, i), normal, bcs=[bc]) 

851 """ 

852 bcs = [] if bcs is None else [bc._cpp_object for bc in bcs] 

853 if space is self.V: 

854 sub_space = False 

855 elif self.V.contains(space): 

856 sub_space = True 

857 else: 

858 raise ValueError("Input space has to be a sub space of the MPC space") 

859 mpc_data = _cpp_function("create_slip_condition", self._dtype)( 

860 space._cpp_object, 

861 facet_marker[0]._cpp_object, 

862 facet_marker[1], 

863 v._cpp_object, 

864 bcs, 

865 sub_space, 

866 ) 

867 self.add_constraint_from_mpc_data(self.V, mpc_data=mpc_data) 

868 

869 def create_general_constraint( 

870 self, 

871 slave_master_dict: Dict[bytes, Dict[bytes, float]], 

872 subspace_slave: Optional[int] = None, 

873 subspace_master: Optional[int] = None, 

874 *, 

875 distance_tol: Optional[float] = None, 

876 ): 

877 """ 

878 Args: 

879 V: The function space 

880 slave_master_dict: Nested dictionary, where the first key is the bit representing the slave dof's 

881 coordinate in the mesh. The item of this key is a dictionary, where each key of this dictionary 

882 is the bit representation of the master dof's coordinate, and the item the coefficient for 

883 the MPC equation. 

884 subspace_slave: If using mixed or vector space, and only want to use dofs from a sub space 

885 as slave add index here 

886 subspace_master: Subspace index for mixed or vector spaces 

887 distance_tol: The largest distance between the coordinate of a key and that of its dof. 

888 Defaults to `500` machine epsilon of the coordinate type of the mesh. 

889 

890 Example: 

891 If the dof `D` located at `[d0, d1]` should be constrained to the dofs 

892 `E` and `F` at `[e0, e1]` and `[f0, f1]` as :math:`D = \\alpha E + \\beta F` 

893 the dictionary should be: 

894 

895 .. highlight:: python 

896 .. code-block:: python 

897 

898 {numpy.array([d0, d1], dtype=mesh.geometry.x.dtype).tobytes(): 

899 {numpy.array([e0, e1], dtype=mesh.geometry.x.dtype).tobytes(): alpha, 

900 numpy.array([f0, f1], dtype=mesh.geometry.x.dtype).tobytes(): beta}} 

901 """ 

902 slaves, masters, coeffs, owners, offsets = create_dictionary_constraint( 

903 self.V, 

904 slave_master_dict, 

905 subspace_slave, 

906 subspace_master, 

907 dtype=self._dtype, 

908 distance_tol=_tolerance(distance_tol, self.V.mesh.geometry.x.dtype), 

909 ) 

910 self.add_constraint(self.V, slaves, masters, coeffs, owners, offsets) 

911 

912 def _tolerances( 

913 self, 

914 distance_tol: Optional[float], 

915 coefficient_tol: Optional[float], 

916 tol: Union[_float_classes, float, None, _Unset] = _UNSET, 

917 eps2: Optional[float] = None, 

918 ) -> tuple[float, float]: 

919 """The distance and coefficient tolerance, by default `500` machine epsilon of the coordinate 

920 type of the mesh and of the real type of the constraint. 

921 

922 The deprecated `tol` of the periodic constraints sets both, or with `None` keeps every 

923 master. The deprecated `eps2` of the contact constraints is a squared distance. 

924 """ 

925 if not isinstance(tol, _Unset): 

926 _deprecated("tol", "`distance_tol` and `coefficient_tol`", stacklevel=4) 

927 if tol is None: 

928 coefficient_tol = 0.0 if coefficient_tol is None else coefficient_tol 

929 else: 

930 distance_tol = tol if distance_tol is None else distance_tol 

931 coefficient_tol = tol if coefficient_tol is None else coefficient_tol 

932 if eps2 is not None: 

933 _deprecated("eps2", "`distance_tol`, a distance rather than a squared distance,", stacklevel=4) 

934 distance_tol = float(numpy.sqrt(eps2)) if distance_tol is None else distance_tol 

935 return ( 

936 _tolerance(distance_tol, self.V.mesh.geometry.x.dtype), 

937 _tolerance(coefficient_tol, self._dtype), 

938 ) 

939 

940 def create_contact_slip_condition( 

941 self, 

942 meshtags: _mesh.MeshTags, 

943 slave_marker: int, 

944 master_marker: int, 

945 normal: _fem.Function, 

946 eps2: Optional[float] = None, 

947 num_threads: Optional[int] = 1, 

948 *, 

949 distance_tol: Optional[float] = None, 

950 coefficient_tol: Optional[float] = None, 

951 ): 

952 """ 

953 Create a slip condition between two sets of facets marker with individual markers. 

954 The interfaces should be within machine precision of eachother, but the vertices does not need to align. 

955 The condition created is :math:`u_s \\cdot normal_s = u_m \\cdot normal_m` where `s` is the 

956 restriction to the slave facets, `m` to the master facets. 

957 

958 Args: 

959 meshtags: The meshtags of the set of facets to tie together 

960 slave_marker: The marker of the slave facets 

961 master_marker: The marker of the master facets 

962 normal: The function used in the dot-product of the constraint 

963 eps2: Deprecated, use `distance_tol`, which is `sqrt(eps2)`. 

964 num_threads: The number of threads to use for certain operations 

965 distance_tol: The largest distance from a slave point to a master cell for the point to 

966 be in the cell, and the padding of the bounding boxes of the cells. Defaults to `500` 

967 machine epsilon of the coordinate type of the mesh. 

968 coefficient_tol: A master whose coefficient is below `coefficient_tol` times the largest of 

969 its slave is dropped. `0` keeps every master. Defaults to `500` 

970 machine epsilon of the real type of the constraint. 

971 """ 

972 mpc_data = _cpp_function("create_contact_slip_condition", self._dtype)( 

973 self.V._cpp_object, 

974 meshtags._cpp_object, 

975 slave_marker, 

976 master_marker, 

977 normal._cpp_object, 

978 *self._tolerances(distance_tol, coefficient_tol, eps2=eps2), 

979 num_threads, 

980 ) 

981 self.add_constraint_from_mpc_data(self.V, mpc_data) 

982 

983 def create_contact_inelastic_condition( 

984 self, 

985 meshtags: _cpp.mesh.MeshTags_int32, 

986 slave_marker: int, 

987 master_marker: int, 

988 eps2: Optional[float] = None, 

989 allow_missing_masters: bool = False, 

990 num_threads: Optional[int] = 1, 

991 *, 

992 distance_tol: Optional[float] = None, 

993 coefficient_tol: Optional[float] = None, 

994 ): 

995 """ 

996 Create a contact inelastic condition between two sets of facets marker with individual markers. 

997 The interfaces should be within machine precision of eachother, but the vertices does not need to align. 

998 The condition created is :math:`u_s = u_m` where `s` is the restriction to the 

999 slave facets, `m` to the master facets. 

1000 

1001 Args: 

1002 meshtags: The meshtags of the set of facets to tie together 

1003 slave_marker: The marker of the slave facets 

1004 master_marker: The marker of the master facets 

1005 eps2: Deprecated, use `distance_tol`, which is `sqrt(eps2)`. 

1006 allow_missing_masters: If true, the function will not throw an error if a degree of freedom 

1007 in the closure of the master entities does not have a corresponding set of slave degree 

1008 of freedom. 

1009 num_threads: The number of threads to use for certain operations 

1010 distance_tol: The largest distance from a slave point to a master cell for the point to 

1011 be in the cell, and the padding of the bounding boxes of the cells. Defaults to `500` 

1012 machine epsilon of the coordinate type of the mesh. 

1013 coefficient_tol: A master whose coefficient is below `coefficient_tol` times the largest of 

1014 its slave is dropped. `0` keeps every master. Defaults to `500` 

1015 machine epsilon of the real type of the constraint. 

1016 """ 

1017 mpc_data = _cpp_function("create_contact_inelastic_condition", self._dtype)( 

1018 self.V._cpp_object, 

1019 meshtags._cpp_object, 

1020 slave_marker, 

1021 master_marker, 

1022 *self._tolerances(distance_tol, coefficient_tol, eps2=eps2), 

1023 allow_missing_masters, 

1024 num_threads, 

1025 ) 

1026 self.add_constraint_from_mpc_data(self.V, mpc_data) 

1027 

1028 @property 

1029 def is_slave(self) -> numpy.ndarray: 

1030 """ 

1031 Returns a vector of integers where the ith entry indicates if a degree of freedom (local to process) is a slave. 

1032 """ 

1033 self._raise_if_not_finalized() 

1034 return self._cpp_object.is_slave 

1035 

1036 @property 

1037 def slaves(self): 

1038 """ 

1039 Returns the degrees of freedom for all slaves local to process 

1040 """ 

1041 self._raise_if_not_finalized() 

1042 return self._cpp_object.slaves 

1043 

1044 @property 

1045 def masters(self) -> _cpp.graph.AdjacencyList_int32: 

1046 """ 

1047 Returns an adjacency-list whose ith node corresponds to 

1048 a degree of freedom (local to process), and links the corresponding master dofs (local to process). 

1049 

1050 Examples: 

1051 

1052 .. highlight:: python 

1053 .. code-block:: python 

1054 

1055 masters = mpc.masters 

1056 masters_of_dof_i = masters.links(i) 

1057 """ 

1058 self._raise_if_not_finalized() 

1059 return self._cpp_object.masters 

1060 

1061 def coefficients(self) -> _float_array_types: 

1062 """ 

1063 Returns a vector containing the coefficients for the constraint, and the corresponding offsets 

1064 for the ith degree of freedom. 

1065 

1066 Examples: 

1067 

1068 .. highlight:: python 

1069 .. code-block:: python 

1070 

1071 coeffs, offsets = mpc.coefficients() 

1072 coeffs_of_slave_i = coeffs[offsets[i]:offsets[i+1]] 

1073 """ 

1074 self._raise_if_not_finalized() 

1075 return self._cpp_object.coefficients() 

1076 

1077 def all_coefficients(self) -> Tuple[_float_array_types, npt.NDArray[numpy.int32]]: 

1078 """ 

1079 Returns the coefficients of all masters, including those eliminated by a Dirichlet condition, 

1080 in the order supplied before :func:`finalize`, and the offsets for the ith degree of freedom. 

1081 This is the layout taken by :func:`update_coefficients`. The corresponding masters are given 

1082 by :func:`all_masters`. 

1083 

1084 Examples: 

1085 

1086 .. highlight:: python 

1087 .. code-block:: python 

1088 

1089 coeffs, offsets = mpc.all_coefficients() 

1090 coeffs_of_slave_i = coeffs[offsets[i]:offsets[i+1]] 

1091 """ 

1092 self._raise_if_not_finalized() 

1093 return self._cpp_object.all_coefficients() 

1094 

1095 def all_masters(self) -> npt.NDArray[numpy.int32]: 

1096 """ 

1097 Returns the masters (local index in :attr:`function_space`) in the layout of 

1098 :func:`all_coefficients`. 

1099 """ 

1100 self._raise_if_not_finalized() 

1101 return self._cpp_object.all_masters() 

1102 

1103 def update_coefficients(self, coeffs: _float_array_types) -> None: 

1104 """ 

1105 Replace the coefficient of every master, including masters eliminated by a Dirichlet 

1106 condition, and recompute the constraint offset :math:`g`. 

1107 

1108 The masters are fixed at creation. A master dropped by `coefficient_tol` or by the `filter` 

1109 of :func:`finalize` cannot be given a coefficient, so create the constraint with 

1110 `coefficient_tol=0` and no filter if the coefficients are to be changed. 

1111 

1112 Args: 

1113 coeffs: The new coefficients, in the layout of :func:`all_coefficients`, for all degrees 

1114 of freedom local to the process (owned and ghost). 

1115 

1116 Note: 

1117 Collective. Must be called by every process. 

1118 """ 

1119 self._raise_if_not_finalized() 

1120 self._cpp_object.update_coefficients(numpy.ascontiguousarray(coeffs, dtype=self._dtype)) 

1121 

1122 def scale_coefficients( 

1123 self, 

1124 scale: Union[_float_classes, float, complex, ufl.core.expr.Expr, _fem.Expression], 

1125 ) -> None: 

1126 """ 

1127 Multiply the coefficients of all masters of each slave :math:`s` by a factor 

1128 :math:`f_s`, and recompute the constraint offset :math:`g`. For a periodic constraint 

1129 :math:`u(x_s) = f_s u(relation(x_s))`, which for instance gives a Floquet-Bloch condition 

1130 with :math:`f=e^{i k\\cdot L}`. 

1131 

1132 The factors are stored in a function in the space of the constraint, and :math:`f_s` is 

1133 the degree of freedom :math:`s` of that function: the value at the slave for a Lagrange 

1134 space, the corresponding moment for e.g. a Nédélec space. 

1135 

1136 Repeated calls compound. Masters eliminated by a Dirichlet condition are scaled as well, 

1137 the user supplied `rhs_coeffs` are not. 

1138 

1139 Args: 

1140 scale: A scalar, a :class:`dolfinx.fem.Function` in the constraint's space (copied 

1141 by interpolation), a UFL expression, compiled into a :class:`dolfinx.fem.Expression` 

1142 at the interpolation points of the space, or such a compiled expression. Pass a 

1143 compiled expression to avoid recompilation when the factor is updated through 

1144 :class:`dolfinx.fem.Constant`'s in it. 

1145 

1146 Note: 

1147 Collective. Must be called by every process. 

1148 """ 

1149 self._raise_if_not_finalized() 

1150 if self._scale_function is None: 

1151 self._scale_function = _fem.Function(self.V, dtype=self._dtype) 

1152 f = self._scale_function 

1153 if isinstance(scale, (_fem.Expression, _fem.Function)): 

1154 f.interpolate(scale) 

1155 elif isinstance(scale, ufl.core.expr.Expr): 

1156 f.interpolate(_fem.Expression(scale, self.V.element.interpolation_points, dtype=self._dtype)) 

1157 else: 

1158 f.x.array[:] = scale 

1159 f.x.scatter_forward() 

1160 # The extended index map appends master ghosts after the ghosts of the input space 

1161 num_dofs_local = len(self._cpp_object.is_slave) 

1162 self._cpp_object.scale_coefficients(f.x.array[:num_dofs_local]) 

1163 

1164 @property 

1165 def num_local_slaves(self): 

1166 """ 

1167 Return the number of slaves owned by the current process. 

1168 """ 

1169 self._raise_if_not_finalized() 

1170 return self._cpp_object.num_local_slaves 

1171 

1172 @property 

1173 def cell_to_slaves(self): 

1174 """ 

1175 Returns an `dolfinx.cpp.graph.AdjacencyList_int32` whose ith node corresponds to 

1176 the ith cell (local to process), and links the corresponding slave degrees of 

1177 freedom in the cell (local to process). 

1178 

1179 Examples: 

1180 

1181 .. highlight:: python 

1182 .. code-block:: python 

1183 

1184 cell_to_slaves = mpc.cell_to_slaves() 

1185 slaves_in_cell_i = cell_to_slaves.links(i) 

1186 """ 

1187 self._raise_if_not_finalized() 

1188 return self._cpp_object.cell_to_slaves 

1189 

1190 @property 

1191 def function_space(self): 

1192 """ 

1193 Return the function space for the multi-point constraint with the updated index map 

1194 """ 

1195 self._raise_if_not_finalized() 

1196 return self.V 

1197 

1198 @property 

1199 def input_space(self) -> _fem.FunctionSpace: 

1200 """ 

1201 The function space the constraint was created with. 

1202 

1203 Forms and Dirichlet conditions are stated on this space, while functions holding a 

1204 solution live in :attr:`function_space`, its extension by the masters of the constraint. 

1205 For a system of several blocks, ``[mpc.input_space for mpc in mpcs]`` gives the spaces in 

1206 the order of the blocks, for instance for a :class:`ufl.MixedFunctionSpace`. 

1207 """ 

1208 return self._input_space 

1209 

1210 def backsubstitution(self, u: Union[_fem.Function, Sequence[_fem.Function], _PETSc.Vec]) -> None: # type: ignore 

1211 """ 

1212 For a Function, impose the multi-point constraint by backsubstiution. 

1213 This function is used after solving the reduced problem to obtain the values 

1214 at the slave degrees of freedom 

1215 

1216 .. note:: 

1217 It is the users responsibility to destroy the PETSc vector 

1218 

1219 Args: 

1220 u: The input function. For a constraint with masters in another block, the function 

1221 of every block, in the order given to :func:`finalize_multipointconstraints`; 

1222 only the function of this constraint's block is changed. The ghosts of the 

1223 functions holding masters must be up to date. 

1224 """ 

1225 self._raise_if_not_finalized() 

1226 if isinstance(u, Sequence): 

1227 self._cpp_object.backsubstitution([u_k.x.array for u_k in u]) # type: ignore 

1228 u[self._cpp_object.block].x.scatter_forward() 

1229 return 

1230 try: 

1231 self._cpp_object.backsubstitution(u.x.array) # type: ignore 

1232 assert isinstance(u, _fem.Function) 

1233 u.x.scatter_forward() 

1234 except AttributeError: 

1235 assert isinstance(u, _PETSc.Vec) 

1236 with u.localForm() as vector_local: 

1237 self._cpp_object.backsubstitution(vector_local.array_w) 

1238 u.ghostUpdate(addv=_PETSc.InsertMode.INSERT, mode=_PETSc.ScatterMode.FORWARD) # type: ignore 

1239 

1240 def homogenize(self, u: _fem.Function) -> None: 

1241 """ 

1242 For a vector, homogenize (set to zero) the vector components at the multi-point 

1243 constraint slave DoF indices. This is particularly useful for nonlinear problems. 

1244 

1245 Args: 

1246 u: The input vector 

1247 """ 

1248 self._cpp_object.homogenize(u.x.array) 

1249 u.x.scatter_forward() 

1250 

1251 def _raise_if_finalized(self): 

1252 """ 

1253 Raise if the multi point constraint has already been finalized 

1254 """ 

1255 if self.finalized: 

1256 raise RuntimeError("MultiPointConstraint has already been finalized") 

1257 

1258 def _raise_if_not_finalized(self): 

1259 """ 

1260 Raise if the multi point constraint has not yet been finalized 

1261 """ 

1262 if not self.finalized: 

1263 raise RuntimeError("MultiPointConstraint has not been finalized") 

1264 

1265 

1266def finalize_multipointconstraints( 

1267 mpcs: Sequence[MultiPointConstraint], filter: Optional[numpy.floating] = None 

1268) -> None: 

1269 """ 

1270 Finalize the multi point constraints of several function spaces together. 

1271 

1272 Entry ``k`` of ``mpcs`` constrains its own function space, for instance the ``k``-th block of a 

1273 :class:`ufl.MixedFunctionSpace`. Each is finalized as by :meth:`MultiPointConstraint.finalize`, 

1274 but the checks that need communication are reduced once for all of them, and the meshes of the 

1275 spaces may be distinct, as long as they live on congruent communicators. 

1276 

1277 Args: 

1278 mpcs: The constraints to finalize. None may be finalized already, and they must all use the 

1279 same ``dtype``. 

1280 filter: See :meth:`MultiPointConstraint.finalize`. Applied to every constraint. 

1281 

1282 Raises: 

1283 ValueError: If the input is inconsistent, or if a dof is both a slave and constrained by a 

1284 Dirichlet condition, a master is also a slave, or the meshes are on communicators 

1285 of different size or rank order. Raised on every process. 

1286 

1287 Note: 

1288 Collective. Must be called by every process, with the constraints in the same order. 

1289 """ 

1290 mpcs = list(mpcs) 

1291 if len(mpcs) == 0: 

1292 raise ValueError("At least one constraint is required") 

1293 if len({id(mpc) for mpc in mpcs}) != len(mpcs): 

1294 raise ValueError("The same constraint was given more than once") 

1295 for mpc in mpcs: 

1296 mpc._raise_if_finalized() 

1297 dtype = numpy.dtype(mpcs[0]._dtype) 

1298 if any(numpy.dtype(mpc._dtype) != dtype for mpc in mpcs): 

1299 raise ValueError("All constraints must have the same dtype") 

1300 if dtype.type not in (numpy.float32, numpy.float64, numpy.complex64, numpy.complex128): 

1301 raise ValueError(f"Unsupported dtype {dtype} for coefficients") 

1302 

1303 # An RBE3 constraint is built now, when the block of each space of its feet is known 

1304 for mpc in mpcs: 

1305 if len(mpc._rbe3) > 0: 

1306 mpc._build_rbe3(mpcs) 

1307 

1308 rhs_coeffs = [] 

1309 for mpc in mpcs: 

1310 if mpc._rhs_coeffs is None: 

1311 rhs_coeffs.append(numpy.zeros(0, dtype=dtype)) 

1312 else: 

1313 num_dofs_local = mpc.V.dofmap.index_map_bs * ( 

1314 mpc.V.dofmap.index_map.size_local + mpc.V.dofmap.index_map.num_ghosts 

1315 ) 

1316 rhs_coeffs.append(mpc._rhs_coeffs.x.array[:num_dofs_local].astype(dtype)) 

1317 

1318 # The block of each master: -2 marks the constraint's own block, -1 the block of the space 

1319 # recorded with it, anything else a block given directly. Every process records the same chunks 

1320 # with the same spaces, so a space that is not one of the blocks raises everywhere. 

1321 master_blocks = [] 

1322 for k, mpc in enumerate(mpcs): 

1323 if all(space is None and (blocks == -2).all() for blocks, space in mpc._master_spaces): 

1324 master_blocks.append(numpy.zeros(0, dtype=numpy.int32)) 

1325 continue 

1326 resolved = [] 

1327 for blocks, space in mpc._master_spaces: 

1328 blocks = blocks.copy() 

1329 blocks[blocks == -2] = k 

1330 if space is not None: 

1331 matches = [j for j, other in enumerate(mpcs) if other.V is space] 

1332 if len(matches) == 0: 

1333 matches = [j for j, other in enumerate(mpcs) if other.V.contains(space)] 

1334 if len(matches) != 1: 

1335 raise ValueError( 

1336 "The master space of a constraint must be the function space, or a subspace of " 

1337 "the function space, of exactly one of the constraints finalized together with it" 

1338 ) 

1339 blocks[blocks == -1] = matches[0] 

1340 resolved.append(blocks) 

1341 master_blocks.append(numpy.concatenate(resolved) if resolved else numpy.zeros(0, dtype=numpy.int32)) 

1342 

1343 # Raises ValueError (as the C++ throws std::invalid_argument), identically on every process 

1344 cpp_objects = dolfinx_mpc.cpp.mpc.create_multipointconstraints( 

1345 [mpc.V._cpp_object for mpc in mpcs], 

1346 [mpc._slaves for mpc in mpcs], 

1347 [mpc._masters for mpc in mpcs], 

1348 [mpc._coeffs.astype(dtype) for mpc in mpcs], 

1349 [mpc._owners for mpc in mpcs], 

1350 [mpc._offsets for mpc in mpcs], 

1351 rhs_coeffs, 

1352 [[bc._cpp_object for bc in mpc._bcs] for mpc in mpcs], 

1353 master_blocks, 

1354 filter, 

1355 ) 

1356 

1357 # The block of each space on a spider mesh, matched before the spaces are replaced 

1358 for mpc in mpcs: 

1359 for entry in mpc._rbe2: 

1360 entry[2] = next(j for j, other in enumerate(mpcs) if other.V is entry[0]) 

1361 

1362 for mpc, cpp_object in zip(mpcs, cpp_objects): 

1363 mpc._cpp_object = cpp_object 

1364 # Replace function space 

1365 mpc.V = _fem.FunctionSpace(mpc.V.mesh, mpc.V.ufl_element(), cpp_object.function_space) 

1366 mpc.finalized = True 

1367 # Delete variables that are no longer required 

1368 del (mpc._slaves, mpc._masters, mpc._coeffs, mpc._owners, mpc._offsets, mpc._master_spaces)