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

417 statements  

« prev     ^ index     » next       coverage.py v7.17.0, created at 2026-11-03 12:11 +0000

1# Copyright (C) 2020-2026 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 mpi4py import MPI as _MPI 

11from petsc4py import PETSc as _PETSc 

12 

13import dolfinx.cpp as _cpp 

14import dolfinx.fem as _fem 

15import dolfinx.mesh as _mesh 

16import numpy 

17import numpy.typing as npt 

18import ufl 

19 

20import dolfinx_mpc.cpp 

21 

22from .container import ( 

23 _UNSET, 

24 MPCData, 

25 _cpp_function, 

26 _deprecated, 

27 _float_array_types, 

28 _float_classes, 

29 _mpc_classes, 

30 _mpc_data_classes, 

31 _scalar_type, 

32 _tolerance, 

33 _Unset, 

34) 

35from .dictcondition import create_dictionary_constraint 

36from .integralcondition import create_integral_constraint 

37from .rbe import create_rbe2, create_rbe3 

38 

39 

40class MultiPointConstraint: 

41 """ 

42 Hold data for multi point constraint relation ships, 

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

44 

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

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

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

48 one. 

49 

50 Args: 

51 V: The function space 

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

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

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

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

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

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

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

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

60 dependent boundary data is supported. 

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

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

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

64 """ 

65 

66 _data: MPCData 

67 _master_spaces: List[_fem.FunctionSpace] 

68 _bcs: List[_fem.DirichletBC] 

69 _rhs_coeffs: Optional[_fem.Function] 

70 _scale_function: Optional[_fem.Function] 

71 _rbe2: List[list] 

72 _rbe3: List[tuple] 

73 _rbe3_data: Optional[tuple] 

74 V: _fem.FunctionSpace 

75 _input_space: _fem.FunctionSpace 

76 finalized: bool 

77 _cpp_object: _mpc_classes 

78 _dtype: npt.DTypeLike 

79 __slots__ = tuple(__annotations__) 

80 

81 def __init__( 

82 self, 

83 V: _fem.FunctionSpace, 

84 dtype: npt.DTypeLike | None = None, 

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

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

87 ): 

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

89 # The rows on this process until finalize. Their master_blocks hold a code per master, as 

90 # the blocks are known only when the constraints are finalized together: 

91 # code >= 0 the block itself, its position among the constraints finalized together 

92 # code == -1 the block of this constraint: a master in its own space 

93 # code == -2 - s the block of the space self._master_spaces[s] 

94 # finalize_multipointconstraints resolves the codes to blocks. 

95 self._data = MPCData.empty(dtype, master_blocks=True) 

96 # The spaces of masters outside the space of this constraint, as given to add_constraint, 

97 # add_constraint_from_mpc_data or extend_masters (master_space), each once, in the order 

98 # first given. Every process appends them in the same order, also without local slaves, 

99 # so that a code means the same space on every process. A space must be the space of one 

100 # of the constraints finalized together with this one, or an uncollapsed subspace of it. 

101 self._master_spaces = [] 

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

103 if rhs_coeffs is not None: 

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

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

106 if rhs_coeffs.function_space != V: 

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

108 self._rhs_coeffs = rhs_coeffs 

109 self._scale_function = None 

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

111 self._rbe2 = [] 

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

113 # finalize, which keeps the arrays for update_rbe3 

114 self._rbe3 = [] 

115 self._rbe3_data = None 

116 self.V = V 

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

118 self._input_space = V 

119 self.finalized = False 

120 self._dtype = dtype 

121 

122 def add_constraint( 

123 self, 

124 V: _fem.FunctionSpace, 

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

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

127 coeffs: _float_array_types, 

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

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

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

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

132 ): 

133 """ 

134 Add new constraint given by numpy arrays. 

135 

136 Args: 

137 V: The function space for the constraint 

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

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

140 coeffs: The coefficients corresponding to each master. 

141 owners: The process each master is owned by. 

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

143 in the slaves arrays, i.e. 

144 

145 .. highlight:: python 

146 .. code-block:: python 

147 

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

149 

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

151 space of another constraint finalized together with this one by 

152 :func:`finalize_multipointconstraints`, or an uncollapsed subspace of it, and `masters` is in 

153 the global numbering of that constraint's space. 

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

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

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

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

158 

159 Note: 

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

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

162 """ 

163 assert V == self.V 

164 self._raise_if_finalized() 

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

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

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

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

169 

170 if master_blocks is not None: 

171 codes = numpy.asarray(master_blocks, dtype=numpy.int32) 

172 elif master_space is not None: 

173 codes = numpy.full(len(masters), self._space_code(master_space), dtype=numpy.int32) 

174 else: 

175 codes = numpy.full(len(masters), -1, dtype=numpy.int32) 

176 if len(slaves) > 0: 

177 self._data = self._data.append(slaves, masters, coeffs, owners, offsets, codes) 

178 

179 def _space_code(self, space: _fem.FunctionSpace) -> int: 

180 """The code of the block of the masters in `space`, recorded on every process in the same 

181 order so that every process resolves it the same way when the constraints are finalized.""" 

182 for s, other in enumerate(self._master_spaces): 

183 if other is space: 

184 return -2 - s 

185 self._master_spaces.append(space) 

186 return -1 - len(self._master_spaces) 

187 

188 def extend_masters( 

189 self, 

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

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

192 coeffs: _float_array_types, 

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

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

195 ): 

196 r"""Add a master to the rows of existing slaves. 

197 

198 A row is the equation :math:`u_s = \sum_j c_j u_{m_j}` of its slave :math:`u_s`, whichever 

199 constraint made it. Entry `i` adds a term :math:`k u_d` to its right-hand side, giving 

200 :math:`u_s = \sum_j c_j u_{m_j} + k u_d`, with :math:`s` = `slaves[i]`, 

201 :math:`u_d` = `masters[i]` and :math:`k` = `coeffs[i]`. If :math:`u_d` is already a master 

202 of the row, in the same space, :math:`k` is added to its coefficient. The ghost copies of 

203 the rows are extended too. 

204 

205 A call adds one term per slave. Further terms of the same slave are added by further calls, 

206 each with its own `master_space`, so that a row may have masters in several spaces. 

207 

208 For instance, a periodic condition :math:`u(x) = u(\mathrm{relation}(x))` extended, by one 

209 call per entry :math:`\bar H_{bk}` of a macroscopic gradient on a point mesh, with 

210 coefficient :math:`d_k = (x - \mathrm{relation}(x))_k` for component :math:`b`, ties `u` to 

211 :math:`\bar H x` plus a periodic field. 

212 

213 Args: 

214 slaves: The slave of each entry: owned dofs of the space of the constraint (local, 

215 unrolled), each already a slave and each at most once. 

216 masters: The master of each entry, in the global (unrolled) numbering of `master_space` 

217 coeffs: The coefficient of each entry, of the scalar type of the constraint 

218 owners: The process owning each master 

219 master_space: The space of the masters if not that of the constraint. It must be the 

220 space of another constraint finalized together with this one by 

221 :func:`finalize_multipointconstraints`, or an uncollapsed subspace of it. 

222 

223 Raises: 

224 ValueError: On every process, if a slave is not owned, appears twice or has no row yet, 

225 or the arrays differ in length. 

226 

227 Note: 

228 Collective. Must be called by every process, with the same `master_space`, before the 

229 constraint is finalized. 

230 """ 

231 self._raise_if_finalized() 

232 code = -1 if master_space is None else self._space_code(master_space) 

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

234 new_data = _cpp_function("extend_mpc_data", self._dtype)( 

235 self._data._cpp_object, 

236 -1, 

237 numpy.asarray(slaves, dtype=numpy.int32), 

238 numpy.asarray(masters, dtype=numpy.int64), 

239 numpy.asarray(coeffs, dtype=self._dtype), 

240 numpy.asarray(owners, dtype=numpy.int32), 

241 code, 

242 self.V._cpp_object, 

243 ) 

244 self._data = MPCData.from_cpp(new_data) 

245 

246 def add_master_from_point( 

247 self, 

248 V: _fem.FunctionSpace, 

249 slave_marker: Callable[[numpy.ndarray], numpy.ndarray], 

250 coefficient: Union[Callable[[numpy.ndarray], numpy.ndarray], float, complex], 

251 point: npt.ArrayLike, 

252 distance_tol: Optional[float] = None, 

253 ): 

254 r"""Add the dofs at a point as a master of the rows of marked slaves. 

255 

256 Each slave of `V` at a coordinate :math:`x` marked by `slave_marker` gets the term 

257 :math:`k(x)\, u(p)` added to the right-hand side of its row, by :meth:`extend_masters`, 

258 with :math:`p` = `point` and :math:`k` = `coefficient`. For a blocked `V`, component 

259 :math:`b` of a slave gets component :math:`b` of the dofs at :math:`p`; for a single 

260 component pass the uncollapsed subspace, for instance `V.sub(1)`. 

261 

262 For instance, the periodic cell of the homogenization demos ties the right edge to the left 

263 edge plus the corner :math:`B`, :math:`u(L, y) = u(0, y) + u^B`: the periodic constraint 

264 gives :math:`u(L, y) = u(0, y)`, and this function, with the right edge marked, 

265 coefficient `1` and `point` :math:`B`, adds :math:`u^B`. 

266 

267 Args: 

268 V: The space of this constraint, or an uncollapsed subspace of it 

269 slave_marker: Marks the coordinates of the slaves, `(3, n)` to `(n,)`. Each marked dof 

270 must already be a slave, and the dofs at `point` must not be marked. 

271 coefficient: The coefficient :math:`k` of each slave, from its coordinates, `(3, n)` 

272 to `(n,)`, or one value for all. 

273 point: The point of the master, of `gdim` or 3 coordinates. Exactly one block of dofs 

274 of `V` must be there (see :func:`dofs_at_point`). 

275 distance_tol: The largest distance between `point` and its dofs. Defaults to `500` 

276 machine epsilon of the coordinate type of the mesh. 

277 

278 Note: 

279 Collective. Must be called by every process, before the constraint is finalized. 

280 """ 

281 dofs_marked, components, x_slaves, _ = _marked_dofs(V, slave_marker) 

282 owned = dofs_marked < self.V.dofmap.index_map.size_local * self.V.dofmap.index_map_bs 

283 slaves, components, x_slaves = dofs_marked[owned], components[owned], x_slaves[owned] 

284 

285 if callable(coefficient): 

286 coeffs = numpy.asarray(coefficient(x_slaves.T.copy()), dtype=self._dtype).reshape(-1) 

287 else: 

288 coeffs = numpy.full(len(slaves), coefficient, dtype=self._dtype) 

289 dofs, owner = dofs_at_point(V, point, distance_tol) 

290 self.extend_masters( 

291 slaves.astype(numpy.int32), 

292 dofs[components].astype(numpy.int64), 

293 coeffs, 

294 numpy.full(len(slaves), owner, dtype=numpy.int32), 

295 ) 

296 

297 def add_integral_constraint( 

298 self, 

299 weight_form, 

300 value, 

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

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

303 *, 

304 coefficient_tol: Optional[float] = None, 

305 ): 

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

307 

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

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

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

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

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

313 constructor. 

314 

315 Args: 

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

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

318 be in the function space of this constraint. 

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

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

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

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

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

324 constructor. 

325 rtol: Deprecated, use `coefficient_tol`. 

326 coefficient_tol: A master whose coefficient is below 

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

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

329 

330 Note: 

331 Collective. Must be called by every process. 

332 """ 

333 self._raise_if_finalized() 

334 if rtol is not None: 

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

336 coefficient_tol = rtol if coefficient_tol is None else coefficient_tol 

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

338 self.V, 

339 weight_form, 

340 value, 

341 self._bcs if bcs is None else bcs, 

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

343 ) 

344 if self._rhs_coeffs is None: 

345 self._rhs_coeffs = rhs 

346 else: 

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

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

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

350 

351 def add_constraint_from_mpc_data( 

352 self, 

353 V: _fem.FunctionSpace, 

354 mpc_data: Union[_mpc_data_classes, MPCData], 

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

356 ): 

357 """ 

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

359 :meth:`add_constraint` for `master_space`. The `master_blocks` of `mpc_data`, if any, 

360 are the `master_blocks` of :meth:`add_constraint`, and exclusive with `master_space`. 

361 """ 

362 self._raise_if_finalized() 

363 self.add_constraint( 

364 V, 

365 mpc_data.slaves, 

366 mpc_data.masters, 

367 mpc_data.coeffs, 

368 mpc_data.owners, 

369 mpc_data.offsets, 

370 master_space=master_space, 

371 master_blocks=mpc_data.master_blocks, 

372 ) 

373 

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

375 """ 

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

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

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

379 

380 Args: 

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

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

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

384 coefficient contributes nothing to the constraint, but still 

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

386 every element matrix modification, so removing them can shrink 

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

388 master supplied is kept. 

389 

390 Note: 

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

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

393 

394 Note: 

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

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

397 """ 

398 finalize_multipointconstraints([self], filter) 

399 

400 def update_constants(self) -> None: 

401 """ 

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

403 conditions supplied to the constructor. 

404 

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

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

407 

408 Note: 

409 Collective. Must be called by every process. 

410 """ 

411 self._raise_if_not_finalized() 

412 if self._rhs_coeffs is not None: 

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

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

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

416 ) 

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

418 self._cpp_object.set_rhs_coeffs(rhs_coeffs) 

419 

420 self._cpp_object.update_constants() 

421 

422 @property 

423 def constants(self) -> _float_array_types: 

424 """ 

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

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

427 """ 

428 self._raise_if_not_finalized() 

429 return self._cpp_object.constants 

430 

431 @property 

432 def has_inhomogeneity(self) -> bool: 

433 """ 

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

435 reduced, so it is identical on every process. 

436 """ 

437 self._raise_if_not_finalized() 

438 return self._cpp_object.has_inhomogeneity 

439 

440 @property 

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

442 """ 

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

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

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

446 """ 

447 self._raise_if_not_finalized() 

448 return self._cpp_object.master_blocks 

449 

450 @property 

451 def dtype(self) -> type: 

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

453 return self._dtype 

454 

455 @property 

456 def has_cross_block_masters(self) -> bool: 

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

458 self._raise_if_not_finalized() 

459 return self._cpp_object.has_cross_block_masters 

460 

461 def create_periodic_constraint_topological( 

462 self, 

463 V: _fem.FunctionSpace, 

464 meshtag: _mesh.MeshTags, 

465 tag: int, 

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

467 bcs: List[_fem.DirichletBC], 

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

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

470 num_threads: Optional[int] = 1, 

471 *, 

472 distance_tol: Optional[float] = None, 

473 coefficient_tol: Optional[float] = None, 

474 ): 

475 """ 

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

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

478 

479 Args: 

480 V: The function space to assign the condition to. Should either be the space of the MPC or an uncollapsed 

481 subspace of it. 

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

483 tag: Tag indicating which entities should be slaves 

484 relation: Lambda-function describing the geometrical relation 

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

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

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

488 `coefficient_tol=0`. 

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

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

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

492 machine epsilon of the coordinate type of the mesh. 

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

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

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

496 machine epsilon of the real type of the constraint. 

497 """ 

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

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

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

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

502 is_input_space = V is self.V 

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

504 raise RuntimeError("The input space has to be an uncollapsed subspace (or the full space) of the MPC") 

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

506 V._cpp_object, 

507 meshtag._cpp_object, 

508 tag, 

509 relation, 

510 bcs_, 

511 scale, 

512 not is_input_space, 

513 distance_tol, 

514 coefficient_tol, 

515 num_threads, 

516 ) 

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

518 

519 def create_periodic_constraint_geometrical( 

520 self, 

521 V: _fem.FunctionSpace, 

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

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

524 bcs: List[_fem.DirichletBC], 

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

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

527 num_threads: Optional[int] = 1, 

528 *, 

529 distance_tol: Optional[float] = None, 

530 coefficient_tol: Optional[float] = None, 

531 ): 

532 """ 

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

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

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

536 

537 Args: 

538 V: The function space to assign the condition to. Should either be the space of the MPC or an uncollapsed 

539 subspace of it. 

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

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

542 bcs: Dirichlet boundary conditions for the problem 

543 (Periodic constraints will be ignored for these dofs) 

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

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

546 `coefficient_tol=0`. 

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

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

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

550 machine epsilon of the coordinate type of the mesh. 

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

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

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

554 machine epsilon of the real type of the constraint. 

555 """ 

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

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

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

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

560 is_input_space = V is self.V 

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

562 raise RuntimeError("The input space has to be an uncollapsed subspace (or the full space) of the MPC") 

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

564 V._cpp_object, 

565 indicator, 

566 relation, 

567 bcs, 

568 scale, 

569 not is_input_space, 

570 distance_tol, 

571 coefficient_tol, 

572 num_threads, 

573 ) 

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

575 

576 def create_submesh_constraint( 

577 self, 

578 V: _fem.FunctionSpace, 

579 master_space: _fem.FunctionSpace, 

580 entity_map: _mesh.EntityMap, 

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

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

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

584 num_threads: int = 1, 

585 *, 

586 coefficient_tol: Optional[float] = None, 

587 ): 

588 r""" 

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

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

591 

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

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

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

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

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

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

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

599 discontinuous `master_space` the side is arbitrary. 

600 

601 Args: 

602 V: The space of the constraint, or an uncollapsed subspace of it 

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

604 :func:`finalize_multipointconstraints`, or an uncollapsed subspace of it. Its mesh and the mesh 

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

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

607 codimension 0 or 1 

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

609 are not made slaves. 

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

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

612 num_threads: The number of threads to use 

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

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

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

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

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

618 

619 Raises: 

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

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

622 different numbers of components. Raised on every process. 

623 

624 Note: 

625 Collective. 

626 """ 

627 self._raise_if_finalized() 

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

629 raise ValueError("V must be the space of the constraint or an uncollapsed subspace of it") 

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

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

632 if not isinstance(tol, _Unset): 

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

634 if coefficient_tol is None: 

635 coefficient_tol = 0.0 if tol is None else tol 

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

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

638 V._cpp_object, 

639 master_space._cpp_object, 

640 entity_map._cpp_object, 

641 bcs_, 

642 scale, 

643 _tolerance(coefficient_tol, self._dtype), 

644 num_threads, 

645 ) 

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

647 

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

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

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

651 mpc_data = create_rbe2( 

652 self.V, 

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

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

655 W, 

656 self._dtype, 

657 x, 

658 ) 

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

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

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

662 

663 def add_rbe2_topological( 

664 self, 

665 dim: int, 

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

667 W: _fem.FunctionSpace, 

668 ): 

669 r""" 

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

671 (a rigid "spider"). 

672 

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

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

675 body, 

676 

677 .. math:: 

678 

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

680 

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

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

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

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

685 

686 Args: 

687 dim: Topological dimension of the entities 

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

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

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

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

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

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

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

695 

696 Note: 

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

698 `entities`. 

699 """ 

700 self._raise_if_finalized() 

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

702 dofs = [] 

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

704 # spiders is caught as constrained twice 

705 for e in per_spider: 

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

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

708 self._add_rbe2(dofs, W) 

709 

710 def add_rbe2_geometrical( 

711 self, 

712 locators: Union[ 

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

714 ], 

715 W: _fem.FunctionSpace, 

716 ): 

717 r""" 

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

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

720 

721 Args: 

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

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

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

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

726 

727 Note: 

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

729 """ 

730 self._raise_if_finalized() 

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

732 dofs = [ 

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

734 if locator is None 

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

736 for locator in per_spider 

737 ] 

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

739 

740 def update_rbe2(self) -> None: 

741 """ 

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

743 

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

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

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

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

748 

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

750 coefficient becomes nonzero. 

751 

752 Note: 

753 Collective. Must be called by every process. 

754 """ 

755 self._raise_if_not_finalized() 

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

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

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

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

760 

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

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

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

764 for k, d in enumerate(dofs): 

765 if weights is None: 

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

767 elif callable(weights): 

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

769 else: 

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

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

772 

773 def add_rbe3_topological( 

774 self, 

775 V: _fem.FunctionSpace, 

776 dim: int, 

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

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

779 ): 

780 r""" 

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

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

783 

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

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

786 least-squares sense, 

787 

788 .. math:: 

789 

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

791 

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

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

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

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

796 rigid. 

797 

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

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

800 the spaces of the feet. 

801 

802 Args: 

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

804 dim: Topological dimension of the entities 

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

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

807 may be `None`. 

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

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

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

811 

812 Note: 

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

814 `entities`. 

815 """ 

816 self._raise_if_finalized() 

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

818 dofs = [] 

819 for e in per_spider: 

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

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

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

823 

824 def add_rbe3_geometrical( 

825 self, 

826 V: _fem.FunctionSpace, 

827 locators: Union[ 

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

829 ], 

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

831 ): 

832 """ 

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

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

835 

836 Args: 

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

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

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

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

841 weights: See :meth:`add_rbe3_topological` 

842 

843 Note: 

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

845 """ 

846 self._raise_if_finalized() 

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

848 dofs = [ 

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

850 if locator is None 

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

852 for locator in per_spider 

853 ] 

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

855 

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

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

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

859 for V, *_ in self._rbe3: 

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

861 spaces.append(V) 

862 blocks = [] 

863 for V in spaces: 

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

865 if len(matches) != 1: 

866 raise ValueError( 

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

868 "constraints finalized together with it" 

869 ) 

870 blocks.append(matches[0]) 

871 

872 def gather(i): 

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

874 

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

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

877 self.add_constraint( 

878 self.V, 

879 mpc_data.slaves, 

880 mpc_data.masters, 

881 mpc_data.coeffs, 

882 mpc_data.owners, 

883 mpc_data.offsets, 

884 master_blocks=numpy.asarray(blocks, dtype=numpy.int32)[mpc_data.master_blocks], 

885 ) 

886 self._rbe3_data = (*data, blocks) 

887 

888 def update_rbe3(self) -> None: 

889 """ 

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

891 

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

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

894 afterwards. 

895 

896 Note: 

897 Collective. Must be called by every process. 

898 """ 

899 self._raise_if_not_finalized() 

900 if self._rbe3_data is None: 

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

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

903 dolfinx_mpc.cpp.mpc.update_rbe3( 

904 self._cpp_object, 

905 W._cpp_object, 

906 [V._cpp_object for V in spaces], 

907 blocks, 

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

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

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

911 ) 

912 

913 def create_slip_constraint( 

914 self, 

915 space: _fem.FunctionSpace, 

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

917 v: _fem.Function, 

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

919 ): 

920 """ 

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

922 

923 Args: 

924 space: Function space (possibly an uncollapsed subspace) for the current constraint 

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

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

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

928 

929 Examples: 

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

931 

932 .. highlight:: python 

933 .. code-block:: python 

934 

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

936 mpc = MultiPointConstraint(V) 

937 n = dolfinx.fem.Function(V) 

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

939 

940 Create slip constaint for a mixed function space: 

941 

942 .. highlight:: python 

943 .. code-block:: python 

944 

945 cellname = mesh.basix_cell() 

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

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

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

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

950 mpc = MultiPointConstraint(W) 

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

952 normal = dolfinx.fem.Function(n_space) 

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

954 

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

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

957 

958 .. highlight:: python 

959 .. code-block:: python 

960 

961 cellname = mesh.basix_cell() 

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

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

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

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

966 mpc = MultiPointConstraint(W) 

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

968 normal = Function(n_space) 

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

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

971 """ 

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

973 if space is self.V: 

974 sub_space = False 

975 elif self.V.contains(space): 

976 sub_space = True 

977 else: 

978 raise ValueError("Input space has to be an uncollapsed subspace of the MPC space") 

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

980 space._cpp_object, 

981 facet_marker[0]._cpp_object, 

982 facet_marker[1], 

983 v._cpp_object, 

984 bcs, 

985 sub_space, 

986 ) 

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

988 

989 def create_general_constraint( 

990 self, 

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

992 subspace_slave: Optional[int] = None, 

993 subspace_master: Optional[int] = None, 

994 *, 

995 distance_tol: Optional[float] = None, 

996 ): 

997 """ 

998 Args: 

999 V: The function space 

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

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

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

1003 the MPC equation. 

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

1005 as slave add index here 

1006 subspace_master: Subspace index for mixed or vector spaces 

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

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

1009 

1010 Example: 

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

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

1013 the dictionary should be: 

1014 

1015 .. highlight:: python 

1016 .. code-block:: python 

1017 

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

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

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

1021 """ 

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

1023 self.V, 

1024 slave_master_dict, 

1025 subspace_slave, 

1026 subspace_master, 

1027 dtype=self._dtype, 

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

1029 ) 

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

1031 

1032 def _tolerances( 

1033 self, 

1034 distance_tol: Optional[float], 

1035 coefficient_tol: Optional[float], 

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

1037 eps2: Optional[float] = None, 

1038 ) -> tuple[float, float]: 

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

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

1041 

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

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

1044 """ 

1045 if not isinstance(tol, _Unset): 

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

1047 if tol is None: 

1048 coefficient_tol = 0.0 if coefficient_tol is None else coefficient_tol 

1049 else: 

1050 distance_tol = tol if distance_tol is None else distance_tol 

1051 coefficient_tol = tol if coefficient_tol is None else coefficient_tol 

1052 if eps2 is not None: 

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

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

1055 return ( 

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

1057 _tolerance(coefficient_tol, self._dtype), 

1058 ) 

1059 

1060 def create_contact_slip_condition( 

1061 self, 

1062 meshtags: _mesh.MeshTags, 

1063 slave_marker: int, 

1064 master_marker: int, 

1065 normal: _fem.Function, 

1066 eps2: Optional[float] = None, 

1067 num_threads: Optional[int] = 1, 

1068 *, 

1069 distance_tol: Optional[float] = None, 

1070 coefficient_tol: Optional[float] = None, 

1071 ): 

1072 """ 

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

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

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

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

1077 

1078 Args: 

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

1080 slave_marker: The marker of the slave facets 

1081 master_marker: The marker of the master facets 

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

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

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

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

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

1087 machine epsilon of the coordinate type of the mesh. 

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

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

1090 machine epsilon of the real type of the constraint. 

1091 """ 

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

1093 self.V._cpp_object, 

1094 meshtags._cpp_object, 

1095 slave_marker, 

1096 master_marker, 

1097 normal._cpp_object, 

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

1099 num_threads, 

1100 ) 

1101 self.add_constraint_from_mpc_data(self.V, mpc_data) 

1102 

1103 def create_contact_inelastic_condition( 

1104 self, 

1105 meshtags: _cpp.mesh.MeshTags_int32, 

1106 slave_marker: int, 

1107 master_marker: int, 

1108 eps2: Optional[float] = None, 

1109 allow_missing_masters: bool = False, 

1110 num_threads: Optional[int] = 1, 

1111 *, 

1112 distance_tol: Optional[float] = None, 

1113 coefficient_tol: Optional[float] = None, 

1114 ): 

1115 """ 

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

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

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

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

1120 

1121 Args: 

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

1123 slave_marker: The marker of the slave facets 

1124 master_marker: The marker of the master facets 

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

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

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

1128 of freedom. 

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

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

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

1132 machine epsilon of the coordinate type of the mesh. 

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

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

1135 machine epsilon of the real type of the constraint. 

1136 """ 

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

1138 self.V._cpp_object, 

1139 meshtags._cpp_object, 

1140 slave_marker, 

1141 master_marker, 

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

1143 allow_missing_masters, 

1144 num_threads, 

1145 ) 

1146 self.add_constraint_from_mpc_data(self.V, mpc_data) 

1147 

1148 @property 

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

1150 """ 

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

1152 """ 

1153 self._raise_if_not_finalized() 

1154 return self._cpp_object.is_slave 

1155 

1156 @property 

1157 def slaves(self): 

1158 """ 

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

1160 """ 

1161 self._raise_if_not_finalized() 

1162 return self._cpp_object.slaves 

1163 

1164 @property 

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

1166 """ 

1167 Returns an adjacency-list whose ith node corresponds to 

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

1169 

1170 Examples: 

1171 

1172 .. highlight:: python 

1173 .. code-block:: python 

1174 

1175 masters = mpc.masters 

1176 masters_of_dof_i = masters.links(i) 

1177 """ 

1178 self._raise_if_not_finalized() 

1179 return self._cpp_object.masters 

1180 

1181 def coefficients(self) -> _float_array_types: 

1182 """ 

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

1184 for the ith degree of freedom. 

1185 

1186 Examples: 

1187 

1188 .. highlight:: python 

1189 .. code-block:: python 

1190 

1191 coeffs, offsets = mpc.coefficients() 

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

1193 """ 

1194 self._raise_if_not_finalized() 

1195 return self._cpp_object.coefficients() 

1196 

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

1198 """ 

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

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

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

1202 by :func:`all_masters`. 

1203 

1204 Examples: 

1205 

1206 .. highlight:: python 

1207 .. code-block:: python 

1208 

1209 coeffs, offsets = mpc.all_coefficients() 

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

1211 """ 

1212 self._raise_if_not_finalized() 

1213 return self._cpp_object.all_coefficients() 

1214 

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

1216 """ 

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

1218 :func:`all_coefficients`. 

1219 """ 

1220 self._raise_if_not_finalized() 

1221 return self._cpp_object.all_masters() 

1222 

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

1224 """ 

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

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

1227 

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

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

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

1231 

1232 Args: 

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

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

1235 

1236 Note: 

1237 Collective. Must be called by every process. 

1238 """ 

1239 self._raise_if_not_finalized() 

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

1241 

1242 def scale_coefficients( 

1243 self, 

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

1245 ) -> None: 

1246 """ 

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

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

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

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

1251 

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

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

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

1255 

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

1257 the user supplied `rhs_coeffs` are not. 

1258 

1259 Args: 

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

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

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

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

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

1265 

1266 Note: 

1267 Collective. Must be called by every process. 

1268 """ 

1269 self._raise_if_not_finalized() 

1270 if self._scale_function is None: 

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

1272 f = self._scale_function 

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

1274 f.interpolate(scale) 

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

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

1277 else: 

1278 f.x.array[:] = scale 

1279 f.x.scatter_forward() 

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

1281 num_dofs_local = len(self._cpp_object.is_slave) 

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

1283 

1284 @property 

1285 def num_local_slaves(self): 

1286 """ 

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

1288 """ 

1289 self._raise_if_not_finalized() 

1290 return self._cpp_object.num_local_slaves 

1291 

1292 @property 

1293 def cell_to_slaves(self): 

1294 """ 

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

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

1297 freedom in the cell (local to process). 

1298 

1299 Examples: 

1300 

1301 .. highlight:: python 

1302 .. code-block:: python 

1303 

1304 cell_to_slaves = mpc.cell_to_slaves() 

1305 slaves_in_cell_i = cell_to_slaves.links(i) 

1306 """ 

1307 self._raise_if_not_finalized() 

1308 return self._cpp_object.cell_to_slaves 

1309 

1310 @property 

1311 def function_space(self): 

1312 """ 

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

1314 """ 

1315 self._raise_if_not_finalized() 

1316 return self.V 

1317 

1318 @property 

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

1320 """ 

1321 The function space the constraint was created with. 

1322 

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

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

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

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

1327 """ 

1328 return self._input_space 

1329 

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

1331 """ 

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

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

1334 at the slave degrees of freedom 

1335 

1336 .. note:: 

1337 It is the users responsibility to destroy the PETSc vector 

1338 

1339 Args: 

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

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

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

1343 functions holding masters must be up to date. 

1344 """ 

1345 self._raise_if_not_finalized() 

1346 if isinstance(u, Sequence): 

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

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

1349 return 

1350 try: 

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

1352 assert isinstance(u, _fem.Function) 

1353 u.x.scatter_forward() 

1354 except AttributeError: 

1355 assert isinstance(u, _PETSc.Vec) 

1356 with u.localForm() as vector_local: 

1357 self._cpp_object.backsubstitution(vector_local.array_w) 

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

1359 

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

1361 """ 

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

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

1364 

1365 Args: 

1366 u: The input vector 

1367 """ 

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

1369 u.x.scatter_forward() 

1370 

1371 def _raise_if_finalized(self): 

1372 """ 

1373 Raise if the multi point constraint has already been finalized 

1374 """ 

1375 if self.finalized: 

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

1377 

1378 def _raise_if_not_finalized(self): 

1379 """ 

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

1381 """ 

1382 if not self.finalized: 

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

1384 

1385 

1386def _marked_dofs( 

1387 V: _fem.FunctionSpace, marker: Callable[[numpy.ndarray], numpy.ndarray] 

1388) -> tuple[npt.NDArray[numpy.int32], npt.NDArray[numpy.int32], npt.NDArray[numpy.floating], int]: 

1389 """The dofs of `V` at the coordinates marked by `marker`, owned and ghosts. 

1390 

1391 Returns: 

1392 Their local, unrolled index in the space `V` is an uncollapsed subspace of (or in `V`), 

1393 their component in `V`, their coordinates `(n, 3)`, and the number of components of `V`. 

1394 The component is that of `V`, from its collapsed numbering: the block size of the parent 

1395 need not be that of `V` (a component of a blocked space, or a block of a mixed space). 

1396 """ 

1397 if len(V.component()) > 0: 

1398 V_c = V.collapse()[0] 

1399 bs = V_c.dofmap.index_map_bs 

1400 parent_dofs, sub_dofs = _fem.locate_dofs_geometrical((V, V_c), marker) 

1401 x = V_c.tabulate_dof_coordinates()[sub_dofs // bs] 

1402 return parent_dofs, sub_dofs % bs, x, bs 

1403 bs = V.dofmap.index_map_bs 

1404 blocks = _fem.locate_dofs_geometrical(V, marker) 

1405 x = numpy.repeat(V.tabulate_dof_coordinates()[blocks], bs, axis=0) 

1406 dofs = (blocks[:, None] * bs + numpy.arange(bs, dtype=numpy.int32)).ravel() 

1407 return dofs, numpy.tile(numpy.arange(bs, dtype=numpy.int32), len(blocks)), x, bs 

1408 

1409 

1410def dofs_at_point( 

1411 V: _fem.FunctionSpace, point: npt.ArrayLike, distance_tol: float | None = None 

1412) -> tuple[npt.NDArray[numpy.int64], int]: 

1413 """The global dofs of the block of `V` at a point, and the process owning them. 

1414 

1415 The dofs are in the global, unrolled numbering of the space `V` is an uncollapsed subspace of 

1416 (or of `V`), as masters are given to :meth:`MultiPointConstraint.add_constraint` and 

1417 :meth:`MultiPointConstraint.extend_masters`: one per component of `V`. Works for a space on a 

1418 point mesh too. 

1419 

1420 Args: 

1421 V: The function space, or an uncollapsed subspace of it 

1422 point: The point, of `gdim` or 3 coordinates 

1423 distance_tol: The largest distance from the point to the dof. Defaults to `500` machine 

1424 epsilon of the coordinate type of the mesh. 

1425 

1426 Returns: 

1427 The dofs, one per component, and the owning process. The same on every process. 

1428 

1429 Raises: 

1430 ValueError: On every process, unless exactly one block of dofs of `V` is at the point: 

1431 none, or several, as in a discontinuous space or on a point mesh with coinciding 

1432 points. 

1433 

1434 Note: 

1435 Collective. 

1436 """ 

1437 mesh = V.mesh 

1438 comm = mesh.comm 

1439 gdim = mesh.geometry.dim 

1440 if distance_tol is None: 

1441 distance_tol = _tolerance(None, mesh.geometry.x.dtype) 

1442 p = numpy.zeros(3, dtype=numpy.float64) 

1443 given = numpy.asarray(point, dtype=numpy.float64).reshape(-1) 

1444 p[: len(given)] = given 

1445 

1446 def at_point(x): 

1447 return numpy.linalg.norm(x[:gdim].T - p[:gdim], axis=1) <= distance_tol 

1448 

1449 imap = V.dofmap.index_map 

1450 parent_bs = V.dofmap.index_map_bs 

1451 parent_dofs, components, _, bs = _marked_dofs(V, at_point) 

1452 owned = parent_dofs < imap.size_local * parent_bs 

1453 

1454 # The point must hold exactly one block of V, counted on the process owning it, which holds all 

1455 # its components. A discontinuous space, or a point mesh with coinciding points, may have several. 

1456 num_blocks = numpy.array([owned.sum() // bs], dtype=numpy.int64) 

1457 comm.Allreduce(_MPI.IN_PLACE, num_blocks, op=_MPI.SUM) 

1458 if num_blocks[0] != 1: 

1459 raise ValueError(f"{num_blocks[0]} blocks of degrees of freedom of the space at {given}, not one") 

1460 

1461 # The dofs and the owner, in one reduction 

1462 found = numpy.full(bs + 1, -1, dtype=numpy.int64) 

1463 found[components[owned]] = imap.local_range[0] * parent_bs + parent_dofs[owned] 

1464 if owned.any(): 

1465 found[bs] = comm.rank 

1466 comm.Allreduce(_MPI.IN_PLACE, found, op=_MPI.MAX) 

1467 return found[:bs], int(found[bs]) 

1468 

1469 

1470def finalize_multipointconstraints( 

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

1472) -> None: 

1473 """ 

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

1475 

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

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

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

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

1480 

1481 Args: 

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

1483 same ``dtype``. 

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

1485 

1486 Raises: 

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

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

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

1490 

1491 Note: 

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

1493 """ 

1494 mpcs = list(mpcs) 

1495 if len(mpcs) == 0: 

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

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

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

1499 for mpc in mpcs: 

1500 mpc._raise_if_finalized() 

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

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

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

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

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

1506 

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

1508 for mpc in mpcs: 

1509 if len(mpc._rbe3) > 0: 

1510 mpc._build_rbe3(mpcs) 

1511 

1512 rhs_coeffs = [] 

1513 for mpc in mpcs: 

1514 if mpc._rhs_coeffs is None: 

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

1516 else: 

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

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

1519 ) 

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

1521 

1522 def block_of(space: _fem.FunctionSpace) -> int: 

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

1524 if len(matches) == 0: 

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

1526 if len(matches) != 1: 

1527 raise ValueError( 

1528 "The master space of a constraint must be the function space, or an uncollapsed subspace of " 

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

1530 ) 

1531 return matches[0] 

1532 

1533 # The block of each master, from its code (see MultiPointConstraint.__init__). Every process 

1534 # records the same spaces in the same order, so a space that is not one of the blocks raises 

1535 # everywhere. 

1536 master_blocks = [] 

1537 for k, mpc in enumerate(mpcs): 

1538 codes = numpy.asarray(mpc._data.master_blocks) 

1539 blocks = codes.copy() 

1540 blocks[codes == -1] = k 

1541 for s, space in enumerate(mpc._master_spaces): 

1542 blocks[codes == -2 - s] = block_of(space) 

1543 if len(mpc._master_spaces) == 0 and (codes == -1).all(): 

1544 blocks = numpy.zeros(0, dtype=numpy.int32) 

1545 master_blocks.append(blocks) 

1546 

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

1548 cpp_objects = dolfinx_mpc.cpp.mpc.create_multipointconstraints( 

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

1550 # Copies: the arrays of the rows are read-only views of their C++ object 

1551 [numpy.array(mpc._data.slaves) for mpc in mpcs], 

1552 [numpy.array(mpc._data.masters) for mpc in mpcs], 

1553 [mpc._data.coeffs.astype(dtype) for mpc in mpcs], 

1554 [numpy.array(mpc._data.owners) for mpc in mpcs], 

1555 [numpy.array(mpc._data.offsets) for mpc in mpcs], 

1556 rhs_coeffs, 

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

1558 master_blocks, 

1559 filter, 

1560 ) 

1561 

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

1563 for mpc in mpcs: 

1564 for entry in mpc._rbe2: 

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

1566 

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

1568 mpc._cpp_object = cpp_object 

1569 # Replace function space 

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

1571 mpc.finalized = True 

1572 # Delete variables that are no longer required 

1573 del (mpc._data, mpc._master_spaces)