Source code for pypeit.core.transform

"""
Provide basic coordinate tranformation functions.

.. include common links, assuming primary doc root is up one directory
.. include:: ../include/links.rst
"""

from IPython import embed

import numpy as np

from pypeit import msgs


[docs]def affine_transform_matrix(scale=None, rotation=None, translation=None): r""" Construct a two-dimensional affine transformation matrix in homogeneous coordinates. The coordinate convention is to assume the coordinates are ordered in an array with the Cartesian :math:`x` in the first column and Cartesian :math:`y` in the second column. See the examples below. This function currently does *not* allow for a shear term. The transformation is returned as a `numpy.ndarray`_ in homogeneous coordinates. If no arguments are provided, this simply returns an identity matrix. Otherwise, the order of operations is to scale, rotate, and then translate (shift). I.e., the following should pass: .. code-block:: python t = affine_transform_matrix(translation=[2,1]) r = affine_transform_matrix(rotation=np.pi/3) a1 = affine_transform_matrix(rotation=np.pi/3, translation=[2,1]) assert np.array_equal(t @ r, a1) s = affine_transform_matrix(scale=(1.5, 2.)) a2 = affine_transform_matrix(scale=(1.5, 2.), rotation=np.pi/3, translation=[2,1]) assert np.array_equal(t @ r @ s, a2) Args: scale (:obj:`float`, array-like, optional): Scale factor to apply to each axis. Can be a single float to apply to both axes, or a two-element array-like with the scaling for each axis. rotation (:obj:`float`, optional): Counter-clockwise rotation in radians. The ordinate is aligned with the first axis, and the abcissa is aligned with the second axis; see :func:`~pypeit.core.transform.coordinate_transform_2d`. If None, the rotation is 0. translation (array-like, optional): An array with two elements that provide the shift to apply in each dimension. Returns: `numpy.ndarray`_: A :math:`3\times3` affine-transformation matrix. Examples: Rotate the unit box by 45 degrees: >>> import numpy as np >>> from pypeit.core import transform >>> # Cartesian: x y >>> coo = np.array([[0., 0.], [1., 0.], [1., 1.], [0., 1.]]) >>> tform = transform.affine_transform_matrix(rotation=np.radians(45.)) >>> tform array([[ 0.70710678, -0.70710678, 0. ], [ 0.70710678, 0.70710678, 0. ], [ 0. , 0. , 1. ]]) >>> transform.coordinate_transform_2d(coo, tform) array([[ 0.00000000e+00, 0.00000000e+00], [ 7.07106781e-01, 7.07106781e-01], [ 1.11022302e-16, 1.41421356e+00], [-7.07106781e-01, 7.07106781e-01]]) Rotation about the center by first shifting, then rotating, then shifting back. >>> shift = affine_transform_matrix(translation=[-0.5,-0.5]) >>> _tform = np.linalg.inv(shift) @ tform @ shift >>> transform.coordinate_transform_2d(coo, _tform) array([[ 0.5 , -0.20710678], [ 1.20710678, 0.5 ], [ 0.5 , 1.20710678], [-0.20710678, 0.5 ]]) """ tform = np.eye(3) if scale is not None: _s = np.atleast_1d(scale) if _s.size == 1: sx = sy = scale elif _s.size == 2: sx, sy = scale else: msgs.error('Scale must be a single scalar or a two-element array.') tform[0,0] = float(sx) tform[1,1] = float(sy) if rotation is not None: tform[:2,:2] = np.array([[np.cos(rotation), -np.sin(rotation)], [np.sin(rotation), np.cos(rotation)]]) @ tform[:2,:2] if translation is not None: _t = np.atleast_1d(translation) if _t.size != 2: msgs.error('Translation must be a two-element array.') tform[0:2,2] = translation return tform
[docs]def affine_transform_series(steps): r""" Construct an affine transform matrix from a set of transformation steps executed in series. Each step in the transformation is provided by a call to :func:`pypeit.core.transform.affine_transform_matrix`. The order of the steps should be provided in the order they should be preformed. The following should pass: .. code-block:: python steps = [dict(scale=(1.5,2)), dict(rotation=np.pi/3), dict(translation=[2,1])] a1 = affine_transform_series(steps) a2 = affine_transform_matrix(scale=steps[0]['scale'], rotation=steps[1]['rotation'], translation=steps[2]['translation']) assert np.array_equal(a1, a2) Args: steps (array-like): A list of dictionaries with each dictionary containing the keyword arguments for a single call to :func:`pypeit.core.transform.affine_transform_matrix`. Returns: `numpy.ndarray`_: A :math:`3\times3` affine-transformation matrix. """ tform = np.eye(3) for kwargs in steps: tform = affine_transform_matrix(**kwargs) @ tform return tform
[docs]def coordinate_transform_2d(coo, matrix, inverse=False): r""" Apply a 2D coordinate transformation using an affine-transformation matrix in homologous coordinates. Args: coo (array-like): Coordinates to transform. Shape must be :math:`(N,2)`, where :math:`N` is the number of coordinates, Cartesian :math:`x` is in the first column (``coo[:,0]``), and Cartesian :math:`y` is in the second column (``coo[:,1]``). matrix (`numpy.ndarray`_): The :math:3\times 3` affine-transformation matrix. See :func:`affine_transform_matrix`. inverse (:obj:`bool`, optional): By default, the function performs the *active* transformation; i.e., applying the transformation to the coordinates, moving them within the existing coordinate frame. Set ``inverse`` to true to instead perform the *passive* transformation; i.e., transforming the coordinate axes and providing the new coordinates in the transformed (e.g., rotated) frame. Returns: `numpy.ndarray`_: Transformed coordinates with shape :math:`(N,2)`. """ _coo = np.atleast_2d(coo) if _coo.ndim != 2: msgs.error('Coordinate array must be 2D.') if _coo.shape[1] != 2: msgs.error('Coordinate array must have 2D coordinates along the last axis.') ncoo = _coo.shape[0] _m = np.linalg.inv(matrix) if inverse else matrix return (np.column_stack((_coo, np.ones(ncoo, dtype=_coo.dtype))) @ _m.T)[:,:2]