# 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
[docs]
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)
[docs]
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
[docs]
def clear(self):
"""Clear all keywords from the deck."""
self._keywords = []
self.comment_header = None
self.title = None
self.format = format_type.default
@property
@format.setter
def format(self, value: format_type) -> None:
"""Set the format type of the deck."""
self._format_type = value
[docs]
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
[docs]
def all_keywords(self) -> typing.List[typing.Union[str, KeywordBase]]:
"""List of all keywords."""
return self._keywords
@property
[docs]
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
[docs]
def keywords(self):
"""List of processed keywords."""
return [kw for kw in self._keywords if isinstance(kw, KeywordBase)]
[docs]
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)
[docs]
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)
[docs]
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)
[docs]
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)
[docs]
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}")
[docs]
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()
[docs]
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)
[docs]
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]
[docs]
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
[docs]
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())
[docs]
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:
if os.name == "nt":
self.write(f)
else:
# TODO - on linux writing to the buffer can insert a spurious newline
# this is less performant but more correct until that is fixed
contents = self.write()
f.write(contents)
@property
@comment_header.setter
def comment_header(self, value: str) -> None:
self._comment_header = value
@property
[docs]
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
[docs]
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
[docs]
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.core.lib.deck_plotter import plot_deck
plot_deck(self, **args)