Skip to content

Commit

Permalink
feat: matrix rotation and translation (#1689)
Browse files Browse the repository at this point in the history
Co-authored-by: pyansys-ci-bot <[email protected]>
Co-authored-by: Roberto Pastor Muela <[email protected]>
  • Loading branch information
3 people authored Jan 28, 2025
1 parent 9105449 commit ab39ca0
Show file tree
Hide file tree
Showing 3 changed files with 242 additions and 1 deletion.
1 change: 1 addition & 0 deletions doc/changelog.d/1689.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
matrix rotation and translation
167 changes: 166 additions & 1 deletion src/ansys/geometry/core/math/matrix.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@
# SOFTWARE.
"""Provides matrix primitive representations."""

from typing import Union
from typing import TYPE_CHECKING, Union

if TYPE_CHECKING:
from ansys.geometry.core.math.vector import Vector3D # For type hints

from beartype import beartype as check_input_types
import numpy as np
Expand Down Expand Up @@ -129,3 +132,165 @@ def __new__(cls, input: np.ndarray | RealSequence | Matrix = DEFAULT_MATRIX44):
raise ValueError("Matrix44 should only be a 2D array of shape (4,4).")

return obj

@classmethod
def create_translation(cls, translation: "Vector3D") -> "Matrix44":
"""Create a matrix representing the specified translation.
Parameters
----------
translation : Vector3D
The translation vector representing the translation. The components of the vector
should be in meters.
Returns
-------
Matrix44
A 4x4 matrix representing the translation.
Examples
--------
>>> translation_vector = Vector3D(1.0, 2.0, 3.0)
>>> translation_matrix = Matrix44.create_translation(translation_vector)
>>> print(translation_matrix)
[[1. 0. 0. 1.]
[0. 1. 0. 2.]
[0. 0. 1. 3.]
[0. 0. 0. 1.]]
"""
matrix = cls(
[
[1, 0, 0, translation.x],
[0, 1, 0, translation.y],
[0, 0, 1, translation.z],
[0, 0, 0, 1],
]
)
return matrix

def is_translation(self, including_identity=False):
"""Check if the matrix represents a translation.
This method checks if the matrix represents a translation transformation.
A translation matrix has the following form:
[1 0 0 tx]
[0 1 0 ty]
[0 0 1 tz]
[0 0 0 1]
Parameters
----------
including_identity : bool, optional
If True, the method will return True for the identity matrix as well.
If False, the method will return False for the identity matrix.
Returns
-------
bool
``True`` if the matrix represents a translation, ``False`` otherwise.
Examples
--------
>>> matrix = Matrix44([[1, 0, 0, 5], [0, 1, 0, 3], [0, 0, 1, 2], [0, 0, 0, 1]])
>>> matrix.is_translation()
True
>>> identity_matrix = Matrix44([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]])
>>> identity_matrix.is_translation()
False
>>> identity_matrix.is_translation(including_identity=True)
True
"""
if not (
self.__is_close(self[0][0], 1)
and self.__is_close(self[0][1], 0)
and self.__is_close(self[0][2], 0)
):
return False
if not (
self.__is_close(self[1][0], 0)
and self.__is_close(self[1][1], 1)
and self.__is_close(self[1][2], 0)
):
return False
if not (
self.__is_close(self[2][0], 0)
and self.__is_close(self[2][1], 0)
and self.__is_close(self[2][2], 1)
):
return False
if not self.__is_close(self[2][2], 1):
return False

if (
not including_identity
and self.__is_close(self[0][3], 0)
and self.__is_close(self[1][3], 0)
and self.__is_close(self[2][3], 0)
):
return False

return True

def __is_close(self, a, b, tol=1e-9):
"""Check if two values are close to each other within a tolerance."""
return np.isclose(a, b, atol=tol)

@classmethod
def create_rotation(
cls, direction_x: "Vector3D", direction_y: "Vector3D", direction_z: "Vector3D" = None
) -> "Matrix44":
"""Create a matrix representing the specified rotation.
Parameters
----------
direction_x : Vector3D
The X direction vector.
direction_y : Vector3D
The Y direction vector.
direction_z : Vector3D, optional
The Z direction vector. If not provided, it will be calculated
as the cross product of direction_x and direction_y.
Returns
-------
Matrix44
A 4x4 matrix representing the rotation.
Examples
--------
>>> direction_x = Vector3D(1.0, 0.0, 0.0)
>>> direction_y = Vector3D(0.0, 1.0, 0.0)
>>> rotation_matrix = Matrix44.create_rotation(direction_x, direction_y)
>>> print(rotation_matrix)
[[1. 0. 0. 0.]
[0. 1. 0. 0.]
[0. 0. 1. 0.]
[0. 0. 0. 1.]]
"""
if not direction_x.is_perpendicular_to(direction_y):
raise ValueError("The provided direction vectors are not orthogonal.")

# Normalize the vectors
direction_x = direction_x.normalize()
direction_y = direction_y.normalize()

# Calculate the third direction vector if not provided
if direction_z is None:
direction_z = direction_x.cross(direction_y)
else:
if not (
direction_x.is_perpendicular_to(direction_z)
and direction_y.is_perpendicular_to(direction_z)
):
raise ValueError("The provided direction vectors are not orthogonal.")
direction_z = direction_z.normalize()

matrix = cls(
[
[direction_x.x, direction_y.x, direction_z.x, 0],
[direction_x.y, direction_y.y, direction_z.y, 0],
[direction_x.z, direction_y.z, direction_z.z, 0],
[0, 0, 0, 1],
]
)
return matrix
75 changes: 75 additions & 0 deletions tests/test_math.py
Original file line number Diff line number Diff line change
Expand Up @@ -752,6 +752,81 @@ def test_matrix_44():
assert "Matrix44 should only be a 2D array of shape (4,4)." in str(val.value)


def test_create_translation_matrix():
"""Test the creation of a translation matrix."""

vector = Vector3D([1, 2, 3])
expected_matrix = Matrix44([[1, 0, 0, 1], [0, 1, 0, 2], [0, 0, 1, 3], [0, 0, 0, 1]])
translation_matrix = Matrix44.create_translation(vector)
assert np.array_equal(expected_matrix, translation_matrix)
assert translation_matrix.is_translation()


def test_is_translation():
"""Test the is_translation method of the Matrix44 class."""
matrix = Matrix44([[1, 0, 0, 5], [0, 1, 0, 3], [0, 0, 1, 2], [0, 0, 0, 1]])
assert matrix.is_translation()
# Test the identity matrix
identity_matrix = Matrix44([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]])
assert identity_matrix.is_translation() is False
# Test a matrix that is not a translation (rotation matrix)
rotation_matrix = Matrix44([[0, -1, 0, 0], [1, 0, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]])
assert rotation_matrix.is_translation() is False
rotation_matrix = Matrix44([[1, 0, 1, 5], [0, 1, 0, 3], [0, 0, 1, 2], [0, 0, 0, 1]])
assert (rotation_matrix.is_translation()) is False


def test_create_rotation_matrix():
"""Test the creation of a rotation matrix."""
# Test 0: No rotation
direction_x = Vector3D([1, 0, 0])
direction_y = Vector3D([0, 1, 0])
expected_matrix = Matrix44([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]])
rotation_matrix = Matrix44.create_rotation(direction_x, direction_y)
assert np.array_equal(expected_matrix, rotation_matrix)

# Test: Rotation around Z-axis by 90 degrees counter clockwise

new_x = Vector3D([0, 1, 0])
new_y = Vector3D([-1, 0, 0])

rotation_matrix = Matrix44.create_rotation(new_x, new_y)
p_before = np.array([1, 0, 0, 0])
p_after = rotation_matrix * p_before
p_expected = np.array([0, 1, 0, 0])
assert np.array_equal(p_after, p_expected), "Rotation around Z-axis failed"

# Test: Rotation around Z-axis by 180 degrees counter clockwise
new_x = Vector3D([-1, 0, 0])
new_y = Vector3D([0, -1, 0])

rotation_matrix = Matrix44.create_rotation(new_x, new_y)
p_before = np.array([1, 0, 0, 0])
p_after = rotation_matrix * p_before
p_expected = np.array([-1, 0, 0, 0])
assert np.array_equal(p_after, p_expected), "Rotation around Z-axis failed"

# Test: Rotation around Z-axis by 270 degrees counter clockwise
new_x = Vector3D([0, -1, 0])
new_y = Vector3D([1, 0, 0])
new_z = Vector3D([0, 0, 1])

rotation_matrix = Matrix44.create_rotation(new_x, new_y, new_z)
p_before = np.array([3, 4, 0, 0])
p_after = rotation_matrix * p_before
p_expected = np.array([4, -3, 0, 0])
assert np.array_equal(p_after, p_expected), "Rotation around Z-axis failed"

# Rotation around Z for one radian
direction_x = Vector3D([1, 0, 0])
direction_y = Vector3D([0, 1, 0])
direction_z = Vector3D([0, 0, 1])

expected_matrix = Matrix44([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]])
rotation_matrix = Matrix44.create_rotation(direction_x, direction_y, direction_z)
assert np.array_equal(expected_matrix, rotation_matrix)


def test_frame():
"""``Frame`` construction and equivalency."""
origin = Point3D([42, 99, 13])
Expand Down

0 comments on commit ab39ca0

Please sign in to comment.