Coverage for install/scipp/spatial/__init__.py: 49%

61 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2024-11-17 01:51 +0000

1# SPDX-License-Identifier: BSD-3-Clause 

2# Copyright (c) 2023 Scipp contributors (https://github.com/scipp) 

3"""Transformations of vectors. 

4 

5Functions in this module can be used to construct, deconstruct, and modify 

6variables with transformations on vectors. 

7 

8Despite the name, transformations in this module can be applied to 3-vectors 

9in any vector space and coordinate system, not just the physical space. 

10The user has to ensure that transformations are applied to the correct vectors. 

11 

12See Also 

13-------- 

14scipp.vector: 

15 Construct a scalar variable holding a 3-vector. 

16scipp.vectors: 

17 Construct an array variable holding 3-vectors. 

18""" 

19 

20from collections.abc import Sequence 

21from typing import Any, TypeVar 

22 

23import numpy as np 

24import numpy.typing as npt 

25 

26from .. import units 

27from .._scipp import core as _core_cpp 

28from ..core import DType, Unit, UnitError, Variable, vectors 

29from ..core._cpp_wrapper_util import call_func as _call_cpp_func 

30 

31# Element type for NumPy arrays. 

32# For sequences in multi-dim functions, 

33# we use `Sequence[Any]` as a simple but incomplete solution for nested lists. 

34_Float = TypeVar('_Float', bound=np.float64 | np.float32, covariant=True) 

35 

36 

37def _to_eigen_layout(a: npt.NDArray[_Float] | Sequence[Any]) -> npt.NDArray[_Float]: 

38 # Numpy and scipp use row-major, but Eigen matrices use column-major, 

39 # transpose matrix axes for copying values. 

40 return np.moveaxis(a, -1, -2) # type: ignore[arg-type] 

41 

42 

43def as_vectors(x: Variable, y: Variable, z: Variable) -> Variable: 

44 """Return inputs combined into vectors. 

45 

46 Inputs may be broadcast to a common shape,. 

47 

48 Parameters 

49 ---------- 

50 x: 

51 Variable containing x components. 

52 y: 

53 Variable containing y components. 

54 z: 

55 Variable containing z components. 

56 

57 Returns 

58 ------- 

59 : 

60 Zip of input x, y and z with dtype ``vector3``. 

61 The output unit is the same as input unit. 

62 

63 Raises 

64 ------ 

65 scipp.DTypeError 

66 If the dtypes of inputs are not ``float64``. 

67 

68 See also 

69 -------- 

70 scipp.vector: 

71 Construct a vector from plain numbers. 

72 scipp.vectors: 

73 Construct vectors from plain numpy arrays or lists. 

74 

75 .. versionadded:: 23.03.1 

76 """ 

77 return _call_cpp_func( # type: ignore[return-value] 

78 _core_cpp.geometry.as_vectors, 

79 *(c.to(dtype='float64', copy=False) for c in (x, y, z)), 

80 ) 

81 

82 

83def translation( 

84 *, 

85 unit: Unit | str = units.dimensionless, 

86 value: npt.NDArray[_Float] | Sequence[_Float], 

87) -> Variable: 

88 """ 

89 Creates a translation transformation from a single provided 3-vector. 

90 

91 Parameters 

92 ---------- 

93 unit: 

94 The unit of the translation 

95 value: 

96 A list or NumPy array of 3 items 

97 

98 Returns 

99 ------- 

100 : 

101 A scalar variable of dtype ``translation3``. 

102 """ 

103 return translations(dims=(), unit=unit, values=value) 

104 

105 

106def translations( 

107 *, 

108 dims: Sequence[str], 

109 unit: Unit | str = units.dimensionless, 

110 values: npt.NDArray[_Float] | Sequence[Any], 

111) -> Variable: 

112 """ 

113 Creates translation transformations from multiple 3-vectors. 

114 

115 Parameters 

116 ---------- 

117 dims: 

118 The dimensions of the created variable 

119 unit: 

120 The unit of the translation 

121 values: 

122 A list or NumPy array of 3-vectors 

123 

124 Returns 

125 ------- 

126 : 

127 An array variable of dtype ``translation3``. 

128 """ 

129 return Variable(dims=dims, unit=unit, values=values, dtype=DType.translation3) 

130 

131 

132def scaling_from_vector(*, value: npt.NDArray[_Float] | Sequence[_Float]) -> Variable: 

133 """ 

134 Creates a scaling transformation from a provided 3-vector. 

135 

136 Parameters 

137 ---------- 

138 value: 

139 A list or NumPy array of 3 values, corresponding to scaling 

140 coefficients in the x, y and z directions respectively. 

141 

142 Returns 

143 ------- 

144 : 

145 A scalar variable of dtype ``linear_transform3``. 

146 """ 

147 return linear_transforms(dims=[], values=np.diag(value)) 

148 

149 

150def scalings_from_vectors( 

151 *, dims: Sequence[str], values: npt.NDArray[_Float] | Sequence[Any] 

152) -> Variable: 

153 """ 

154 Creates scaling transformations from corresponding to the provided 3-vectors. 

155 

156 Parameters 

157 ---------- 

158 dims: 

159 The dimensions of the variable. 

160 values: 

161 A list or NumPy array of 3-vectors, each corresponding to scaling 

162 coefficients in the x, y and z directions respectively. 

163 

164 Returns 

165 ------- 

166 : 

167 An array variable of dtype ``linear_transform3``. 

168 """ 

169 identity = linear_transform(value=np.identity(3)) 

170 matrices = identity.broadcast( 

171 dims=dims, # type: ignore[arg-type] # shortcoming of annotations of broadcast 

172 shape=(len(values),), 

173 ).copy() 

174 for field_name, index in (("xx", 0), ("yy", 1), ("zz", 2)): 

175 matrices.fields[field_name] = Variable( 

176 dims=dims, values=np.asarray(values)[:, index], dtype="float64" 

177 ) 

178 return matrices 

179 

180 

181def rotation(*, value: npt.NDArray[_Float] | Sequence[_Float]) -> Variable: 

182 """ 

183 Creates a rotation-type variable from the provided quaternion coefficients. 

184 

185 The quaternion coefficients are provided in scalar-last order (x, y, z, w), where 

186 x, y, z and w form the quaternion 

187 

188 .. math:: 

189 

190 q = w + xi + yj + zk. 

191 

192 Attention 

193 --------- 

194 The quaternion must be normalized in order to represent a rotation. 

195 You can use, e.g. 

196 

197 >>> q = np.array([1, 2, 3, 4]) 

198 >>> rot = sc.spatial.rotation(value=q / np.linalg.norm(q)) 

199 

200 Parameters 

201 ---------- 

202 value: 

203 A NumPy array or list with length 4, corresponding to the quaternion 

204 coefficients (x*i, y*j, z*k, w) 

205 

206 Returns 

207 ------- 

208 : 

209 A scalar variable of dtype ``rotation3``. 

210 """ 

211 return rotations(dims=(), values=value) 

212 

213 

214def rotations( 

215 *, dims: Sequence[str], values: npt.NDArray[_Float] | Sequence[Any] 

216) -> Variable: 

217 """ 

218 Creates a rotation-type variable from the provided quaternion coefficients. 

219 

220 The quaternion coefficients are provided in scalar-last order (x, y, z, w), where 

221 x, y, z and w form the quaternion 

222 

223 .. math:: 

224 

225 q = w + xi + yj + zk. 

226 

227 Attention 

228 --------- 

229 The quaternions must be normalized in order to represent a rotation. 

230 You can use, e.g. 

231 

232 >>> q = np.array([[1, 2, 3, 4], [-1, -2, -3, -4]]) 

233 >>> rot = sc.spatial.rotations( 

234 ... dims=['x'], 

235 ... values=q / np.linalg.norm(q, axis=1)[:, np.newaxis]) 

236 

237 Parameters 

238 ---------- 

239 dims: 

240 The dimensions of the variable. 

241 values: 

242 A NumPy array of NumPy arrays corresponding to the quaternion 

243 coefficients (w, x*i, y*j, z*k) 

244 

245 Returns 

246 ------- 

247 : 

248 An array variable of dtype ``rotation3``. 

249 """ 

250 values = np.asarray(values) 

251 if values.shape[-1] != 4: 

252 raise ValueError( 

253 "Inputs must be Quaternions to create a rotation, i.e., have " 

254 "4 components. If you want to pass a rotation matrix, use " 

255 "sc.linear_transforms instead." 

256 ) 

257 return Variable(dims=dims, values=values, dtype=DType.rotation3) 

258 

259 

260def rotations_from_rotvecs(rotation_vectors: Variable) -> Variable: 

261 """ 

262 Creates rotation transformations from rotation vectors. 

263 

264 This requires ``scipy`` to be installed, as is wraps 

265 :meth:`scipy.spatial.transform.Rotation.from_rotvec`. 

266 

267 A rotation vector is a 3 dimensional vector which is co-directional to the axis of 

268 rotation and whose norm gives the angle of rotation. 

269 

270 Parameters 

271 ---------- 

272 rotation_vectors: 

273 A Variable with vector dtype 

274 

275 Returns 

276 ------- 

277 : 

278 An array variable of dtype ``rotation3``. 

279 """ 

280 from scipy.spatial.transform import Rotation as R 

281 

282 supported = [units.deg, units.rad] 

283 unit = rotation_vectors.unit 

284 if unit not in supported: 

285 raise UnitError(f"Rotation vector unit must be one of {supported}.") 

286 r = R.from_rotvec(rotation_vectors.values, degrees=unit == units.deg) 

287 return rotations(dims=rotation_vectors.dims, values=r.as_quat()) 

288 

289 

290def rotation_as_rotvec(rotation: Variable, *, unit: Unit | str = 'rad') -> Variable: 

291 """ 

292 Represent a rotation matrix (or matrices) as rotation vector(s). 

293 

294 This requires ``scipy`` to be installed, as is wraps 

295 :meth:`scipy.spatial.transform.Rotation.as_rotvec`. 

296 

297 A rotation vector is a 3 dimensional vector which is co-directional to the axis of 

298 rotation and whose norm gives the angle of rotation. 

299 

300 Parameters 

301 ---------- 

302 rotation: 

303 A variable with rotation matrices. 

304 unit: 

305 Angle unit for the rotation vectors. 

306 

307 Returns 

308 ------- 

309 : 

310 An array variable with rotation vectors of dtype ``vector3``. 

311 """ 

312 unit = Unit(unit) if not isinstance(unit, Unit) else unit 

313 supported = [units.deg, units.rad] 

314 if unit not in supported: 

315 raise UnitError(f"Rotation vector unit must be one of {supported}.") 

316 from scipy.spatial.transform import Rotation as R 

317 

318 r = R.from_matrix(rotation.values) 

319 if rotation.unit not in [units.one, units.dimensionless]: 

320 raise UnitError(f"Rotation matrix must be dimensionless, got {rotation.unit}.") 

321 return vectors( 

322 dims=rotation.dims, values=r.as_rotvec(degrees=unit == units.deg), unit=unit 

323 ) 

324 

325 

326def affine_transform( 

327 *, 

328 unit: Unit | str = units.dimensionless, 

329 value: npt.NDArray[_Float] | Sequence[_Float], 

330) -> Variable: 

331 """ 

332 Initializes a single affine transformation from the provided affine matrix 

333 coefficients. 

334 

335 Parameters 

336 ---------- 

337 unit: 

338 The unit of the affine transformation's translation component. 

339 value: 

340 A 4x4 matrix of affine coefficients. 

341 

342 Returns 

343 ------- 

344 : 

345 A scalar variable of dtype ``affine_transform3``. 

346 """ 

347 return affine_transforms(dims=[], unit=unit, values=value) 

348 

349 

350def affine_transforms( 

351 *, 

352 dims: Sequence[str], 

353 unit: Unit | str = units.dimensionless, 

354 values: npt.NDArray[_Float] | Sequence[Any], 

355) -> Variable: 

356 """ 

357 Initializes affine transformations from the provided affine matrix 

358 coefficients. 

359 

360 Parameters 

361 ---------- 

362 dims: 

363 The dimensions of the variable. 

364 unit: 

365 The unit of the affine transformation's translation component. 

366 values: 

367 An array of 4x4 matrices of affine coefficients. 

368 

369 Returns 

370 ------- 

371 : 

372 An array variable of dtype ``affine_transform3``. 

373 """ 

374 return Variable( 

375 dims=dims, 

376 unit=unit, 

377 values=_to_eigen_layout(values), 

378 dtype=DType.affine_transform3, 

379 ) 

380 

381 

382def linear_transform( 

383 *, 

384 unit: Unit | str = units.dimensionless, 

385 value: npt.NDArray[_Float] | Sequence[_Float], 

386) -> Variable: 

387 """Constructs a zero dimensional :class:`Variable` holding a single 3x3 

388 matrix. 

389 

390 Parameters 

391 ---------- 

392 value: 

393 Initial value, a list of lists or 2-D NumPy array. 

394 unit: 

395 Optional, unit. Default=dimensionless 

396 

397 Returns 

398 ------- 

399 : 

400 A scalar variable of dtype ``linear_transform3``. 

401 """ 

402 return linear_transforms( 

403 dims=(), 

404 unit=unit, 

405 values=value, 

406 ) 

407 

408 

409def linear_transforms( 

410 *, 

411 dims: Sequence[str], 

412 unit: Unit | str = units.dimensionless, 

413 values: npt.NDArray[_Float] | Sequence[Any], 

414) -> Variable: 

415 """Constructs a :class:`Variable` with given dimensions holding an array 

416 of 3x3 matrices. 

417 

418 Parameters 

419 ---------- 

420 dims: 

421 Dimension labels. 

422 values: 

423 Initial values. 

424 unit: 

425 Optional, data unit. Default=dimensionless 

426 

427 Returns 

428 ------- 

429 : 

430 An array variable of dtype ``linear_transform3``. 

431 """ 

432 return Variable( 

433 dims=dims, 

434 unit=unit, 

435 values=_to_eigen_layout(values), 

436 dtype=DType.linear_transform3, 

437 ) 

438 

439 

440def inv(var: Variable) -> Variable: 

441 """Return the inverse of a spatial transformation. 

442 

443 Parameters 

444 ---------- 

445 var: 

446 Input variable. 

447 Its ``dtype`` must be one of 

448 

449 - :attr:`scipp.DType.linear_transform3` 

450 - :attr:`scipp.DType.affine_transform3` 

451 - :attr:`scipp.DType.rotation3` 

452 - :attr:`scipp.DType.translation3` 

453 

454 Returns 

455 ------- 

456 : 

457 A variable holding the inverse transformation to ``var``. 

458 """ 

459 return _call_cpp_func(_core_cpp.inv, var) # type: ignore[return-value] 

460 

461 

462__all__ = [ 

463 'rotation', 

464 'rotations', 

465 'rotations_from_rotvecs', 

466 'rotation_as_rotvec', 

467 'scaling_from_vector', 

468 'scalings_from_vectors', 

469 'translation', 

470 'translations', 

471 'affine_transform', 

472 'affine_transforms', 

473 'linear_transform', 

474 'linear_transforms', 

475 'inv', 

476]