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
« 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.
5Functions in this module can be used to construct, deconstruct, and modify
6variables with transformations on vectors.
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.
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"""
20from collections.abc import Sequence
21from typing import Any, TypeVar
23import numpy as np
24import numpy.typing as npt
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
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)
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]
43def as_vectors(x: Variable, y: Variable, z: Variable) -> Variable:
44 """Return inputs combined into vectors.
46 Inputs may be broadcast to a common shape,.
48 Parameters
49 ----------
50 x:
51 Variable containing x components.
52 y:
53 Variable containing y components.
54 z:
55 Variable containing z components.
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.
63 Raises
64 ------
65 scipp.DTypeError
66 If the dtypes of inputs are not ``float64``.
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.
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 )
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.
91 Parameters
92 ----------
93 unit:
94 The unit of the translation
95 value:
96 A list or NumPy array of 3 items
98 Returns
99 -------
100 :
101 A scalar variable of dtype ``translation3``.
102 """
103 return translations(dims=(), unit=unit, values=value)
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.
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
124 Returns
125 -------
126 :
127 An array variable of dtype ``translation3``.
128 """
129 return Variable(dims=dims, unit=unit, values=values, dtype=DType.translation3)
132def scaling_from_vector(*, value: npt.NDArray[_Float] | Sequence[_Float]) -> Variable:
133 """
134 Creates a scaling transformation from a provided 3-vector.
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.
142 Returns
143 -------
144 :
145 A scalar variable of dtype ``linear_transform3``.
146 """
147 return linear_transforms(dims=[], values=np.diag(value))
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.
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.
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
181def rotation(*, value: npt.NDArray[_Float] | Sequence[_Float]) -> Variable:
182 """
183 Creates a rotation-type variable from the provided quaternion coefficients.
185 The quaternion coefficients are provided in scalar-last order (x, y, z, w), where
186 x, y, z and w form the quaternion
188 .. math::
190 q = w + xi + yj + zk.
192 Attention
193 ---------
194 The quaternion must be normalized in order to represent a rotation.
195 You can use, e.g.
197 >>> q = np.array([1, 2, 3, 4])
198 >>> rot = sc.spatial.rotation(value=q / np.linalg.norm(q))
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)
206 Returns
207 -------
208 :
209 A scalar variable of dtype ``rotation3``.
210 """
211 return rotations(dims=(), values=value)
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.
220 The quaternion coefficients are provided in scalar-last order (x, y, z, w), where
221 x, y, z and w form the quaternion
223 .. math::
225 q = w + xi + yj + zk.
227 Attention
228 ---------
229 The quaternions must be normalized in order to represent a rotation.
230 You can use, e.g.
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])
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)
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)
260def rotations_from_rotvecs(rotation_vectors: Variable) -> Variable:
261 """
262 Creates rotation transformations from rotation vectors.
264 This requires ``scipy`` to be installed, as is wraps
265 :meth:`scipy.spatial.transform.Rotation.from_rotvec`.
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.
270 Parameters
271 ----------
272 rotation_vectors:
273 A Variable with vector dtype
275 Returns
276 -------
277 :
278 An array variable of dtype ``rotation3``.
279 """
280 from scipy.spatial.transform import Rotation as R
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())
290def rotation_as_rotvec(rotation: Variable, *, unit: Unit | str = 'rad') -> Variable:
291 """
292 Represent a rotation matrix (or matrices) as rotation vector(s).
294 This requires ``scipy`` to be installed, as is wraps
295 :meth:`scipy.spatial.transform.Rotation.as_rotvec`.
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.
300 Parameters
301 ----------
302 rotation:
303 A variable with rotation matrices.
304 unit:
305 Angle unit for the rotation vectors.
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
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 )
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.
335 Parameters
336 ----------
337 unit:
338 The unit of the affine transformation's translation component.
339 value:
340 A 4x4 matrix of affine coefficients.
342 Returns
343 -------
344 :
345 A scalar variable of dtype ``affine_transform3``.
346 """
347 return affine_transforms(dims=[], unit=unit, values=value)
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.
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.
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 )
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.
390 Parameters
391 ----------
392 value:
393 Initial value, a list of lists or 2-D NumPy array.
394 unit:
395 Optional, unit. Default=dimensionless
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 )
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.
418 Parameters
419 ----------
420 dims:
421 Dimension labels.
422 values:
423 Initial values.
424 unit:
425 Optional, data unit. Default=dimensionless
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 )
440def inv(var: Variable) -> Variable:
441 """Return the inverse of a spatial transformation.
443 Parameters
444 ----------
445 var:
446 Input variable.
447 Its ``dtype`` must be one of
449 - :attr:`scipp.DType.linear_transform3`
450 - :attr:`scipp.DType.affine_transform3`
451 - :attr:`scipp.DType.rotation3`
452 - :attr:`scipp.DType.translation3`
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]
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]