Skip to content

Commit b10fb50

Browse files
umutsoysalansyspyansys-ci-botRobPasMue
committed
feat: matrix helper methods (#1806)
Co-authored-by: pyansys-ci-bot <[email protected]> Co-authored-by: Roberto Pastor Muela <[email protected]>
1 parent c01276f commit b10fb50

File tree

4 files changed

+172
-0
lines changed

4 files changed

+172
-0
lines changed

doc/changelog.d/1806.added.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
matrix helper methods

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

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
from ansys.geometry.core.typing import Real, RealSequence
3131

3232
if TYPE_CHECKING:
33+
from ansys.geometry.core.math.frame import Frame
3334
from ansys.geometry.core.math.vector import Vector3D # For type hints
3435

3536
DEFAULT_MATRIX33 = np.identity(3)
@@ -308,3 +309,61 @@ def create_rotation(
308309
]
309310
)
310311
return matrix
312+
313+
@classmethod
314+
def create_matrix_from_rotation_about_axis(cls, axis: "Vector3D", angle: float) -> "Matrix44":
315+
"""
316+
Create a matrix representing a rotation about a given axis.
317+
318+
Parameters
319+
----------
320+
axis : Vector3D
321+
The axis of rotation.
322+
angle : float
323+
The angle of rotation in radians.
324+
325+
Returns
326+
-------
327+
Matrix44
328+
A 4x4 matrix representing the rotation.
329+
"""
330+
axis_dir = axis.normalize()
331+
x, y, z = axis_dir[0], axis_dir[1], axis_dir[2]
332+
333+
k = np.array([[0, -z, y], [z, 0, -x], [-y, x, 0]])
334+
335+
identity = np.eye(3)
336+
cos_theta = np.cos(angle)
337+
sin_theta = np.sin(angle)
338+
339+
# Rodrigues' rotation formula
340+
rotation_3x3 = identity + sin_theta * k + (1 - cos_theta) * (k @ k)
341+
342+
# Convert to a 4x4 homogeneous matrix
343+
rotation_matrix = np.eye(4)
344+
rotation_matrix[:3, :3] = rotation_3x3
345+
346+
return cls(rotation_matrix)
347+
348+
@classmethod
349+
def create_matrix_from_mapping(cls, frame: "Frame") -> "Matrix44":
350+
"""
351+
Create a matrix representing the specified mapping.
352+
353+
Parameters
354+
----------
355+
frame : Frame
356+
The frame containing the origin and direction vectors.
357+
358+
Returns
359+
-------
360+
Matrix44
361+
A 4x4 matrix representing the translation and rotation defined by the frame.
362+
"""
363+
from ansys.geometry.core.math.vector import Vector3D
364+
365+
translation_matrix = Matrix44.create_translation(
366+
Vector3D([frame.origin[0], frame.origin[1], frame.origin[2]])
367+
)
368+
rotation_matrix = Matrix44.create_rotation(frame.direction_x, frame.direction_y)
369+
return translation_matrix * rotation_matrix

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
from ansys.geometry.core.math.point import Point2D, Point3D
3333
from ansys.geometry.core.misc.accuracy import Accuracy
3434
from ansys.geometry.core.misc.checks import check_ndarray_is_float_int
35+
from ansys.geometry.core.misc.measurements import Angle
3536
from ansys.geometry.core.misc.units import UNITS
3637
from ansys.geometry.core.typing import Real, RealSequence
3738

@@ -163,6 +164,22 @@ def transform(self, matrix: "Matrix44") -> "Vector3D":
163164
result_vector = Vector3D(result_4x1[0:3])
164165
return result_vector
165166

167+
@check_input_types
168+
def rotate_vector(self, vector: "Vector3D", angle: Real | Quantity | Angle) -> "Vector3D":
169+
"""Rotate a vector around a given axis by a specified angle."""
170+
if self.is_zero:
171+
raise Exception("Invalid vector operation: rotation axis cannot be zero.")
172+
173+
# Convert angle to Angle object and get its value in radians
174+
angle = angle if isinstance(angle, Angle) else Angle(angle)
175+
angle_m = angle.value.m_as(UNITS.radian)
176+
177+
axis = self.normalize()
178+
parallel = axis * (vector.dot(axis))
179+
perpendicular1 = vector - parallel
180+
perpendicular2 = axis.cross(perpendicular1)
181+
return parallel + perpendicular1 * np.cos(angle_m) + perpendicular2 * np.sin(angle_m)
182+
166183
@check_input_types
167184
def get_angle_between(self, v: "Vector3D") -> Quantity:
168185
"""Get the angle between this 3D vector and another 3D vector.

tests/test_math.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
from beartype.roar import BeartypeCallHintParamViolation
2626
import numpy as np
27+
from pint import Quantity
2728
import pytest
2829

2930
from ansys.geometry.core.math import (
@@ -628,6 +629,24 @@ def test_vector2d_errors():
628629
v1.get_angle_between(v2)
629630

630631

632+
def test_rotate_vector():
633+
"""Test the rotate_vector method."""
634+
# Define the vectors and angle
635+
axis = Vector3D([0.0, 0.0, 1.0])
636+
vector = Vector3D([1.0, 0.0, 0.0])
637+
638+
angle = Quantity(np.pi / 2) # 90 degrees
639+
640+
# Expected result after rotating vector around axis by 90 degrees
641+
expected_vector = Vector3D([0.0, 1.0, 0.0])
642+
643+
# Call the method under test
644+
result_vector = axis.rotate_vector(vector, angle)
645+
646+
# Assert that the result matches the expected vector
647+
assert np.allclose(result_vector, expected_vector)
648+
649+
631650
def test_matrix():
632651
"""Simple test to create a ``Matrix``."""
633652
# Create two matrix objects
@@ -828,6 +847,82 @@ def test_create_rotation_matrix():
828847
assert np.array_equal(expected_matrix, rotation_matrix)
829848

830849

850+
def test_create_matrix_from_rotation_about_axis_x():
851+
"""Test the create_matrix_from_rotation_about_axis method for rotation about the x-axis."""
852+
axis = Vector3D([1.0, 0.0, 0.0])
853+
angle = np.pi / 2 # 90 degrees
854+
expected_matrix = Matrix44([[1, 0, 0, 0], [0, 0, -1, 0], [0, 1, 0, 0], [0, 0, 0, 1]])
855+
856+
result_matrix = Matrix44.create_matrix_from_rotation_about_axis(axis, angle)
857+
858+
print(result_matrix)
859+
assert np.allclose(result_matrix, expected_matrix)
860+
861+
862+
def test_create_matrix_from_rotation_about_axis_y():
863+
"""Test the create_matrix_from_rotation_about_axis method for rotation about the y-axis."""
864+
axis = Vector3D([0.0, 1.0, 0.0])
865+
angle = np.pi / 2 # 90 degrees
866+
expected_matrix = Matrix44([[0, 0, 1, 0], [0, 1, 0, 0], [-1, 0, 0, 0], [0, 0, 0, 1]])
867+
868+
result_matrix = Matrix44.create_matrix_from_rotation_about_axis(axis, angle)
869+
assert np.allclose(result_matrix, expected_matrix)
870+
871+
872+
def test_create_matrix_from_rotation_about_axis_z():
873+
"""Test the create_matrix_from_rotation_about_axis method for rotation about the z-axis."""
874+
axis = Vector3D([0.0, 0.0, 1.0])
875+
angle = np.pi / 2 # 90 degrees
876+
expected_matrix = Matrix44([[0, -1, 0, 0], [1, 0, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]])
877+
878+
result_matrix = Matrix44.create_matrix_from_rotation_about_axis(axis, angle)
879+
assert np.allclose(result_matrix, expected_matrix)
880+
881+
882+
def test_create_matrix_from_rotation_about_arbitrary_axis():
883+
"""Test the create_matrix_from_rotation_about_axis method for
884+
rotation about an arbitrary axis.
885+
"""
886+
axis = Vector3D([1.0, 1.0, 1.0]).normalize()
887+
angle = np.pi / 3 # 60 degrees
888+
# Expected matrix calculated using external tools or libraries
889+
expected_matrix = Matrix44(
890+
[
891+
[0.66666667, -0.33333333, 0.66666667, 0],
892+
[0.66666667, 0.66666667, -0.33333333, 0],
893+
[-0.33333333, 0.66666667, 0.66666667, 0],
894+
[0, 0, 0, 1],
895+
]
896+
)
897+
898+
result_matrix = Matrix44.create_matrix_from_rotation_about_axis(axis, angle)
899+
assert np.allclose(result_matrix, expected_matrix)
900+
901+
902+
def test_create_matrix_from_mapping():
903+
"""Test the create_matrix_from_mapping method."""
904+
# Define the frame with origin and direction vectors
905+
origin = Vector3D([1.0, 2.0, 3.0])
906+
direction_x = Vector3D([1.0, 0.0, 0.0])
907+
direction_y = Vector3D([0.0, 1.0, 0.0])
908+
frame = Frame(origin, direction_x, direction_y)
909+
910+
# Create the expected translation matrix
911+
expected_translation_matrix = Matrix44.create_translation(origin)
912+
913+
# Create the expected rotation matrix
914+
expected_rotation_matrix = Matrix44.create_rotation(direction_x, direction_y)
915+
916+
# Create the expected result by multiplying the translation and rotation matrices
917+
expected_matrix = expected_translation_matrix * expected_rotation_matrix
918+
919+
# Call the method under test
920+
result_matrix = Matrix44.create_matrix_from_mapping(frame)
921+
922+
# Assert that the result matches the expected matrix
923+
assert np.allclose(result_matrix, expected_matrix)
924+
925+
831926
def test_frame():
832927
"""``Frame`` construction and equivalency."""
833928
origin = Point3D([42, 99, 13])

0 commit comments

Comments
 (0)