Skip to content

Commit 8c4be07

Browse files
koubaaMohamed Koubaapyansys-ci-bot
authored
feat: begin to handle *DEFINE_TRANSFORMATION when expanding include decks (#740)
Co-authored-by: Mohamed Koubaa <[email protected]> Co-authored-by: pyansys-ci-bot <[email protected]>
1 parent 4fa63c0 commit 8c4be07

File tree

7 files changed

+335
-1
lines changed

7 files changed

+335
-1
lines changed

doc/changelog/740.test.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
feat: begin to handle *DEFINE_TRANSFORMATION when expanding include decks

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ dependencies = ["ansys-dpf-core>=0.7.2",
3232
"hollerith>=0.6.0",
3333
"numpy>=1",
3434
"pandas>=2.0",
35-
"appdirs>=1.4.4"
35+
"appdirs>=1.4.4",
36+
"transformations==2025.1.1",
3637
]
3738

3839
[project.optional-dependencies]

src/ansys/dyna/core/lib/transforms/node_transform.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,47 @@
2121
# SOFTWARE.
2222

2323
"""Transformation handler for *NODE."""
24+
import warnings
25+
26+
import numpy as np
27+
import pandas as pd
2428

2529
from ansys.dyna.core import keywords as kwd
2630
from ansys.dyna.core.lib.transforms.base_transform import Transform
31+
from ansys.dyna.core.lib.transforms.utils.define_transformation import get_transform_matrix
32+
33+
34+
def apply_rigid_transform(mtx: np.ndarray, nodes: pd.DataFrame) -> None:
35+
locations = nodes[["x", "y", "z"]]
36+
locations = locations.fillna(0.0)
37+
locations["w"] = 1.0
38+
locs = locations.to_numpy()
39+
for i, loc in enumerate(locs):
40+
res = np.dot(mtx, loc) # transform the point
41+
locs[i] = res
42+
43+
locations[["x", "y", "z", "w"]] = locs
44+
nodes[["x", "y", "z"]] = locations[["x", "y", "z"]]
2745

2846

2947
class TransformNode(Transform):
3048
def transform(self, keyword: kwd.Node) -> None:
49+
self._apply_offset(keyword)
50+
try:
51+
self._apply_transform(keyword)
52+
except Exception as e:
53+
warnings.warn(f"Error applying transformation to *NODE: {e}")
54+
raise e
55+
56+
def _apply_offset(self, keyword: kwd.Node) -> None:
3157
offset = self._xform.idnoff
3258
if offset is None or offset == 0:
3359
return
3460
keyword.nodes["nid"] = keyword.nodes["nid"] + offset
61+
62+
def _apply_transform(self, keyword: kwd.Node) -> None:
63+
define_transform = self._xform.tranid_link
64+
mtx = get_transform_matrix(define_transform)
65+
if mtx is None:
66+
return
67+
apply_rigid_transform(mtx, keyword.nodes)

src/ansys/dyna/core/lib/transforms/utils/__init__.py

Whitespace-only changes.
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
# Copyright (C) 2021 - 2024 ANSYS, Inc. and/or its affiliates.
2+
# SPDX-License-Identifier: MIT
3+
#
4+
#
5+
# Permission is hereby granted, free of charge, to any person obtaining a copy
6+
# of this software and associated documentation files (the "Software"), to deal
7+
# in the Software without restriction, including without limitation the rights
8+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
# copies of the Software, and to permit persons to whom the Software is
10+
# furnished to do so, subject to the following conditions:
11+
#
12+
# The above copyright notice and this permission notice shall be included in all
13+
# copies or substantial portions of the Software.
14+
#
15+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
# SOFTWARE.
22+
23+
"""Algorithms to compute 4x4 transformation matrices from a *DEFINE_TRANSFORMATION keyword."""
24+
25+
import math
26+
import typing
27+
import warnings
28+
29+
import numpy as np
30+
import pandas as pd
31+
import transformations as tfm
32+
33+
from ansys.dyna.core.keywords.keyword_classes.auto.define_transformation import DefineTransformation
34+
35+
36+
def _get_translation_matrix(a1: float, a2: float, a3: float) -> np.ndarray:
37+
return tfm.translation_matrix((a1, a2, a3))
38+
39+
40+
def _get_rotation_matrix(a1: float, a2: float, a3: float, a4: float, a5: float, a6: float, a7: float) -> np.ndarray:
41+
if (a4, a5, a6) == (0.0, 0.0, 0.0):
42+
if a7 != 0.0:
43+
# simple rotation about an axis going through the origin
44+
assert (a1, a2, a3) != (0.0, 0.0, 0.0), "Direction vector A1, A2, A3 cannot be all zero!"
45+
return tfm.rotation_matrix(math.radians(a7), [a1, a2, a3])
46+
47+
parameters = (a1, a2, a3, a4, a5, a6, a7)
48+
warnings.warn(f"DEFINE_TRANFORMATION ROTATE option with parameters {parameters} not handled yet by pydyna!")
49+
return None
50+
51+
52+
def _get_row_transform_matrix(transform: pd.Series) -> np.ndarray:
53+
"""Transform is of a series of the form:
54+
55+
option ROTATE
56+
a1 0.0
57+
a2 0.0
58+
a3 1.0
59+
a4 0.0
60+
a5 0.0
61+
a6 0.0
62+
a7 -25.0
63+
64+
whose behavior is according to the rules of *DEFINE_TRANSFORM
65+
"""
66+
mtx = tfm.identity_matrix()
67+
xf = transform.fillna(0.0)
68+
option, a1, a2, a3, a4, a5, a6, a7 = (
69+
xf["option"],
70+
xf["a1"],
71+
xf["a2"],
72+
xf["a3"],
73+
xf["a4"],
74+
xf["a5"],
75+
xf["a6"],
76+
xf["a7"],
77+
)
78+
if option == "TRANSL":
79+
return _get_translation_matrix(a1, a2, a3)
80+
elif option == "ROTATE":
81+
return _get_rotation_matrix(a1, a2, a3, a4, a5, a6, a7)
82+
else:
83+
warnings.warn(f"DEFINE_TRANFORMATION option {option} not handled yet by pydyna!")
84+
return None
85+
86+
87+
def _get_transform_matrix(transforms: pd.DataFrame) -> np.ndarray:
88+
mtx = tfm.identity_matrix()
89+
for index, transform in transforms.iterrows():
90+
row_mtx = _get_row_transform_matrix(transform)
91+
if row_mtx is None:
92+
return None
93+
mtx = tfm.concatenate_matrices(mtx, row_mtx)
94+
return mtx
95+
96+
97+
def get_transform_matrix(kwd: typing.Optional[DefineTransformation]) -> typing.Optional[np.ndarray]:
98+
if kwd is None:
99+
return None
100+
transforms = kwd.transforms
101+
if len(transforms) == 0:
102+
return None
103+
mtx = _get_transform_matrix(transforms)
104+
return mtx

tests/test_deck.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
# SOFTWARE.
2222

2323
import os
24+
import re
2425

2526
import pandas as pd
2627

@@ -460,6 +461,84 @@ def _transform_part_ids(self, elements: pd.DataFrame):
460461
assert deck.keywords[1].elements["pid"][0] == 44
461462
assert deck.keywords[1].elements["pid"][3] == 44
462463

464+
465+
@pytest.mark.keywords
466+
def test_deck_expand_with_define_transform(file_utils):
467+
"""Test using a custom transform handler as an override."""
468+
469+
# single translation
470+
include_path = file_utils.get_asset_file_path("transform")
471+
define_transform_kwd = kwd.DefineTransformation(tranid=1, option="TRANSL", a1=-100.0)
472+
xform = kwd.IncludeTransform(filename = os.path.join(include_path, "test.k"))
473+
xform.tranid_link = define_transform_kwd
474+
475+
deck = Deck()
476+
deck.extend([define_transform_kwd, xform])
477+
expanded = deck.expand()
478+
assert len(expanded.keywords) == 4
479+
assert expanded.keywords[3].nodes["x"][0] == -600
480+
481+
# error case
482+
deck = Deck()
483+
define_transform_kwd = kwd.DefineTransformation(tranid=1, option="ROTATE", a1=0.0, a2=0.0, a3=0.0, a7=45.0)
484+
xform = kwd.IncludeTransform(filename = os.path.join(include_path, "test.k"))
485+
xform.tranid_link = define_transform_kwd
486+
deck.extend([define_transform_kwd, xform])
487+
expected_warning = "Error applying transformation to *NODE: Direction vector A1, A2, A3 cannot be all zero!"
488+
expected_warning_expression = re.escape(expected_warning)
489+
with pytest.warns(UserWarning, match=expected_warning_expression):
490+
deck.expand()
491+
492+
# unhandled case
493+
deck = Deck()
494+
define_transform_kwd = kwd.DefineTransformation(tranid=1, option="ROTATE", a4=1.0)
495+
xform = kwd.IncludeTransform(filename = os.path.join(include_path, "test.k"))
496+
xform.tranid_link = define_transform_kwd
497+
deck.extend([define_transform_kwd, xform])
498+
with pytest.warns(UserWarning, match="DEFINE_TRANFORMATION ROTATE option with parameters"):
499+
deck.expand()
500+
501+
# multiple transforms
502+
deck = Deck()
503+
define_transform_kwd = kwd.DefineTransformation()
504+
define_transform_kwd.transforms = pd.DataFrame(
505+
{
506+
"option": ["ROTATE", "TRANSL"],
507+
"a1": [0.0, -100.0],
508+
"a2": [0.0, 0.0],
509+
"a3": [1.0, 0.0],
510+
"a4": [0.0, 0.0],
511+
"a5": [0.0, 0.0],
512+
"a6": [0.0, 0.0],
513+
"a7": [25.0, 0.0],
514+
}
515+
)
516+
xform = kwd.IncludeTransform(filename = os.path.join(include_path, "test.k"))
517+
xform.tranid_link = define_transform_kwd
518+
deck.extend([define_transform_kwd, xform])
519+
expanded = deck.expand()
520+
assert len(expanded.keywords) == 4
521+
print(expanded.keywords[3].nodes)
522+
assert expanded.keywords[3].nodes["x"][0] == pytest.approx(-543.784672)
523+
assert expanded.keywords[3].nodes["y"][0] == pytest.approx(-253.570957)
524+
525+
526+
@pytest.mark.keywords
527+
def test_deck_clear():
528+
node = kwd.Node()
529+
deck = Deck()
530+
deck.append(node)
531+
deck.clear()
532+
# ok after clear
533+
deck = Deck()
534+
deck.append(node)
535+
536+
# not ok without clear
537+
deck2 = Deck()
538+
with pytest.raises(Exception):
539+
deck2.append(node)
540+
541+
463542
@pytest.mark.keywords
464543
def test_deck_unprocessed(ref_string):
465544
deck = Deck()

tests/test_transform.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
2+
import math
3+
import re
4+
5+
from ansys.dyna.core import keywords as kwd
6+
from ansys.dyna.core.lib.transforms.utils.define_transformation import get_transform_matrix
7+
8+
import numpy as np
9+
import pandas as pd
10+
import pytest
11+
import transformations as tfm
12+
13+
@pytest.mark.keywords
14+
def test_transform_none():
15+
"""Verify no transformation for no input."""
16+
mtx = get_transform_matrix(None)
17+
assert mtx is None
18+
19+
@pytest.mark.keywords
20+
def test_transform_empty():
21+
"""Verify no transfomration matrix for an empty DEFINE_TRANFORMATION."""
22+
define_transform_kwd = kwd.DefineTransformation()
23+
mtx = get_transform_matrix(None)
24+
assert mtx is None
25+
26+
@pytest.mark.keywords
27+
def test_transform_rotation_1():
28+
"""Verify the transfomration matrix for single ROTATE."""
29+
30+
# The transformation of a rotation of 45 degrees about the X axis
31+
define_transform_kwd = kwd.DefineTransformation(option="ROTATE", a1=1, a2=0, a3=0, a7=45)
32+
mtx = get_transform_matrix(define_transform_kwd)
33+
ref_45x = tfm.rotation_matrix(math.radians(45),[1,0,0])
34+
assert np.allclose(mtx, ref_45x)
35+
36+
# not yet supported if a4-a7 are zero
37+
define_transform_kwd = kwd.DefineTransformation(option="ROTATE", a1=1, a2=0, a3=0)
38+
expected_warning = "DEFINE_TRANFORMATION ROTATE option with parameters (1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0) not handled yet by pydyna!"
39+
expected_warning_expression = re.escape(expected_warning)
40+
with pytest.warns(UserWarning, match=expected_warning_expression):
41+
mtx = get_transform_matrix(define_transform_kwd)
42+
assert mtx is None
43+
44+
# not yet supported if a4-a7 are zero
45+
define_transform_kwd = kwd.DefineTransformation(option="ROTATE", a1=0, a2=0, a3=0, a7=0)
46+
expected_warning = "DEFINE_TRANFORMATION ROTATE option with parameters (0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0) not handled yet by pydyna!"
47+
expected_warning_expression = re.escape(expected_warning)
48+
with pytest.warns(UserWarning, match=expected_warning_expression):
49+
mtx = get_transform_matrix(define_transform_kwd)
50+
assert mtx is None
51+
52+
# 360 degrees about any arbitrary axis is the identity matrix
53+
define_transform_kwd = kwd.DefineTransformation(option="ROTATE", a1=0.4, a2=3.2, a3=1.1, a7=360.0)
54+
mtx = get_transform_matrix(define_transform_kwd)
55+
ref_i = tfm.identity_matrix()
56+
assert np.allclose(mtx,ref_i)
57+
58+
# a1, a2, a3 cannot be zero if the angle of rotation is nonzero
59+
define_transform_kwd = kwd.DefineTransformation(option="ROTATE", a1=0, a2=0, a3=0, a7=90)
60+
with pytest.raises(AssertionError):
61+
get_transform_matrix(define_transform_kwd)
62+
63+
@pytest.mark.keywords
64+
def test_transform_matrix_two_translations():
65+
"""Verify the transformation matrix for multiple TRANSL"""
66+
define_transform_kwd = kwd.DefineTransformation()
67+
define_transform_kwd.transforms = pd.DataFrame(
68+
{
69+
"option": ["TRANSL", "TRANSL"],
70+
"a1": [0.0, -100.0],
71+
"a2": [0.0, 0.0],
72+
"a3": [200.0, 0.0],
73+
}
74+
)
75+
mtx = get_transform_matrix(define_transform_kwd)
76+
ref = tfm.translation_matrix((-100,0,200))
77+
assert np.allclose(mtx, ref)
78+
79+
@pytest.mark.keywords
80+
def test_transform_matrix_one_translations():
81+
"""Verify the transformation matrix for a single TRANSL."""
82+
define_transform_kwd = kwd.DefineTransformation(option="TRANSL", a1=-200, a2=0.0, a3=0.0)
83+
mtx = get_transform_matrix(define_transform_kwd)
84+
ref = tfm.translation_matrix((-200,0,0))
85+
assert np.allclose(mtx, ref)
86+
87+
@pytest.mark.keywords
88+
def test_transform_matrix_translation_rotation():
89+
"""Verify the transformation matrix for a single TRANSL."""
90+
define_transform_kwd = kwd.DefineTransformation()
91+
define_transform_kwd.transforms = pd.DataFrame(
92+
{
93+
"option": ["ROTATE", "TRANSL"],
94+
"a1": [0.0, -100.0],
95+
"a2": [0.0, 0.0],
96+
"a3": [1.0, 0.0],
97+
"a4": [0.0, 0.0],
98+
"a5": [0.0, 0.0],
99+
"a6": [0.0, 0.0],
100+
"a7": [25.0, 0.0],
101+
}
102+
)
103+
t_25z = tfm.rotation_matrix(math.radians(25),[0,0,1])
104+
t_trans = tfm.translation_matrix((-100,0,0))
105+
ref = tfm.concatenate_matrices(t_25z, t_trans)
106+
mtx = get_transform_matrix(define_transform_kwd)
107+
assert np.allclose(mtx, ref)
108+
109+
@pytest.mark.keywords
110+
def test_transform_unhandled():
111+
"""Verify warning and no transformation when an unhandled DEFINE_TRANSFORMATION option is used."""
112+
option = "TRANSL2ND"
113+
define_transform_kwd = kwd.DefineTransformation(option=option)
114+
with pytest.warns(UserWarning, match=f"DEFINE_TRANFORMATION option {option} not handled yet by pydyna!"):
115+
mtx = get_transform_matrix(define_transform_kwd)
116+
assert mtx is None

0 commit comments

Comments
 (0)