Skip to content

Commit

Permalink
add keyword lib (#550)
Browse files Browse the repository at this point in the history
Co-authored-by: Mohamed Koubaa <koubaa@github.com>
  • Loading branch information
koubaa and Mohamed Koubaa authored Nov 4, 2024
1 parent 12f695d commit d41e122
Show file tree
Hide file tree
Showing 35 changed files with 5,556 additions and 3 deletions.
34 changes: 33 additions & 1 deletion .github/workflows/ci_cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
1 change: 1 addition & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
[pytest]
markers =
run: tests that exercise the `run` subpackage
keywords: tests that exercies the `keyword` subpackage
1 change: 1 addition & 0 deletions src/ansys/dyna/core/__init__.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down
21 changes: 21 additions & 0 deletions src/ansys/dyna/core/lib/__init__.py
Original file line number Diff line number Diff line change
@@ -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.
62 changes: 62 additions & 0 deletions src/ansys/dyna/core/lib/array.py
Original file line number Diff line number Diff line change
@@ -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
124 changes: 124 additions & 0 deletions src/ansys/dyna/core/lib/card.py
Original file line number Diff line number Diff line change
@@ -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
68 changes: 68 additions & 0 deletions src/ansys/dyna/core/lib/card_interface.py
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit d41e122

Please sign in to comment.