Source code for pyrigi.framework._transformations.transformations

"""
This module provides transformations of frameworks.
"""

from __future__ import annotations

from copy import deepcopy

import numpy as np
import sympy as sp
from sympy import Matrix

import pyrigi._utils._input_check as _input_check
from pyrigi._utils._conversion import point_to_vector
from pyrigi._utils._zero_check import is_zero_vector
from pyrigi._utils.linear_algebra import (
    _generate_three_orthonormal_vectors,
    _generate_two_orthonormal_vectors,
)
from pyrigi.data_type import (
    Number,
    Point,
    Sequence,
    Vertex,
)
from pyrigi.framework.base import FrameworkBase


[docs] def translate( framework: FrameworkBase, vector: Point | Matrix, inplace: bool = True ) -> None | FrameworkBase: """ Translate the framework. Parameters ---------- framework: vector Translation vector inplace If ``True`` (default), then this framework is translated. Otherwise, a new translated framework is returned. """ vector = point_to_vector(vector) if inplace: if vector.shape[0] != framework.dim: raise ValueError( "The dimension of the vector has to be the same as of the framework!" ) for v in framework._realization.keys(): framework._realization[v] += vector return new_framework = deepcopy(framework) translate(new_framework, vector, True) return new_framework
[docs] def rescale( framework: FrameworkBase, factor: Number, inplace: bool = True ) -> None | FrameworkBase: """ Scale the framework. Parameters ---------- framework: factor: Scaling factor inplace: If ``True`` (default), then this framework is translated. Otherwise, a new translated framework is returned. """ if isinstance(factor, str): factor = sp.sympify(factor) if inplace: for v in framework._realization.keys(): framework._realization[v] = framework._realization[v] * factor return new_framework = deepcopy(framework) rescale(new_framework, factor, True) return new_framework
[docs] def rotate2D( framework: FrameworkBase, angle: float, rotation_center: Point = [0, 0], inplace: bool = True, ) -> None | FrameworkBase: """ Rotate the planar framework counterclockwise. Parameters ---------- framework: angle: Rotation angle. If you want the coordinates to stay symbolic even after the rotation, you need to input the angle as a `sympy` expression. rotation_center: The center of rotation. By default, this is the origin. inplace: If ``True`` (default), then this framework is rotated. Otherwise, a new rotated framework is returned. """ _input_check.dimension_for_algorithm(framework.dim, [2], "rotate2D") _input_check.equal(len(rotation_center), 2, "length of the `rotation_center`") rotation_matrix = Matrix( [[sp.cos(angle), -sp.sin(angle)], [sp.sin(angle), sp.cos(angle)]] ) opposite_rotation_center = [-rotation_center[0], -rotation_center[1]] if inplace: rotated_framework = framework else: rotated_framework = deepcopy(framework) translate(rotated_framework, opposite_rotation_center, inplace=True) for v, pos in rotated_framework._realization.items(): rotated_framework._realization[v] = rotation_matrix * pos translate(rotated_framework, rotation_center, inplace=True) if inplace: return else: return rotated_framework
[docs] def rotate3D( framework: FrameworkBase, angle: Number, axis_direction: Sequence[Number] = [0, 0, 1], axis_shift: Point = [0, 0, 0], inplace: bool = True, ) -> None | FrameworkBase: """ Rotate the spatial framework counterclockwise around a specified rotation axis. Parameters ---------- framework: angle: Rotation angle around the axis of rotation. If you want the coordinates to stay symbolic even after the rotation, you need to input the angle as a `sympy` expression. axis_direction: Direction of the rotation axis. By default, this is the ``z``-axis. axis_shift: A point through which the rotation axis passes. By default, this is the origin. inplace: If ``True`` (default), then this framework is rotated. Otherwise, a new rotated framework is returned. """ _input_check.dimension_for_algorithm(framework.dim, [3], "rotate3D") _input_check.equal(len(axis_direction), 3, "length of the `axis_direction`") _input_check.equal(len(axis_shift), 3, "length of the `axis_shift`") if is_zero_vector(axis_direction): raise ValueError( "The parameter `axis_direction` needs to be a non-zero vector." ) versor_dir_axis = [ pos / sp.sqrt(sum(coord**2 for coord in axis_direction)) for pos in axis_direction ] rotation_matrix = Matrix( [ [ sp.cos(angle) + versor_dir_axis[0] ** 2 * (1 - sp.cos(angle)), versor_dir_axis[0] * versor_dir_axis[1] * (1 - sp.cos(angle)) - versor_dir_axis[2] * sp.sin(angle), versor_dir_axis[0] * versor_dir_axis[2] * (1 - sp.cos(angle)) + versor_dir_axis[1] * sp.sin(angle), ], [ versor_dir_axis[0] * versor_dir_axis[1] * (1 - sp.cos(angle)) + versor_dir_axis[2] * sp.sin(angle), sp.cos(angle) + versor_dir_axis[1] ** 2 * (1 - sp.cos(angle)), versor_dir_axis[1] * versor_dir_axis[2] * (1 - sp.cos(angle)) - versor_dir_axis[0] * sp.sin(angle), ], [ versor_dir_axis[0] * versor_dir_axis[2] * (1 - sp.cos(angle)) - versor_dir_axis[1] * sp.sin(angle), versor_dir_axis[1] * versor_dir_axis[2] * (1 - sp.cos(angle)) + versor_dir_axis[0] * sp.sin(angle), sp.cos(angle) + versor_dir_axis[2] ** 2 * (1 - sp.cos(angle)), ], ] ) opposite_axis_shift = [-axis_shift[0], -axis_shift[1], -axis_shift[2]] if inplace: rotated_framework = framework else: rotated_framework = deepcopy(framework) translate(rotated_framework, opposite_axis_shift, inplace=True) for v, pos in rotated_framework._realization.items(): rotated_framework._realization[v] = rotation_matrix * pos translate(rotated_framework, axis_shift, inplace=True) if inplace: return else: return rotated_framework
[docs] def rotate(framework: FrameworkBase, **kwargs) -> None | FrameworkBase: """ Alias for rotating frameworks based on :func:`~.rotate2D` and :func:`~.rotate3D`. For implementation details and possible parameters, see :func:`~.rotate2D` and :func:`~.rotate3D`. """ _input_check.dimension_for_algorithm(framework.dim, [2, 3], "rotate") if framework.dim == 2: return rotate2D(framework, **kwargs) elif framework.dim == 3: return rotate3D(framework, **kwargs)
[docs] def projected_realization( framework: FrameworkBase, proj_dim: int = None, projection_matrix: Matrix = None, random_seed: int = None, coordinates: Sequence[int] = None, ) -> tuple[dict[Vertex, Point], Matrix]: """ Return the realization projected to a lower dimension and the projection matrix. Parameters ---------- framework: proj_dim: The dimension to which the framework is projected. This is determined from ``projection_matrix`` if it is provided. projection_matrix: The matrix used for projecting the placement of vertices. The matrix must have dimensions ``(proj_dim, dim)``, where ``dim`` is the dimension of the given framework. If ``None``, a numerical random projection matrix is generated. random_seed: The random seed used for generating the projection matrix. coordinates: Indices of coordinates to which projection is applied. Providing the parameter overrides the previous ones. Suggested Improvements ---------------------- Generate random projection matrix over symbolic rationals. """ if coordinates is not None: if not isinstance(coordinates, tuple) and not isinstance(coordinates, list): raise TypeError("The parameter ``coordinates`` must be a tuple or a list.") if max(coordinates) >= framework.dim: raise ValueError( f"Index {np.max(coordinates)} out of range" + f" with placement in dim: {framework.dim}." ) if isinstance(proj_dim, int) and len(coordinates) != proj_dim: raise ValueError( f"The number of coordinates ({len(coordinates)}) does not match" + f" proj_dim ({proj_dim})." ) matrix = np.zeros((len(coordinates), framework.dim)) for i, coord in enumerate(coordinates): matrix[i, coord] = 1 return ( { v: tuple([pos[coord] for coord in coordinates]) for v, pos in framework._realization.items() }, Matrix(matrix), ) if projection_matrix is not None: projection_matrix = np.array(projection_matrix) if projection_matrix.shape[1] != framework.dim: raise ValueError( "The projection matrix has wrong number of columns." + f"{projection_matrix.shape[1]} instead of {framework.dim}." ) if isinstance(proj_dim, int) and projection_matrix.shape[0] != proj_dim: raise ValueError( "The projection matrix has wrong number of rows." + f"{projection_matrix.shape[0]} instead of {framework.dim}." ) if projection_matrix is None: if proj_dim == 2: projection_matrix = _generate_two_orthonormal_vectors( framework.dim, random_seed=random_seed ) elif proj_dim == 3: projection_matrix = _generate_three_orthonormal_vectors( framework.dim, random_seed=random_seed ) else: raise ValueError( "An automatically generated random matrix is supported" + f" only in dimension 2 or 3. {proj_dim} was given instead." ) projection_matrix = projection_matrix.T return ( { v: tuple([float(s[0]) for s in np.dot(projection_matrix, np.array(pos))]) for v, pos in framework.realization(as_points=False, numerical=True).items() }, projection_matrix, )