diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index 3ef1c4aa3..40d3f4e9e 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -134,6 +134,38 @@ jobs: operating-system: ${{ matrix.os }} python-version: ${{ matrix.python-version }} + # this is already done when running tests, but this is done before smoke-tests which is expensive. + # TODO - remove me after the keywords migration is done + keyword-tests: + name: Test the keywords module + runs-on: ubuntu-latest + needs: [code-style] + container: + image: ubuntu:latest + steps: + - uses: actions/checkout@v4 + + - name: Install dependencies + run: | + apt update -y + apt install --reinstall ca-certificates -y + apt install software-properties-common git -y + add-apt-repository ppa:deadsnakes/ppa -y + apt install python3.11 python3.11-venv -y + python3.11 -m ensurepip --default-pip + python3.11 -m pip install --upgrade pip + python3.11 -m venv /env + + - name: Install library + run: | + . /env/bin/activate + pip install .[tests] + + - name: Unit testing + run: | + . /env/bin/activate + pytest -m keywords + tests: name: "Testing" runs-on: ubuntu-latest @@ -227,7 +259,7 @@ jobs: build-library: name: "Build library" - needs: [doc, tests, run-testing] + needs: [doc, tests, run-testing, keyword-tests] runs-on: ubuntu-latest steps: - uses: ansys/actions/build-library@v8 diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index b32feadae..1d6531e4c 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -3,7 +3,6 @@ ## Project Lead * [Srikanth Adya](https://github.com/kanthadya) -* [Wenhui Yu](https://github.com/wenhuiuy) * [Zhanqun Zhang](https://github.com/zhangzhanqun) ## Individual Contributors @@ -15,3 +14,4 @@ * [Maxime Rey](https://github.com/MaxJPRey) * [Revathy Venugopal](https://github.com/Revathyvenugopal162) * [Roberto Pastor](https://github.com/RobPasMue) +* [Mohamed Koubaa](https://github.com/koubaa) diff --git a/pyproject.toml b/pyproject.toml index 931bb37e4..ea4aa7ba9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,6 +30,9 @@ dependencies = ["ansys-dpf-core>=0.7.2", "ansys-tools-path>=0.6.0", "ansys-platform-instancemanagement~=1.0", "pyvista>=0.43.4", + "hollerith==0.4.1", + "numpy>=2.0.0", + "pandas>=2.0" ] [project.optional-dependencies] diff --git a/pytest.ini b/pytest.ini index 332e77383..6e2d1ae52 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,3 +1,4 @@ [pytest] markers = run: tests that exercise the `run` subpackage + keywords: tests that exercies the `keyword` subpackage diff --git a/src/ansys/dyna/core/__init__.py b/src/ansys/dyna/core/__init__.py index 53b878c16..f19c357d5 100644 --- a/src/ansys/dyna/core/__init__.py +++ b/src/ansys/dyna/core/__init__.py @@ -1,3 +1,4 @@ +from ansys.dyna.core.lib.deck import Deck from ansys.dyna.core.solver.dynasolver import * import ansys.dyna.core.solver.grpc_tags diff --git a/src/ansys/dyna/core/lib/__init__.py b/src/ansys/dyna/core/lib/__init__.py new file mode 100644 index 000000000..41b99feeb --- /dev/null +++ b/src/ansys/dyna/core/lib/__init__.py @@ -0,0 +1,21 @@ +# 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. diff --git a/src/ansys/dyna/core/lib/array.py b/src/ansys/dyna/core/lib/array.py new file mode 100644 index 000000000..09f1830de --- /dev/null +++ b/src/ansys/dyna/core/lib/array.py @@ -0,0 +1,62 @@ +# 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 array as ar +import math + + +def array(element_type: type, reserved_size: int = 0, default_value=None): + """A resizable array that supports optional values for any type + Right now - array.array is used for single and double precision floating points + for everything else - python list is used. This is because no existing array + type in numpy, pandas, and python meet the above requirements. Specifically, + numpy integer arrays do not have optional values and are not resizable, pandas + integer arrays support optional values but are also not resizable, while python + array arrays are resizable but do not support optional values. + + The problem with this approach is memory usage. For 100k integers, a python list + appears to take about 5300K, while a pandas array and numpy array take 488K and 584K + respectively. pandas arrays take more memory than numpy because of the masking used + to support optional integer values. + + Given a python list of optional integer, where None is used to represent a missing value, + - this is how you convert to either type: + numpy: np.array([item or 0 for item in the_list], dtype=np.int32) + pandas: pd.array(the_list,dtype=pd.Int32Dtype()) + + In the future - A dynamic array class based on some of the above types can be used for + integer arrays. For string arrays, pandas arrays don't offer any value over python + lists. + """ + if element_type == float: + arr = ar.array("d") + if reserved_size == 0: + return arr + if default_value == None: + default_value = math.nan + for i in range(reserved_size): + arr.append(default_value) + return arr + if reserved_size == 0: + return list() + else: + return [default_value] * reserved_size diff --git a/src/ansys/dyna/core/lib/card.py b/src/ansys/dyna/core/lib/card.py new file mode 100644 index 000000000..177894b2b --- /dev/null +++ b/src/ansys/dyna/core/lib/card.py @@ -0,0 +1,124 @@ +# 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 io +import typing + +from ansys.dyna.core.lib.card_interface import CardInterface +from ansys.dyna.core.lib.field import Field, Flag, to_long # noqa: F401 +from ansys.dyna.core.lib.field_writer import write_comment_line, write_fields +from ansys.dyna.core.lib.format_type import format_type +from ansys.dyna.core.lib.io_utils import write_or_return +from ansys.dyna.core.lib.kwd_line_formatter import load_dataline, read_line + + +class Card(CardInterface): + def __init__(self, fields: typing.List[Field], active_func=None, format=format_type.default): + self._fields = fields + self._active_func = active_func + self._format_type = format + + @property + def format(self): + return self._format_type + + @format.setter + def format(self, value: format_type) -> None: + self._format_type = value + + def _convert_fields_to_long_format(self) -> typing.List[Field]: + fields = [] + offset = 0 + for field in self._fields: + new_field = to_long(field, offset) + offset += new_field.width + fields.append(new_field) + return fields + + def read(self, buf: typing.TextIO) -> bool: + if not self._is_active(): + return False + line, to_exit = read_line(buf) + if to_exit: + return True + self._load(line) + return False + + def _load(self, data_line: str) -> None: + """loads the card data from a list of strings""" + fields = self._fields + if self.format == format_type.long: + fields = self._convert_fields_to_long_format() + format = [(field.offset, field.width, field.type) for field in fields] + values = load_dataline(format, data_line) + num_fields = len(fields) + for field_index in range(num_fields): + self._fields[field_index].value = values[field_index] + + def write( + self, + format: typing.Optional[format_type] = None, + buf: typing.Optional[typing.TextIO] = None, + comment: typing.Optional[bool] = True, + ) -> typing.Union[str, None]: + if format == None: + format = self._format_type + + def _write(buf: typing.TextIO): + if self._is_active(): + if comment: + write_comment_line(buf, self._fields, format) + buf.write("\n") + write_fields(buf, self._fields, None, format) + + return write_or_return(buf, _write) + + def _is_active(self) -> bool: + if self._active_func == None: + return True + return True if self._active_func() else False + + # only used by tests, TODO move to conftest + def _get_comment(self, format: format_type) -> str: + s = io.StringIO() + write_comment_line(s, self._fields, format) + return s.getvalue() + + def _get_field_by_name(self, prop: str) -> Field: + return [f for f in self._fields if f.name == prop][0] + + # not needed by subclasses - only used by methods on keyword classes + def get_value(self, prop: str) -> typing.Any: + """gets the value of the field in the card""" + field = self._get_field_by_name(prop) + return field.value + + def set_value(self, prop: str, value: typing.Any) -> None: + """sets the value of the field in the card""" + self._get_field_by_name(prop).value = value + + def __repr__(self) -> str: + """Returns a console-friendly representation of the desired parameters for the card""" + content_lines = [] + content_lines.append(self._get_comment(self._format_type)) + output = "\n".join(content_lines) + return "StandardCard:" + output diff --git a/src/ansys/dyna/core/lib/card_interface.py b/src/ansys/dyna/core/lib/card_interface.py new file mode 100644 index 000000000..37106ec9b --- /dev/null +++ b/src/ansys/dyna/core/lib/card_interface.py @@ -0,0 +1,68 @@ +# 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 abc +import typing + +from ansys.dyna.core.lib.format_type import format_type + +# TODO - implement __repr__ on all cards + + +class CardInterface(metaclass=abc.ABCMeta): + """Abstract base class for all the implementations of keyword cards.""" + + @classmethod + def __subclasshook__(cls, subclass): + return ( + hasattr(subclass, "write") + and callable(subclass.write) + and hasattr(subclass, "read") + and callable(subclass.read) + ) + + @abc.abstractmethod + def read(self, buf: typing.TextIO) -> None: + """Reads the card data from an input text buffer.""" + raise NotImplementedError + + @abc.abstractmethod + def write( + self, format: typing.Optional[format_type], buf: typing.Optional[typing.TextIO], comment: typing.Optional[bool] + ) -> typing.Union[str, None]: + """Renders the card in the dyna keyword format. + :param buf: Buffer to write to. If None, the output is returned as a string + :param format: format_type to use. Default to standard. + """ + raise NotImplementedError + + @property + @abc.abstractmethod + def format(self) -> format_type: + """Get the card format type.""" + raise NotImplementedError + + @format.setter + @abc.abstractmethod + def format(self, value: format_type) -> None: + """Set the card format type.""" + raise NotImplementedError diff --git a/src/ansys/dyna/core/lib/card_set.py b/src/ansys/dyna/core/lib/card_set.py new file mode 100644 index 000000000..4d313388c --- /dev/null +++ b/src/ansys/dyna/core/lib/card_set.py @@ -0,0 +1,173 @@ +# 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. + +"""Set of cards that act as one card. + +It is a generic card, so it needs to be given a type as an argument. +That type is used for each card, and behaves like a keyword. +""" + +import typing + +from ansys.dyna.core.lib.card_interface import CardInterface +from ansys.dyna.core.lib.cards import Cards +from ansys.dyna.core.lib.format_type import format_type +from ansys.dyna.core.lib.io_utils import write_or_return +from ansys.dyna.core.lib.kwd_line_formatter import at_end_of_keyword +from ansys.dyna.core.lib.option_card import OptionSpec + + +class CardSet(CardInterface): + def __init__( + self, + set_type: type, + length_func: typing.Callable = None, + active_func: typing.Callable = None, + option_specs: typing.List[OptionSpec] = None, + **kwargs, + ): + self._set_type = set_type + self._items: typing.List[Cards] = list() + self._format_type: format_type = kwargs.get("format", format_type.default) + self._length_func = length_func + self._active_func = active_func + self._bounded = length_func != None + self._parent = kwargs.get("parent", None) + self._keyword = kwargs.get("keyword", None) + if option_specs == None: + option_specs = [] + self._option_specs = option_specs + self._initialized: bool = False + + @property + def _items(self) -> typing.List[Cards]: + return self._base_items + + @_items.setter + def _items(self, value: typing.List[Cards]) -> None: + self._base_items = value + + def _initialize(self): + if self._initialized: + return + if self._bounded and self._active: + self._initialize_data(self._length_func()) + self._initialized = True + + def initialize(self): + self._initialize() + + @property + def option_specs(self): + return self._option_specs + + @property + def _active(self) -> bool: + if self._active_func == None: + return True + return self._active_func() + + def _initialize_data(self, num_items: int) -> None: + for _ in range(num_items): + self.add_item() + + def _add_item_simple(self) -> int: + self._items.append(self._set_type(parent=self._parent, keyword=self._keyword)) + + def add_item(self, **kwargs) -> int: + """Add a card to the set. Return the index of the added card.""" + self._items.append(self._set_type(**kwargs, parent=self._parent, keyword=self._keyword)) + return len(self._items) - 1 + + def items(self) -> typing.List[Cards]: + if not self._initialized: + self._initialize() + return self._items + + @property + def bounded(self) -> bool: + return self._bounded + + @property + def format(self) -> format_type: + """Get the card format type.""" + return self._format_type + + @format.setter + def format(self, value: format_type) -> None: + """Set the card format type.""" + self._format_type = value + + def write( + self, + format: typing.Optional[format_type] = None, + buf: typing.Optional[typing.TextIO] = None, + comment: typing.Optional[bool] = True, + ) -> typing.Union[str, None]: + """Renders the card in the dyna keyword format. + :param buf: Buffer to write to. If None, the output is returned as a string + :param format: format_type to use. Default to standard. + """ + + def _write(buf: typing.TextIO): + for item_index, item in enumerate([item for item in self._items]): + write_comment = comment and item_index == 0 + if item_index != 0: + buf.write("\n") + item.write(buf, format, write_comment) + + return write_or_return(buf, _write) + + def _read_item_cards(self, buf: typing.TextIO, index: int) -> bool: + item = self._items[index] + for card in item._get_all_cards(): + ret = card.read(buf) + if ret: + # according to the card, we are at the end of the keyword, so + # we can break out of the card reading loop. + return True + return False + + def _load_bounded_from_buffer(self, buf: typing.TextIO) -> None: + length = self._length_func() + for index in range(length): + if self._read_item_cards(buf, index): + break + + def _load_unbounded_from_buffer(self, buf: typing.TextIO) -> None: + index = -1 + while True: + self._add_item_simple() + index += 1 + self._read_item_cards(buf, index) + if at_end_of_keyword(buf): + # the buffer is at the end of the keyword, exit + return + + def read(self, buf: typing.TextIO) -> bool: + self._initialize() + if self.bounded: + self._load_bounded_from_buffer(buf) + return False + else: + self._load_unbounded_from_buffer(buf) + return True diff --git a/src/ansys/dyna/core/lib/card_writer.py b/src/ansys/dyna/core/lib/card_writer.py new file mode 100644 index 000000000..dc6476406 --- /dev/null +++ b/src/ansys/dyna/core/lib/card_writer.py @@ -0,0 +1,57 @@ +# 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. + +"""Function to write cards.""" + +import typing + +from ansys.dyna.core.lib.card_interface import CardInterface +from ansys.dyna.core.lib.format_type import format_type + + +def write_cards( + cards: typing.List[CardInterface], + buf: typing.TextIO, + write_format: format_type, + comment: typing.Optional[bool] = True, +) -> bool: + """Write the cards. Return whether a superfluous trailing newline was added.""" + + # this code tries its best to avoid adding superfluous trailing newlines, but + # is not always successful. If one or more empty cards exist at the end of the + # keyword, a single newline will be added before them. Streams are typically + # write-only, and it is hard to remove the trailing newline from the stream. + # HOWEVER - if no buffer is passed in, this scenario can be detected and the + # trailing newline can be removed from the return value. In addition, if the + # keywords are written as part of a deck, the deck can detect if any keyword + # added a trailing newline and seek back one character to continue writing + # more keywords. + + pos = buf.tell() # record the position of the last newline + for card in cards: + if buf.tell() != pos: + # if we have written since the last newline, we need to prepend a new line + # (unless this is the last newline to write?) + buf.write("\n") + pos = buf.tell() + card.write(write_format, buf, comment) + return pos == buf.tell() diff --git a/src/ansys/dyna/core/lib/cards.py b/src/ansys/dyna/core/lib/cards.py new file mode 100644 index 000000000..6ce390668 --- /dev/null +++ b/src/ansys/dyna/core/lib/cards.py @@ -0,0 +1,154 @@ +# 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 class for cards and I/O""" + +import typing + +from ansys.dyna.core.lib.card_interface import CardInterface +from ansys.dyna.core.lib.card_writer import write_cards +from ansys.dyna.core.lib.format_type import format_type +from ansys.dyna.core.lib.kwd_line_formatter import read_line +from ansys.dyna.core.lib.option_card import OptionCardSet, OptionsAPI + + +class Cards: + def __init__(self, keyword): + self._cards = [] + self._kw = keyword + self._options_api = OptionsAPI(keyword) + + @property + def options(self) -> OptionsAPI: + """Gets the options_api of this keyword, if any""" + return self._options_api + + @property + def _cards(self) -> typing.List[CardInterface]: + return self._base_cards + + @_cards.setter + def _cards(self, value: typing.List[CardInterface]) -> None: + self._base_cards = value + + def _get_non_option_cards(self) -> typing.List[CardInterface]: + return [card for card in self._cards if type(card) != OptionCardSet] + + def _get_sorted_option_cards(self) -> typing.List[OptionCardSet]: + option_cards = [card for card in self._cards if type(card) == OptionCardSet] + option_cards.sort() + return option_cards + + def _get_post_options_with_no_title_order(self): + option_cards = [card for card in self._get_sorted_option_cards() if card.title_order == 0] + for option_card in option_cards: + assert option_card.card_order > 0, "Cards with a title order of 0 must have a positive card order" + return option_cards + + def _get_active_options(self) -> typing.List[OptionCardSet]: + """Return all active option card sets, sorted by card order.""" + option_cards = self._get_sorted_option_cards() + active_option_cards = [o for o in option_cards if self._kw.is_option_active(o.name)] + return active_option_cards + + def _flatten_2d_card_list(self, card_list: typing.List[typing.List[CardInterface]]) -> typing.List[CardInterface]: + """Given a list of lists of cards, flatten into a single list of cards.""" + flattened = sum(card_list, []) + return flattened + + def _unwrap_option_sets( + self, option_sets: typing.List[OptionCardSet], fn_filter: typing.Callable + ) -> typing.List[typing.List[CardInterface]]: + """Given a list of card sets, turn it into a list of lists of cards. + + Apply the filter of fn_filter + """ + return [option_set.cards for option_set in option_sets if fn_filter(option_set)] + + def _get_pre_option_cards(self) -> typing.List[CardInterface]: + """Get the option cards that go before the non-optional cards.""" + active_option_sets = self._get_active_options() + pre_option_cards = self._unwrap_option_sets(active_option_sets, lambda o: o.card_order < 0) + return self._flatten_2d_card_list(pre_option_cards) + + def _get_post_option_cards(self) -> typing.List[CardInterface]: + """Get the option cards that go after the non-optional cards.""" + active_option_sets = self._get_active_options() + post_option_cards = self._unwrap_option_sets(active_option_sets, lambda o: o.card_order > 0) + return self._flatten_2d_card_list(post_option_cards) + + def _get_all_cards(self) -> typing.List[CardInterface]: + cards = self._get_pre_option_cards() + cards.extend(self._get_non_option_cards()) + cards.extend(self._get_post_option_cards()) + return cards + + def write( + self, + buf: typing.TextIO, + format: format_type, + comment: typing.Optional[bool] = True, + ) -> bool: + """Writes the cards to `buf` using `format`. + Returns whether a superfluous newline is added + """ + superfluous_newline = write_cards(self._get_all_cards(), buf, format, comment) + return superfluous_newline + + def _try_read_options_with_no_title(self, buf: typing.TextIO) -> None: + # some cards are not active until we read.. how to handle? + # if there are monotonically increasing options with a title order of 0 + # *AND* all active cards have been read with the maximum title order less + # than the monotonically increasing options, then + # the solution is to read more lines, activating one option at a time + # they are assumed not to be activated here. when writing, they will + # assumed to have been activated either manually or from being read. + pos = buf.tell() + any_options_read = False + cards = self._get_post_options_with_no_title_order() + exit = False + while True: + if len(cards) == 0: + break + linepos = buf.tell() + _, exit = read_line(buf) + if exit: + break + buf.seek(linepos) + card = cards.pop(0) + self._kw.activate_option(card.name) + any_options_read = True + card.read(buf) + if not any_options_read: + buf.seek(pos) + + def _read_data(self, buf: typing.TextIO) -> None: + for card in self._get_pre_option_cards(): + card.read(buf) + + for card in self._get_non_option_cards(): + card.read(buf) + + for card in self._get_post_option_cards(): + card.read(buf) + + self._try_read_options_with_no_title(buf) diff --git a/src/ansys/dyna/core/lib/deck.py b/src/ansys/dyna/core/lib/deck.py new file mode 100644 index 000000000..cee680857 --- /dev/null +++ b/src/ansys/dyna/core/lib/deck.py @@ -0,0 +1,427 @@ +# 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. +"""Module provides a collection of keywords that can read and write to a keyword file.""" + +import collections +import os +import typing +from typing import Union +import warnings + +from ansys.dyna.core.lib.format_type import format_type +from ansys.dyna.core.lib.io_utils import write_or_return +from ansys.dyna.core.lib.keyword_base import KeywordBase + + +class Deck: + """Provides a collection of keywords that can read and write to a keyword file.""" + + def __init__(self, title: str = None, **kwargs): + """Initialize the deck.""" + self._keywords: typing.List = kwargs.get("keywords", []) + self.comment_header: str = None + self.title: str = title + self.format: format_type = kwargs.get("format", format_type.default) + + def __add__(self, other): + """Add two decks together.""" + sum_keyword = self._keywords + other._keywords + sum_deck = Deck() + sum_deck.extend(sum_keyword) + return sum_deck + + def clear(self): + """Clear all keywords from the deck.""" + self._keywords = [] + self.comment_header = None + self.title = None + self.format = format_type.default + + @property + def format(self) -> format_type: + """Format type of the deck.""" + return self._format_type + + @format.setter + def format(self, value: format_type) -> None: + """Set the format type of the deck.""" + self._format_type = value + + def append(self, keyword: Union[KeywordBase, str], check=False) -> None: + """Add a keyword to the collection. + + Parameters + ---------- + keyword : Union[KeywordBase, str] + Keyword. The keyword can be either an implementation of the ``KeywordBase`` + instance or a string. + check : bool, optional + The default is ``False``. + """ + assert isinstance(keyword, KeywordBase) or isinstance( + keyword, str + ), "Keywords or strings can only be appended to the deck." + if isinstance(keyword, str): + self._keywords.append(self._formatstring(keyword, check)) + else: + self._keywords.append(keyword) + + def _formatstring(self, string, check=False): + """Format a string to be appended to the deck.""" + linelist = string.split("\n") + if check: + assert linelist[0][0] == "*", "Appended string must begin with a keyword." + kwcount = 0 + for idx, line in enumerate(linelist): + if len(line) > 0: + if line[0] == "*": + kwcount += 1 + assert kwcount == 1, "Appended string must contain only one keyword." + width = 80 + if self.format == format_type.long: + width = 200 + if len(line) > width and check: + linelist[idx] = line[0:width] + print(f"truncated line {idx} to {width} characters") + newstring = "\n".join(linelist) + return newstring + return string + + @property + def all_keywords(self) -> typing.List[typing.Union[str, KeywordBase]]: + """List of all keywords.""" + return self._keywords + + @property + def string_keywords(self) -> typing.List[str]: + """List of keywords as a raw string.""" + return [kw for kw in self._keywords if isinstance(kw, str)] + + @property + def keywords(self): + """List of processed keywords.""" + return [kw for kw in self._keywords if isinstance(kw, KeywordBase)] + + def extend(self, kwlist: list) -> None: + """Add a list of keywords to the deck. + + Parameters + ---------- + kwlist : list + List of keywords. + """ + for kw in kwlist: + self.append(kw) + + def _expand_helper(self, kwd_list, cwd) -> typing.List[KeywordBase]: + """Recursively outputs a list of keywords within Includes.""" + if len(list(self.get_kwds_by_type("INCLUDE"))) == 0: + return kwd_list + for kwd in self.get_kwds_by_type("INCLUDE"): + try: + temp_deck = Deck(format=kwd.format) + temp_deck.import_file(os.path.join(cwd, kwd.filename)) + self._keywords.remove(kwd) + except FileNotFoundError: + temp_deck = Deck() + temp_list = temp_deck.all_keywords + return kwd_list + temp_deck._expand_helper(temp_list, cwd) + + def expand(self, cwd=None): + """Get a new deck that is flat. + + This method makes the ``include`` method obsolete. + """ + cwd = cwd or os.getcwd() + new_deck = Deck(title=self.title) + new_deck.comment_header = self.comment_header + new_deck.extend(self._expand_helper(self.all_keywords, cwd)) + return new_deck + + def _get_title_lines(self) -> typing.List[str]: + """Get the title lines.""" + if self.title is None: + return [] + return ["*TITLE", self.title] + + def _get_comment_header_lines(self) -> typing.List[str]: + """Get the comment header lines.""" + comment_header = self.comment_header + if comment_header is None: + return [] + split_lines = comment_header.split("\n") + line_start = "$" + return [f"{line_start}{line}" for line in split_lines] + + def _get_keyword_line(self, format: format_type) -> str: + """Get the keyword line.""" + keyword_line = "*KEYWORD" + if format == format_type.long: + keyword_line += " LONG=Y" + elif format == format_type.standard: + keyword_line += " LONG=S" + return keyword_line + + def _get_header(self, format: format_type) -> str: + """Get the header of the keyword file.""" + comment_lines = self._get_comment_header_lines() + keyword_lines = [self._get_keyword_line(format)] + title_lines = self._get_title_lines() + header_lines = comment_lines + keyword_lines + title_lines + return "\n".join(header_lines) + + def dumps(self) -> str: + """Get the keyword file representation of all keywords as a string. + + Returns + ------- + str + Keyword file representation of all keywords as a string. + """ + warnings.warn("The 'dumps()' method is deprecated. Use the 'write()' method instead.") + return self.write() + + def _write_keyword(self, buf: typing.TextIO, kwd: typing.Union[str, KeywordBase], format: format_type) -> None: + """Write a keyword to the buffer.""" + if isinstance(kwd, KeywordBase): + kwd.write(buf, None, format) + elif isinstance(kwd, str): + buf.write(kwd) + + def _remove_trailing_newline(self, buf: typing.TextIO) -> None: + """If the last character is a newline, seek back so that it can be overwritten. + + Otherwise, leave the buffer unmodified. + """ + pos = buf.tell() + buf.seek(pos - 1) + last_char = buf.read(1) + if last_char == "\n": + buf.seek(pos - 1) + + def write( + self, + buf: typing.Optional[typing.TextIO] = None, + format: typing.Optional[format_type] = None, + ): + """Write the card in the dyna keyword format. + + Parameters + ---------- + buf : optional + Buffer to write to. The default is ``None``, + in which case the output is returned as a string. + format : optional + Format to write in. The default is ``None``. + """ + if format is None: + format = self._format_type + + def _write(buf): + buf.write(self._get_header(format)) + for kwd in self._keywords: + self._remove_trailing_newline(buf) + buf.write("\n") + self._write_keyword(buf, kwd, format) + buf.write("\n*END") + + return write_or_return(buf, _write) + + def loads(self, value: str) -> "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 + overwrites the title and user comment, if any. + + Parameters + ---------- + value : str + + """ + # import this only when loading to avoid the circular + # imports + # ansys.dyna.keywords imports deck, deck imports deck_loader, + # deck_loader imports ansys.dyna.keywords + from .deck_loader import load_deck + + result = load_deck(self, value) + return result + + def _check_unique(self, type: str, field: str) -> None: + """Check that all keywords of a given type have a unique field value.""" + ids = [] + for kwd in self.get_kwds_by_type(type): + if not hasattr(kwd, field): + raise Exception(f"kwd of type {type} does not have field {field}.") + ids.append(getattr(kwd, field)) + duplicates = [id for id, count in collections.Counter(ids).items() if count > 1] + if len(duplicates) > 0: + raise Exception(f"kwds of type {type} have the following duplicate {field} values: {duplicates}") + + def _check_valid(self) -> None: + """Check that all keywords are valid.""" + for kwd in self._keywords: + is_valid, msg = kwd._is_valid() + if not is_valid: + raise Exception(f"{kwd} is not valid due to {msg}") + + def validate(self) -> None: + """Validate the collection of keywords.""" + # TODO - globally unique keywords (like CONTROL_TIME_STEP) are unique + self._check_unique("SECTION", "secid") + self._check_valid() + + def get_kwds_by_type(self, type: str) -> typing.Iterator[KeywordBase]: + """Get all keywords for a given type. + + Parameters + ---------- + type : str + Keyword type. + + Returns + ------- + typing.Iterator[KeywordBase] + + Examples + -------- + Get all SECTION keyword instances in the collection. + + >>>deck.get_kwds_by_type("SECTION") + """ + return filter(lambda kwd: not isinstance(kwd, str) and kwd.keyword == type, self._keywords) + + def get_section_by_id(self, id: int) -> typing.Optional[KeywordBase]: + """Get the SECTION keyword in the collection for a given section ID. + + Parameters + ---------- + id : int + Section ID. + + Returns + ------- + SECTION keyword or ``None`` if there is no SECTION keyword that matches the section ID. + + Raises + ------ + Exception + If multiple SECTION keywords use the given section ID. + """ + sections = self.get(type="SECTION", filter=lambda kwd: kwd.secid == id) + if len(sections) == 0: + return None + assert ( + len(sections) == 1 + ), f"Failure in `deck.get_section_by_id() method`. Multiple SECTION keywords use matid {id}." # noqa: E501 + return sections[0] + + def get(self, **kwargs) -> typing.List[KeywordBase]: + """Get a list of keywords. + + Parameters + ---------- + :Keyword Arguments: + * *type* (``str``) -- + The type of keyword to get. For example, "SECTION" returns all section keywords. + * *filter* (``callable``) -- + The filter to apply to the result. Only keywords which pass the filter will be returned. + + """ + if "type" in kwargs: + kwds = list(self.get_kwds_by_type(kwargs["type"])) + else: + kwds = self.keywords + if "filter" in kwargs: + return [kwd for kwd in kwds if kwargs["filter"](kwd)] + return kwds + + def import_file( + self, path: str, encoding="utf-8" + ) -> "ansys.dyna.keywords.lib.deck_loader.DeckLoaderResult": # noqa: F821 + """Import a keyword file. + + Parameters + ---------- + path : str + Full path for the keyword file. + """ + with open(path, encoding=encoding) as f: + return self.loads(f.read()) + + def export_file(self, path: str, encoding="utf-8") -> None: + """Export the keyword file to a new keyword file. + + Parameters + ---------- + path : str + Full path for the new keyword file. + """ + with open(path, "w+", encoding=encoding) as f: + self.write(f) + + @property + def comment_header(self) -> typing.Optional[str]: + """Comment header of the keyword database.""" + return self._comment_header + + @comment_header.setter + def comment_header(self, value: str) -> None: + self._comment_header = value + + @property + def title(self) -> typing.Optional[str]: + """Title of the keyword database.""" + return self._title + + @title.setter + def title(self, value: str) -> None: + self._title = value + + def __repr__(self) -> str: + """Get a console-friendly representation of a list of all keywords in the deck.""" + kwlist = self.all_keywords + if len(kwlist) == 0: + content_lines = ["Empty"] + else: + content_lines = [] + for kw in kwlist: + if isinstance(kw, KeywordBase): + content_lines.append(f"kwd: {kw.get_title()}") + elif isinstance(kw, str): + content_lines.append("str: " + kw.split("\n")[0] + "...") + + output = "\n".join(content_lines) + return output + + def plot(self, **args): + """Plot the node and element of the mesh using PyVista. + + Parameters + ---------- + **args : + Keyword arguments. Use * *cwd* (``int``) if the deck and include files are in + a separate directory. + """ + from ansys.dyna.keywords.lib.deck_plotter import plot_deck + + plot_deck(self, **args) diff --git a/src/ansys/dyna/core/lib/deck_loader.py b/src/ansys/dyna/core/lib/deck_loader.py new file mode 100644 index 000000000..1a172fe5d --- /dev/null +++ b/src/ansys/dyna/core/lib/deck_loader.py @@ -0,0 +1,191 @@ +# 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 io +import typing + +import ansys.dyna.core +from ansys.dyna.core.lib.format_type import format_type +from ansys.dyna.core.lib.keyword_base import KeywordBase + + +class IterState: + USERCOMMENT = 0 + KEYWORD_BLOCK = 1 + TITLE = 2 + KEYWORDS = 3 + END = 4 + + +class DeckLoaderResult: + """A class containing the result of an attempted deck load.""" + + def __init__(self): + self._unprocessed_keywords = [] + + def add_unprocessed_keyword(self, name): + self._unprocessed_keywords.append(name) + + def get_summary(self) -> str: + summary = io.StringIO() + for unprocessed_keyword in self._unprocessed_keywords: + summary.write(f"Failed to process: {unprocessed_keyword}\n") + return summary.getvalue() + + +def _get_kwd_class_and_format(keyword_name: str) -> str: + # handle spaces in keyword_name, such as + # *ELEMENT_SOLID (ten nodes format) => *ELEMENT_SOLID + # the spaces are used as hints for LSPP but not needed + # by the dyna solver + from ansys.dyna.keywords.keyword_classes.type_mapping import TypeMapping + + keyword_name = keyword_name.split()[0] + title_tokens = keyword_name.split("_") + + # Handling title is a hack right now. Should be able to find the correct + # keyword object type given any prefix and suffix options + if keyword_name.endswith("-"): + format = format_type.standard + keyword_name = keyword_name[:-1] + elif keyword_name.endswith("+"): + format = format_type.long + keyword_name = keyword_name[:-1] + else: + format = format_type.default + + keyword_object_type = TypeMapping.get(keyword_name, None) + + while keyword_object_type is None: + if len(title_tokens) == 0: + break + title_tokens = title_tokens[:-1] + keyword_name = "_".join(title_tokens) + keyword_object_type = TypeMapping.get(keyword_name, None) + return keyword_object_type, format + + +def _try_load_deck(deck: "ansys.dyna.core.deck.Deck", text: str, result: DeckLoaderResult) -> None: + lines = text.splitlines() + iterator = iter(lines) + iterstate = IterState.USERCOMMENT + + def update_iterstate(line: str): + if line.startswith("*KEYWORD"): + return IterState.KEYWORD_BLOCK + if line.startswith("*TITLE"): + return IterState.TITLE + if line.startswith("*END"): + return IterState.END + return IterState.KEYWORDS + + def update_deck_format(block: typing.List[str], deck: "ansys.dyna.core.deck.Deck") -> None: + assert len(block) == 1 + line = block[0].upper() + if "LONG" in line: + format_setter = line[line.find("LONG") + 4 :].strip() + tokens = format_setter.split("=") + assert len(tokens) >= 2 + format = tokens[1] + if format == "S": + deck.format = format_type.default + if format == "K": + deck.format = format_type.standard + if format == "Y": + deck.format = format_type.long + + def update_deck_comment(block: typing.List[str], deck: "ansys.dyna.core.deck.Deck") -> None: + def remove_comment_symbol(line: str): + if not line.startswith("$"): + raise Exception("Only comments can precede *KEYWORD") + return line[1:] + + block_without_comment_symbol = [remove_comment_symbol(line) for line in block] + deck.comment_header = "\n".join(block_without_comment_symbol) + + def update_deck_title(block: typing.List[str], deck: "ansys.dyna.core.deck.Deck") -> None: + block = [line for line in block if not line.startswith("$")] + assert len(block) == 2, "Title block can only have one line" + deck.title = block[1] + + def handle_keyword(block: typing.List[str], deck: "ansys.dyna.core.deck.Deck") -> None: + keyword = block[0].strip() + keyword_data = "\n".join(block) + keyword_object_type, format = _get_kwd_class_and_format(keyword) + if keyword_object_type == None: + result.add_unprocessed_keyword(keyword) + deck.append(keyword_data) + else: + import ansys.dyna.keywords + + keyword_object: KeywordBase = getattr(ansys.dyna.keywords.keywords, keyword_object_type)() + if format == format_type.default: + format = deck.format + keyword_object.format = format + try: + keyword_object.loads(keyword_data) + deck.append(keyword_object) + except Exception: + result.add_unprocessed_keyword(keyword) + deck.append(keyword_data) + + def handle_block(iterstate: int, block: typing.List[str]) -> bool: + if iterstate == IterState.END: + return True + if iterstate == IterState.USERCOMMENT: + update_deck_comment(block, deck) + elif iterstate == IterState.KEYWORD_BLOCK: + update_deck_format(block, deck) + elif iterstate == IterState.TITLE: + update_deck_title(block, deck) + else: + handle_keyword(block, deck) + return False + + block = [] + while True: + try: + line = next(iterator) + if line.startswith("*"): + # handle the previous block + end = handle_block(iterstate, block) + if end: + return + # set the new iterstate, start building the next block + iterstate = update_iterstate(line) + block = [line] + else: + if iterstate == IterState.KEYWORD_BLOCK: + # reset back to user comment after the keyword line? + iterstate = IterState.USERCOMMENT + block = [] + else: + block.append(line) + except StopIteration: + handle_block(iterstate, block) + return + + +def load_deck(deck: "ansys.dyna.core.deck.Deck", text: str) -> DeckLoaderResult: + result = DeckLoaderResult() + _try_load_deck(deck, text, result) + return result diff --git a/src/ansys/dyna/core/lib/deck_plotter.py b/src/ansys/dyna/core/lib/deck_plotter.py new file mode 100644 index 000000000..c6906aa50 --- /dev/null +++ b/src/ansys/dyna/core/lib/deck_plotter.py @@ -0,0 +1,333 @@ +# 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 typing + +import numpy as np +import pandas as pd + +from ansys.dyna.core import Deck + + +def get_nid_to_index_mapping(nodes) -> typing.Dict: + """Given a node id, output the node index as a dict""" + mapping = {} + for idx, node in nodes.iterrows(): + mapping[node["nid"]] = idx + return mapping + + +def merge_keywords( + deck: Deck, +) -> typing.Tuple[pd.DataFrame, typing.Dict]: + """ + Given a deck, merges specific keywords (NODE, ELEMENT_SHELL, ELEMENT_BEAM, ELEMENT_SOLID) + and returns tham as data frames. + """ + nodes_temp = [kwd.nodes for kwd in deck.get_kwds_by_type("NODE")] + nodes = pd.concat(nodes_temp) if len(nodes_temp) else pd.DataFrame() + + df_list = {} + for item in ["SHELL", "BEAM", "SOLID"]: + matching_elements = [kwd.elements for kwd in deck.get_kwds_by_type("ELEMENT") if kwd.subkeyword == item] + df_list[item] = pd.concat(matching_elements) if len(matching_elements) else pd.DataFrame() + + return ( + nodes, + df_list, + ) # solids + + +def process_nodes(nodes_df): + nodes_xyz = nodes_df[["x", "y", "z"]] + return nodes_xyz.to_numpy() + + +def shell_facet_array(facets: pd.DataFrame) -> np.array: + """ + facets are a pandas frame that is a sequence of integers + or NAs with max length of 8. + valid rows contain 3,4,6, or 8 items consecutive from the + left. we don't plot quadratic edges so 6/8 collapse to 3/4 + invalid rows are ignored, meaning they return an empty array + return an array of length 4 or 5 using the pyvista spec + for facets which includes a length prefix + [1,2,3]=>[3,1,2,3] + [1,2,3,0]=>[3,1,2,3] + [1,2,3,NA]=>[3,1,2,3] + """ + facet_array = np.empty(5, dtype=np.int32) + + for idx, item in enumerate(facets): + # find the first empty column + if pd.isna(item) or item == 0: + if idx == 3 or idx == 6: + facet_array[0] = 3 + return facet_array[:-1] + elif idx == 4: + facet_array[0] = 4 + return facet_array + else: + # invalid + return np.empty(0) + # fill the output to the right of the prefix + if idx < 4: + facet_array[idx + 1] = item + facet_array[0] = 4 + return facet_array + + +def solid_array(solids: pd.DataFrame): + """ + solids are a pandas frame that is a sequence of integers + or NAs with max length of 28. + valid rows contain 3, 4, 6, or 8 items consecutive from the + left. We don't plot quadratic edges so 6/8 collapse to 3/4 + invalid rows are ignored, meaning they return an empty array + return an array of length 4 or 5 using the pyvista spec + for facets which includes a length prefix + [1,2,3]=>[3,1,2,3] + [1,2,3,0]=>[3,1,2,3] + [1,2,3,NA]=>[3,1,2,3] + """ + + # FACES CREATED BY THE SOLIDS BASED ON MANUAL + # A DUMMY ZERO IS PUT AS A PLACEHOLDER FOR THE LEN PREFIX + four_node_faces = [[0, 1, 2, 3], [0, 1, 2, 4], [0, 1, 3, 4], [0, 2, 3, 4]] + six_node_faces = [ + [0, 1, 2, 5], + [0, 3, 4, 6], + [0, 2, 3, 5, 6], + [0, 1, 5, 6, 4], + [0, 1, 2, 3, 4], + ] + eight_node_faces = [ + [0, 1, 2, 3, 4], + [0, 1, 2, 5, 6], + [0, 5, 6, 7, 8], + [0, 3, 4, 7, 8], + [0, 2, 3, 6, 7], + [0, 1, 4, 5, 8], + ] + + facet_array = [] + + for idx, item in enumerate(solids): + # find the first empty column + if pd.isna(item) or item == 0: + if idx == 4: + facet_array = [len(facet) - 1 if i == 0 else solids[i - 1] for facet in four_node_faces for i in facet] + return facet_array + elif idx == 6: + facet_array = [len(facet) - 1 if i == 0 else solids[i - 1] for facet in six_node_faces for i in facet] + return facet_array + elif idx == 8: + facet_array = [len(facet) - 1 if i == 0 else solids[i - 1] for facet in eight_node_faces for i in facet] + return facet_array + else: + # invalid + return [] + # fill the output to the right of the prefix + return np.array(facet_array) + + +def line_array(lines: pd.DataFrame) -> np.array: + """ + line are a pandas frame that is a sequence of integers + or NAs with max length of 2. + valid rows contain 2 items consecutive from the + left. + invalid rows are ignored, meaning they return an empty array + return an array of length 3 using the pyvista spec + for facets which includes a length prefix + [1,2,]=>[2,1,2] + [1,2,3,0]=>[] + [1,2,3,NA]=>[] + """ + line_array = np.empty(3, dtype=np.int32) + + for idx, item in enumerate(lines): + # find the first empty column + if pd.isna(item) or item == 0: + if idx == 0 or idx == 1: + return np.empty(0) + # fill the output to the right of the prefix + if idx < 2: + line_array[idx + 1] = item + + line_array[0] = 2 + return line_array + + +def map_facet_nid_to_index(flat_facets: np.array, mapping: typing.Dict) -> np.array: + """Given a flat list of facets or lines, use the mapping from nid to python index + to output the numbering system for pyvista from the numbering from dyna + """ + # Map the indexes but skip the prefix + flat_facets_indexed = np.empty(len(flat_facets), dtype=np.int32) + + skip_flag = 0 + for idx, item in np.ndenumerate(flat_facets): + if skip_flag == 0: + flat_facets_indexed[idx] = item + skip_flag -= int(item) + else: + flat_facets_indexed[idx] = mapping[item] + skip_flag += 1 + + return flat_facets_indexed + + +def extract_shell_facets(shells: pd.DataFrame, mapping): + """shells table comes in with the form + | eid | nid1 | nid2 | nid3 | nid4 + 1 10 11 12 + 20 21 22 23 24 + + but the array needed for pyvista polydata is + of the form where each element is prefixed by the length of the element node list + [3,10,11,12,4,21,22,23,24] + + Take individual rows, extract the appropriate nid's and output a flat list of + facets for pyvista + """ + + if len(shells) == 0: + return [] + + # extract only the node information + # could keep in the future to separate the parts or elements + shells = shells.drop(columns=["eid", "pid"]) + + facet_with_prefix = [] + + idx = 0 + for row in shells.itertuples(index=False): + array = shell_facet_array(row) + facet_with_prefix.append(array) + idx += 1 + + flat_facets = np.concatenate(facet_with_prefix, axis=0) + + flat_facets_indexed = map_facet_nid_to_index(flat_facets, mapping) + + return flat_facets_indexed + + +def extract_lines(beams: pd.DataFrame, mapping: typing.Dict[int, int]) -> np.ndarray: + """beams table comes in with the form with extra information not supported, + | eid | nid1 | nid2 + 1 10 11 + 20 21 22 + + we only care about nid 1 and 2 + + but the array needed for pyvista polydata is the same as in extract facets + of the form where each element is prefixed by the length of the element node list + [2,10,11,2,21,22] + + Take individual rows, extract the appropriate nid's and output a flat list of + facets for pyvista + """ + # dont need to do this if there is no beams + if len(beams) == 0: + return np.empty((0), dtype=int) + + # extract only the node information + # could keep in the future to separate the parts or elements + beams = beams[["n1", "n2"]] + + line_with_prefix = [] + + for row in beams.itertuples(index=False): + line_with_prefix.append(line_array(row)) + + flat_lines = np.concatenate(line_with_prefix, axis=0) + + flat_lines_indexed = map_facet_nid_to_index(flat_lines, mapping) + + return flat_lines_indexed + + +def extract_solids(solids: pd.DataFrame, mapping: typing.Dict[int, int]): + if len(solids) == 0: + return [] + + solids = solids.drop(columns=["eid", "pid"]) + + idx = 0 + + solid_with_prefix = [] + + for row in solids.itertuples(index=False): + solid_with_prefix.append(solid_array(row)) + idx += 1 + + flat_solids = np.concatenate(solid_with_prefix, axis=0) + + flat_solids_indexed = map_facet_nid_to_index(flat_solids, mapping) + + return flat_solids_indexed + + +def get_pyvista(): + try: + import pyvista as pv + except ImportError: + raise Exception("plot is only supported if pyvista is installed") + return pv + + +def get_polydata(deck: Deck, cwd=None): + """Creates the PolyData Object for plotting from a given deck with nodes and elements""" + + # import this lazily (otherwise this adds over a second to the import time of dynalib) + pv = get_pyvista() + + # check kwargs for cwd. future more arguments to plot + flat_deck = deck.expand(cwd) + nodes_df, element_dict = merge_keywords(flat_deck) + + shells_df = element_dict["SHELL"] + beams_df = element_dict["BEAM"] + solids_df = element_dict["SOLID"] + + nodes_list = process_nodes(nodes_df) + + if len(nodes_df) == 0 or len(shells_df) + len(beams_df) + len(solids_df) == 0: + raise Exception("missing node or element keyword to plot") + + mapping = get_nid_to_index_mapping(nodes_df) + + facets = extract_shell_facets(shells_df, mapping) + lines = extract_lines(beams_df, mapping) + solids = extract_solids(solids_df, mapping) + plot_data = pv.PolyData(nodes_list, [*facets, *solids]) + if len(lines) > 0: + plot_data.lines = lines + return plot_data + + +def plot_deck(deck, **args): + """Plot wrapper""" + plot_data = get_polydata(deck, args.pop("cwd", "")) + return plot_data.plot() diff --git a/src/ansys/dyna/core/lib/duplicate_card.py b/src/ansys/dyna/core/lib/duplicate_card.py new file mode 100644 index 000000000..88edea9c8 --- /dev/null +++ b/src/ansys/dyna/core/lib/duplicate_card.py @@ -0,0 +1,212 @@ +# 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 io +import typing + +import numpy as np +import pandas as pd + +from ansys.dyna.core.lib.card import Card, Field +from ansys.dyna.core.lib.field_writer import write_c_dataframe +from ansys.dyna.core.lib.format_type import format_type +from ansys.dyna.core.lib.io_utils import write_or_return +from ansys.dyna.core.lib.kwd_line_formatter import buffer_to_lines + +CHECK_TYPE = True + + +def _check_type(value): + global CHECK_TYPE + if CHECK_TYPE: + assert isinstance(value, pd.DataFrame), "value must be a DataFrame" + + +class DuplicateCard(Card): + def __init__( + self, fields: typing.List[Field], length_func, active_func=None, data=None, format=format_type.default + ): + super().__init__(fields, active_func) + self._format = [(field.offset, field.width) for field in self._fields] + if length_func == None: + self._bounded = False + self._length_func = lambda: len(self.table) + else: + self._bounded = True + self._length_func = length_func + + self._format_type = format + self._initialized = False + if data is not None: + self.table = data + + def _initialize(self): + if self._bounded: + self._initialize_data(self._length_func()) + else: + self._initialize_data(0) + self._initialized = True + + @property + def table(self): + if not self._initialized: + self._initialize() + return self._table + + @table.setter + def table(self, value: pd.DataFrame): + _check_type(value) + self._table = pd.DataFrame() + for field in self._fields: + if field.name in value: + field_type = field.type + if field_type == float: + field_type = np.float64 + elif field_type == int: + field_type = pd.Int32Dtype() + self._table[field.name] = value[field.name].astype(field_type) + else: + self._table[field.name] = self._make_column(field.type, len(value)) + self._initialized = True + + @property + def format(self): + return self._format_type + + @format.setter + def format(self, value: format_type) -> None: + self._format_type = value + + def _make_column(self, type, length): + if type == float: + arr = np.empty((length,)) + arr[:] = np.nan + return arr + elif type == str: + return [None] * length + elif type == int: + return pd.Series([None] * length, dtype=pd.Int32Dtype()) + raise Exception("unexpected type") + + def _initialize_data(self, length): + data = {} + num_fields = len(self._fields) + column_names = np.ndarray(num_fields, "object") + for index in range(num_fields): + field = self._fields[index] + value = self._make_column(field.type, length) + column_names[index] = field.name + data[field.name] = value + self._table = pd.DataFrame(data, columns=column_names) + + def _get_row_values(self, index: int) -> list: + # Used by Duplicate Card Group only + if index >= len(self.table): + return [None] * len(self._fields) + values = [] + for key in self.table.keys(): + col = self.table[key] + val = col[index] + values.append(val) + return values + + def _get_read_options(self): + fields = self._get_fields() + colspecs = [(field.offset, field.offset + field.width) for field in fields] + type_mapping = {float: np.float64, int: pd.Int32Dtype(), str: str} + dtype = {field.name: type_mapping[field.type] for field in fields} + names = [field.name for field in fields] + options = {"names": names, "colspecs": colspecs, "dtype": dtype, "comment": "$"} + return options + + def _read_buffer_as_dataframe(self, buffer: typing.TextIO, fields: typing.Iterable[Field]) -> pd.DataFrame: + read_options = self._get_read_options() + df = pd.read_fwf(buffer, **read_options) + return df + + def _get_fields(self) -> typing.List[Field]: + fields = self._fields + if self.format == format_type.long: + fields = self._convert_fields_to_long_format() + return fields + + def _load_bounded_from_buffer(self, buf: typing.TextIO) -> None: + read_options = self._get_read_options() + read_options["nrows"] = self._num_rows() + df = pd.read_fwf(buf, **read_options) + self._table = df + self._initialized = True + + def _load_unbounded_from_buffer(self, buf: typing.TextIO) -> None: + data_lines = buffer_to_lines(buf) + self._load_lines(data_lines) + + def read(self, buf: typing.TextIO) -> None: + if self.bounded: + self._initialized = True + self._load_bounded_from_buffer(buf) + else: + self._initialize_data(0) + self._initialized = True + self._load_unbounded_from_buffer(buf) + + def _load_lines(self, data_lines: typing.List[str]) -> None: + fields = self._get_fields() + buffer = io.StringIO() + [(buffer.write(line), buffer.write("\n")) for line in data_lines] + buffer.seek(0) + self._table = self._read_buffer_as_dataframe(buffer, fields) + self._initialized = True + + def write( + self, + format: typing.Optional[format_type] = None, + buf: typing.Optional[typing.TextIO] = None, + comment: typing.Optional[bool] = True, + ) -> str: + if format == None: + format = self._format_type + + def _write(buf: typing.TextIO): + if self._num_rows() > 0: + if comment: + buf.write(self._get_comment(format)) + buf.write("\n") + write_c_dataframe(buf, self._fields, self.table, format) + + return write_or_return(buf, _write) + + @property + def bounded(self) -> bool: + return self._bounded + + def _num_rows(self) -> int: + if not self._is_active(): + return 0 + return self._length_func() + + def __repr__(self) -> str: + """Returns a console-friendly representation of the desired parameters for the card""" + content_lines = [] + content_lines.append(self._get_comment(self._format_type)) + output = "\n".join(content_lines) + return "DuplicateCard: \n" + output diff --git a/src/ansys/dyna/core/lib/duplicate_card_group.py b/src/ansys/dyna/core/lib/duplicate_card_group.py new file mode 100644 index 000000000..2aac91931 --- /dev/null +++ b/src/ansys/dyna/core/lib/duplicate_card_group.py @@ -0,0 +1,218 @@ +# 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 io +import math +import typing + +import pandas as pd + +from ansys.dyna.core.lib.card import Card +from ansys.dyna.core.lib.card_interface import CardInterface +from ansys.dyna.core.lib.duplicate_card import DuplicateCard +from ansys.dyna.core.lib.format_type import format_type +from ansys.dyna.core.lib.io_utils import write_or_return +from ansys.dyna.core.lib.kwd_line_formatter import buffer_to_lines + + +def _to_duplicate_card(card: Card, length_func: typing.Callable) -> DuplicateCard: + return DuplicateCard(card._fields, length_func, card._active_func) + + +class DuplicateCardGroup(CardInterface): + def __init__( + self, + cards: typing.List[Card], + length_func: typing.Callable, + active_func: typing.Callable = None, + data=None, + format: format_type = format_type.default, + ): + self._cards = [_to_duplicate_card(card, length_func) for card in cards] + self._length_func = length_func + self._active_func = active_func + self.format = format + if length_func == None: + self._bounded = False + self._length_func = self._get_unbounded_length + else: + self._bounded = True + self._length_func = length_func + self._initialized = False + if data is not None: + self.table = data + + def _initialize(self) -> None: + if self._initialized: + return + [card._initialize() for card in self._cards] + data = pd.concat([card.table for card in self._cards], axis=1) + self._table = data + self._initialized = True + + @property + def table(self): + self._initialize() + return self._table + + @table.setter + def table(self, value: pd.DataFrame): + # remove duplicate columns + value = value.loc[:, ~value.columns.duplicated()] + # store the table + self._table = value + # propagate table to child duplicate card objects + self._propagate() + self._initialized = True + + @property + def format(self) -> format_type: + return self._format + + def _propagate(self) -> None: + """Propagate view of data frame to all child cards.""" + for card in self._cards: + card.table = self._table + + @format.setter + def format(self, value: format_type) -> None: + self._format = value + for card in self._cards: + card.format = value + + def _load_unbounded_from_buffer(self, buf: typing.TextIO) -> None: + data_lines = buffer_to_lines(buf) + self._load_lines(data_lines) + + def _load_bounded_from_buffer(self, buf: typing.TextIO) -> None: + data_lines = buffer_to_lines(buf, self._num_rows()) + self._load_lines(data_lines) + + def read(self, buf: typing.TextIO) -> None: + if self.bounded: + self._load_bounded_from_buffer(buf) + else: + self._load_unbounded_from_buffer(buf) + + def _load_lines(self, data_lines: typing.List[str]) -> None: + """Load the card data from a list of strings.""" + card_lines = self._divide_data_lines(data_lines) + for index, lines in enumerate(card_lines): + self._cards[index]._load_lines(lines) + self.table = pd.concat([card.table for card in self._cards], axis=1) + + def write( + self, + format: typing.Optional[format_type] = None, + buf: typing.Optional[typing.TextIO] = None, + comment: typing.Optional[bool] = True, + ) -> str: + self._initialize() + self._propagate() + + if format == None: + format = self.format + + def _as_buffer(card: DuplicateCard, add_newline: bool) -> io.StringIO: + card_buf = io.StringIO() + card.write(format, card_buf, True) + if add_newline: + card_buf.write("\n") + card_buf.seek(0) + return card_buf + + def _write(buf: typing.TextIO): + if self._num_rows() > 0: + card_buffers = [] + active_cards = self._get_active_cards() + for idx, card in enumerate(active_cards): + card_buffer = _as_buffer(card, idx != len(active_cards) - 1) + card_buffers.append(card_buffer) + + iter = zip(*card_buffers) + comment_lines = next(iter) + first_lines = next(iter) + for comment_line, first_line in zip(comment_lines, first_lines): + if comment: + buf.write(comment_line) + buf.write(first_line) + for lines in zip(*card_buffers): + for line in lines: + buf.write(line) + + return write_or_return(buf, _write) + + def _divide_data_lines(self, data_lines: typing.List[str]) -> typing.List: + """divides the data lines into a set of lines, one for each sub-card""" + card_lines = [[] for i in range(len(self._cards))] + for index, line in enumerate(data_lines): + card_index = self._get_index_of_which_card(index) + card_lines[card_index].append(line) + return card_lines + + def _get_index_of_which_card(self, overall_index: int) -> int: + """given the overall index, returns the index into self._cards + to identify which sub-card the overall index indexes into""" + return overall_index % len(self._get_active_cards()) + + def _get_index_of_given_card(self, overall_index: int) -> int: + """given the overall index, returns the index to be used to + index into the card given by _get_index_of_which_card""" + return math.floor(overall_index / len(self._get_active_cards())) + + def _is_active(self) -> bool: + if self._active_func == None: + return True + return self._active_func() + + def _is_card_active(self, card) -> bool: + if card._active_func != None: + return card._active_func() + return True + + def _get_active_cards(self) -> typing.List[DuplicateCard]: + return [card for card in self._cards if self._is_card_active(card)] + + def _get_unbounded_length(self) -> int: + """the unbounded length is the minimum of all sub-card's unbounded length""" + self._initialize() # Need to initialize first, so that the sub card can calculate num_rows + lens = [card._num_rows() for card in self._get_active_cards()] + return min(lens) + + @property + def bounded(self) -> bool: + return self._bounded + + def _num_rows(self) -> int: + if not self._is_active(): + return 0 + num_active_cards = len(self._get_active_cards()) + return self._length_func() * num_active_cards + + def __repr__(self) -> str: + """Returns a console-friendly representation of the desired parameters for the card""" + self._propagate() + content_lines = [] + for card in self._get_active_cards(): + content_lines.append(card._get_comment(self._format)) + output = "\n".join(content_lines) + return "DuplicateCardGroup: \n" + output diff --git a/src/ansys/dyna/core/lib/field.py b/src/ansys/dyna/core/lib/field.py new file mode 100644 index 000000000..1bbf5444b --- /dev/null +++ b/src/ansys/dyna/core/lib/field.py @@ -0,0 +1,104 @@ +# 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 copy +import dataclasses +import typing + + +@dataclasses.dataclass +class Flag: + value: bool = None + true_value: str = None + false_value: str = None + + +class Field: + def __init__(self, name: str, type: type, offset: int, width: int, value: typing.Any = None): + self._name = name + self._type = type + self._offset = offset + self._width = width + self._value = value + + @property + def name(self) -> str: + return self._name + + @name.setter + def name(self, value: str) -> None: + self._name = value + + @property + def type(self) -> type: + return self._type + + @type.setter + def type(self, value: type) -> None: + self._type = value + + @property + def offset(self) -> int: + return self._offset + + @offset.setter + def offset(self, value: int) -> None: + self._offset = value + + @property + def width(self) -> int: + return self._width + + @width.setter + def width(self, value: int) -> None: + self._width = value + + @property + def value(self) -> typing.Any: + if self._value and type(self._value) == Flag: + return self._value.value + return self._value + + @value.setter + def value(self, value: typing.Any) -> None: + if self._value and type(self._value) == Flag: + self._value.value = value + else: + self._value = value + + def io_info(self) -> typing.Tuple[str, typing.Type]: + """Return the value and type used for io.""" + if self._value and type(self._value) == Flag: + value = self._value.true_value if self._value.value else self._value.false_value + return value, str + return self.value, self.type + + +def to_long(field: Field, offset: int) -> Field: + field = copy.copy(field) + width = field.width + if width < 20: + width = 20 + field.offset = offset + field.width = width + offset += width + return field diff --git a/src/ansys/dyna/core/lib/field_writer.py b/src/ansys/dyna/core/lib/field_writer.py new file mode 100644 index 000000000..0220fcaf7 --- /dev/null +++ b/src/ansys/dyna/core/lib/field_writer.py @@ -0,0 +1,183 @@ +# 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 io +import typing + +import hollerith as holler +import pandas as pd +import pandas._libs.missing as libmissing + +from ansys.dyna.core.lib.field import Field, to_long +from ansys.dyna.core.lib.format_type import format_type + + +def _write_string_right(value, width): + # TODO - put a left/right flag in hollerith + return f"{{0:>{width}}}".format(value) + + +def _write_null(width): + buffer = io.StringIO() + holler.write_spaces(buffer, width) + return buffer.getvalue() + + +def _field_iterator(fields: typing.List[Field], long_format: bool) -> typing.Iterable[Field]: + assert len(fields) > 0, "at least one field is needed" + if fields[0].offset > 0: + # insert a blank field in the beginning up to the offset + blank_field = Field(name=None, type=None, offset=0, width=fields[0].offset) + fields = [blank_field] + fields + + offset = 0 + for field in fields: + if long_format: + field = to_long(field, offset) + pos, width = (field.offset, field.width) + # check pos, add a null if its not correct + assert pos >= offset + if pos != offset: + empty_width = pos - offset + yield Field(name=None, type=None, offset=offset, width=empty_width) + offset += empty_width + assert pos == offset + yield field + offset += width + + +def check_field_type(field_type: type): + assert field_type == str or field_type == int or field_type == float, "Unexpected type" + + +def write_field_c(buf: typing.IO[typing.AnyStr], field_type: type, value: typing.Any, width: int) -> None: + if libmissing.checknull(value): + holler.write_spaces(buf, width) + elif field_type == str: + holler.write_string(buf, value, width) + elif field_type == int: + holler.write_int(buf, value, width) + elif field_type == float: + holler.write_float(buf, value, width) + + +def write_field(buf: typing.IO[typing.AnyStr], field_type: type, value: typing.Any, width: int) -> None: + check_field_type(field_type) + write_field_c(buf, field_type, value, width) + + +def write_c_dataframe( + buf: typing.IO[typing.AnyStr], fields: typing.List[Field], table: pd.DataFrame, format: format_type +) -> None: + def convert_field(field: Field) -> holler.Field: + return holler.Field(type=field.type, width=field.width) + + long_format = format == format_type.long + converted_fields = list(_field_iterator(fields, long_format)) + spec = [convert_field(field) for field in converted_fields] + num_defined_rows = len(table) + index = 0 + any_none = False + for field in converted_fields: + if field.type == None: + any_none = True + if any_none: + full_table = pd.DataFrame() + for field in converted_fields: + if field.type == None: + values = [None] * num_defined_rows + index += 1 + field.name = f"unused {index}" + else: + values = table[field.name] + full_table[field.name] = values + table = full_table + holler.write_table(buf, table, num_defined_rows, spec) + + +def write_fields( + buf: typing.IO[typing.AnyStr], + fields: typing.List[Field], + values: typing.Optional[typing.List[typing.Any]] = None, + format: typing.Optional[format_type] = format_type.default, +) -> None: + """Write fields line to buf + writes a keyword card line with fixed column offsets and width + parameters: + buf: buffer to write to + fields: fields to write + values: optional - list of values for the field. If not set, use the value property of each field. + this is used by DuplicateCard + format: optional - format to write + example: + >>> s=io.String() + >>> fields = [ + ... Field("a", int, 0, 10, 1), + ... Field("b", str, 10, 10, "hello") + ... ] + >>> write_fields(s, fields) + >>> s.getvalue() + ' 1 hello' + """ + index = 0 + + for field in _field_iterator(fields, format == format_type.long): + if field.type is None: + buf.write(_write_null(field.width)) + else: + field_value, field_type = field.io_info() + if values != None: + field_value = values[index] + index += 1 + write_field(buf, field_type, field_value, field.width) + + +def write_comment_line( + buf: typing.IO[typing.AnyStr], + fields: typing.List[Field], + format: typing.Optional[format_type] = format_type.default, +) -> None: + """Writes the comment line to the buffer. + parameters: + buf: buffer to write to + fields: fields to write + format: format to write in + example: + >>> s=io.String() + >>> fields = [ + ... Field("a", int, 0, 10, 1), + ... Field("b", str, 10, 10, "hello") + ... ] + >>> write_comment_line(s, fields) + >>> s.getvalue() + ' a b' + """ + pos = buf.tell() + for field in _field_iterator(fields, format == format_type.long): + if field.name is None: + buf.write(_write_null(field.width)) + else: + buf.write(_write_string_right(field.name, field.width)) + endpos = buf.tell() + buf.seek(pos) + buf.write("$#") + buf.seek(endpos) diff --git a/src/ansys/dyna/core/lib/format_type.py b/src/ansys/dyna/core/lib/format_type.py new file mode 100644 index 000000000..ccee23f3d --- /dev/null +++ b/src/ansys/dyna/core/lib/format_type.py @@ -0,0 +1,29 @@ +# 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. + +"""Format type for dyna keywords.""" + + +class format_type: # noqa: N801 + default = "s" + standard = "k" + long = "y" diff --git a/src/ansys/dyna/core/lib/io_utils.py b/src/ansys/dyna/core/lib/io_utils.py new file mode 100644 index 000000000..47d3303d9 --- /dev/null +++ b/src/ansys/dyna/core/lib/io_utils.py @@ -0,0 +1,43 @@ +# 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. + +"""Utils for i/o.""" + +import io +import typing + + +def write_or_return(buf: typing.Optional[typing.TextIO], func: typing.Callable) -> typing.Optional[str]: + """Write to buffer or returns a string. + + Uses the callable `func` to write. If `buf` is None, then the function will create a string buffer + before calling `func` and return the result as a string. + """ + if buf == None: + to_return = True + buf = io.StringIO() + else: + to_return = False + func(buf) + if to_return: + retval = buf.getvalue() + return retval diff --git a/src/ansys/dyna/core/lib/keyword_base.py b/src/ansys/dyna/core/lib/keyword_base.py new file mode 100644 index 000000000..346e82143 --- /dev/null +++ b/src/ansys/dyna/core/lib/keyword_base.py @@ -0,0 +1,278 @@ +# 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 io +import typing +import warnings + +from ansys.dyna.core.lib.card_interface import CardInterface +from ansys.dyna.core.lib.cards import Cards +from ansys.dyna.core.lib.format_type import format_type +from ansys.dyna.core.lib.option_card import OptionsAPI + + +class KeywordBase(Cards): + """base class for all keywords. + Derived class must provide: + - _cards + - keyword + - subkeyword + """ + + def __init__(self, **kwargs): + super().__init__(self) + self.user_comment = kwargs.get("user_comment", "") + self._options_api: OptionsAPI = OptionsAPI(self) + self._format_type: format_type = kwargs.get("format", format_type.default) + self._active_options: typing.Set[str] = set() + + @property + def format(self) -> format_type: + """Get or set the format for this keyword.""" + return self._format_type + + @format.setter + def format(self, value: format_type) -> None: + self._format_type = value + for card in self._cards: + card.format = value + + def _get_base_title(self) -> str: + kwd: str = self.keyword + subkwd: str = self.subkeyword + if kwd == subkwd: + return f"{kwd}" + return f"{kwd}_{subkwd}" + + def get_title(self, format_symbol: str = "") -> str: + """Get the title of this keyword.""" + base_title = self._get_base_title() + titles = [base_title] + if self.options != None: + options_specs = self.options.option_specs + title_suffix_options = [o for o in options_specs if self.is_option_active(o.name) and o.title_order > 0] + title_suffix_options.sort(key=lambda option: option.title_order) + suffix_names = [op.name for op in title_suffix_options] + titles = titles + suffix_names + return f"*{'_'.join(titles)}{format_symbol}" + + @property + def user_comment(self) -> str: + """Get or set the "user comment" for this keyword.""" + return self._user_comment + + @user_comment.setter + def user_comment(self, value: str) -> None: + self._user_comment = value + + @property + def cards(self) -> typing.List[CardInterface]: + """Gets the cards of the keyword""" + return self._get_all_cards() + + def _get_user_comment_lines(self) -> typing.List[str]: + user_comment = self.user_comment + if user_comment == "": + return [] + split_lines = user_comment.split("\n") + line_start = "$" + return [f"{line_start}{line}" for line in split_lines] + + def _is_valid(self) -> typing.Tuple[bool, str]: + return True, "" + + def is_option_active(self, option: str) -> bool: + return option in self._active_options + + def activate_option(self, option: str) -> None: + self._active_options.add(option) + + def deactivate_option(self, option: str) -> None: + if option in self._active_options: + self._active_options.remove(option) + + def _try_activate_options(self, names: typing.List[str]) -> None: + for option in self.options.option_specs: + if option.name in names: + self.activate_option(option.name) + + def _activate_options(self, title: str) -> None: + if self.options is None: + return + title_list = title.split("_") + self._try_activate_options(title_list) + + def __repr__(self) -> str: + """Returns a console-friendly representation of the keyword data as it would appear in the .k file""" + + max_rows = 60 # TODO - make these configurable somewhere + + class TruncatedStringException(Exception): + pass + + class TruncatedStringIO(io.IOBase): + def __init__(self, max_lines: int) -> None: + self._io = io.StringIO() + self._num_lines: int = 0 + self._max_lines: int = max_lines + + def getvalue(self) -> str: + return self._io.getvalue() + + def seek(self, whence): + return self._io.seek(whence) + + def tell(self): + return self._io.tell() + + def write(self, value: str) -> None: + if "\n" not in value: + self._io.write(value) + return + + value_lines = value.splitlines() + num_lines = len(value_lines) + can_write_all_lines = self._num_lines + num_lines < self._max_lines + if can_write_all_lines: + self._num_lines += num_lines + self._io.write(value) + return + + self._num_lines += len(value.splitlines()) + if self._num_lines > self._max_lines: + num_lines_to_write = self._max_lines - num_lines - 1 + lines_to_write = value_lines[0:num_lines_to_write] + self._io.write("\n".join(lines_to_write)) + self._io.write(f"\n...console output truncated at {self._max_lines} rows") + raise TruncatedStringException() + else: + self._io.write(value) + + try: + buf = TruncatedStringIO(max_rows) + self.write(buf) + except TruncatedStringException: + pass + + return buf.getvalue() + + def _format_to_symbol(self, format: format_type): + if format == format_type.long: + return "+" + if format == format_type.standard: + return "-" + if format == format_type.default: + return "" + raise RuntimeError("Unexpected format!") + + def _get_write_format(self, format: format_type, deck_format: typing.Optional[format_type] = None) -> format_type: + """Gets the write format.""" + if format == format_type.default: + return deck_format + + return format + + def _get_symbol(self, format: format_type, deck_format: typing.Optional[format_type] = None) -> str: + """Gets the format symbol (+ or -) used when writing the keyword. Depends on the deck format, if any.""" + if format == format_type.default: + return "" + + if deck_format == format_type.default: + return self._format_to_symbol(format) + + if deck_format == format: + # deck uses the same format as the keyword, no need to use a format symbol + return "" + else: + return self._format_to_symbol(format) + + def _write_header(self, buf: typing.TextIO, symbol: str) -> None: + buf.write(self.get_title(symbol) + "\n") + for comment_line in self._get_user_comment_lines(): + buf.write(comment_line + "\n") + + def write( + self, + buf: typing.Optional[typing.TextIO] = None, + format: typing.Optional[format_type] = None, + deck_format: format_type = format_type.default, + ) -> str: + """Renders the keyword in the dyna keyword format. + :param buf: Optional - buffer to write to. If None, the output is returned as a string + """ + if format == None: + format = self.format + will_return = buf == None + if will_return: + buf = io.StringIO() + self._write_header(buf, self._get_symbol(format, deck_format)) + format = self._get_write_format(format, deck_format) + superfluous_newline = Cards.write(self, buf, format) + if will_return: + keyword_string = buf.getvalue() + if superfluous_newline: # remove last character before returning + return keyword_string[:-1] + return keyword_string + else: + if superfluous_newline: + buf.seek(buf.tell() - 1) + + def dumps(self) -> str: + """Returns the string representation of the keyword""" + warnings.warn("dumps is deprecated - use write instead") + return self.write() + + def before_read(self, buf: typing.TextIO) -> None: + # subclasses can do custom logic before reading. + return + + def _process_title(self, title_line: str) -> None: + # Verify the title line and set the format, remove trailing +/- if set + title_line = title_line.strip() + + # the options are not activated yet, therefore get_title only returns title_prime + assert self.get_title().strip("*") in title_line, "first line in loads must contain the keyword title" + + if title_line.endswith("-"): + self.format = format_type.standard + return title_line[:-1] + if title_line.endswith("+"): + self.format = format_type.long + return title_line[:-1] + return title_line + + def read(self, buf: typing.TextIO) -> None: + title_line = buf.readline() + title_line = self._process_title(title_line) + self.before_read(buf) + if title_line != self.get_title(): + self._activate_options(title_line.strip("*")) + # TODO: self.user_comment should come from somewhere. + # maybe after the keyword but before any $# + self._read_data(buf) + + def loads(self, value: str) -> None: + """Loads the keyword from string. TODO - add a load from buffer""" + s = io.StringIO() + s.write(value) + s.seek(0) + self.read(s) diff --git a/src/ansys/dyna/core/lib/kwd_line_formatter.py b/src/ansys/dyna/core/lib/kwd_line_formatter.py new file mode 100644 index 000000000..f5944efbe --- /dev/null +++ b/src/ansys/dyna/core/lib/kwd_line_formatter.py @@ -0,0 +1,135 @@ +# 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 typing + + +def read_line(buf: typing.TextIO, skip_comment=True) -> typing.Tuple[str, bool]: + """Read and return the line, and a flag on whether to stop reading.""" + while True: + line = buf.readline() + len_line = len(line) + if len_line == 0: + return None, True + if skip_comment and line.startswith("$"): + continue + if line.startswith("*"): + # walk back to the start of the line + buf.seek(buf.tell() - len_line) + return None, True + if line.endswith("\n"): + line = line[:-1] + return line, False + + +def at_end_of_keyword(buf: typing.TextIO) -> bool: + """Return whether the buffer is at the end of the keyword""" + pos = buf.tell() + _, end_of_keyword = read_line(buf, True) + if end_of_keyword: + return True + buf.seek(pos) + return False + + +def buffer_to_lines(buf: typing.TextIO, max_num_lines: int = -1) -> typing.List[str]: + """Read from the buffer into a list of string. + buf: buffer to read from + max_num_lines: number of lines to read. -1 means no limit + """ + # used by tabular cards (duplicate card, duplicate card group) + # store all lines until one that starts with * into an array and then call load with it. + # perhaps pandas will support a iteration system that allows exiting + # on a certain line, but that doesn't appear to be possible yet. + # alternatively we could wrap the buffer in a class that does this, that might + # end up being better for performance + # https://pandas.pydata.org/docs/user_guide/io.html#iterating-through-files-chunk-by-chunk + data_lines: typing.List[str] = [] + if max_num_lines == 0: + return data_lines + + index = -1 + while True: + index += 1 + if max_num_lines > 0 and index == max_num_lines: + break + line, exit_loop = read_line(buf) + if exit_loop: + break + data_lines.append(line) + return data_lines + + +def load_dataline(spec: typing.List[tuple], line_data: str) -> tuple: + """loads a keyword card line with fixed column offsets and width from string + spec: list of tuples representing the (offset, width, type) of each field + line_data: string with keyword data + example: + >>> load_dataline([(0,10, int),(10,10, str)], ' 1 hello') + (1, 'hello') + """ + + def seek_text_block(line_data: str, position: int, width: int) -> str: + """Returns the text block from the line at the given position and width + If the position is past the end, it will return an empty string""" + end_position = position + width + text_block = line_data[position:end_position] + return end_position, text_block + + def has_value(text_block: str) -> bool: + """Given a text block - determine if a keyword value exists. + if its an empty string (i.e. the seek_text_block returned an empty + string) then there is no value. If its just whitespace, then + the keyword file did not include data for that field. + """ + if text_block == "": + return False + if text_block.isspace(): + return False + return True + + def get_none_value(item_type): + if item_type is float: + return float("nan") + return None + + data = [] + end_position = 0 + for item_spec in spec: + position, width, item_type = item_spec + end_position, text_block = seek_text_block(line_data, position, width) + if not has_value(text_block): + value = get_none_value(item_type) + elif item_type is int: + value = int(float(text_block)) + elif item_type is str: + value = text_block.strip() + elif item_type is float: + value = float(text_block) + else: + raise Exception(f"Unexpected type in load_dataline spec: {item_type}") + data.append(value) + + if end_position < len(line_data): + raise Exception("Data line is too long!") + + return tuple(data) diff --git a/src/ansys/dyna/core/lib/option_card.py b/src/ansys/dyna/core/lib/option_card.py new file mode 100644 index 000000000..c332bbccc --- /dev/null +++ b/src/ansys/dyna/core/lib/option_card.py @@ -0,0 +1,175 @@ +# 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. + +from dataclasses import dataclass +import io +import typing + +from ansys.dyna.core.lib.card_interface import CardInterface +from ansys.dyna.core.lib.card_writer import write_cards +from ansys.dyna.core.lib.format_type import format_type +from ansys.dyna.core.lib.io_utils import write_or_return + + +@dataclass +class OptionSpec: + name: str + card_order: int + title_order: int + + +class OptionCardSet(CardInterface): + def __init__( + self, + option_spec: OptionSpec, + cards: typing.List[CardInterface], + **kwargs, + ): + self._keyword: typing.Any = kwargs.get("keyword", None) + self._option_spec = option_spec + self._cards: typing.List[CardInterface] = cards + self._format_type: format_type = kwargs.get("format", format_type.default) + + @property + def cards(self) -> typing.List[CardInterface]: + return self._cards + + @property + def option_spec(self) -> OptionSpec: + return self._option_spec + + @property + def name(self) -> str: + return self._option_spec.name + + @property + def title_order(self) -> int: + return self._option_spec.title_order + + @property + def card_order(self) -> int: + return self._option_spec.card_order + + @property + def active(self) -> bool: + return self._keyword.is_option_active(self.name) + + @active.setter + def active(self, value: bool) -> None: + if value: + self._keyword.activate_option(self.name) + else: + self._keyword.deactivate_option(self.name) + + @property + def format(self) -> format_type: + """Get the card format type.""" + return self._format_type + + @format.setter + def format(self, value: format_type) -> None: + """Set the card format type.""" + self._format_type = value + + def __hash__(self): + return hash(self.card_order) + + def __lt__(self, other: "OptionCardSet"): + return self.card_order < other.card_order + + def read(self, buf: typing.TextIO) -> bool: + """Read from buf.""" + for card in self._cards: + card.read(buf) + + def write( + self, + format: typing.Optional[format_type] = None, + buf: typing.Optional[typing.TextIO] = None, + comment: typing.Optional[bool] = True, + ) -> typing.Union[str, None]: + """Renders the card in the dyna keyword format. + :param buf: Buffer to write to. If None, the output is returned as a string + :param format: format_type to use. Default to standard. + """ + + def _write(buf): + # TODO - write_cards should check the active func + if self.active: + write_cards(self._cards, buf, format, comment) + + return write_or_return(buf, _write) + + +class OptionAPI: + """API for an individual option associated with a keyword.""" + + def __init__(self, kw, name): + self._kw = kw + self._name = name + + @property + def active(self) -> bool: + return self._kw.is_option_active(self._name) + + @active.setter + def active(self, value: bool) -> None: + if value: + self._kw.activate_option(self._name) + else: + self._kw.deactivate_option(self._name) + + +class OptionsAPI: + """API for options associated with a keyword.""" + + def __init__(self, kw): + self._kw = kw + + def __getitem__(self, name: str) -> OptionAPI: + """Gets the option with the given name.""" + return OptionAPI(self._kw, name) + + def __repr__(self) -> str: + option_card_specs = self.option_specs + if len(option_card_specs) == 0: + return "" + sio = io.StringIO() + sio.write("Options:") + for option in option_card_specs: + active = self._kw.is_option_active(option.name) + active_string = "active" if active else "not active" + sio.write(f"\n {option.name} option is {active_string}.") + return sio.getvalue() + + def _get_option_specs(self, cards: typing.List[CardInterface]) -> typing.List[OptionSpec]: + option_specs: typing.List[OptionSpec] = [] + for card in cards: + if hasattr(card, "option_spec"): + option_specs.append(card.option_spec) + elif hasattr(card, "option_specs"): + option_specs.extend(card.option_specs) + return option_specs + + @property + def option_specs(self) -> typing.List[OptionSpec]: + return self._get_option_specs(self._kw._cards) diff --git a/src/ansys/dyna/core/lib/text_card.py b/src/ansys/dyna/core/lib/text_card.py new file mode 100644 index 000000000..33e9bd57e --- /dev/null +++ b/src/ansys/dyna/core/lib/text_card.py @@ -0,0 +1,95 @@ +# 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 typing + +from ansys.dyna.core.lib.format_type import format_type +from ansys.dyna.core.lib.kwd_line_formatter import read_line + +from .card_interface import CardInterface + +# TODO - should TextCard do anything special for long format? + + +class TextCard(CardInterface): + def __init__(self, name: str, content: str = None, format=format_type.default): + self.value = content + self._name = name + self._format_type = format + + @property + def bounded(self) -> bool: + """Text cards are always unbounded.""" + return False + + @property + def format(self) -> format_type: + return self._format_type + + @format.setter + def format(self, value: format_type) -> None: + self._format_type = value + + def _get_comment(self, format: typing.Optional[format_type]): + if format == None: + format = self._format_type + if format != format_type.long: + return "$#" + f"{{0:>{78}}}".format(self._name) + else: + return "$#" + f"{{0:>{158}}}".format(self._name) + + def read(self, buf: typing.TextIO) -> None: + self._content_lines = [] + while True: + line, exit_loop = read_line(buf) + if exit_loop: + break + self._content_lines.append(line) + + def write( + self, + format: typing.Optional[format_type] = None, + buf: typing.Optional[typing.TextIO] = None, + comment: typing.Optional[bool] = True, + ) -> str: + if format == None: + format = self._format_type + rows = [] + if comment: + rows.append(self._get_comment(format)) + rows.extend(self._content_lines) + lines = [row for row in rows if row] + output = "\n".join(lines) + if buf == None: + return output + buf.write(output) + + @property + def value(self) -> str: + return "\n".join(self._content_lines) + + @value.setter + def value(self, value: str): + if value == None: + self._content_lines = [] + else: + self._content_lines = value.split("\n") diff --git a/src/ansys/dyna/core/lib/variable_card.py b/src/ansys/dyna/core/lib/variable_card.py new file mode 100644 index 000000000..d2d670ff8 --- /dev/null +++ b/src/ansys/dyna/core/lib/variable_card.py @@ -0,0 +1,270 @@ +# 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 io +import math +import typing + +import ansys.dyna.core.lib.array as arr +from ansys.dyna.core.lib.card_interface import CardInterface +from ansys.dyna.core.lib.field import Field +from ansys.dyna.core.lib.field_writer import write_fields +from ansys.dyna.core.lib.format_type import format_type +from ansys.dyna.core.lib.kwd_line_formatter import load_dataline, read_line + + +class VariableCard(CardInterface): + """Variable length card""" + + def __init__( + self, + name: str, + card_size: int, + element_width: int, + type: type, + length_func: typing.Callable = None, + active_fn: typing.Callable = None, + data=None, + format: format_type = format_type.default, + ): + self._name = name + self._card_size = card_size + self._element_width = element_width + self._active_func = active_fn + self._type = type + self._initialize_data(0) + if isinstance(data, list): + self.data = data + if length_func == None: + self._bounded = False + self._length_func = lambda: len(self._data) + else: + self._bounded = True + self._length_func = length_func + self._format_type = format + + @property + def format(self) -> format_type: + return self._format_type + + @format.setter + def format(self, value: format_type) -> None: + self._format_type = value + + def _initialize_data(self, length): + self._data = arr.array(self._type, length) + + def _is_last_card(self, card_index: int): + count = self._length_func() + start = card_index * self._card_size + end = start + self._card_size + return end >= count + + def _get_card_range(self, card_index: int): + start_index = self._card_size * card_index + if self._is_last_card(card_index): + # last card, only use the remainder for number of fields + remainder = self._length_func() % self._card_size + if remainder == 0: + remainder = self._card_size + end_index = start_index + remainder + else: + # not last card, use all fields + end_index = start_index + self._card_size + return start_index, end_index + + def _get_comment(self, format: format_type) -> str: + if not self._is_last_card(0): + count = self._card_size + else: + # TODO test case when amount == card_size + count = self._length_func() % self._card_size or self._card_size + element_width = self._get_width(format) + element = " " * element_width + assert len(self._name) <= element_width - 2, "name of variable field is too long" + element = element[: -len(self._name)] + self._name + array = element * count + return "$#" + array[2:] + + def __get_value(self, index: int): + if index < len(self._data): + return self._data[index] + return self.__get_null_value() + + # TODO this should be on an Array class + def __get_null_value(self): + if self._type == float: + return math.nan + return None + + def _is_active(self) -> bool: + if self._active_func == None: + return True + return self._active_func() + + def _num_rows(self): + return math.ceil(self._length_func() / self._card_size) + + def __getitem__(self, index): + err_string = f"get indexer for VariableCard must be of the form [index] or [start:end]. End must be greater than start" # noqa : E501 + assert type(index) in (slice, int), err_string + if type(index) == int: + return self.__get_value(index) + assert index.stop > index.start and index.step == None, err_string + start = index.start + end = index.stop + assert type(start) == int, err_string + assert type(end) == int, err_string + assert end > start, err_string + return [self.__get_value(i) for i in range(start, end)] + + def __setitem__(self, index: int, value): + # first resize up to index. TODO this should be on an Array class + while len(self._data) <= index: + self._data.append(self.__get_null_value()) + # then set value at the specified index + self._data[index] = value + + def append(self, value) -> None: + self._data.append(value) + + def extend(self, valuelist) -> None: + self._data.extend(valuelist) + + def _get_width(self, format: typing.Optional[format_type] = None): + if format == None: + format = self.format + width = self._element_width + if format == format_type.long: + width = 20 + return width + + def _load_bounded_from_buffer(self, buf: typing.TextIO) -> None: + width = self._get_width() + num_lines = self._num_rows() + for index in range(num_lines): + line, exit_loop = read_line(buf) + if exit_loop: + break + start, end = self._get_card_range(index) + size = end - start + read_format = [(i * width, width, self._type) for i in range(size)] + values = load_dataline(read_format, line) + for j, value in zip(range(start, end), values): + self[j] = value + + def _load_unbounded_from_buffer(self, buf: typing.TextIO) -> None: + width = self._get_width() + self._initialize_data(0) + while True: + line, exit_loop = read_line(buf) + if exit_loop: + break + # this is going to be slower... because we don't know how many + # lines there are going to be. + size = math.ceil(len(line) / width) + trailing_spaces = len(line) - width * size + if trailing_spaces > 0: + print("Trailing spaces, TODO - write a test!") + line = line + " " * trailing_spaces + read_format = [(i * width, width, self._type) for i in range(size)] + values = load_dataline(read_format, line) + self.extend(values) + + def read(self, buf: typing.TextIO) -> bool: + if self.bounded: + self._load_bounded_from_buffer(buf) + return False + else: + self._load_unbounded_from_buffer(buf) + return True + + def _get_lines(self, format: typing.Optional[format_type], comment: bool) -> typing.List[str]: + if self._num_rows() == 0: + return [] + if format == None: + format = self._format_type + content_lines = [] + if comment: + content_lines.append(self._get_comment(format)) + for i in range(self._num_rows()): + content_lines.append(self._get_row_data(i, format)) + return content_lines + + def write( + self, + format: typing.Optional[format_type] = None, + buf: typing.Optional[typing.TextIO] = None, + comment: typing.Optional[bool] = True, + ) -> str: + if format == None: + format = self._format_type + output = "" + if self._is_active(): + lines = [row for row in self._get_lines(format, comment) if row] + output = "\n".join(lines) + if buf == None: + return output + buf.write(output) + + def _write_row(self, format: format_type, start_index: int, end_index: int) -> str: + """Fields aren't really the right abstraction for a variable card, + but its an easy way to reuse the code in write_fields so we create fields + on the fly here. TODO - reuse less of the code without creating fields on the fly""" + row_values = self[start_index:end_index] + width = self._element_width + size = end_index - start_index + row_fields = [Field(self._name, self._type, i * width, width) for i in range(size)] + s = io.StringIO() + write_fields(s, row_fields, row_values, format) + return s.getvalue() + + def _get_row_data(self, index: int, format: format_type) -> str: + start_index, end_index = self._get_card_range(index) + return self._write_row(format, start_index, end_index) + + def __len__(self) -> int: + return self._length_func() + + @property + def bounded(self) -> bool: + return self._bounded + + def __repr__(self) -> str: + """Returns a console-friendly representation of the desired parameters for the card""" + content_lines = [] + content_lines.append(self._get_comment(self._format_type)) + output = "\n".join(content_lines) + return "VariableCard: \n" + output + + @property + def data(self): + """Gets or sets the data list of parameter values""" + return self._data + + @data.setter + def data(self, vallist: typing.List) -> None: + self._initialize_data(0) + self._data.extend(vallist) + + def extend(self, values: typing.List) -> None: + self._data.extend(values) diff --git a/tests/conftest.py b/tests/conftest.py index 625de53f4..a923d58c5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,6 +4,9 @@ pytest as a sesson fixture """ import os +import io +import pathlib + import pytest from ansys.dyna.core.pre.launcher import ServerThread @@ -148,7 +151,7 @@ def Connect_Server(): """Connect to the kwserver.""" path = get_server_path() threadserver = ServerThread(1,port=50051,ip="127.0.0.1",server_path = path) - threadserver.setDaemon(True) + threadserver.daemon = True threadserver.start() @@ -159,3 +162,59 @@ def pytest_collection_modifyitems(config, items): return # command line has a -k or -m, let pytest handle it skip_run = pytest.mark.skip(reason="run not selected for pytest run (`pytest -m run`). Skip by default") [item.add_marker(skip_run) for item in items if "run" in item.keywords] + + +class StringUtils: + def as_buffer(self, string: str) -> io.StringIO: + s = io.StringIO(string) + s.seek(0) + return s + + +@pytest.fixture +def string_utils() -> StringUtils: + string_utils = StringUtils() + return string_utils + + +class FileUtils: + def __normalize_line_endings(self, text: str) -> str: + return text.replace("\r\n", "\n").replace("\r", "\n") + + @property + def assets_folder(self) -> pathlib.Path: + return pathlib.Path(__file__).parent / "testfiles" / "keywords" + + def read_file(self, file: pathlib.Path) -> str: + with open(file, encoding="utf-8") as ref: + return self.__normalize_line_endings(ref.read()) + + def get_asset_file_path(self, reference_file: str) -> str: + reference_file: pathlib.Path = self.assets_folder / reference_file + return str(reference_file.resolve()) + + def compare_string_with_file(self, output: str, reference_file: str) -> None: + """compare the string in output, with the contents of reference_file + normalize all line endinges to \\n + """ + reference_file: pathlib.Path = self.assets_folder / reference_file + output = self.__normalize_line_endings(output) + ref_contents = FileUtils().read_file(reference_file) + assert output == ref_contents + + +@pytest.fixture +def file_utils() -> FileUtils: + file_utils = FileUtils() + return file_utils + + +@pytest.fixture +def ref_string(file_utils: FileUtils): + import importlib.util + + ref_string_file = file_utils.get_asset_file_path("reference_string.py") + spec = importlib.util.spec_from_file_location("reference_string", ref_string_file) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module diff --git a/tests/test_card.py b/tests/test_card.py new file mode 100644 index 000000000..489c07ecf --- /dev/null +++ b/tests/test_card.py @@ -0,0 +1,84 @@ +# 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 pytest + +from ansys.dyna.core.lib.card import Card, Field +from ansys.dyna.core.lib.format_type import format_type + + +@pytest.mark.keywords +def test_load_card_errors(string_utils): + """Error test for loading a card.""" + fields = [ + Field("foo", int, 0, 10, None), + Field("bar", int, 10, 10, None), + ] + + card = Card(fields) + with pytest.raises(Exception): + # cards can only load a readable buffer + card.read("") + + with pytest.raises(Exception): + # error if the line that is too long + buf = " " + card.read(string_utils.as_buffer(buf)) + + +@pytest.mark.keywords +def test_load_card_basic(string_utils): + fields = [ + Field("foo", int, 0, 10, None), + Field("bar", int, 10, 10, None), + ] + card = Card(fields) + card.read(string_utils.as_buffer(" ")) + assert card.get_value("foo") == None + assert card.get_value("bar") == None + card = Card(fields) + card.read(string_utils.as_buffer(" 8 4")) + assert card.get_value("foo") == 8 + assert card.get_value("bar") == 4 + + +@pytest.mark.keywords +def test_load_card_long(string_utils): + fields = [ + Field("foo", int, 0, 10, None), + Field("bar", int, 10, 10, None), + ] + card = Card(fields, format=format_type.long) + buf = string_utils.as_buffer(" 4") + card.read(buf) + assert card.get_value("foo") == None + assert card.get_value("bar") == 4 + + +@pytest.mark.keywords +def test_write_inactive_card(): + fields = [ + Field("foo", int, 0, 10, None), + Field("bar", int, 10, 10, None), + ] + card = Card(fields, lambda: False, format=format_type.long) + assert card.write() == "" diff --git a/tests/test_duplicate_card.py b/tests/test_duplicate_card.py new file mode 100644 index 000000000..3cc549af2 --- /dev/null +++ b/tests/test_duplicate_card.py @@ -0,0 +1,216 @@ +# 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 numpy as np +import pandas as pd +import pytest + +from ansys.dyna.core.lib.card import Field +from ansys.dyna.core.lib.duplicate_card import DuplicateCard +from ansys.dyna.core.lib.format_type import format_type + + +@pytest.mark.keywords +def test_duplicate_card_read_bounded(string_utils): + """test reading fixed number of lines""" + d = DuplicateCard( + [ + Field("nid", int, 0, 8), + Field("x", float, 8, 16), + Field("y", float, 24, 16), + Field("z", float, 40, 16), + Field("tc", int, 56, 8), + Field("rc", int, 64, 8), + ], + lambda: 3, + ) + + card_text = """ 2000000 + 2000001 -2772.1652832 643.8095703 376.7990417 + 2000002 -3093.8891602 685.0078125 811.2246704 2 5""" + d.read(string_utils.as_buffer(card_text)) + table = d.table + assert len(table) == 3 + node_repr = """ nid x y z tc rc +0 2000000 NaN NaN NaN +1 2000001 -2772.165283 643.809570 376.799042 +2 2000002 -3093.889160 685.007812 811.224670 2 5""" + assert repr(table) == node_repr + assert table["x"][1] == -2772.1652832 + assert table["rc"][2] == 5 + + +@pytest.mark.keywords +def test_duplicate_card_read_unbounded(string_utils): + """test reading an unknown number of lines into an unbounded card""" + d = DuplicateCard( + [ + Field("nid", int, 0, 8), + Field("x", float, 8, 16), + Field("y", float, 24, 16), + Field("z", float, 40, 16), + Field("tc", int, 56, 8), + Field("rc", int, 64, 8), + ], + None, + ) + card_text = """ 2000000 + 2000001 -2772.1652832 643.8095703 376.7990417 + 2000002 -3093.8891602 685.0078125 811.2246704 2 5""" + d.read(string_utils.as_buffer(card_text)) + table = d.table + assert len(table) == 3 + node_repr = """ nid x y z tc rc +0 2000000 NaN NaN NaN +1 2000001 -2772.165283 643.809570 376.799042 +2 2000002 -3093.889160 685.007812 811.224670 2 5""" + assert repr(table) == node_repr + assert table["x"][1] == -2772.1652832 + assert table["rc"][2] == 5 + + +@pytest.mark.keywords +def test_duplicate_card_assign(): + """test assigning dataframe to duplicate card""" + d = DuplicateCard( + [ + Field("nid", int, 0, 8), + Field("x", float, 8, 16), + Field("y", float, 24, 16), + Field("z", float, 40, 16), + Field("tc", int, 56, 8), + Field("rc", int, 64, 8), + ], + None, + ) + + node_ids = np.arange(30) + 1 + xs = np.zeros(30) + 0.1 + ys = np.zeros(30) + 0.2 + zs = np.zeros(30) + 0.3 + df = pd.DataFrame({"nid": node_ids, "x": xs, "y": ys, "z": zs}) + d.table = df # assign the dataframe + table = d.table # get the dataframe, see if the contents match what was assigned + assert (len(table)) == 30 + for column in ["nid", "x", "y", "z"]: + assert len(df[column]) == len(table[column]), f"Length of {column} column doesn't match" + assert len(df[column].compare(table[column])) == 0, f"{column} column values don't match" + + +@pytest.mark.keywords +def test_duplicate_card_assign_wrong_types(): + """test assigning wrong type as dataframe to duplicate card""" + + def assign(): + d = DuplicateCard( + [ + Field("nid", int, 0, 8), + Field("x", float, 8, 16), + Field("y", float, 24, 16), + Field("z", float, 40, 16), + Field("tc", int, 56, 8), + Field("rc", int, 64, 8), + ], + None, + ) + node_ids = np.arange(30) + 1 + d.table = node_ids + + pytest.raises(AssertionError, assign) + + +@pytest.mark.keywords +def test_duplicate_card_write_long_format(string_utils, ref_string): + """Test writing a duplicate card with the long format.""" + d = DuplicateCard( + [ + Field("nid", int, 0, 8), + Field("x", float, 8, 16), + Field("y", float, 24, 16), + Field("z", float, 40, 16), + Field("tc", int, 56, 8), + Field("rc", int, 64, 8), + ], + None, + ) + card_text = """ 2000000 + 2000001 -2772.1652832 643.8095703 376.7990417 + 2000002 -3093.8891602 685.0078125 811.2246704 2 5""" + d.read(string_utils.as_buffer(card_text)) + d_str = d.write(format=format_type.long) + assert d_str == ref_string.test_mesh_string_long + + +@pytest.mark.keywords +def test_duplicate_card_read_long(string_utils, ref_string): + """Test writing a duplicate card with the long format.""" + d = DuplicateCard( + [ + Field("nid", int, 0, 8), + Field("x", float, 8, 16), + Field("y", float, 24, 16), + Field("z", float, 40, 16), + Field("tc", int, 56, 8), + Field("rc", int, 64, 8), + ], + None, + ) + d.format = format_type.long + d.read(string_utils.as_buffer(ref_string.test_mesh_string_long)) + table = d.table + assert len(table) == 3 + assert table["x"][1] == -2772.1652832 + assert pd.isna(table["x"][0]) + + +@pytest.mark.keywords +def test_write_inactive_duplicate_card(): + card = DuplicateCard( + [ + Field("nid", int, 0, 8), + Field("x", float, 8, 16), + Field("y", float, 24, 16), + Field("z", float, 40, 16), + Field("tc", int, 56, 8), + Field("rc", int, 64, 8), + ], + None, + lambda: False, + ) + assert card.write() == "" + + +@pytest.mark.keywords +def test_write_empty_duplicate_card(): + card = DuplicateCard( + [ + Field("nid", int, 0, 8), + Field("x", float, 8, 16), + Field("y", float, 24, 16), + Field("z", float, 40, 16), + Field("tc", int, 56, 8), + Field("rc", int, 64, 8), + ], + lambda: 0, + lambda: True, + ) + assert card.write() == "" diff --git a/tests/test_duplicate_card_group.py b/tests/test_duplicate_card_group.py new file mode 100644 index 000000000..0a863add7 --- /dev/null +++ b/tests/test_duplicate_card_group.py @@ -0,0 +1,228 @@ +# 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 io + +import pandas as pd + +from ansys.dyna.core.lib.card import Card, Field +from ansys.dyna.core.lib.duplicate_card_group import DuplicateCard, DuplicateCardGroup +from ansys.dyna.core.lib.field_writer import write_fields +from ansys.dyna.core.lib.format_type import format_type + +import pytest + + +def _get_test_duplicate_group(bounded: bool) -> DuplicateCardGroup: + if bounded: + lengthfunc = lambda: 2 + else: + lengthfunc = None + return DuplicateCardGroup( + [ + Card( + [ + Field("eid", int, 0, 8), + Field("pid", int, 8, 8), + Field("n1", int, 16, 8), + Field("n2", int, 24, 8), + Field("n3", int, 32, 8), + Field("n4", int, 40, 8), + Field("n5", int, 48, 8), + Field("n6", int, 56, 8), + Field("n7", int, 64, 8), + Field("n8", int, 72, 8), + ], + ), + Card( + [ + Field("a1", float, 0, 16), + Field("a2", float, 16, 16), + Field("a3", float, 32, 16), + ], + ), + Card( + [ + Field("d1", float, 0, 16), + Field("d2", float, 16, 16), + Field("d3", float, 32, 16), + ], + ), + ], + lengthfunc, + ) + + +def _get_row_data(dcg: DuplicateCardGroup, index: int) -> str: + which_card_index = dcg._get_index_of_which_card(index) + card_index = dcg._get_index_of_given_card(index) + card = dcg._get_active_cards()[which_card_index] + + def _get_card_row_data(card: DuplicateCard, index: int, format: format_type) -> str: + """this is not optimized - used only by test cases""" + values = card._get_row_values(index) + s = io.StringIO() + write_fields(s, card._fields, values, format) + return s.getvalue() + + return _get_card_row_data(card, card_index, dcg.format) + + +@pytest.mark.keywords +def test_duplicate_card_group_bounded_empty(): + """test bounded duplicate group""" + d = _get_test_duplicate_group(True) + # the length is 6 even before data is assigned + assert d._num_rows() == 6 + + # row data are just blank lines + assert _get_row_data(d, 0) == " " + assert _get_row_data(d, 1) == " " + assert _get_row_data(d, 2) == " " + assert _get_row_data(d, 3) == " " + assert _get_row_data(d, 4) == " " + assert _get_row_data(d, 5) == " " + + +@pytest.mark.keywords +def test_duplicate_card_group_unbounded_read(string_utils): + """test reading row data into duplicate group""" + d = _get_test_duplicate_group(False) + card_text = """ 1 2 1 2 3 4 5 6 7 8 + 0.1 0.2 0.3 + 0.3 0.4 0.5 + 1 2 5 6 7 8 1 3 2 4 + 0.2 0.3 0.4 + 0.4 0.5 0.6""" + d.read(string_utils.as_buffer(card_text)) + tables = [card.table for card in d._cards] + for table in tables: + assert len(table) == 2 + + assert tables[0]["eid"][0] == 1 + assert tables[0]["eid"][1] == 1 + assert tables[0]["n1"][0] == 1 + assert tables[0]["n8"][0] == 8 + assert tables[0]["n4"][1] == 8 + table0_repr = """ eid pid n1 n2 n3 n4 n5 n6 n7 n8 +0 1 2 1 2 3 4 5 6 7 8 +1 1 2 5 6 7 8 1 3 2 4""" + assert repr(tables[0]) == table0_repr + + assert d._num_rows() == 6 + + +@pytest.mark.keywords +def test_duplicate_card_group_unbounded_empty(): + """test unbounded duplicate group""" + d = _get_test_duplicate_group(False) + d.table = pd.DataFrame( + { + "eid": [1, 1], + "pid": [2, 2], + "n1": [1, 5], + "n2": [2, 6], + "n3": [3, 7], + "n4": [4, 8], + "n5": [5, 1], + "n6": [6, 3], + "n7": [7, 2], + "n8": [8, 4], + "a1": [0.1, 0.2], + "a2": [0.2, 0.3], + "a3": [0.3, 0.4], + "d1": [0.3, 0.4], + "d2": [0.4, 0.5], + "d3": [0.5, 0.6], + } + ) + + # there are 2 in each card, so the total length should be 6 + assert d._num_rows() == 6 + + +@pytest.mark.keywords +def test_duplicate_card_group_unbounded_read_long(string_utils, ref_string): + """test reading long row data into duplicate group""" + d = _get_test_duplicate_group(False) + card_text = ref_string.test_duplicate_card_group_long + d.format = format_type.long + d.read(string_utils.as_buffer(card_text)) + tables = [card.table for card in d._cards] + for table in tables: + assert len(table) == 2 + + assert tables[0]["eid"][0] == 1 + assert tables[0]["eid"][1] == 1 + assert tables[0]["n1"][0] == 1 + assert tables[0]["n8"][0] == 8 + assert tables[0]["n4"][1] == 8 + table0_repr = """ eid pid n1 n2 n3 n4 n5 n6 n7 n8 +0 1 2 1 2 3 4 5 6 7 8 +1 1 2 5 6 7 8 1 3 2 4""" + assert repr(tables[0]) == table0_repr + + assert d._num_rows() == 6 + + +@pytest.mark.keywords +def test_duplicate_card_group_assign_column(): + d = _get_test_duplicate_group(False) + d.table = pd.DataFrame( + { + "eid": [1, 1], + "pid": [2, 2], + "n1": [1, 5], + "n2": [2, 6], + "n3": [3, 7], + "n4": [4, 8], + "n5": [5, 1], + "n6": [6, 3], + "n7": [7, 2], + "n8": [8, 4], + "a1": [0.1, 0.2], + "a2": [0.2, 0.3], + "a3": [0.3, 0.4], + "d1": [0.3, 0.4], + "d2": [0.4, 0.5], + "d3": [0.5, 0.6], + } + ) + assert d.table["a1"][0] == 0.1 + d.table.loc[0, "a1"] = 0.2 + assert d.table["a1"][0] == 0.2 + + +@pytest.mark.keywords +def test_write_inactive_duplicate_card_group(): + card = _get_test_duplicate_group(False) + card._active_func = lambda: False + assert card.write() == "" + card = _get_test_duplicate_group(True) + card._active_func = lambda: False + assert card.write() == "" + + +@pytest.mark.keywords +def test_write_empty_duplicate_card_group(): + d = _get_test_duplicate_group(False) + assert d.write() == "" diff --git a/tests/test_field_writer.py b/tests/test_field_writer.py new file mode 100644 index 000000000..62928b2eb --- /dev/null +++ b/tests/test_field_writer.py @@ -0,0 +1,136 @@ +# 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 io +import typing + +from ansys.dyna.core.lib.field import Field +import ansys.dyna.core.lib.field_writer as field_writer +from ansys.dyna.core.lib.format_type import format_type + + +import pytest + +def _get_comment_line(fields: typing.List[Field]) -> str: + s = io.StringIO() + field_writer.write_comment_line(s, fields) + return s.getvalue() + + +def _get_field_value(fields: typing.List[Field], long: bool = False) -> str: + s = io.StringIO() + format = format_type.default + if long: + format = format_type.long + field_writer.write_fields(s, fields, None, format) + return s.getvalue() + +@pytest.mark.keywords +def test_comment(): + fields = [Field("a", int, 0, 10, 1), Field("b", str, 10, 10, "hello")] + result = _get_comment_line(fields) + assert result == "$# a b" + + +@pytest.mark.keywords +def test_comment_with_gap(): + """test writing comment line with a gap""" + fields = [ + Field("secid", int, 0, 10, None), + Field("elform", int, 10, 10, 1), + Field("aet", int, 20, 10, 0), + Field("cohoff", float, 60, 10, None), + Field("gaskeit", float, 70, 10, None), + ] + result = _get_comment_line(fields) + assert result == "$# secid elform aet cohoff gaskeit" + + +@pytest.mark.keywords +def test_comment_with_offset(): + """test writing comment line with an offset at the beginning""" + fields = [ + Field("elform", int, 10, 10, 1), + Field("aet", int, 20, 10, 0), + Field("cohoff", float, 60, 10, None), + Field("gaskeit", float, 70, 10, None), + ] + result = _get_comment_line(fields) + assert result == "$# elform aet cohoff gaskeit" + + +@pytest.mark.keywords +def test_field_values_int_string(): + """test integer and string field values""" + fields = [Field("a", int, 0, 10, 1), Field("b", str, 10, 10, "hello")] + result = _get_field_value(fields) + assert result == " 1hello " + + +@pytest.mark.keywords +def test_field_values_int_string_gap(): + """test integer and string field values with a gap""" + fields = [Field("a", int, 0, 10, 1), Field("b", str, 20, 10, "hello")] + result = _get_field_value(fields) + assert result == " 1 hello " + + +@pytest.mark.keywords +def test_field_values_int_float_string(): + fields = [Field("a", int, 0, 10, 1), Field("b", float, 10, 10, 2.0), Field("c", str, 20, 10, "hello")] + result = _get_field_value(fields) + assert result == " 1 2.0hello " + + +@pytest.mark.keywords +def test_field_values_with_nan(): + fields = [Field("a", int, 0, 10, 1), Field("b", float, 10, 10, float("nan")), Field("c", str, 20, 10, "hello")] + result = _get_field_value(fields) + assert result == " 1 hello " + + +@pytest.mark.keywords +def test_field_overriden_values(): + fields = [Field("a", int, 0, 10, 1), Field("b", float, 10, 10, float("nan")), Field("c", str, 20, 10, "hello")] + s = io.StringIO() + values = [12, 2.2109, "bye"] + field_writer.write_fields(s, fields, values) + result = s.getvalue() + assert result == " 12 2.2109bye " + + +@pytest.mark.keywords +def test_field_overriden_values_with_gap(): + fields = [Field("a", int, 0, 10, 1), Field("c", str, 20, 10, "hello")] + s = io.StringIO() + values = [12, "bye"] + field_writer.write_fields(s, fields, values) + result = s.getvalue() + assert result == " 12 bye " + + +@pytest.mark.keywords +def test_field_values_int_string_long(): + """test long format for integer and string field values""" + fields = [Field("a", int, 0, 10, 1), Field("b", str, 10, 10, "hello")] + result = _get_field_value(fields, True) + assert result == " 1hello " diff --git a/tests/test_load_dataline.py b/tests/test_load_dataline.py new file mode 100644 index 000000000..1cab82c89 --- /dev/null +++ b/tests/test_load_dataline.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. + +import math + +import pytest + +from ansys.dyna.core.lib.kwd_line_formatter import load_dataline + +@pytest.mark.keywords +def test_load_dataline_001(): + """test loading a line of right-justified int and string""" + x = load_dataline([(0, 10, int), (10, 10, str)], " 1 hello") + assert x == (1, "hello") + + +@pytest.mark.keywords +def test_load_dataline_002(): + """test loading a line of right-justified int and left-justified string""" + x = load_dataline([(0, 10, int), (10, 10, str)], " 1hello ") + assert x == (1, "hello") + + +@pytest.mark.keywords +def test_load_dataline_003(): + """test loading a line of float and int, where the int is written as a float""" + x = load_dataline([(0, 8, int), (8, 8, float)], " 0.0 1.0") + assert x == (0, 1.0) + + +def test_load_dataline_004(): + """test loading a partial line""" + x = load_dataline([(0, 8, int), (8, 8, float), (16, 8, str)], " 0.0 1.0") + assert x == (0, 1.0, None) + + +def test_load_dataline_005(): + """test loading a partial line with missing float""" + a, b, c = load_dataline([(0, 8, int), (8, 8, float), (16, 8, float)], " 0.0 1.0") + assert a == 0 and b == 1.0 and math.isnan(c) + + +def test_load_dataline_006(): + """test loading a data line that is too long.""" + with pytest.raises(Exception): + load_dataline([(0, 8, int), (8, 8, float), (16, 8, float)], " ") diff --git a/tests/test_text_card.py b/tests/test_text_card.py new file mode 100644 index 000000000..5d08a4b89 --- /dev/null +++ b/tests/test_text_card.py @@ -0,0 +1,69 @@ +# 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. + +from ansys.dyna.core.lib.format_type import format_type +from ansys.dyna.core.lib.text_card import TextCard + +import pytest + + +@pytest.mark.keywords +def test_assign_text_card(): + """test assigning one-line value to text card""" + t = TextCard("function", "f(a,b,c)=a*2+b*c+sqrt(a*c)") + assert t.value == "f(a,b,c)=a*2+b*c+sqrt(a*c)" + assert len(t._content_lines) == 1 + assert t._content_lines[0] == "f(a,b,c)=a*2+b*c+sqrt(a*c)" + ref = "$# function" + assert t._get_comment(format_type.default) == ref + + +@pytest.mark.keywords +def test_read_text_card(string_utils): + card_text = """1,x-velo +x(t)=1000*sin(100*t)""" + t = TextCard("function") + t.read(string_utils.as_buffer(card_text)) + assert t.value == card_text + assert len(t._content_lines) == 2 + assert t._content_lines[1] == "x(t)=1000*sin(100*t)" + + +@pytest.mark.keywords +def test_text_card_write_long(ref_string): + """test writing long format text card.""" + t = TextCard("function", "f(a,b,c)=a*2+b*c+sqrt(a*c)") + assert t.value == "f(a,b,c)=a*2+b*c+sqrt(a*c)" + assert len(t._content_lines) == 1 + assert t._content_lines[0] == "f(a,b,c)=a*2+b*c+sqrt(a*c)" + assert t._get_comment(format_type.long) == ref_string.test_text_card_long + + +@pytest.mark.keywords +def test_text_card_read_long(string_utils): + """test reading long format text card.""" + t = TextCard("function", format=format_type.long) + # read one long line + data = "-" * 160 + t.read(string_utils.as_buffer(data)) + assert len(t._content_lines) == 1 + assert t.value == data diff --git a/tests/test_variable_card.py b/tests/test_variable_card.py new file mode 100644 index 000000000..e0e217343 --- /dev/null +++ b/tests/test_variable_card.py @@ -0,0 +1,130 @@ +# 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 math + +from ansys.dyna.core import Deck +from ansys.dyna.core.lib.format_type import format_type +from ansys.dyna.core.lib.variable_card import VariableCard + +import pytest + + +@pytest.mark.keywords +def test_variable_card_length_function(): + """test variable card""" + v = VariableCard("bi", 8, 10, float, lambda: 4) + assert len(v) == 4 + assert (0, 4) == v._get_card_range(0), "card range incorrect" + assert v._is_last_card(0), "first card should be last" + assert v._get_comment(format_type.default) == "$# bi bi bi bi" + v._length_func = lambda: 9 + assert (0, 8) == v._get_card_range(0), "card range incorrect" + assert (8, 9) == v._get_card_range(1), "card range incorrect" + assert not v._is_last_card(0), "first card should be last" + assert ( + v._get_comment(format_type.default) + == "$# bi bi bi bi bi bi bi bi" + ) + + +@pytest.mark.keywords +def test_variable_card_read(string_utils): + """test loading from buffer""" + string = " 1.0 2.0 " + v = VariableCard("bi", 8, 10, float, lambda: 3) + v.read(string_utils.as_buffer(string)) + assert v[0] == 1.0 + assert v[1] == 2.0 + assert math.isnan(v[2]) + + +@pytest.mark.keywords +def test_variable_card_unbounded(string_utils): + """test unbounded variable card""" + v = VariableCard("bi", 8, 10, float) + string = " 1.0 2.0 " + v.read(string_utils.as_buffer(string)) + assert v._num_rows() == 1 + assert len(v) == 3 + v[2] = 3.0 + assert v.write(format_type.default) == "$# bi bi bi\n 1.0 2.0 3.0" + for i in range(10): + v.append(i) + assert len(v) == 13 + assert v._num_rows() == 2 + + +@pytest.mark.keywords +def test_write_inactive_variable_card(): + card = VariableCard("bi", 8, 10, float, None, lambda: False) + assert card.write() == "" + + +@pytest.mark.keywords +def test_write_empty_variable_card(): + card = VariableCard("bi", 8, 10, float, lambda: 0) + assert card.write() == "" + + +@pytest.mark.keywords +def test_variable_card_set_single_value(): + """test setting single value of bounded variable card""" + v = VariableCard("pi", 8, 10, float, lambda: 3) + v[0] = 22 + rowdata = v._get_row_data(0, format_type.default) + assert rowdata.startswith(" 22.0"), f"incorrect rowdata: {rowdata}" + + +@pytest.mark.keywords +def test_variable_car_write_long(ref_string): + """test writing unbounded long variable card with two rows.""" + v = VariableCard("bi", 8, 10, float) + v.format_type = format_type.long + for i in range(10): + v.append(i) + assert v._num_rows() == 2 + assert v.write(format_type.long) == ref_string.test_variable_card_string + + +@pytest.mark.keywords +def test_variable_card_read_long(string_utils): + """test reading unbounded long variable card with one row.""" + v = VariableCard("bi", 8, 10, float) + v.format = format_type.long + string = " 1.0 2.0 " + v.read(string_utils.as_buffer(string)) + assert v._num_rows() == 1 + assert len(v) == 3 + assert v[0] == 1.0 + assert v[1] == 2.0 + assert math.isnan(v[2]) + + +@pytest.mark.xfail(reason = "Keyword module not yet available.") +@pytest.mark.keywords +def test_variable_card_read_write_set(ref_string): + """test to read and write variable cards, especially checking case where last card contains all fields""" + set_string = ref_string.test_variable_card_sets_string + input_deck = Deck() + input_deck.loads(set_string) + assert input_deck.write() == set_string diff --git a/tests/testfiles/keywords/reference_string.py b/tests/testfiles/keywords/reference_string.py new file mode 100644 index 000000000..ca9f8b9c7 --- /dev/null +++ b/tests/testfiles/keywords/reference_string.py @@ -0,0 +1,1177 @@ +# 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. + +test_deck_004_string = """*INCLUDE +$# filename + /path/to/test.k + +*DEFINE_CONTACT_VOLUME +$# cvid cid type xc yc zc + 0 +$# xmn xmx ymn ymx zmn zmx + 0.0 0.0 0.0 0.0 0.0 0.0 +*END +*DEFINE_CONTACT_VOLUME +$# cvid cid type xc yc zc + 1 +$# length rinner router d_angc + 0.0 0.0 0.0 0.0 +*END + """ + +test_deck_006_string_1 = """*INCLUDE +$# filename + /path/to/test.k + +*DEFINE_CONTACT_VOLUME +$# cvid cid type xc yc zc + 0 +$# xmn xmx ymn ymx zmn zmx + 0.0 0.0 0.0 0.0 0.0 0.0 +*END +""" + +test_deck_006_string_2 = """*DEFINE_CONTACT_VOLUME +$# cvid cid type xc yc zc + 1 +$# length rinner router d_angc + 0.0 0.0 0.0 0.0 +*END +""" + +test_deck_006_string_sum = """*INCLUDE +$# filename + /path/to/test.k + +*DEFINE_CONTACT_VOLUME +$# cvid cid type xc yc zc + 0 +$# xmn xmx ymn ymx zmn zmx + 0.0 0.0 0.0 0.0 0.0 0.0 +*DEFINE_CONTACT_VOLUME +$# cvid cid type xc yc zc + 1 +$# length rinner router d_angc + 0.0 0.0 0.0 0.0 +*END +""" + +test_kwdeck_basic_001_string = ["""*ALE_SMOOTHING +$# dnid nid1 nid2 ipre xco yco zco + 3 0 0.0 0.0 0.0""", +"""*BOUNDARY_PRECRACK +$# pid ctype np + 1 +$# x y z + """, +"""*BOUNDARY_ACOUSTIC_COUPLING +$# ssid + """, +"""*BOUNDARY_TEMPERATURE_SET +$# nsid lcid cmult loc tdeath tbirth + 0 1.0 0 1e+20 0.0"""] + + +test_kwlist_string = ["""*ALE_SMOOTHING +$# dnid nid1 nid2 ipre xco yco zco + 3 0 0.0 0.0 0.0""", +"""*BOUNDARY_PRECRACK +$# pid ctype np + 1 +$# x y z + """] + + +test_title_string = """*Keyword +*DEFINE_CURVE_TITLE +title +$# lcid sidr sfa sfo offa offo dattyp lcint + 1 +$# a1 o1 + 0.0 0.0 + 1.0 1.0""" + +test_node_long_id = """*NODE +$# nid x y z tc rc +69000001 683.94961562637 -477.9565044024 -65.1 0 0""" + +test_set_node_title = """*SET_NODE_TITLE +nodeset + 69000017 + 70332693 70540826 70540837 70540840 70540846 70540853 70540857 70540869 + 70540871 70540875 70540887 70540888 70540890 70563790 70563792 70563794 + 70573162""" + +test_mat_piecewise_linear_plasticity_title = """*MAT_PIECEWISE_LINEAR_PLASTICITY_TITLE +mat24 + 690000022.24507E-6 1. 0.3 0.1 0. 1.E30 + 0. 0. 69000001 0 0. + 0. 0. 0. 0. 0. 0. 0. 0. + 0. 0. 0. 0. 0. 0. 0. 0.""" + +test_constrained_nodal_rigid_body_inertia_title = """*CONSTRAINED_NODAL_RIGID_BODY_INERTIA_TITLE +Rigid Body Connection Element + 69002781 69000269 0 0 0 + 0. 0 + 0. 0. 0. 0. 0. 0. + """ + +test_contact_tied_shell_edge_to_surface_id = """*CONTACT_TIED_SHELL_EDGE_TO_SURFACE_ID +69000005 TIED_TEST3 + 69000268 69000267 2 2 0 0 + 0 + -2. -2. + 0 0.1 1.025 0. 2 0 0 + 0. 0 0 0 0 0 0. 0. + 0 0. 0. 0. 0 + 0 0 + 0 0 1 0 0 0""" + +test_section_solid_title_deck_string = """$ +*KEYWORD +*SECTION_SOLID_TITLE +$# title +section3 +$# secid elform aet unused unused unused cohoff gaskeit + 69000314 13 +*END""" + +test_hourglass_title = """*HOURGLASS_TITLE +$# title +hello +$# hgid ihq qm ibq q1 q2 qb/vdc qw + 0 0 0.1 1.5 0.06 0.1 0.1""" + +test_mesh_string = """*NODE +$# nid x y z tc rc + 100 -0.2969848 0.2969848 0.0 + 101 -0.2687006 0.2687006 0.0 + 104 -0.160727 0.3880294 0.0 + 105 -0.1454197 0.3510742 0.0 + 106 -0.2969848 0.2969848 0.25 + 107 -0.2687006 0.2687006 0.25 + 108 -0.1454197 0.3510742 0.25 + 109 -0.160727 0.3880294 0.25 + 110 -0.2969848 0.2969848 0.5 + 111 -0.2687006 0.2687006 0.5 + 112 -0.1454197 0.3510742 0.5 + 113 -0.160727 0.3880294 0.5 """ + +test_mesh_string_long = """$# nid x y z tc rc + 2000000 + 2000001-2772.16528319999998 643.809570300000019 376.799041699999975 + 2000002-3093.88916019999988 685.0078125 811.224670400000036 2 5""" + +test_section_shell_long = """*SECTION_TSHELL+ +$# secid elform shrf nip propt qr icomp tshear + 1 1.0 2 1.0 0 0 0""" + +test_text_card_long = "$# function" + +test_duplicate_card_group_long = """ 1 2 1 2 3 4 5 6 7 8 + 0.1 0.2 0.3 + 0.3 0.4 0.5 + 1 2 5 6 7 8 1 3 2 4 + 0.2 0.3 0.4 + 0.4 0.5 0.6""" + +test_read_segment_string = """*SET_SEGMENT +$# sid da1 da2 da3 da4 solver + 2 0.0 0.0 0.0 0.0MECH +$# n1 n2 n3 n4 a1 a2 a3 a4 + 2145 2124 2004 2045 0.0 0.0 0.0 0.0 + 262 265 264 263 0.0 0.0 0.0 0.0 + 304 262 263 305 0.0 0.0 0.0 0.0 + 263 264 385 384 0.0 0.0 0.0 0.0 + 344 345 265 262 0.0 0.0 0.0 0.0 + 265 445 444 264 0.0 0.0 0.0 0.0 + 1265 1285 405 525 0.0 0.0 0.0 0.0 + 1865 1844 1784 1805 0.0 0.0 0.0 0.0 + 444 584 665 545 0.0 0.0 0.0 0.0 + 905 845 803 344 0.0 0.0 0.0 0.0 + 182 2204 2084 2165 0.0 0.0 0.0 0.0""" + +test_read_nodes_string = """*NODE +$# nid x y z tc rc + 2000000 + 2000001 -2772.1652832 643.8095703 376.7990417 + 2000002 -3093.8891602 685.0078125 811.2246704 1 5""" + + +test_load_segment_string = """*LOAD_SEGMENT +$# lcid sf at n1 n2 n3 n4 n5 + 1.0 0.0 +$# n6 n7 n8 + """ + +test_load_segment_id_string = """*LOAD_SEGMENT_ID +$# id heading + +$# lcid sf at n1 n2 n3 n4 n5 + 1.0 0.0 +$# n6 n7 n8 + """ + +test_ss_string = """*SECTION_SOLID +$# secid elform aet unused unused unused cohoff gaskeit + 1 0 """ + +test_ss_elform_101_string = """*SECTION_SOLID +$# secid elform aet unused unused unused cohoff gaskeit + 101 0 +$# nip nxdof ihgf itaj lmc nhsv + 0 0 0 0 0 0""" + +test_ss_elform_101_nip_2_string = """*SECTION_SOLID +$# secid elform aet unused unused unused cohoff gaskeit + 101 0 +$# nip nxdof ihgf itaj lmc nhsv + 2 0 0 0 0 0 +$# xi eta zeta wgt + 1.0 2.0 3.0 + 0.0 3.0 5.0""" + +test_ss_elform_101_nip_2_lmc_9_string = """*SECTION_SOLID +$# secid elform aet unused unused unused cohoff gaskeit + 101 0 +$# nip nxdof ihgf itaj lmc nhsv + 2 0 0 0 9 0 +$# xi eta zeta wgt + 1.0 2.0 3.0 + 0.0 3.0 5.0 +$# pi pi pi pi pi pi pi pi + 22.0 + 3.7""" + +test_repr_truncate = """*NODE +$# nid x y z tc rc + 1 0.1 0.2 0.3 + 2 0.1 0.2 0.3 + 3 0.1 0.2 0.3 + 4 0.1 0.2 0.3 + 5 0.1 0.2 0.3 + 6 0.1 0.2 0.3 + 7 0.1 0.2 0.3 + 8 0.1 0.2 0.3 + 9 0.1 0.2 0.3 + 10 0.1 0.2 0.3 + 11 0.1 0.2 0.3 + 12 0.1 0.2 0.3 + 13 0.1 0.2 0.3 + 14 0.1 0.2 0.3 + 15 0.1 0.2 0.3 + 16 0.1 0.2 0.3 + 17 0.1 0.2 0.3 + 18 0.1 0.2 0.3 + 19 0.1 0.2 0.3 + 20 0.1 0.2 0.3 + 21 0.1 0.2 0.3 + 22 0.1 0.2 0.3 + 23 0.1 0.2 0.3 + 24 0.1 0.2 0.3 + 25 0.1 0.2 0.3 + 26 0.1 0.2 0.3 + 27 0.1 0.2 0.3 + 28 0.1 0.2 0.3 + 29 0.1 0.2 0.3 + 30 0.1 0.2 0.3 + 31 0.1 0.2 0.3 + 32 0.1 0.2 0.3 + 33 0.1 0.2 0.3 + 34 0.1 0.2 0.3 + 35 0.1 0.2 0.3 + 36 0.1 0.2 0.3 + 37 0.1 0.2 0.3 + 38 0.1 0.2 0.3 + 39 0.1 0.2 0.3 + 40 0.1 0.2 0.3 + 41 0.1 0.2 0.3 + 42 0.1 0.2 0.3 + 43 0.1 0.2 0.3 + 44 0.1 0.2 0.3 + 45 0.1 0.2 0.3 + 46 0.1 0.2 0.3 + 47 0.1 0.2 0.3 + 48 0.1 0.2 0.3 + 49 0.1 0.2 0.3 + 50 0.1 0.2 0.3 + 51 0.1 0.2 0.3 + 52 0.1 0.2 0.3 + 53 0.1 0.2 0.3 + 54 0.1 0.2 0.3 + 55 0.1 0.2 0.3 + 56 0.1 0.2 0.3 + 57 0.1 0.2 0.3 + 58 0.1 0.2 0.3 + 59 0.1 0.2 0.3 +...console output truncated at 60 rows""" + +test_control_timestep_string = """*CONTROL_TIMESTEP +$# dtinit tssfac isdo tslimt dt2ms lctm erode ms1st + 0.000 1.000000 0 0.000 1 0 0 0 +$# dt2msf dt2mslc imscl unused unused rmscl + 0.000 0 0 0 0 0.000""" + +element_shell_thickness_string = """*ELEMENT_SHELL_THICKNESS +$# eid pid n1 n2 n3 n4 n5 n6 n7 n8 + 1 1 1 105 2 2 +$# thic1 thic2 thic3 thic4 beta + 2.0 1.97992622 1.97992622 1.97992622 149.965326 + 2 1 136 133 2834 2834 + 1.98166233 1.98166233 1.98296441 1.98296441 146.006557 + 3 1 141 146 135 135 + 1.98187934 1.97949219 1.98280165 1.98280165 90.0245614""" + +element_solid_ortho_legacy = """*ELEMENT_SOLID_ORTHO +$# eid pid n1 n2 n3 n4 n5 n6 n7 n8 + 1 1 100 101 105 104 106 107 108 109 +$# a1 a2 a3 + 0.4 0.3 0.1 +$# d1 d2 d3 + 0.1 0.8 0.2 + 2 1 106 107 108 109 110 111 112 113 + 0.1 0.9 0.6 + 0.0 0.0 0.1""" + +element_solid_ortho = """*ELEMENT_SOLID_ORTHO + 2 1 + 113460 84468 108513 93160 93160 93160 93160 93160 + -0.38202947E+00 -0.54167800E+00 -0.74875793E+00 + -0.69952126E+00 -0.35575810E+00 0.61978420E+00 + 4 1 + 120411 117416 107358 95326 95326 95326 95326 95326 + -0.38362982E+00 -0.60972085E+00 -0.69359112E+00 + -0.66810543E+00 -0.33064264E+00 0.66629140E+00 + 13 1 + 137596 111105 86994 73710 73710 73710 73710 73710 + -0.42550327E+00 0.82862371E+00 0.36377153E+00 + 0.91675701E+00 0.47040764E+00 -0.20517038E+00 + 18 1 + 88443 89157 11329 75544 75544 75544 75544 75544 + -0.30579024E+00 0.61987494E+00 -0.72266686E+00 + -0.76423011E+00 -0.59932774E+00 -0.20244976E+00 + 22 1 + 150916 13334 97846 13266 13266 13266 13266 13266 + -0.97424306E+00 -0.84574605E-01 0.20903969E+00 + 0.13655429E+00 -0.93923419E+00 0.29649258E+00 + 25 1 + 135033 73847 97135 103790 103790 103790 103790 103790 + 0.22365008E+00 -0.25664759E+00 -0.94027265E+00 + -0.68651675E+00 -0.72919995E+00 0.48139922E-01 + 26 1 + 91937 112774 23012 84735 84735 84735 84735 84735 + 0.70414946E+00 0.70112248E+00 0.11225330E+00 + 0.46290576E+00 -0.22225871E+00 -0.83740122E+00 + 32 1 + 133811 17236 17212 93623 93623 93623 93623 93623 + -0.13305977E+00 0.95968095E+00 -0.24760367E+00 + -0.49610372E+00 -0.28415181E+00 -0.82298215E+00 + 33 1 + 144681 117038 105023 109628 109628 109628 109628 109628 + -0.33819581E+00 -0.31573994E+00 -0.88652799E+00 + -0.87251946E+00 -0.23142307E+00 0.42387370E+00 + 35 1 + 101955 21559 82864 147841 147841 147841 147841 147841 + -0.17810912E-01 -0.84876699E+00 -0.52846701E+00 + -0.90909081E+00 0.65187542E+00 0.29298234E+00 + 36 1 + 105239 95883 12707 76218 76218 76218 76218 76218 + -0.85562496E+00 -0.45162384E+00 0.25286722E+00 + 0.25206356E+00 -0.79120031E+00 -0.55517463E+00 + 39 1 + 139037 111414 115163 129585 129585 129585 129585 129585 + -0.20323083E+00 0.56606251E+00 0.79891831E+00 + 0.12017049E+01 0.12070042E+00 -0.10234373E+00 + 40 1 + 149782 21384 21293 21341 21341 21341 21341 21341 + -0.73838156E-01 -0.96973688E+00 -0.23271939E+00 + -0.97928824E+00 0.37993690E-01 0.21714321E+00 + 41 1 + 75238 44648 62782 95224 95224 95224 95224 95224 + -0.39247078E+00 0.76295983E+00 -0.51367206E+00 + 0.42604186E+00 -0.21652302E+00 -0.80781553E+00 + 42 1 + 149388 9991 102057 105002 105002 105002 105002 105002 + -0.31492819E+00 -0.89028936E+00 0.32894542E+00 + 0.24494516E+00 -0.31474117E+00 -0.37481684E+00 + 43 1 + 140819 107613 104959 116934 116934 116934 116934 116934 + 0.11852383E+00 0.25962342E+00 -0.95840898E+00 + -0.90649154E+00 -0.36575259E+00 -0.21347239E+00 + 50 1 + 25200 25107 25053 87191 87191 87191 87191 87191 + 0.42532909E+00 -0.34368036E+00 -0.83724487E+00 + -0.76251208E+00 -0.64664040E+00 -0.99452525E-01 + 55 1 + 140506 116408 117768 105768 105768 105768 105768 105768 + 0.42266038E+00 0.90056089E+00 0.10172658E+00 + 0.11288187E+00 0.59722057E-01 -0.99234141E+00 + 58 1 + 77215 23362 23263 89535 89535 89535 89535 89535 + 0.68015335E+00 0.49033793E-01 -0.73142813E+00 + -0.72779464E+00 -0.10228868E+00 -0.67930969E+00 + 61 1 + 150863 17503 90262 17332 17332 17332 17332 17332 + -0.99851596E+00 0.10192491E-01 -0.53497602E-01 + 0.14054508E-01 -0.65924820E+00 -0.73557459E+00 + 89 1 + 135402 93139 107339 104073 104073 104073 104073 104073 + 0.64308696E+00 0.69647747E+00 -0.31836817E+00 + -0.41275050E+00 -0.17576379E-01 -0.91063548E+00 + 92 1 + 148523 83927 112660 10967 10967 10967 10967 10967 + 0.35467294E-02 0.64626611E+00 0.76310388E+00 + 0.66636303E+00 0.56410311E+00 -0.46429149E+00 + 93 1 + 115624 112781 100995 131602 131602 131602 131602 131602 + 0.48485856E+00 0.87458968E+00 -0.22521597E-02 + 0.13657631E+00 -0.55675866E-01 -0.98928195E+00 + 100 1 + 81043 23878 23945 92222 92222 92222 92222 92222 + 0.74361308E+00 0.20384523E+00 -0.63677838E+00 + -0.66461622E+00 0.20578186E+00 -0.71680878E+00 + 104 1 + 66790 83928 26832 150823 150823 150823 150823 150823 + 0.99598687E+00 0.82739953E-01 -0.34121223E-01 + -0.98113061E-01 0.95291686E+00 -0.29432579E+00""" + +test_initial_strain_shell_string = """*INITIAL_STRAIN_SHELL +$# eid nplane nthick large unused unused unused ilocal + 1 1 5 0 0 +$# epsxx epsyy epszz epsxy epsyz epszx t + 1.0 + 1.0 + 1.0 + 1.0 + 1.0 + 2 1 5 0 0 + 22.0 + 2.0 + 2.0 + 2.0 + 2.0 """ + +test_initial_stress_shell_string_single_element_single_layer = """*INITIAL_STRESS_SHELL + 1 1 1 19 0 0 0 0 + -1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.194 + 0.0 0.0 0.0 0.0 0.0 0.0968 0.0 0.0 + 0.44 0.0 0.0 0.0 0.0 0.119 0.0 0.0 + 0.0 1.0E-4 0.311""" + +test_initial_stress_shell_string_single_element_multiple_layers = """*INITIAL_STRESS_SHELL + 1 1 5 19 0 0 0 0 + -1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.194 + 0.0 0.0 0.0 0.0 0.0 0.0968 0.0 0.0 + 0.44 0.0 0.0 0.0 0.0 0.119 0.0 0.0 + 0.0 1.0E-4 0.311 + -0.5 0.0 0.0 0.0 0.0 0.0 0.0 0.192 + 0.0 0.0 0.0 0.0 0.0 0.102 0.0 0.0 + 0.448 0.0 0.0 0.0 0.0 0.126 0.0 0.0 + 0.0 1.0E-4 0.32 + 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.191 + 0.0 0.0 0.0 0.0 0.0 0.107 0.0 0.0 + 0.455 0.0 0.0 0.0 0.0 0.132 0.0 0.0 + 0.0 1.0E-4 0.327 + 0.5 0.0 0.0 0.0 0.0 0.0 0.0 0.192 + 0.0 0.0 0.0 0.0 0.0 0.109 0.0 0.0 + 0.455 0.0 0.0 0.0 0.0 0.135 0.0 0.0 + 0.0 1.0E-4 0.33 + 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.193 + 0.0 0.0 0.0 0.0 0.0 0.111 0.0 0.0 + 0.455 0.0 0.0 0.0 0.0 0.137 0.0 0.0 + 0.0 1.0E-4 0.333""" + +test_initial_stress_shell_string = """*INITIAL_STRESS_SHELL + 1 1 5 19 0 0 0 0 + -1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.194 + 0.0 0.0 0.0 0.0 0.0 0.0968 0.0 0.0 + 0.44 0.0 0.0 0.0 0.0 0.119 0.0 0.0 + 0.0 1.0E-4 0.311 + -0.5 0.0 0.0 0.0 0.0 0.0 0.0 0.192 + 0.0 0.0 0.0 0.0 0.0 0.102 0.0 0.0 + 0.448 0.0 0.0 0.0 0.0 0.126 0.0 0.0 + 0.0 1.0E-4 0.32 + 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.191 + 0.0 0.0 0.0 0.0 0.0 0.107 0.0 0.0 + 0.455 0.0 0.0 0.0 0.0 0.132 0.0 0.0 + 0.0 1.0E-4 0.327 + 0.5 0.0 0.0 0.0 0.0 0.0 0.0 0.192 + 0.0 0.0 0.0 0.0 0.0 0.109 0.0 0.0 + 0.455 0.0 0.0 0.0 0.0 0.135 0.0 0.0 + 0.0 1.0E-4 0.33 + 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.193 + 0.0 0.0 0.0 0.0 0.0 0.111 0.0 0.0 + 0.455 0.0 0.0 0.0 0.0 0.137 0.0 0.0 + 0.0 1.0E-4 0.333 + 2 1 5 19 0 0 0 0 + -1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.208 + 0.0 0.0 0.0 0.0 0.0 0.104 0.0 0.0 + 0.433 0.0 0.0 0.0 0.0 0.129 0.0 0.0 + 0.0 1.0E-4 0.323 + -0.5 0.0 0.0 0.0 0.0 0.0 0.0 0.207 + 0.0 0.0 0.0 0.0 0.0 0.122 0.0 0.0 + 0.451 0.0 0.0 0.0 0.0 0.151 0.0 0.0 + 0.0 1.0E-4 0.35 + 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.205 + 0.0 0.0 0.0 0.0 0.0 0.141 0.0 0.0 + 0.468 0.0 0.0 0.0 0.0 0.175 0.0 0.0 + 0.0 1.0E-4 0.376 + 0.5 0.0 0.0 0.0 0.0 0.0 0.0 0.212 + 0.0 0.0 0.0 0.0 0.0 0.152 0.0 0.0 + 0.469 0.0 0.0 0.0 0.0 0.188 0.0 0.0 + 0.0 1.0E-4 0.39 + 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.219 + 0.0 0.0 0.0 0.0 0.0 0.163 0.0 0.0 + 0.469 0.0 0.0 0.0 0.0 0.201 0.0 0.0 + 0.0 1.0E-4 0.404 + 3 1 5 19 0 0 0 0 + -1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.723 + 0.0 0.0 0.0 0.0 0.0 0.0209 0.0 0.0 + -0.137 0.0 0.0 0.0 0.0 0.0698 0.0 0.0 + 0.0 1.0E-4 0.145 + -0.5 0.0 0.0 0.0 0.0 0.0 0.0 0.636 + 0.0 0.0 0.0 0.0 0.0 0.0332 0.0 0.0 + -0.0591 0.0 0.0 0.0 0.0 0.0753 0.0 0.0 + 0.0 1.0E-4 0.182 + 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.564 + 0.0 0.0 0.0 0.0 0.0 0.292 0.0 0.0 + 0.0423 0.0 0.0 0.0 0.0 0.361 0.0 0.0 + 0.0 1.0E-4 0.541 + 0.5 0.0 0.0 0.0 0.0 0.0 0.0 0.582 + 0.0 0.0 0.0 0.0 0.0 0.7 0.0 0.0 + 0.13 0.0 0.0 0.0 0.0 0.974 0.0 0.0 + 0.0 1.0E-4 0.837 + 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.508 + 0.0 0.0 0.0 0.0 0.0 0.7 0.63 1.0 + 0.209 0.0 0.0 0.0 0.0 1.0 0.0 0.0 + 0.0 1.0E-4 0.837""" + +test_define_function_string = """*DEFINE_FUNCTION +$# fid heading + +$# function +1,x-velo +x(t)=1000*sin(100*t) +*DEFINE_FUNCTION +2,z-velo +a(t)=x(t)+200""" + +test_set_shell_intersect_ref_1 = """*SET_SHELL_INTERSECT +$# sid + """ + +test_set_shell_intersect_ref_2 = """*SET_SHELL_INTERSECT_TITLE +$# title + +$# sid + """ + +test_set_shell_intersect_ref_3 = """*SET_SHELL_INTERSECT_TITLE +$# title +hello +$# sid + """ + +test_parameter_expression_ref = """*PARAMETER_EXPRESSION +$# prmr expression +R PE_200 1+1+1 +R PE_300 1+1+1 +R PE_400 1+1+1 """ + +test_define_transformation_ref = """*DEFINE_TRANSFORMATION +$# tranid + 1 +$# option a1 a2 a3 a4 a5 a6 a7 +TRANSL 1.0 0.0 0.0 +TRANSL 2.0 0.0 0.0 +TRANSL 3.0 0.0 0.0 """ + +test_mat_plastic_kinematic_ref = """*MAT_PLASTIC_KINEMATIC +$# mid ro e pr sigy etan beta + 1 0.0 0.0 0.0 0.0 0.0 0.0 +$# src srp fs vp + 0.0 0.0 0.0 0.0""" + +test_mat_null_ref = """*MAT_NULL +$# mid ro pc mu terod cerod ym pr + 2 0.0 0.0 0.0 0.0 0.0 0.0 0.0""" + +test_mat_piecewise_linear_plasticity_ref = """*MAT_PIECEWISE_LINEAR_PLASTICITY +$# mid ro e pr sigy etan fail tdel + 3 0.0 0.0 0.0 0.0 0.0 1e+21 0.0 +$# c p lcss lcsr vp + 0.0 0.0 0 0 0.0 +$# eps1 eps2 eps3 eps4 eps5 eps6 eps7 eps8 + 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 +$# es1 es2 es3 es4 es5 es6 es7 es8 + 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0""" + +test_mat_piecewise_linear_plasticity_2d_ref = """*MAT_PIECEWISE_LINEAR_PLASTICITY_2D +$# mid ro e pr sigy etan fail tdel + 4 0.0 0.0 0.0 0.0 0.0 1e+21 0.0 +$# c p lcss lcsr vp + 0.0 0.0 0 0 0.0 +$# eps1 eps2 eps3 eps4 eps5 eps6 eps7 eps8 + 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 +$# es1 es2 es3 es4 es5 es6 es7 es8 + 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0""" + +test_mat_piecewise_linear_plasticity_haz_ref = """*MAT_PIECEWISE_LINEAR_PLASTICITY_HAZ +$# mid ro e pr sigy etan fail tdel + 5 0.0 0.0 0.0 0.0 0.0 1e+21 0.0 +$# c p lcss lcsr vp + 0.0 0.0 0 0 0.0 +$# eps1 eps2 eps3 eps4 eps5 eps6 eps7 eps8 + 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 +$# es1 es2 es3 es4 es5 es6 es7 es8 + 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0""" + +test_mat_piecewise_linear_plasticity_log_interpolation_ref = """*MAT_PIECEWISE_LINEAR_PLASTICITY_LOG_INTERPOLATION +$# mid ro e pr sigy etan fail tdel + 6 0.0 0.0 0.0 0.0 0.0 1e+21 0.0 +$# c p lcss lcsr vp + 0.0 0.0 0 0 0.0 +$# eps1 eps2 eps3 eps4 eps5 eps6 eps7 eps8 + 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 +$# es1 es2 es3 es4 es5 es6 es7 es8 + 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0""" + +test_mat_piecewise_linear_plasticity_midfail_ref = """*MAT_PIECEWISE_LINEAR_PLASTICITY_MIDFAIL +$# mid ro e pr sigy etan fail tdel + 7 0.0 0.0 0.0 0.0 0.0 1e+21 0.0 +$# c p lcss lcsr vp + 0.0 0.0 0 0 0.0 +$# eps1 eps2 eps3 eps4 eps5 eps6 eps7 eps8 + 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 +$# es1 es2 es3 es4 es5 es6 es7 es8 + 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0""" + +test_mat_piecewise_linear_plasticity_stochastic_ref = """*MAT_PIECEWISE_LINEAR_PLASTICITY_STOCHASTIC +$# mid ro e pr sigy etan fail tdel + 8 0.0 0.0 0.0 0.0 0.0 1e+21 0.0 +$# c p lcss lcsr vp + 0.0 0.0 0 0 0.0 +$# eps1 eps2 eps3 eps4 eps5 eps6 eps7 eps8 + 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 +$# es1 es2 es3 es4 es5 es6 es7 es8 + 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0""" + +test_mat_laminated_composite_fabric_ref = """*MAT_LAMINATED_COMPOSITE_FABRIC +$# mid ro ea eb ec prba tau1 gamma1 + 9 0.0 0.0 0.0 0.0 0.0 0.0 0.0 +$# gab gbc gca slimt1 slimc1 slimt2 slimc2 slims + 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 +$# aopt tsize erods soft fs epsf epsr tsmd + 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.9 +$# xp yp zp a1 a2 a3 prca prcb + 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 +$# v1 v2 v3 d1 d2 d3 beta lcdfail + 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0 +$# e11c e11t e22c e22t gms + 0.0 0.0 0.0 0.0 0.0 +$# xc xt yc yt sc + 0.0 0.0 0.0 0.0 0.0 +$# lcxc lcxt lcyc lcyt lcsc lctau lcgam dt + 0 0 0 0 0 0 0 0.0 +$# lce11c lce11t lce22c lce22t lcgms lcefs + 0 0 0 0 0 0""" + +test_mat_laminated_composite_fabric_solid_ref = """*MAT_LAMINATED_COMPOSITE_FABRIC_SOLID +$# mid ro ea eb ec prba tau1 gamma1 + 10 0.0 0.0 0.0 0.0 0.0 0.0 0.0 +$# gab gbc gca slimt1 slimc1 slimt2 slimc2 slims + 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 +$# aopt tsize erods soft fs epsf epsr tsmd + 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.9 +$# xp yp zp a1 a2 a3 prca prcb + 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 +$# v1 v2 v3 d1 d2 d3 beta lcdfail + 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0 +$# e11c e11t e22c e22t gms + 0.0 0.0 0.0 0.0 0.0 +$# xc xt yc yt sc + 0.0 0.0 0.0 0.0 0.0 +$# e33c e33t gm23 gm31 + 0.0 0.0 0.0 0.0 +$# zc zt sc23 sc31 + 0.0 0.0 0.0 0.0 +$# slimt3 slimc3 slims23 lsims31 tau2 gamma2 tau3 gamma3 + 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 +$# lcxc lcxt lcyc lcyt lcsc lctau lcgam dt + 0 0 0 0 0 0 0 0.0 +$# lce11c lce11t lce22c lce22t lcgms lcefs + 0 0 0 0 0 0 +$# lczc lczt lcsc23 lcsc31 lctau2 lcgam2 lctau3 lcgam3 + 0 0 0 0 0 0 0 0 +$# lce33c lce33t lcgms23 lcgms31 + 0 0 0 0""" + +test_mat_hyperelastic_rubber_ref = """*MAT_HYPERELASTIC_RUBBER +$# mid ro pr n nv g sigf ref + 11 0.0 0.0 0 0 0.0 0.0 0.0 +$# c10 c01 c11 c20 c02 c30 therml + 0.0 0.0 0.0 0.0 0.0 0.0 0.0""" + +test_mat_ogden_rubber_ref = """*MAT_OGDEN_RUBBER +$# mid ro pr n nv g sigf ref + 12 0.0 0.0 0 6 0.0 0.0 0.0 +$# mu1 mu2 mu3 mu4 mu5 mu6 mu7 mu8 + 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 +$# alpha1 alpha2 alpha3 alpha4 alpha5 alpha6 alpha7 alpha8 + 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0""" + +test_mat_fu_chang_foam_ref = """*MAT_FU_CHANG_FOAM +$# mid ro e kcon tc fail damp tbid + 13 0.0 0.0 0.0 1e+20 0.0 0.0 0 +$# bvflag sflag rflag tflag pvid sraf ref hu + 0.0 0.0 0.0 0.0 0 0.0 0.0 0.0 +$# d0 n0 n1 n2 n3 c0 c1 c2 + 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 +$# c3 c4 c5 aij sij minr maxr shape + 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 +$# expon riuld + 1.0 0.0""" + +test_mat_fu_chang_foam_damage_decay_ref = """*MAT_FU_CHANG_FOAM_DAMAGE_DECAY +$# mid ro e kcon tc fail damp tbid + 14 0.0 0.0 0.0 1e+20 0.0 0.0 0 +$# bvflag sflag rflag tflag pvid sraf ref hu + 0.0 0.0 0.0 0.0 0 0.0 0.0 0.0 +$# minr maxr shape betat betac + 0.0 0.0 0.0 0.0 0.0 +$# expon riuld + 1.0 0.0""" + +test_mat_fu_chang_foam_log_log_interpolation_ref = """*MAT_FU_CHANG_FOAM_LOG_LOG_INTERPOLATION +$# mid ro e kcon tc fail damp tbid + 15 0.0 0.0 0.0 1e+20 0.0 0.0 0 +$# bvflag sflag rflag tflag pvid sraf ref hu + 0.0 0.0 0.0 0.0 0 0.0 0.0 0.0 +$# d0 n0 n1 n2 n3 c0 c1 c2 + 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 +$# c3 c4 c5 aij sij minr maxr shape + 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 +$# expon riuld + 1.0 0.0""" + +test_mat_modified_johnson_cook_ref = """*MAT_MODIFIED_JOHNSON_COOK +$# mid ro e pr beta xsi cp alpha + 16 0.0 0.0 0.0 0.0 0.0 0.0 0.0 +$# e0dot tr tm t0 flag1 flag2 + 0.0 0.0 0.0 0.0 0.0 0.0 +$# a/siga b/b n/beta0 c/beta1 m/na + 0.0 0.0 0.0 0.0 0.0 +$# q1/a c1/n q2/alpha0 c2/alpha1 + 0.0 0.0 0.0 0.0 +$# dc/dc pd/wc d1/na d2/na d3/na d4/na d5/na + 0.0 0.0 0.0 0.0 0.0 0.0 0.0 +$# tc tauc + 0.0 0.0""" + +test_mat_modified_piecewise_linear_plasticity_ref = """*MAT_MODIFIED_PIECEWISE_LINEAR_PLASTICITY +$# mid ro e pr sigy etan fail tdel + 17 0.0 0.0 0.0 0.0 0.0 1e+21 0.0 +$# c p lcss lcsr vp epsthin epsmaj numint + 0.0 0.0 0 0 0.0 0.0 0.0 0.0 +$# eps1 eps2 eps3 eps4 eps5 eps6 eps7 eps8 + 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 +$# es1 es2 es3 es4 es5 es6 es7 es8 + 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0""" + +test_mat_modified_piecewise_linear_plasticity_log_interpolation_ref = """*MAT_MODIFIED_PIECEWISE_LINEAR_PLASTICITY_LOG_INTERPOLATION +$# mid ro e pr sigy etan fail tdel + 18 0.0 0.0 0.0 0.0 0.0 1e+21 0.0 +$# c p lcss lcsr vp epsthin epsmaj numint + 0.0 0.0 0 0 0.0 0.0 0.0 0.0 +$# eps1 eps2 eps3 eps4 eps5 eps6 eps7 eps8 + 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 +$# es1 es2 es3 es4 es5 es6 es7 es8 + 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0""" + +test_mat_modified_piecewise_linear_plasticity_prestrain_ref = """*MAT_MODIFIED_PIECEWISE_LINEAR_PLASTICITY_PRESTRAIN +$# mid ro e pr sigy etan fail tdel + 19 0.0 0.0 0.0 0.0 0.0 1e+21 0.0 +$# c p lcss lcsr vp epsthin epsmaj numint + 0.0 0.0 0 0 0.0 0.0 0.0 0.0 +$# eps1 eps2 eps3 eps4 eps5 eps6 eps7 eps8 + 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 +$# es1 es2 es3 es4 es5 es6 es7 es8 + 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 +$# lctsrf eps0 triax ips lcemod beta rfiltf + 0 0.0 0.0 0 0 0.0 0.0""" + +test_mat_modified_piecewise_linear_plasticity_rate_ref = """*MAT_MODIFIED_PIECEWISE_LINEAR_PLASTICITY_RATE +$# mid ro e pr sigy etan fail tdel + 20 0.0 0.0 0.0 0.0 0.0 1e+21 0.0 +$# c p lcss lcsr vp epsthin epsmaj numint + 0.0 0.0 0 0 0.0 0.0 0.0 0.0 +$# eps1 eps2 eps3 eps4 eps5 eps6 eps7 eps8 + 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 +$# es1 es2 es3 es4 es5 es6 es7 es8 + 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 +$# lctsrf eps0 triax ips lcemod beta rfiltf + 0 0.0 0.0 0 0 0.0 0.0""" + +test_mat_modified_piecewise_linear_plasticity_rtcl_ref = """*MAT_MODIFIED_PIECEWISE_LINEAR_PLASTICITY_RTCL +$# mid ro e pr sigy etan fail tdel + 21 0.0 0.0 0.0 0.0 0.0 1e+21 0.0 +$# c p lcss lcsr vp epsthin epsmaj numint + 0.0 0.0 0 0 0.0 0.0 0.0 0.0 +$# eps1 eps2 eps3 eps4 eps5 eps6 eps7 eps8 + 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 +$# es1 es2 es3 es4 es5 es6 es7 es8 + 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 +$# lctsrf eps0 triax ips lcemod beta rfiltf + 0 0.0 0.0 0 0 0.0 0.0""" + +test_mat_modified_piecewise_linear_plasticity_stochastic_ref = """*MAT_MODIFIED_PIECEWISE_LINEAR_PLASTICITY_STOCHASTIC +$# mid ro e pr sigy etan fail tdel + 22 0.0 0.0 0.0 0.0 0.0 1e+21 0.0 +$# c p lcss lcsr vp epsthin epsmaj numint + 0.0 0.0 0 0 0.0 0.0 0.0 0.0 +$# eps1 eps2 eps3 eps4 eps5 eps6 eps7 eps8 + 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 +$# es1 es2 es3 es4 es5 es6 es7 es8 + 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0""" + +test_mat_plasticity_compression_tension_ref = """*MAT_PLASTICITY_COMPRESSION_TENSION +$# mid ro e pr c p fail tdel + 23 0.0 0.0 0.0 0.0 0.0 1e+20 0.0 +$# lcidc lcidt lcsrc lcsrt srflag lcfail ec rpct + 0 0 0 0 0.0 0 0.0 0.0 +$# pc pt pcutc pcutt pcutf unused unused srfilt + 0.0 0.0 0.0 0.0 0.0 0.0 +$# k + 0.0""" + +test_mat_cohesive_mixed_mode_ref = """*MAT_COHESIVE_MIXED_MODE +$# mid ro roflg intfail en et gic giic + 24 0.0 0 0.0 0.0 0.0 0.0 0.0 +$# xmu t s und utd gamma + 0.0 0.0 0.0 0.0 0.0 1.0""" + +test_mat_simplified_rubber_foam_ref = """*MAT_SIMPLIFIED_RUBBER/FOAM +$# mid ro km mu g sigf ref prten + 25 0.0 0.0 0.1 0.0 0.0 0.0 0.0 +$# sgl sw st lc/tbid tension rtype avgopt pra + 0.0 0.0 0.0 0 -1.0 0.0 0.0 0.0 +$# lcunld hu shape stol visco hisout + 0 1.0 0.0 0.0 0.0 0.0""" + +test_mat_simplified_rubber_foam_log_log_interpolation_ref = """*MAT_SIMPLIFIED_RUBBER/FOAM_LOG_LOG_INTERPOLATION +$# mid ro km mu g sigf ref prten + 26 0.0 0.0 0.1 0.0 0.0 0.0 0.0 +$# sgl sw st lc/tbid tension rtype avgopt pra + 0.0 0.0 0.0 0 -1.0 0.0 0.0 0.0 +$# lcunld hu shape stol visco hisout + 0 1.0 0.0 0.0 0.0 0.0""" + +test_mat_simplified_rubber_foam_with_failure_ref = """*MAT_SIMPLIFIED_RUBBER/FOAM_WITH_FAILURE +$# mid ro km mu g sigf ref prten + 27 0.0 0.0 0.1 0.0 0.0 0.0 0.0 +$# sgl sw st lc/tbid tension rtype avgopt pra + 0.0 0.0 0.0 0 -1.0 0.0 0.0 0.0 +$# k gama1 gama2 eh + 0.0 0.0 0.0 0.0 +$# lcunld hu shape stol visco hisout + 0 1.0 0.0 0.0 0.0 0.0""" + +test_mat_simplified_rubber_foam_with_failure_log_log_interpolation_ref = """*MAT_SIMPLIFIED_RUBBER/FOAM_WITH_FAILURE_LOG_LOG_INTERPOLATION +$# mid ro km mu g sigf ref prten + 28 0.0 0.0 0.1 0.0 0.0 0.0 0.0 +$# sgl sw st lc/tbid tension rtype avgopt pra + 0.0 0.0 0.0 0 -1.0 0.0 0.0 0.0 +$# k gama1 gama2 eh + 0.0 0.0 0.0 0.0 +$# lcunld hu shape stol visco hisout + 0 1.0 0.0 0.0 0.0 0.0""" + +test_mat_295_ref = """*MAT_295 +$# mid rho aopt + 1 0.001 2.0 +$# title itype beta nu +ISO -3 0.0 0.499 +$# k1 k2 + 0.00236 1.75 +$# title atype intype nf +ANISO -1 0 1 +$# theta a b + 0.0 0.0 1.0 +$# ftype fcid k1 k2 + 1 0 0.00049 9.01 +$# title actype acdir acid acthr sf ss sn +ACTIVE 1 1 2 2.175 1.0 0.0 0.0 +$# t0 ca2ion ca2ionm n taumax stf b l0 + 4.35 2.0 0.125 0.0 4.75 1.58 +$# l dtmax mr tr + 1.85 150.0 1048.9 -1429.0 +$# xp yp zp a1 a2 a3 macf unused + 1.0 0.0 0.0 1 +$# v1 v2 v3 d1 d2 d3 beta ref + 0.0 1.0 0.0 """ + +test_set_part_list_ref = """*SET_PART_LIST +$# sid da1 da2 da3 da4 solver + 1 0.0 0.0 0.0 0.0MECH +$# parts parts parts + 1 2 3""" + +test_contact_1d_id_mpp1_mpp2 = """*CONTACT_1D_ID_MPP +$# cid heading + +$# ignore bckt lcbckt ns2trk inititr parmax unused cparm8 + 0 200 3 2 1.0005 0 +$# mpp2 chksegs pensf grpable +& 0 1.0 0 +$# nsidr nsidc err sigc gb smax exp + 0.0 0.0 0.0 0.0 0.0""" + +test_contact_1d_mpp1 = """*CONTACT_1D_MPP +$# ignore bckt lcbckt ns2trk inititr parmax unused cparm8 + 0 200 3 2 1.0005 0 +$# nsidr nsidc err sigc gb smax exp + 0.0 0.0 0.0 0.0 0.0""" + +test_contact_automatic_single_surface = """*CONTACT_AUTOMATIC_SINGLE_SURFACE +$# ssid msid sstyp mstyp sboxid mboxid spr mpr + 1 +$# fs fd dc vc vdc penchk bt dt + 0.0 0.0 0.0 0.0 0.0 0 0.0 0.0 +$# sfs sfm sst mst sfst sfmt fsf vsf + 1.0 1.0 1.0 1.0 1.0 1.0""" + +test_em_control_string = """*EM_CONTROL +$# emsol numls macrodt dimtype nperio unused ncylfem ncylbem + -1 100 0 2 5000 5000""" + +test_long_deck_standard_keyword_string = """*KEYWORD LONG=Y +*SECTION_SEATBELT- +$# secid area thick + +*END""" + +test_standard_deck_string = """*KEYWORD LONG=S +*SECTION_SEATBELT +$# secid area thick + +*END""" + +test_long_deck_string = """*KEYWORD LONG=Y +*SECTION_SEATBELT +$# secid area thick + +*END""" + +test_variable_card_string = """$# bi bi bi bi bi bi bi bi + 0.0 1.0 2.0 3.0 4.0 5.0 6.0 7.0 + 8.0 9.0""" + +test_deck_with_unknown_keywords = """*KEYWORD +*NOT_REAL_KEYWORD +$# what is this + +*END""" + +test_variable_card_sets_string = """$ +*KEYWORD +*SET_PART_LIST_TITLE +$# title +test +$# sid da1 da2 da3 da4 solver + 1 +$# parts + 1 +*SET_PART_LIST_TITLE +$# title +test2 +$# sid da1 da2 da3 da4 solver + 2 +$# parts parts parts parts parts parts parts parts + 1 2 3 4 5 6 7 8 +*END""" + +test_default_card_em_isopotential_connect_string = """*EM_ISOPOTENTIAL_CONNECT +$# conid contype isoid1 isoid2 vallcid/rdlid psid + 1 """ + +test_conditional_card_em_isopotential_connect_string = """*EM_ISOPOTENTIAL_CONNECT +$# conid contype isoid1 isoid2 vallcid/rdlid psid + 6 +$# l c v0 + """ +test_contact_tied_shell_edge_to_surface_beam_offset_opt_cards1 = """*CONTACT_TIED_SHELL_EDGE_TO_SURFACE_BEAM_OFFSET +$# surfa surfb surfatyp surfbtyp saboxid sbboxid sapr sbpr + 1 2 3 3 0 0 0 0 +$# fs fd dc vc vdc penchk bt dt + 0.3 0.0 0.0 20.0 0.0 0 0.0 1e+20 +$# sfsa sfsb sast sbst sfsat sfsbt fsf vsf + 1.0 1.0 0.0 0.0 1.0 1.0 1.0 1.0 +$# soft sofscl lcidab maxpar sbopt depth bsort frcfrq + 2 0.1 0 1.025 2 2 0 1 +$# penmax thkopt shlthk snlog isym i2d3d sldthk sldstf + 0.0 0 0 0 0 0 0.0 0.0 +$# igap ignore dprfac dtstif edgek flangl cid_rcf + 1 2 0.0 0.0 0.0 0.0 0 +$# q2tri dtpchk sfnbr fnlscl dnlscl tcso tiedid shledg + 0 0.0 0.0 0.0 0.0 0 1 0 +$# sharec cparm8 ipback srnde fricsf icor ftorq region + 0 0 0 0 1.0 0 0 0 +$# pstiff ignroff fstol 2dbinr ssftyp swtpr tetfac + 1 0 2.0 0 0 0 0.0 +$# shloff + 0.0""" + +test_contact_tied_shell_edge_to_surface_beam_offset_opt_cards2 = """*CONTACT_TIED_SHELL_EDGE_TO_SURFACE_BEAM_OFFSET +$# surfa surfb surfatyp surfbtyp saboxid sbboxid sapr sbpr + 1 2 3 3 0 0 0 0 +$# fs fd dc vc vdc penchk bt dt + 0.3 0.0 0.0 20.0 0.0 0 0.0 1e+20 +$# sfsa sfsb sast sbst sfsat sfsbt fsf vsf + 1.0 1.0 0.0 0.0 1.0 1.0 1.0 1.0 +$# soft sofscl lcidab maxpar sbopt depth bsort frcfrq + 2 0.1 0 1.025 2 2 0 1 +$# penmax thkopt shlthk snlog isym i2d3d sldthk sldstf + 0.0 0 0 0 0 0 0.0 0.0 +$# igap ignore dprfac dtstif edgek flangl cid_rcf + 1 2 0.0 0.0 0.0 0.0 0 +$# q2tri dtpchk sfnbr fnlscl dnlscl tcso tiedid shledg + 0 0.0 0.0 0.0 0.0 0 1 0 +$# sharec cparm8 ipback srnde fricsf icor ftorq region + 0 0 0 0 1.0 0 0 0""" + +test_deck_contact_tied_shell_edge_to_surface_id2 = """$ +*KEYWORD +*CONTACT_TIED_SHELL_EDGE_TO_SURFACE_BEAM_OFFSET_ID +$# cid heading + 999999TEST_CONTACT_WITH_ID +$# surfa surfb surfatyp surfbtyp saboxid sbboxid sapr sbpr + 0 0 0 0 0 0 0 0 +$# fs fd dc vc vdc penchk bt dt + 0.0 0.0 0.0 0.0 0.0 0 0.0 1e+20 +$# sfsa sfsb sast sbst sfsat sfsbt fsf vsf + 1.0 1.0 0.0 0.0 1.0 1.0 1.0 1.0 +*END""" + +test_deck_contact_tied_shell_edge_to_surface_id3 = """$ +*KEYWORD +*CONTACT_TIED_SHELL_EDGE_TO_SURFACE_BEAM_OFFSET_ID +$# cid heading + 999999TEST_CONTACT_WITH_OPTIONAL_CARDS_AND_ID +$# surfa surfb surfatyp surfbtyp saboxid sbboxid sapr sbpr + 0 0 0 0 0 0 0 0 +$# fs fd dc vc vdc penchk bt dt + 0.0 0.0 0.0 0.0 0.0 0 0.0 1e+20 +$# sfsa sfsb sast sbst sfsat sfsbt fsf vsf + 1.0 1.0 0.0 0.0 1.0 1.0 1.0 1.0 +$# soft sofscl lcidab maxpar sbopt depth bsort frcfrq + 0 0.1 0 1.025 2 2 0 1 +$# penmax thkopt shlthk snlog isym i2d3d sldthk sldstf + 0.0 0 0 0 0 0 0.0 0.0 +$# igap ignore dprfac dtstif edgek flangl cid_rcf + 1 0 0.0 0.0 0.0 0.0 0 +$# q2tri dtpchk sfnbr fnlscl dnlscl tcso tiedid shledg + 0 0.0 0.0 0.0 0.0 0 0 0 +$# sharec cparm8 ipback srnde fricsf icor ftorq region + 0 0 0 0 1.0 0 0 0 +$# pstiff ignroff fstol 2dbinr ssftyp swtpr tetfac + 0 0 2.0 0 0 0 0.0 +$# shloff + 0.0 +*END""" + +test_em_randles_batmac_rdltype_0_1 = """*EM_RANDLES_BATMAC +$# rdlid rdltype rdlarea psid + 1 1 +$# q cq socinit soctou + +$# r0cha r0dis r10cha r10dis c10cha c10dis + +$# temp frther r0toth dudt tempu + 0.0 0 0 0 +$# usesocs tau flcid + 0 """ + +test_em_randles_batmac_rdltype_2_3 = """*EM_RANDLES_BATMAC +$# rdlid rdltype rdlarea psid + 3 1 +$# q cq socinit soctou + +$# r0cha r0dis r10cha r10dis c10cha c10dis + +$# r20cha r20dis c20cha c20dis r30cha r30dis c30cha c30dis + +$# temp frther r0toth dudt tempu + 0.0 0 0 0 +$# usesocs tau flcid + 0 """ + +test_em_randles_solid_rdltype_0_1 = """*EM_RANDLES_SOLID +$# rdlid rdltype rdlarea ccppart ccnpart seppart pelpart nelpart + 1 2 +$# q cq socinit soctou + +$# r0cha r0dis r10cha r10dis c10cha c10dis + +$# temp frther r0toth dudt tempu + 0.0 0 0 0.0 0 +$# usesocs tau flcid + 0 """ + +test_em_randles_solid_rdltype_2_3 = """*EM_RANDLES_SOLID +$# rdlid rdltype rdlarea ccppart ccnpart seppart pelpart nelpart + 3 2 +$# q cq socinit soctou + +$# r0cha r0dis r10cha r10dis c10cha c10dis + +$# r20cha r20dis c20cha c20dis r30cha r30dis c30cha c30dis + +$# temp frther r0toth dudt tempu + 0.0 0 0 0.0 0 +$# usesocs tau flcid + 0 """ + +test_em_randles_tshell_rdltype_0_1 = """*EM_RANDLES_TSHELL +$# rdlid rdltype rdlarea psid + 1 2 +$# q cq socinit soctou + +$# r0cha r0dis r10cha r10dis c10cha c10dis + +$# temp frther r0toth dudt tempu + 0.0 0 0 0.0 0 +$# usesocs tau flcid + 0 """ + +test_em_randles_tshell_rdltype_2_3 = """*EM_RANDLES_TSHELL +$# rdlid rdltype rdlarea psid + 3 2 +$# q cq socinit soctou + +$# r0cha r0dis r10cha r10dis c10cha c10dis + +$# r20cha r20dis c20cha c20dis r30cha r30dis c30cha c30dis + +$# temp frther r0toth dudt tempu + 0.0 0 0 0.0 0 +$# usesocs tau flcid + 0 """