Coverage for python/src/dolfinx_mpc/container.py: 95%

79 statements  

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

1from __future__ import annotations 

2 

3import warnings 

4from typing import Callable, Optional, Union 

5 

6import dolfinx 

7import numpy 

8import numpy.typing as npt 

9 

10import dolfinx_mpc.cpp.mpc 

11 

12_mpc_data_classes = Union[ 

13 dolfinx_mpc.cpp.mpc.mpc_data_double, 

14 dolfinx_mpc.cpp.mpc.mpc_data_float, 

15 dolfinx_mpc.cpp.mpc.mpc_data_complex_double, 

16 dolfinx_mpc.cpp.mpc.mpc_data_complex_float, 

17] 

18_float_array_types = Union[ 

19 npt.NDArray[numpy.float32], 

20 npt.NDArray[numpy.float64], 

21 npt.NDArray[numpy.complex64], 

22 npt.NDArray[numpy.complex128], 

23] 

24 

25_mpc_classes = Union[ 

26 dolfinx_mpc.cpp.mpc.MultiPointConstraint_double, 

27 dolfinx_mpc.cpp.mpc.MultiPointConstraint_float, 

28 dolfinx_mpc.cpp.mpc.MultiPointConstraint_complex_double, 

29 dolfinx_mpc.cpp.mpc.MultiPointConstraint_complex_float, 

30] 

31_float_classes = Union[numpy.float32, numpy.float64, numpy.complex128, numpy.complex64] 

32 

33_type_names = { 

34 numpy.float32: "float", 

35 numpy.float64: "double", 

36 numpy.complex64: "complex_float", 

37 numpy.complex128: "complex_double", 

38} 

39 

40 

41class _Unset: 

42 """The default of a deprecated argument, to tell whether it was passed.""" 

43 

44 def __repr__(self) -> str: 

45 return "<unset>" 

46 

47 

48_UNSET = _Unset() 

49 

50 

51def _deprecated(old: str, new: str, stacklevel: int = 3): 

52 """Warn that the argument `old` is deprecated in favour of `new`, at the caller of the public 

53 function `stacklevel - 2` frames up.""" 

54 warnings.warn(f"`{old}` is deprecated, use {new} instead.", DeprecationWarning, stacklevel=stacklevel) 

55 

56 

57def _default_tolerance(dtype: npt.DTypeLike) -> float: 

58 """The default distance and coefficient tolerance: 500 machine epsilon of the real type of `dtype`. 

59 

60 Mirrors `dolfinx_mpc::default_tolerance` in C++. 

61 """ 

62 return float(500 * numpy.finfo(dtype).eps) 

63 

64 

65def _tolerance(value: Optional[float], dtype: npt.DTypeLike) -> float: 

66 """`value` as a Python float, by default :func:`_default_tolerance` of `dtype`.""" 

67 return _default_tolerance(dtype) if value is None else float(value) 

68 

69 

70def _cpp_function(name: str, dtype: npt.DTypeLike) -> Callable: 

71 """The C++ function `name` for constraints of scalar type `dtype`, bound as `name_<type>`.""" 

72 return getattr(dolfinx_mpc.cpp.mpc, f"{name}_{_type_names[numpy.dtype(dtype).type]}") 

73 

74 

75def _scalar_type(real_type: npt.DTypeLike, dtype: npt.DTypeLike | None = None) -> type: 

76 """The scalar type of a constraint on a mesh with coordinates of `real_type`. 

77 

78 Args: 

79 real_type: The type of the mesh coordinates 

80 dtype: The scalar type asked for. Defaults to the default scalar type of DOLFINx, real or 

81 complex, at the precision of the mesh. 

82 

83 Raises: 

84 ValueError: If `dtype` is not one of float32, float64, complex64 and complex128, or its 

85 precision differs from the mesh's. 

86 """ 

87 real = numpy.dtype(real_type) 

88 if dtype is None: 

89 is_complex = numpy.issubdtype(dolfinx.default_scalar_type, numpy.complexfloating) 

90 dtype = numpy.promote_types(real, numpy.complex64) if is_complex else real 

91 scalar = numpy.dtype(dtype) 

92 if scalar.type not in (numpy.float32, numpy.float64, numpy.complex64, numpy.complex128): 

93 raise ValueError(f"Unsupported scalar type {scalar} for a constraint") 

94 if numpy.finfo(scalar).dtype != real: 

95 raise ValueError( 

96 f"A constraint of scalar type {scalar} needs a mesh of {numpy.finfo(scalar).dtype}, not {real}" 

97 ) 

98 return scalar.type 

99 

100 

101class MPCData: 

102 r"""The rows of a constraint on one process, before it is finalized. 

103 

104 Row `i` is the equation :math:`u_s = \sum_j c_j u_{m_j}` of the slave :math:`s` = 

105 `slaves[i]`, with :math:`m_j` = `masters[j]` and :math:`c_j` = `coeffs[j]` for 

106 `offsets[i] <= j < offsets[i + 1]`. 

107 

108 Attributes: 

109 slaves: The slave of each row: a dof of the space of the slaves, local to the process 

110 (owned or ghost) and unrolled 

111 masters: The masters of all rows, row after row: global, unrolled dofs of the space of 

112 each master 

113 coeffs: The coefficient of each master 

114 owners: The process owning each master 

115 offsets: The masters of row `i` are `masters[offsets[i]:offsets[i + 1]]`, so `offsets` 

116 has one entry more than `slaves` 

117 master_blocks: The block of each master, in the numbering of whoever made the rows, or 

118 `None` if every master is in the space of the slaves. Given on every process or on 

119 none. A :class:`MultiPointConstraint` stores codes here until it is finalized (see 

120 :meth:`MultiPointConstraint.__init__`). 

121 

122 The arrays are views of the C++ object, valid while this object is. 

123 """ 

124 

125 _cpp_object: _mpc_data_classes 

126 

127 def __init__( 

128 self, 

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

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

131 coeffs: _float_array_types, 

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

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

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

135 ): 

136 args = ( 

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

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

139 coeffs, 

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

141 numpy.asarray(offsets, dtype=numpy.int32), 

142 None if master_blocks is None else numpy.asarray(master_blocks, dtype=numpy.int32), 

143 ) 

144 if coeffs.dtype.type == numpy.float32: 

145 self._cpp_object = dolfinx_mpc.cpp.mpc.mpc_data_float(*args) 

146 elif coeffs.dtype.type == numpy.float64: 

147 self._cpp_object = dolfinx_mpc.cpp.mpc.mpc_data_double(*args) 

148 elif coeffs.dtype.type == numpy.complex64: 

149 self._cpp_object = dolfinx_mpc.cpp.mpc.mpc_data_complex_float(*args) 

150 elif coeffs.dtype.type == numpy.complex128: 

151 self._cpp_object = dolfinx_mpc.cpp.mpc.mpc_data_complex_double(*args) 

152 else: 

153 raise ValueError(f"Unsupported dtype {coeffs.dtype.type} for coefficients") 

154 

155 @classmethod 

156 def empty(cls, dtype: npt.DTypeLike, master_blocks: bool = False) -> MPCData: 

157 """No rows, with coefficients of `dtype`, and with (empty) blocks if `master_blocks`.""" 

158 return cls( 

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

160 numpy.zeros(0, dtype=numpy.int64), 

161 numpy.zeros(0, dtype=dtype), 

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

163 numpy.zeros(1, dtype=numpy.int32), 

164 numpy.zeros(0, dtype=numpy.int32) if master_blocks else None, 

165 ) 

166 

167 @classmethod 

168 def from_cpp(cls, cpp_object: _mpc_data_classes) -> MPCData: 

169 """Wrap rows made in C++, such as the result of a constraint generator.""" 

170 data = cls.__new__(cls) 

171 data._cpp_object = cpp_object 

172 return data 

173 

174 def append( 

175 self, 

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

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

178 coeffs: _float_array_types, 

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

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

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

182 ) -> MPCData: 

183 """These rows followed by the rows given, as new data with the coefficient type of these. 

184 

185 The blocks are kept if either has them; missing ones must then be given by the caller. 

186 """ 

187 if (self.master_blocks is None) != (master_blocks is None): 

188 raise ValueError("Either both or neither of the rows must have master blocks") 

189 dtype = self.coeffs.dtype 

190 return MPCData( 

191 numpy.concatenate([self.slaves, slaves]), 

192 numpy.concatenate([self.masters, masters]), 

193 numpy.concatenate([self.coeffs, numpy.asarray(coeffs, dtype=dtype)]), 

194 numpy.concatenate([self.owners, owners]), 

195 numpy.concatenate([self.offsets, numpy.asarray(offsets[1:]) + self.offsets[-1]]), 

196 None if master_blocks is None else numpy.concatenate([self.master_blocks, master_blocks]), 

197 ) 

198 

199 @property 

200 def slaves(self): 

201 return self._cpp_object.slaves 

202 

203 @property 

204 def masters(self): 

205 return self._cpp_object.masters 

206 

207 @property 

208 def coeffs(self): 

209 return self._cpp_object.coeffs 

210 

211 @property 

212 def owners(self): 

213 return self._cpp_object.owners 

214 

215 @property 

216 def offsets(self): 

217 return self._cpp_object.offsets 

218 

219 @property 

220 def master_blocks(self): 

221 return self._cpp_object.master_blocks