Skip to content

Commit ab39ca0

Browse files
umutsoysalansyspyansys-ci-botRobPasMue
authored
feat: matrix rotation and translation (#1689)
Co-authored-by: pyansys-ci-bot <[email protected]> Co-authored-by: Roberto Pastor Muela <[email protected]>
1 parent 9105449 commit ab39ca0

File tree

3 files changed

+242
-1
lines changed

3 files changed

+242
-1
lines changed

doc/changelog.d/1689.added.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
matrix rotation and translation

src/ansys/geometry/core/math/matrix.py

Lines changed: 166 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@
2121
# SOFTWARE.
2222
"""Provides matrix primitive representations."""
2323

24-
from typing import Union
24+
from typing import TYPE_CHECKING, Union
25+
26+
if TYPE_CHECKING:
27+
from ansys.geometry.core.math.vector import Vector3D # For type hints
2528

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

131134
return obj
135+
136+
@classmethod
137+
def create_translation(cls, translation: "Vector3D") -> "Matrix44":
138+
"""Create a matrix representing the specified translation.
139+
140+
Parameters
141+
----------
142+
translation : Vector3D
143+
The translation vector representing the translation. The components of the vector
144+
should be in meters.
145+
146+
Returns
147+
-------
148+
Matrix44
149+
A 4x4 matrix representing the translation.
150+
151+
Examples
152+
--------
153+
>>> translation_vector = Vector3D(1.0, 2.0, 3.0)
154+
>>> translation_matrix = Matrix44.create_translation(translation_vector)
155+
>>> print(translation_matrix)
156+
[[1. 0. 0. 1.]
157+
[0. 1. 0. 2.]
158+
[0. 0. 1. 3.]
159+
[0. 0. 0. 1.]]
160+
"""
161+
matrix = cls(
162+
[
163+
[1, 0, 0, translation.x],
164+
[0, 1, 0, translation.y],
165+
[0, 0, 1, translation.z],
166+
[0, 0, 0, 1],
167+
]
168+
)
169+
return matrix
170+
171+
def is_translation(self, including_identity=False):
172+
"""Check if the matrix represents a translation.
173+
174+
This method checks if the matrix represents a translation transformation.
175+
A translation matrix has the following form:
176+
[1 0 0 tx]
177+
[0 1 0 ty]
178+
[0 0 1 tz]
179+
[0 0 0 1]
180+
181+
Parameters
182+
----------
183+
including_identity : bool, optional
184+
If True, the method will return True for the identity matrix as well.
185+
If False, the method will return False for the identity matrix.
186+
187+
Returns
188+
-------
189+
bool
190+
``True`` if the matrix represents a translation, ``False`` otherwise.
191+
192+
Examples
193+
--------
194+
>>> matrix = Matrix44([[1, 0, 0, 5], [0, 1, 0, 3], [0, 0, 1, 2], [0, 0, 0, 1]])
195+
>>> matrix.is_translation()
196+
True
197+
>>> identity_matrix = Matrix44([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]])
198+
>>> identity_matrix.is_translation()
199+
False
200+
>>> identity_matrix.is_translation(including_identity=True)
201+
True
202+
"""
203+
if not (
204+
self.__is_close(self[0][0], 1)
205+
and self.__is_close(self[0][1], 0)
206+
and self.__is_close(self[0][2], 0)
207+
):
208+
return False
209+
if not (
210+
self.__is_close(self[1][0], 0)
211+
and self.__is_close(self[1][1], 1)
212+
and self.__is_close(self[1][2], 0)
213+
):
214+
return False
215+
if not (
216+
self.__is_close(self[2][0], 0)
217+
and self.__is_close(self[2][1], 0)
218+
and self.__is_close(self[2][2], 1)
219+
):
220+
return False
221+
if not self.__is_close(self[2][2], 1):
222+
return False
223+
224+
if (
225+
not including_identity
226+
and self.__is_close(self[0][3], 0)
227+
and self.__is_close(self[1][3], 0)
228+
and self.__is_close(self[2][3], 0)
229+
):
230+
return False
231+
232+
return True
233+
234+
def __is_close(self, a, b, tol=1e-9):
235+
"""Check if two values are close to each other within a tolerance."""
236+
return np.isclose(a, b, atol=tol)
237+
238+
@classmethod
239+
def create_rotation(
240+
cls, direction_x: "Vector3D", direction_y: "Vector3D", direction_z: "Vector3D" = None
241+
) -> "Matrix44":
242+
"""Create a matrix representing the specified rotation.
243+
244+
Parameters
245+
----------
246+
direction_x : Vector3D
247+
The X direction vector.
248+
direction_y : Vector3D
249+
The Y direction vector.
250+
direction_z : Vector3D, optional
251+
The Z direction vector. If not provided, it will be calculated
252+
as the cross product of direction_x and direction_y.
253+
254+
Returns
255+
-------
256+
Matrix44
257+
A 4x4 matrix representing the rotation.
258+
259+
Examples
260+
--------
261+
>>> direction_x = Vector3D(1.0, 0.0, 0.0)
262+
>>> direction_y = Vector3D(0.0, 1.0, 0.0)
263+
>>> rotation_matrix = Matrix44.create_rotation(direction_x, direction_y)
264+
>>> print(rotation_matrix)
265+
[[1. 0. 0. 0.]
266+
[0. 1. 0. 0.]
267+
[0. 0. 1. 0.]
268+
[0. 0. 0. 1.]]
269+
"""
270+
if not direction_x.is_perpendicular_to(direction_y):
271+
raise ValueError("The provided direction vectors are not orthogonal.")
272+
273+
# Normalize the vectors
274+
direction_x = direction_x.normalize()
275+
direction_y = direction_y.normalize()
276+
277+
# Calculate the third direction vector if not provided
278+
if direction_z is None:
279+
direction_z = direction_x.cross(direction_y)
280+
else:
281+
if not (
282+
direction_x.is_perpendicular_to(direction_z)
283+
and direction_y.is_perpendicular_to(direction_z)
284+
):
285+
raise ValueError("The provided direction vectors are not orthogonal.")
286+
direction_z = direction_z.normalize()
287+
288+
matrix = cls(
289+
[
290+
[direction_x.x, direction_y.x, direction_z.x, 0],
291+
[direction_x.y, direction_y.y, direction_z.y, 0],
292+
[direction_x.z, direction_y.z, direction_z.z, 0],
293+
[0, 0, 0, 1],
294+
]
295+
)
296+
return matrix

tests/test_math.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -752,6 +752,81 @@ def test_matrix_44():
752752
assert "Matrix44 should only be a 2D array of shape (4,4)." in str(val.value)
753753

754754

755+
def test_create_translation_matrix():
756+
"""Test the creation of a translation matrix."""
757+
758+
vector = Vector3D([1, 2, 3])
759+
expected_matrix = Matrix44([[1, 0, 0, 1], [0, 1, 0, 2], [0, 0, 1, 3], [0, 0, 0, 1]])
760+
translation_matrix = Matrix44.create_translation(vector)
761+
assert np.array_equal(expected_matrix, translation_matrix)
762+
assert translation_matrix.is_translation()
763+
764+
765+
def test_is_translation():
766+
"""Test the is_translation method of the Matrix44 class."""
767+
matrix = Matrix44([[1, 0, 0, 5], [0, 1, 0, 3], [0, 0, 1, 2], [0, 0, 0, 1]])
768+
assert matrix.is_translation()
769+
# Test the identity matrix
770+
identity_matrix = Matrix44([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]])
771+
assert identity_matrix.is_translation() is False
772+
# Test a matrix that is not a translation (rotation matrix)
773+
rotation_matrix = Matrix44([[0, -1, 0, 0], [1, 0, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]])
774+
assert rotation_matrix.is_translation() is False
775+
rotation_matrix = Matrix44([[1, 0, 1, 5], [0, 1, 0, 3], [0, 0, 1, 2], [0, 0, 0, 1]])
776+
assert (rotation_matrix.is_translation()) is False
777+
778+
779+
def test_create_rotation_matrix():
780+
"""Test the creation of a rotation matrix."""
781+
# Test 0: No rotation
782+
direction_x = Vector3D([1, 0, 0])
783+
direction_y = Vector3D([0, 1, 0])
784+
expected_matrix = Matrix44([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]])
785+
rotation_matrix = Matrix44.create_rotation(direction_x, direction_y)
786+
assert np.array_equal(expected_matrix, rotation_matrix)
787+
788+
# Test: Rotation around Z-axis by 90 degrees counter clockwise
789+
790+
new_x = Vector3D([0, 1, 0])
791+
new_y = Vector3D([-1, 0, 0])
792+
793+
rotation_matrix = Matrix44.create_rotation(new_x, new_y)
794+
p_before = np.array([1, 0, 0, 0])
795+
p_after = rotation_matrix * p_before
796+
p_expected = np.array([0, 1, 0, 0])
797+
assert np.array_equal(p_after, p_expected), "Rotation around Z-axis failed"
798+
799+
# Test: Rotation around Z-axis by 180 degrees counter clockwise
800+
new_x = Vector3D([-1, 0, 0])
801+
new_y = Vector3D([0, -1, 0])
802+
803+
rotation_matrix = Matrix44.create_rotation(new_x, new_y)
804+
p_before = np.array([1, 0, 0, 0])
805+
p_after = rotation_matrix * p_before
806+
p_expected = np.array([-1, 0, 0, 0])
807+
assert np.array_equal(p_after, p_expected), "Rotation around Z-axis failed"
808+
809+
# Test: Rotation around Z-axis by 270 degrees counter clockwise
810+
new_x = Vector3D([0, -1, 0])
811+
new_y = Vector3D([1, 0, 0])
812+
new_z = Vector3D([0, 0, 1])
813+
814+
rotation_matrix = Matrix44.create_rotation(new_x, new_y, new_z)
815+
p_before = np.array([3, 4, 0, 0])
816+
p_after = rotation_matrix * p_before
817+
p_expected = np.array([4, -3, 0, 0])
818+
assert np.array_equal(p_after, p_expected), "Rotation around Z-axis failed"
819+
820+
# Rotation around Z for one radian
821+
direction_x = Vector3D([1, 0, 0])
822+
direction_y = Vector3D([0, 1, 0])
823+
direction_z = Vector3D([0, 0, 1])
824+
825+
expected_matrix = Matrix44([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]])
826+
rotation_matrix = Matrix44.create_rotation(direction_x, direction_y, direction_z)
827+
assert np.array_equal(expected_matrix, rotation_matrix)
828+
829+
755830
def test_frame():
756831
"""``Frame`` construction and equivalency."""
757832
origin = Point3D([42, 99, 13])

0 commit comments

Comments
 (0)