diff --git a/doc/changelog/709.miscellaneous.md b/doc/changelog/709.miscellaneous.md new file mode 100644 index 000000000..8f1f24f93 --- /dev/null +++ b/doc/changelog/709.miscellaneous.md @@ -0,0 +1 @@ +feat: Start to handle *INCLUDE_TRANSFORM in Deck.expand() \ No newline at end of file diff --git a/src/ansys/dyna/core/lib/deck.py b/src/ansys/dyna/core/lib/deck.py index 332920bfb..2bcaebfe1 100644 --- a/src/ansys/dyna/core/lib/deck.py +++ b/src/ansys/dyna/core/lib/deck.py @@ -28,9 +28,11 @@ import warnings from ansys.dyna.core.lib.format_type import format_type +from ansys.dyna.core.lib.import_handler import ImportContext, ImportHandler from ansys.dyna.core.lib.io_utils import write_or_return from ansys.dyna.core.lib.keyword_base import KeywordBase from ansys.dyna.core.lib.parameter_set import ParameterSet +from ansys.dyna.core.lib.transform import TransformHandler class Deck: @@ -43,6 +45,8 @@ def __init__(self, title: str = None, **kwargs): self.comment_header: str = None self.title: str = title self.format: format_type = kwargs.get("format", format_type.default) + self._import_handlers: typing.List[ImportHandler] = list() + self._transform_handler = TransformHandler() def __add__(self, other): """Add two decks together.""" @@ -58,6 +62,14 @@ def clear(self): self.title = None self.format = format_type.default + @property + def transform_handler(self) -> TransformHandler: + return self._transform_handler + + def register_import_handler(self, import_handler: ImportHandler) -> None: + """Registers an ImportHandler object""" + self._import_handlers.append(import_handler) + @property def parameters(self) -> ParameterSet: return self._parameter_set @@ -174,8 +186,16 @@ def _expand_helper(self, search_paths: typing.List[str], recurse: bool) -> typin for search_path in search_paths: include_file = os.path.join(search_path, keyword.filename) include_deck = Deck(format=keyword.format) + for import_handler in self._import_handlers: + include_deck.register_import_handler(import_handler) try: - include_deck.import_file(include_file) + xform = None + if keyword.subkeyword == "TRANSFORM": + xform = keyword + include_deck.register_import_handler(self.transform_handler) + context = ImportContext(xform, self, include_file) + encoding = "utf-8" # TODO - how to control encoding in expand? + include_deck._import_file(include_file, "utf-8", context) success = True break except FileNotFoundError: @@ -295,7 +315,9 @@ def _write(buf): return write_or_return(buf, _write) - def loads(self, value: str) -> "ansys.dyna.keywords.lib.deck_loader.DeckLoaderResult": # noqa: F821 + def loads( + self, value: str, context: typing.Optional[ImportContext] = None + ) -> "ansys.dyna.keywords.lib.deck_loader.DeckLoaderResult": # noqa: F821 """Load all keywords from the keyword file as a string. When adding all keywords from the file, this method @@ -304,15 +326,17 @@ def loads(self, value: str) -> "ansys.dyna.keywords.lib.deck_loader.DeckLoaderRe Parameters ---------- value : str + context: ImportContext + the context """ - # import this only when loading to avoid the circular - # imports - # ansys.dyna.keywords imports deck, deck imports deck_loader, + # import deck_loader only when loading to avoid circular imports + + # ansys.dyna.keywords imports deck, deck imports deck_loader # deck_loader imports ansys.dyna.keywords - from .deck_loader import load_deck + from ansys.dyna.core.lib.deck_loader import load_deck - result = load_deck(self, value) + result = load_deck(self, value, context, self._import_handlers) return result def _check_unique(self, type: str, field: str) -> None: @@ -404,8 +428,12 @@ def get(self, **kwargs) -> typing.List[KeywordBase]: return [kwd for kwd in kwds if kwargs["filter"](kwd)] return kwds + def _import_file(self, path: str, encoding: str, context: ImportContext): + with open(path, encoding=encoding) as f: + return self.loads(f.read(), context) + def import_file( - self, path: str, encoding="utf-8" + self, path: str, encoding: str = "utf-8" ) -> "ansys.dyna.keywords.lib.deck_loader.DeckLoaderResult": # noqa: F821 """Import a keyword file. @@ -413,9 +441,11 @@ def import_file( ---------- path : str Full path for the keyword file. + encoding: str + String encoding used to read the keyword file. """ - with open(path, encoding=encoding) as f: - return self.loads(f.read()) + context = ImportContext(None, self, path) + self._import_file(path, encoding, context) def export_file(self, path: str, encoding="utf-8") -> None: """Export the keyword file to a new keyword file. diff --git a/src/ansys/dyna/core/lib/deck_loader.py b/src/ansys/dyna/core/lib/deck_loader.py index 9f66baecf..472d30c26 100644 --- a/src/ansys/dyna/core/lib/deck_loader.py +++ b/src/ansys/dyna/core/lib/deck_loader.py @@ -25,6 +25,7 @@ import ansys.dyna.core from ansys.dyna.core.lib.format_type import format_type +from ansys.dyna.core.lib.import_handler import ImportContext, ImportHandler from ansys.dyna.core.lib.keyword_base import KeywordBase @@ -84,7 +85,13 @@ def _get_kwd_class_and_format(keyword_name: str) -> str: return keyword_object_type, format -def _try_load_deck(deck: "ansys.dyna.core.deck.Deck", text: str, result: DeckLoaderResult) -> None: +def _try_load_deck( + deck: "ansys.dyna.core.deck.Deck", + text: str, + result: DeckLoaderResult, + context: typing.Optional[ImportContext], + import_handlers: typing.List[ImportHandler], +) -> None: lines = text.splitlines() iterator = iter(lines) iterstate = IterState.USERCOMMENT @@ -127,13 +134,40 @@ def update_deck_title(block: typing.List[str], deck: "ansys.dyna.core.deck.Deck" assert len(block) == 2, "Title block can only have one line" deck.title = block[1] + def before_import(block: typing.List[str], keyword: str, keyword_data: str) -> bool: + if len(import_handlers) == 0: + return True + + assert context != None + s = io.StringIO() + s.write(keyword_data) + s.seek(0) + + for handler in import_handlers: + if not handler.before_import(context, keyword, s): + return False + s.seek(0) + return True + + def on_error(error): + for handler in import_handlers: + handler.on_error(error) + + def after_import(keyword): + for handler in import_handlers: + handler.after_import(context, keyword) + def handle_keyword(block: typing.List[str], deck: "ansys.dyna.core.deck.Deck") -> None: keyword = block[0].strip() keyword_data = "\n".join(block) + do_import = before_import(block, keyword, keyword_data) + if not do_import: + return keyword_object_type, format = _get_kwd_class_and_format(keyword) if keyword_object_type == None: result.add_unprocessed_keyword(keyword) deck.append(keyword_data) + after_import(keyword_data) else: import ansys.dyna.core.keywords @@ -144,9 +178,12 @@ def handle_keyword(block: typing.List[str], deck: "ansys.dyna.core.deck.Deck") - try: keyword_object.loads(keyword_data, deck.parameters) deck.append(keyword_object) + after_import(keyword_object) except Exception as e: + on_error(e) result.add_unprocessed_keyword(keyword) deck.append(keyword_data) + after_import(keyword_data) def handle_block(iterstate: int, block: typing.List[str]) -> bool: if iterstate == IterState.END: @@ -185,7 +222,12 @@ def handle_block(iterstate: int, block: typing.List[str]) -> bool: return -def load_deck(deck: "ansys.dyna.core.deck.Deck", text: str) -> DeckLoaderResult: +def load_deck( + deck: "ansys.dyna.core.deck.Deck", + text: str, + context: typing.Optional[ImportContext], + import_handlers: typing.List[ImportHandler], +) -> DeckLoaderResult: result = DeckLoaderResult() - _try_load_deck(deck, text, result) + _try_load_deck(deck, text, result, context, import_handlers) return result diff --git a/src/ansys/dyna/core/lib/import_handler.py b/src/ansys/dyna/core/lib/import_handler.py new file mode 100644 index 000000000..6a23aec65 --- /dev/null +++ b/src/ansys/dyna/core/lib/import_handler.py @@ -0,0 +1,71 @@ +# Copyright (C) 2021 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +"""Import handler used by the import deck feature""" + +import dataclasses +import typing +import warnings + +from ansys.dyna.core.lib.keyword_base import KeywordBase + + +@dataclasses.dataclass +class ImportContext: + """Optional transformation to apply, using type `IncludeTransform`""" + + xform: typing.Any = None + + """Deck into which the import is occurring.""" + deck: typing.Any = None + + """Path of file that is importing.""" + path: str = None + + +class ImportHandler: + """Base class for import handlers.""" + + def before_import(self, context: ImportContext, keyword: str, buffer: typing.TextIO): + """Event called before reading a keyword. + + `keyword` is the string label of the keyword + `buffer` is a copy of the buffer to read from. + + Usage: + Return True if the keyword is to be imported as usual. + Return False if the keyword is not to be imported. + """ + return True + + def after_import(self, context: ImportContext, keyword: typing.Union[str, KeywordBase]): + """Event called after a keyword is imported. + + `keyword` is the imported keyword. It could be a string or a keyword object + + Depending on the `context` is a + """ + pass + + def on_error(self, error): + # TODO - use logging + warnings.warn(f"error in importhandler {self}: {error}") diff --git a/src/ansys/dyna/core/lib/transform.py b/src/ansys/dyna/core/lib/transform.py new file mode 100644 index 000000000..184666dca --- /dev/null +++ b/src/ansys/dyna/core/lib/transform.py @@ -0,0 +1,65 @@ +# Copyright (C) 2021 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +"""Transformation handler for INCLUDE_TRANSFORM.""" +import typing +import warnings + +from ansys.dyna.core.lib.import_handler import ImportContext, ImportHandler +from ansys.dyna.core.lib.keyword_base import KeywordBase +from ansys.dyna.core.lib.transforms.base_transform import Transform +from ansys.dyna.core.lib.transforms.element_transform import TransformElement +from ansys.dyna.core.lib.transforms.node_transform import TransformNode + + +class TransformHandler(ImportHandler): + def __init__(self): + self._handlers: typing.Dict[typing.Union[str, typing.Tuple[str, str]], Transform] = { + "NODE": TransformNode, + "ELEMENT": TransformElement, + } + + def register_transform_handler( + self, identity: typing.Union[str, typing.Tuple[str, str]], handler: Transform + ) -> None: + self._handlers[identity] = handler + + def after_import(self, context: ImportContext, keyword: typing.Union[KeywordBase, str]) -> None: + if isinstance(keyword, str): + return + if context.xform is None: + return + print(self._handlers.keys()) + # first try to get the specialized handler for the keyword + subkeyword + identity = (keyword.keyword, keyword.subkeyword) + handler = self._handlers.get(identity, None) + if handler is None: + # then try to get the handler for the keyword + identity = keyword.keyword + handler = self._handlers.get(identity, None) + if handler is None: + warnings.warn() + return + + # if a handler was found, initialize it with the xform from the context + # and transform the keyword with it + handler(context.xform).transform(keyword) diff --git a/src/ansys/dyna/core/lib/transforms/__init__.py b/src/ansys/dyna/core/lib/transforms/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/ansys/dyna/core/lib/transforms/base_transform.py b/src/ansys/dyna/core/lib/transforms/base_transform.py new file mode 100644 index 000000000..fbf224905 --- /dev/null +++ b/src/ansys/dyna/core/lib/transforms/base_transform.py @@ -0,0 +1,33 @@ +# Copyright (C) 2021 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +"""Base Transform class.""" + +from ansys.dyna.core import keywords as kwd + + +class Transform: + def __init__(self, xform: kwd.IncludeTransform): + self._xform: kwd.IncludeTransform = xform + + def transform(self, keyword) -> None: + raise Exception("Implementation required for transform") diff --git a/src/ansys/dyna/core/lib/transforms/element_transform.py b/src/ansys/dyna/core/lib/transforms/element_transform.py new file mode 100644 index 000000000..6ead8e0b9 --- /dev/null +++ b/src/ansys/dyna/core/lib/transforms/element_transform.py @@ -0,0 +1,57 @@ +import typing +import warnings + +import pandas as pd + +from ansys.dyna.core.lib.transform import Transform + + +class TransformElement(Transform): + def transform(self, keyword: typing.Any): + elements = self._get_elements_dataframe(keyword) + if elements is None: + return + self._transform_node_ids(elements) + self._transform_element_ids(elements) + self._transform_part_ids(elements) + + def _get_elements_dataframe(self, keyword) -> typing.Optional[pd.DataFrame]: + warning = f"keyword {keyword.keyword}_{keyword.subkeyword} not transformed!" + if not hasattr(keyword, "elements"): + warnings.warn(warning) + return None + elements = keyword.elements + if not isinstance(elements, pd.DataFrame): + warnings.warn(warning) + return None + return elements + + def _offset_column(self, df: pd.DataFrame, column: str, offset: int) -> None: + if column in df: + # TODO - check if the value is na, not just != 0 + df[column] = df[column].mask(df[column] != 0, df[column] + offset) + + def _transform_node_ids(self, elements: pd.DataFrame): + offset = self._xform.idnoff + if offset is None or offset == 0: + return + self._offset_column(elements, "n1", offset) + self._offset_column(elements, "n2", offset) + self._offset_column(elements, "n3", offset) + self._offset_column(elements, "n4", offset) + self._offset_column(elements, "n5", offset) + self._offset_column(elements, "n6", offset) + self._offset_column(elements, "n7", offset) + self._offset_column(elements, "n8", offset) + + def _transform_element_ids(self, elements: pd.DataFrame): + offset = self._xform.ideoff + if offset is None or offset == 0: + return + self._offset_column(elements, "eid", offset) + + def _transform_part_ids(self, elements: pd.DataFrame): + offset = self._xform.idpoff + if offset is None or offset == 0: + return + self._offset_column(elements, "pid", offset) diff --git a/src/ansys/dyna/core/lib/transforms/node_transform.py b/src/ansys/dyna/core/lib/transforms/node_transform.py new file mode 100644 index 000000000..30ec919bc --- /dev/null +++ b/src/ansys/dyna/core/lib/transforms/node_transform.py @@ -0,0 +1,34 @@ +# Copyright (C) 2021 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +"""Transformation handler for *NODE.""" + +from ansys.dyna.core import keywords as kwd +from ansys.dyna.core.lib.transforms.base_transform import Transform + + +class TransformNode(Transform): + def transform(self, keyword: kwd.Node) -> None: + offset = self._xform.idnoff + if offset is None or offset == 0: + return + keyword.nodes["nid"] = keyword.nodes["nid"] + offset diff --git a/test.py b/test.py new file mode 100644 index 000000000..f66466aa6 --- /dev/null +++ b/test.py @@ -0,0 +1,75 @@ +import os +import pandas as pd +import typing +import warnings + +from ansys.dyna.core import Deck +from ansys.dyna.core import keywords as kwd +from ansys.dyna.core.lib.transform import Transform + + +class TransformElementBeam(Transform): + def transform(self, keyword: typing.Any): + elements = self._get_elements_dataframe(keyword) + if elements is None: + return + self._transform_node_ids(elements) + self._transform_element_ids(elements) + self._transform_part_ids(elements) + + def _get_elements_dataframe(self, keyword) -> typing.Optional[pd.DataFrame]: + warning = f"keyword {keyword.keyword}_{keyword.subkeyword} not transformed!" + if not hasattr(keyword, "elements"): + warnings.warn(warning) + return None + elements = keyword.elements + if not isinstance(elements, pd.DataFrame): + warnings.warn(warning) + return None + return elements + + def _offset_column(self, df: pd.DataFrame, column: str, offset: int) -> None: + if column in df: + #TODO - check if the value is na, not just != 0 + df[column] = df[column].mask(df[column] != 0, df[column] + offset) + + def _transform_node_ids(self, elements: pd.DataFrame): + offset = self._xform.idnoff + if offset is None or offset == 0: + return + self._offset_column(elements, 'n1', offset) + self._offset_column(elements, 'n2', offset) + self._offset_column(elements, 'n3', offset) + self._offset_column(elements, 'n4', offset) + self._offset_column(elements, 'n5', offset) + self._offset_column(elements, 'n6', offset) + self._offset_column(elements, 'n7', offset) + self._offset_column(elements, 'n8', offset) + + def _transform_element_ids(self, elements: pd.DataFrame): + offset = self._xform.ideoff + if offset is None or offset == 0: + return + self._offset_column(elements, 'eid', offset) + + def _transform_part_ids(self, elements: pd.DataFrame): + offset = self._xform.idpoff + if offset is None or offset == 0: + return + self._offset_column(elements, 'pid', offset) + +#include_path = file_utils.get_asset_file_path("transform") +#filename = os.path.join(include_path, "test.k") +filename = r"C:\AnsysDev\code\pyansys\pydyna\tests\testfiles\keywords\transform\test.k" + +xform = kwd.IncludeTransform() +xform.filename = filename +xform.idnoff = 10 +xform.ideoff = 40 +xform.idpoff = 100 + +deck = Deck() +deck.append(xform) +#deck.transform_handler.register_transform_handler(("ELEMENT", "BEAM"), TransformElementBeam) +deck = deck.expand() +print(deck.write()) diff --git a/tests/test_deck.py b/tests/test_deck.py index fc1a5f63f..374e709b0 100644 --- a/tests/test_deck.py +++ b/tests/test_deck.py @@ -26,6 +26,7 @@ from ansys.dyna.core import Deck from ansys.dyna.core.lib.format_type import format_type +from ansys.dyna.core.lib.import_handler import ImportHandler from ansys.dyna.core import keywords as kwd import pytest @@ -74,11 +75,32 @@ def test_deck_002(): @pytest.mark.keywords def test_deck_003(file_utils): + """Import the deck with a custom handler. + + The custom handler skips all BOUNDARY_PRECRACK + and sets the nid1 property on ALE_SMOOTHING.""" deck = Deck() - keyword_string = file_utils.read_file(file_utils.assets_folder / "test.k") - deck.loads(keyword_string) + filepath = file_utils.assets_folder / "test.k" + class TestImportHandler(ImportHandler): + def __init__(self): + self._num_keywords = 0 + def before_import(self, context, keyword, buffer): + self._num_keywords += 1 + if keyword == "*BOUNDARY_PRECRACK": + return False + return True + def after_import(self, context, keyword): + if isinstance(keyword, kwd.AleSmoothing): + keyword.nid1 = 1 + import_handler = TestImportHandler() + deck.register_import_handler(import_handler) + deck.import_file(filepath) assert deck.title == "Basic 001" - assert len(deck._keywords) == 12 + assert import_handler._num_keywords == 12 + assert len(deck.keywords) == 11 + assert len(deck.all_keywords) == 11 + assert deck.get(type="ALE")[0].nid1 == 1 + @pytest.mark.keywords @@ -357,10 +379,59 @@ def test_deck_expand_recursive_include_path(file_utils): deck.append(kwd.IncludePath(path=include_path2)) deck.append(kwd.Include(filename='bird_B.k')) deck = deck.expand(recurse=True) - print(deck) assert len(deck.all_keywords) == 40 assert len(deck.keywords) == 36 +@pytest.mark.keywords +def test_deck_expand_transform(file_utils): + deck = Deck() + include_path = file_utils.get_asset_file_path("transform") + xform = kwd.IncludeTransform(filename = os.path.join(include_path, "test.k")) + xform.idnoff = 10 + xform.ideoff = 40 + xform.idpoff = 100 + deck.append(xform) + deck = deck.expand(recurse=True) + assert len(deck.keywords) == 3 + assert deck.keywords[0].elements["eid"][2] == 47 + assert deck.keywords[0].elements["pid"][5] == 101 + assert deck.keywords[0].elements["n1"][1] == 11 + assert deck.keywords[0].elements["n8"][2] == 0 + assert deck.keywords[0].elements["n5"][3] == 15 + assert deck.keywords[1].elements["eid"][3] == 45043 + assert deck.keywords[1].elements["pid"][0] == 145 + assert deck.keywords[1].elements["n1"][1] == 31 + assert pd.isna(deck.keywords[1].elements["n2"][2]) + assert deck.keywords[1].elements["n3"][0] == 22 + assert deck.keywords[2].nodes["nid"][0] == 11 + assert deck.keywords[2].nodes["nid"][20] == 31 + +@pytest.mark.keywords +def test_deck_expand_transform_custom_handler(file_utils): + """Test using a custom transform handler as an override.""" + deck = Deck() + include_path = file_utils.get_asset_file_path("transform") + xform = kwd.IncludeTransform(filename = os.path.join(include_path, "test.k")) + xform.idnoff = 10 + xform.ideoff = 40 + xform.idpoff = 100 + deck.append(xform) + + from ansys.dyna.core.lib.transform import Transform + + class TransformElementBeam(Transform): + def transform(self, keyword): + self._transform_part_ids(keyword.elements) + def _transform_part_ids(self, elements: pd.DataFrame): + offset = -1 + elements['pid'] = elements['pid'] + offset + + deck.transform_handler.register_transform_handler(("ELEMENT", "BEAM"), TransformElementBeam) + deck = deck.expand(recurse=True) + assert len(deck.keywords) == 3 + assert deck.keywords[1].elements["pid"][0] == 44 + assert deck.keywords[1].elements["pid"][3] == 44 + @pytest.mark.keywords def test_deck_unprocessed(ref_string): deck = Deck() diff --git a/tests/testfiles/keywords/transform/test.k b/tests/testfiles/keywords/transform/test.k new file mode 100644 index 000000000..0d58a4a9f --- /dev/null +++ b/tests/testfiles/keywords/transform/test.k @@ -0,0 +1,42 @@ +*KEYWORD +*ELEMENT_SHELL +$# eid pid n1 n2 n3 n4 n5 n6 n7 n8 + 2 1 1 2 3 4 0 0 0 0 + 3 1 1 2 3 0 0 0 0 0 + 7 1 1 2 3 4 5 6 7 0 + 8 1 1 2 3 4 5 6 7 8 + 9 1 1 2 3 4 5 6 0 0 + 10 1 1 2 + 11 1 1 + +*ELEMENT_BEAM +$ eid pid n1 n2 n3 + 45000 45 20 4 12 + 45001 45 21 10 + 45002 45 20 + 45003 45 20 0 0 + +*NODE +$# nid x y z tc rc + 1 -500.0 0.0 0.0 0 0 + 2 -450.0 0.0 0.0 0 0 + 3 -500.0 50.0 0.0 0 0 + 4 -450.0 50.0 0.0 0 0 + 5 -500.0 50.0 50.0 0 0 + 6 -450.0 50.0 50.0 0 0 + 7 -500.0 50.0 100.0 0 0 + 8 -450.0 50.0 100.0 0 0 + 9 -500.0 50.0 150.0 0 0 + 10 -450.0 50.0 150.0 0 0 + 11 -500.0 50.0 200.0 0 0 + 12 -450.0 50.0 200.0 0 0 + 13 -500.0 50.0 250.0 0 0 + 14 -450.0 50.0 250.0 0 0 + 15 -500.0 50.0 300.0 0 0 + 16 -450.0 50.0 300.0 0 0 + 17 -500.0 50.0 350.0 0 0 + 18 -450.0 50.0 350.0 0 0 + 19 -500.0 50.0 400.0 0 0 + 20 -450.0 50.0 400.0 0 0 + 21 -500.0 50.0 450.0 0 0 +*END