From 4e0212b327b3d7ff8128179313c3c6749776f8a9 Mon Sep 17 00:00:00 2001 From: Mohamed Koubaa Date: Tue, 21 Jan 2025 14:08:18 -0600 Subject: [PATCH 01/10] add tests for option card also implement ability for options to be mutually exclusive --- src/ansys/dyna/core/lib/keyword_base.py | 14 +++- src/ansys/dyna/core/lib/option_card.py | 79 +++++++++++++----- tests/test_option_card.py | 103 ++++++++++++++++++++++++ 3 files changed, 176 insertions(+), 20 deletions(-) create mode 100644 tests/test_option_card.py diff --git a/src/ansys/dyna/core/lib/keyword_base.py b/src/ansys/dyna/core/lib/keyword_base.py index 42516b918..a4e1231cf 100644 --- a/src/ansys/dyna/core/lib/keyword_base.py +++ b/src/ansys/dyna/core/lib/keyword_base.py @@ -27,7 +27,7 @@ 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 +from ansys.dyna.core.lib.option_card import OptionsAPI, OptionSpec from ansys.dyna.core.lib.parameter_set import ParameterSet @@ -123,6 +123,18 @@ def _activate_options(self, title: str) -> None: title_list = title.split("_") self._try_activate_options(title_list) + def get_option_spec(self, name: str) -> OptionSpec: + for card in self._cards: + if hasattr(card, "option_spec"): + option_spec = card.option_spec + if option_spec.name == name: + return option_spec + elif hasattr(card, "option_specs"): + for option_spec in card.option_specs: + if option_spec.name == name: + return option_spec + raise Exception(f"No option spec with name `{name}` found") + def __repr__(self) -> str: """Returns a console-friendly representation of the keyword data as it would appear in the .k file""" diff --git a/src/ansys/dyna/core/lib/option_card.py b/src/ansys/dyna/core/lib/option_card.py index a4ec560d4..bff0e2401 100644 --- a/src/ansys/dyna/core/lib/option_card.py +++ b/src/ansys/dyna/core/lib/option_card.py @@ -31,11 +31,44 @@ from ansys.dyna.core.lib.parameter_set import ParameterSet -@dataclass class OptionSpec: - name: str - card_order: int - title_order: int + def __init__(self, name: str, card_order: int, title_order: int, excludes: typing.List[str] = None): + self._name = name + self._card_order = card_order + self._title_order = title_order + self._excludes = excludes + + @property + def name(self) -> str: + return self._name + + @name.setter + def name(self, value: str) -> None: + self._name = value + + @property + def card_order(self) -> int: + return self._card_order + + @card_order.setter + def card_order(self, value: int) -> None: + self._card_order = value + + @property + def title_order(self) -> int: + return self._title_order + + @title_order.setter + def title_order(self, value: int) -> None: + self._title_order = value + + @property + def excludes(self) -> typing.Optional[typing.List[str]]: + return self._excludes + + @excludes.setter + def excludes(self, value: typing.List[str]) -> None: + self._excludes = value class OptionCardSet(CardInterface): @@ -124,45 +157,49 @@ def _write(buf): class OptionAPI: """API for an individual option associated with a keyword.""" - def __init__(self, kw, name): - self._kw = kw + def __init__(self, options_api, name): + self._options_api = options_api self._name = name @property def active(self) -> bool: - return self._kw.is_option_active(self._name) + return self._options_api.is_option_active(self._name) @active.setter def active(self, value: bool) -> None: + option_spec: OptionSpec = self._options_api.get_option_spec(self._name) if value: - self._kw.activate_option(self._name) + self._options_api.activate_option(self._name) + if option_spec.excludes is not None: + for exclude in option_spec.excludes: + self._options_api.deactivate_option(exclude) else: - self._kw.deactivate_option(self._name) + self._options_api.deactivate_option(self._name) class OptionsAPI: """API for options associated with a keyword.""" - def __init__(self, kw): - self._kw = kw + def __init__(self, api): + self._api = api def __getitem__(self, name: str) -> OptionAPI: """Gets the option with the given name.""" - return OptionAPI(self._kw, name) + return OptionAPI(self._api, name) def __repr__(self) -> str: - option_card_specs = self.option_specs - if len(option_card_specs) == 0: + option_names = self.get_option_names() + if len(option_names) == 0: return "" sio = io.StringIO() sio.write("Options:") - for option in option_card_specs: - active = self._kw.is_option_active(option.name) + for option_name in option_names: + active = self._api.is_option_active(option_name) active_string = "active" if active else "not active" - sio.write(f"\n {option.name} option is {active_string}.") + 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]: + def _load_option_specs(self, cards: typing.List[CardInterface]) -> typing.List[OptionSpec]: option_specs: typing.List[OptionSpec] = [] for card in cards: if hasattr(card, "option_spec"): @@ -173,4 +210,8 @@ def _get_option_specs(self, cards: typing.List[CardInterface]) -> typing.List[Op @property def option_specs(self) -> typing.List[OptionSpec]: - return self._get_option_specs(self._kw._cards) + return self._load_option_specs(self._api._cards) + + def get_option_names(self) -> typing.List[str]: + option_specs = self._load_option_specs + return [o.name for o in option_specs] diff --git a/tests/test_option_card.py b/tests/test_option_card.py new file mode 100644 index 000000000..05bfb8ca5 --- /dev/null +++ b/tests/test_option_card.py @@ -0,0 +1,103 @@ +# 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 pytest + +from ansys.dyna.core.lib.card import Card, Field +from ansys.dyna.core.lib.option_card import OptionCardSet, OptionSpec, OptionsAPI + +class OptionAPIImplementation: + def __init__(self, **kwargs): + self._active_options = set(["FOO"]) + self._option_specs = { + "FOO": OptionSpec("FOO", 1, 0, ["BAR"]), + "BAR": OptionSpec("BAR", 1, 0, ["FOO"]) + } + _cards = [ + OptionCardSet( + option_spec = self._option_specs["FOO"], + cards = [ + Card( + [ + Field( + "a", + int, + 0, + 10, + ) + ], + ), + ], + **kwargs + ), + OptionCardSet( + option_spec = self._option_specs["BAR"], + cards = [ + Card( + [ + Field( + "b", + int, + 0, + 10, + ) + ], + ), + ], + **kwargs + ), + ] + + 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 get_option_spec(self, name: str) -> OptionSpec: + return self._option_specs[name] + + +@pytest.mark.keywords +def test_options_basic(): + impl = OptionAPIImplementation() + options = OptionsAPI(impl) + assert options["FOO"].active is True + assert options["BAR"].active is False + options["BAR"].active = True + assert options["BAR"].active is True + +@pytest.mark.keywords +def test_options_union(): + impl = OptionAPIImplementation() + options = OptionsAPI(impl) + assert options["FOO"].active is True + assert options["BAR"].active is False + options["BAR"].active = True + assert options["BAR"].active is True + assert options["FOO"].active is False From 491134c6139227897d1e271ce1516917a2fe6472 Mon Sep 17 00:00:00 2001 From: Mohamed Koubaa Date: Tue, 21 Jan 2025 14:33:48 -0600 Subject: [PATCH 02/10] simplify options api --- src/ansys/dyna/core/lib/keyword_base.py | 18 +++-- src/ansys/dyna/core/lib/option_card.py | 90 +++++++++++++++---------- tests/test_option_card.py | 8 ++- 3 files changed, 71 insertions(+), 45 deletions(-) diff --git a/src/ansys/dyna/core/lib/keyword_base.py b/src/ansys/dyna/core/lib/keyword_base.py index a4e1231cf..f7a64149d 100644 --- a/src/ansys/dyna/core/lib/keyword_base.py +++ b/src/ansys/dyna/core/lib/keyword_base.py @@ -70,7 +70,7 @@ def get_title(self, format_symbol: str = "") -> str: base_title = self._get_base_title() titles = [base_title] if self.options != None: - options_specs = self.options.option_specs + options_specs = self.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] @@ -113,7 +113,7 @@ def deactivate_option(self, option: str) -> None: self._active_options.remove(option) def _try_activate_options(self, names: typing.List[str]) -> None: - for option in self.options.option_specs: + for option in self.option_specs: if option.name in names: self.activate_option(option.name) @@ -124,16 +124,20 @@ def _activate_options(self, title: str) -> None: self._try_activate_options(title_list) def get_option_spec(self, name: str) -> OptionSpec: + for option_spec in self.option_specs: + if option_spec.name == name: + return option_spec + raise Exception(f"No option spec with name `{name}` found") + + @property + def option_specs(self) -> typing.Iterable[OptionSpec]: for card in self._cards: if hasattr(card, "option_spec"): option_spec = card.option_spec - if option_spec.name == name: - return option_spec + yield option_spec elif hasattr(card, "option_specs"): for option_spec in card.option_specs: - if option_spec.name == name: - return option_spec - raise Exception(f"No option spec with name `{name}` found") + yield option_spec def __repr__(self) -> str: """Returns a console-friendly representation of the keyword data as it would appear in the .k file""" diff --git a/src/ansys/dyna/core/lib/option_card.py b/src/ansys/dyna/core/lib/option_card.py index bff0e2401..fbafefd0e 100644 --- a/src/ansys/dyna/core/lib/option_card.py +++ b/src/ansys/dyna/core/lib/option_card.py @@ -20,6 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +import abc from dataclasses import dataclass import io import typing @@ -32,11 +33,10 @@ class OptionSpec: - def __init__(self, name: str, card_order: int, title_order: int, excludes: typing.List[str] = None): + def __init__(self, name: str, card_order: int, title_order: int): self._name = name self._card_order = card_order self._title_order = title_order - self._excludes = excludes @property def name(self) -> str: @@ -62,14 +62,6 @@ def title_order(self) -> int: def title_order(self, value: int) -> None: self._title_order = value - @property - def excludes(self) -> typing.Optional[typing.List[str]]: - return self._excludes - - @excludes.setter - def excludes(self, value: typing.List[str]) -> None: - self._excludes = value - class OptionCardSet(CardInterface): def __init__( @@ -153,11 +145,51 @@ def _write(buf): return write_or_return(buf, _write) +class OptionCardAPIInterface(metaclass=abc.ABCMeta): + """Abstract base class for all the implementations of keyword cards.""" + + @classmethod + def __subclasshook__(cls, subclass): + return ( + hasattr(subclass, "is_option_active") + and callable(subclass.is_option_active) + and hasattr(subclass, "option_specs") + and callable(subclass.option_specs) + and hasattr(subclass, "activate_option") + and callable(subclass.activate_option) + and hasattr(subclass, "deactivate_option") + and callable(subclass.deactivate_option) + and hasattr(subclass, "get_option_spec") + and callable(subclass.get_option_spec) + ) + + @abc.abstractmethod + def get_option_spec(self, name: str) -> OptionSpec: + raise NotImplementedError + + @abc.abstractmethod + def deactivate_option(self, name: str) -> None: + raise NotImplementedError + + @abc.abstractmethod + def activate_option(self, name: str) -> None: + raise NotImplementedError + + @abc.abstractmethod + def is_option_active(self, name: str) -> bool: + raise NotImplementedError + + @property + @abc.abstractmethod + def option_specs(self) -> typing.Iterable[OptionSpec]: + """Get the card format type.""" + raise NotImplementedError + class OptionAPI: """API for an individual option associated with a keyword.""" - def __init__(self, options_api, name): + def __init__(self, options_api: OptionCardAPIInterface, name: str): self._options_api = options_api self._name = name @@ -170,9 +202,12 @@ def active(self, value: bool) -> None: option_spec: OptionSpec = self._options_api.get_option_spec(self._name) if value: self._options_api.activate_option(self._name) - if option_spec.excludes is not None: - for exclude in option_spec.excludes: - self._options_api.deactivate_option(exclude) + # deactivate all other options with the same card order and title order, since they will be mutually exclusive + for any_option_spec in self._options_api.option_specs: + if any_option_spec.name == self._name: + continue + if any_option_spec.title_order == option_spec.title_order and any_option_spec.card_order == option_spec.card_order: + self._options_api.deactivate_option(any_option_spec.name) else: self._options_api.deactivate_option(self._name) @@ -180,7 +215,7 @@ def active(self, value: bool) -> None: class OptionsAPI: """API for options associated with a keyword.""" - def __init__(self, api): + def __init__(self, api: OptionCardAPIInterface): self._api = api def __getitem__(self, name: str) -> OptionAPI: @@ -188,30 +223,13 @@ def __getitem__(self, name: str) -> OptionAPI: return OptionAPI(self._api, name) def __repr__(self) -> str: - option_names = self.get_option_names() - if len(option_names) == 0: + option_specs = self._api.option_specs + if len(option_specs) == 0: return "" sio = io.StringIO() sio.write("Options:") - for option_name in option_names: - active = self._api.is_option_active(option_name) + for option_spec in option_specs: + active = self._api.is_option_active(option_spec.name) active_string = "active" if active else "not active" sio.write(f"\n {option_name} option is {active_string}.") return sio.getvalue() - - def _load_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._load_option_specs(self._api._cards) - - def get_option_names(self) -> typing.List[str]: - option_specs = self._load_option_specs - return [o.name for o in option_specs] diff --git a/tests/test_option_card.py b/tests/test_option_card.py index 05bfb8ca5..8c1efbe67 100644 --- a/tests/test_option_card.py +++ b/tests/test_option_card.py @@ -31,8 +31,8 @@ class OptionAPIImplementation: def __init__(self, **kwargs): self._active_options = set(["FOO"]) self._option_specs = { - "FOO": OptionSpec("FOO", 1, 0, ["BAR"]), - "BAR": OptionSpec("BAR", 1, 0, ["FOO"]) + "FOO": OptionSpec("FOO", 1, 0), + "BAR": OptionSpec("BAR", 1, 0) } _cards = [ OptionCardSet( @@ -82,6 +82,10 @@ def deactivate_option(self, option: str) -> None: def get_option_spec(self, name: str) -> OptionSpec: return self._option_specs[name] + @property + def option_specs(self) -> typing.Iterable[OptionSpec]: + return [self._option_specs["FOO"], self._option_specs["BAR"]] + @pytest.mark.keywords def test_options_basic(): From 7bfb60827376239f5d01620696bdc9f4642c8bc9 Mon Sep 17 00:00:00 2001 From: Mohamed Koubaa Date: Tue, 21 Jan 2025 15:16:01 -0600 Subject: [PATCH 03/10] style --- src/ansys/dyna/core/lib/cards.py | 68 +++++++++++++++++++++---- src/ansys/dyna/core/lib/deck_loader.py | 2 +- src/ansys/dyna/core/lib/keyword_base.py | 40 --------------- src/ansys/dyna/core/lib/option_card.py | 41 +++++++-------- tests/test_deck.py | 3 -- tests/test_option_card.py | 7 +-- 6 files changed, 79 insertions(+), 82 deletions(-) diff --git a/src/ansys/dyna/core/lib/cards.py b/src/ansys/dyna/core/lib/cards.py index ddd72bb8e..7669cc08b 100644 --- a/src/ansys/dyna/core/lib/cards.py +++ b/src/ansys/dyna/core/lib/cards.py @@ -28,20 +28,65 @@ 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 +from ansys.dyna.core.lib.option_card import OptionCardSet, Options, OptionsInterface, OptionSpec from ansys.dyna.core.lib.parameter_set import ParameterSet -class Cards: +class Cards(OptionsInterface): def __init__(self, keyword): self._cards = [] - self._kw = keyword - self._options_api = OptionsAPI(keyword) + + # The instance of "Cards" may be part of a card set or it may be the keyword itself + # Though OptionsInterface is implemented here, the API for options should come from + # The keyword if it is a card set. # TODO - can this be improved? + self._options = Options(keyword) + self._active_options: typing.Set[str] = set() + + # options API interface implementation @property - def options(self) -> OptionsAPI: + def options(self) -> Options: """Gets the options_api of this keyword, if any""" - return self._options_api + return self._options + + 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.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 get_option_spec(self, name: str) -> OptionSpec: + for option_spec in self.option_specs: + if option_spec.name == name: + return option_spec + raise Exception(f"No option spec with name `{name}` found") + + @property + def option_specs(self) -> typing.Iterable[OptionSpec]: + for card in self._cards: + if hasattr(card, "option_spec"): + option_spec = card.option_spec + yield option_spec + elif hasattr(card, "option_specs"): + for option_spec in card.option_specs: + yield option_spec + + # end options API interface implementation @property def _cards(self) -> typing.List[CardInterface]: @@ -68,7 +113,8 @@ def _get_post_options_with_no_title_order(self): 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)] + + active_option_cards = [o for o in option_cards if self.options.api.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]: @@ -126,17 +172,17 @@ def _try_read_options_with_no_title(self, buf: typing.TextIO) -> None: pos = buf.tell() any_options_read = False cards = self._get_post_options_with_no_title_order() - exit = False + exit_loop = False while True: if len(cards) == 0: break linepos = buf.tell() - _, exit = read_line(buf) - if exit: + _, exit_loop = read_line(buf) + if exit_loop: break buf.seek(linepos) card = cards.pop(0) - self._kw.activate_option(card.name) + self.options.api.activate_option(card.name) any_options_read = True card.read(buf) if not any_options_read: diff --git a/src/ansys/dyna/core/lib/deck_loader.py b/src/ansys/dyna/core/lib/deck_loader.py index a3246fd75..9f66baecf 100644 --- a/src/ansys/dyna/core/lib/deck_loader.py +++ b/src/ansys/dyna/core/lib/deck_loader.py @@ -144,7 +144,7 @@ def handle_keyword(block: typing.List[str], deck: "ansys.dyna.core.deck.Deck") - try: keyword_object.loads(keyword_data, deck.parameters) deck.append(keyword_object) - except: + except Exception as e: result.add_unprocessed_keyword(keyword) deck.append(keyword_data) diff --git a/src/ansys/dyna/core/lib/keyword_base.py b/src/ansys/dyna/core/lib/keyword_base.py index f7a64149d..4b45a6eec 100644 --- a/src/ansys/dyna/core/lib/keyword_base.py +++ b/src/ansys/dyna/core/lib/keyword_base.py @@ -27,7 +27,6 @@ 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, OptionSpec from ansys.dyna.core.lib.parameter_set import ParameterSet @@ -43,9 +42,7 @@ class KeywordBase(Cards): 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: @@ -102,43 +99,6 @@ def _get_user_comment_lines(self) -> typing.List[str]: 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.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 get_option_spec(self, name: str) -> OptionSpec: - for option_spec in self.option_specs: - if option_spec.name == name: - return option_spec - raise Exception(f"No option spec with name `{name}` found") - - @property - def option_specs(self) -> typing.Iterable[OptionSpec]: - for card in self._cards: - if hasattr(card, "option_spec"): - option_spec = card.option_spec - yield option_spec - elif hasattr(card, "option_specs"): - for option_spec in card.option_specs: - yield option_spec - def __repr__(self) -> str: """Returns a console-friendly representation of the keyword data as it would appear in the .k file""" diff --git a/src/ansys/dyna/core/lib/option_card.py b/src/ansys/dyna/core/lib/option_card.py index fbafefd0e..f98ca20e0 100644 --- a/src/ansys/dyna/core/lib/option_card.py +++ b/src/ansys/dyna/core/lib/option_card.py @@ -21,7 +21,6 @@ # SOFTWARE. import abc -from dataclasses import dataclass import io import typing @@ -145,23 +144,9 @@ def _write(buf): return write_or_return(buf, _write) -class OptionCardAPIInterface(metaclass=abc.ABCMeta): - """Abstract base class for all the implementations of keyword cards.""" - - @classmethod - def __subclasshook__(cls, subclass): - return ( - hasattr(subclass, "is_option_active") - and callable(subclass.is_option_active) - and hasattr(subclass, "option_specs") - and callable(subclass.option_specs) - and hasattr(subclass, "activate_option") - and callable(subclass.activate_option) - and hasattr(subclass, "deactivate_option") - and callable(subclass.deactivate_option) - and hasattr(subclass, "get_option_spec") - and callable(subclass.get_option_spec) - ) + +class OptionsInterface(metaclass=abc.ABCMeta): + """Abstract base class for option card api interface.""" @abc.abstractmethod def get_option_spec(self, name: str) -> OptionSpec: @@ -189,7 +174,7 @@ def option_specs(self) -> typing.Iterable[OptionSpec]: class OptionAPI: """API for an individual option associated with a keyword.""" - def __init__(self, options_api: OptionCardAPIInterface, name: str): + def __init__(self, options_api: OptionsInterface, name: str): self._options_api = options_api self._name = name @@ -202,20 +187,24 @@ def active(self, value: bool) -> None: option_spec: OptionSpec = self._options_api.get_option_spec(self._name) if value: self._options_api.activate_option(self._name) - # deactivate all other options with the same card order and title order, since they will be mutually exclusive + # deactivate all other options with the same card order and title order + # since they will be mutually exclusive for any_option_spec in self._options_api.option_specs: if any_option_spec.name == self._name: continue - if any_option_spec.title_order == option_spec.title_order and any_option_spec.card_order == option_spec.card_order: + if ( + any_option_spec.title_order == option_spec.title_order + and any_option_spec.card_order == option_spec.card_order + ): self._options_api.deactivate_option(any_option_spec.name) else: self._options_api.deactivate_option(self._name) -class OptionsAPI: - """API for options associated with a keyword.""" +class Options: + """Option collection associated with an options API.""" - def __init__(self, api: OptionCardAPIInterface): + def __init__(self, api: OptionsInterface): self._api = api def __getitem__(self, name: str) -> OptionAPI: @@ -233,3 +222,7 @@ def __repr__(self) -> str: active_string = "active" if active else "not active" sio.write(f"\n {option_name} option is {active_string}.") return sio.getvalue() + + @property + def api(self) -> OptionsInterface: + return self._api diff --git a/tests/test_deck.py b/tests/test_deck.py index 2fe538c95..a32acf744 100644 --- a/tests/test_deck.py +++ b/tests/test_deck.py @@ -268,9 +268,6 @@ def test_deck_kwlist(ref_string): def test_deck_load_title(ref_string): deck = Deck() deck.loads(ref_string.test_title_string) - print(type(deck)) - print(type(kwd.DefineCurve())) - print(type(deck.keywords[0])) assert isinstance(deck.keywords[0], kwd.DefineCurve) assert deck.keywords[0].title == "title" assert deck.keywords[0].options["TITLE"].active diff --git a/tests/test_option_card.py b/tests/test_option_card.py index 8c1efbe67..437eb54f7 100644 --- a/tests/test_option_card.py +++ b/tests/test_option_card.py @@ -25,7 +25,7 @@ import pytest from ansys.dyna.core.lib.card import Card, Field -from ansys.dyna.core.lib.option_card import OptionCardSet, OptionSpec, OptionsAPI +from ansys.dyna.core.lib.option_card import OptionCardSet, OptionSpec, Options class OptionAPIImplementation: def __init__(self, **kwargs): @@ -90,16 +90,17 @@ def option_specs(self) -> typing.Iterable[OptionSpec]: @pytest.mark.keywords def test_options_basic(): impl = OptionAPIImplementation() - options = OptionsAPI(impl) + options = Options(impl) assert options["FOO"].active is True assert options["BAR"].active is False options["BAR"].active = True assert options["BAR"].active is True + @pytest.mark.keywords def test_options_union(): impl = OptionAPIImplementation() - options = OptionsAPI(impl) + options = Options(impl) assert options["FOO"].active is True assert options["BAR"].active is False options["BAR"].active = True From e1970f1718bd6f2b8b77829f5f24d970356b05e1 Mon Sep 17 00:00:00 2001 From: Mohamed Koubaa Date: Tue, 21 Jan 2025 15:27:17 -0600 Subject: [PATCH 04/10] Fix CONSTRAINED_BEAM_IN_SOLID --- codegen/manifest.json | 16 ++++++- .../auto/constrained_beam_in_solid.py | 47 ++++++++++++++++++- src/ansys/dyna/core/lib/option_card.py | 4 ++ tests/test_keywords.py | 8 ++++ tests/testfiles/keywords/reference_string.py | 8 ++++ 5 files changed, 80 insertions(+), 3 deletions(-) diff --git a/codegen/manifest.json b/codegen/manifest.json index 7f596e77d..d86a1e385 100644 --- a/codegen/manifest.json +++ b/codegen/manifest.json @@ -112,8 +112,8 @@ "generation-options": { "add-option": [ { - "card-order": 1, - "title-order": 0, + "card-order": -1, + "title-order": 1, "cards": [ { "source": "kwd-data", @@ -122,6 +122,18 @@ } ], "option-name": "ID" + }, + { + "card-order": -1, + "title-order": 1, + "cards": [ + { + "source": "kwd-data", + "keyword-name": "CONSTRAINED_BEAM_IN_SOLID", + "card-index": 0 + } + ], + "option-name": "TITLE" } ], "skip-card": 0, diff --git a/src/ansys/dyna/core/keywords/keyword_classes/auto/constrained_beam_in_solid.py b/src/ansys/dyna/core/keywords/keyword_classes/auto/constrained_beam_in_solid.py index ec70b7d42..12bcad321 100644 --- a/src/ansys/dyna/core/keywords/keyword_classes/auto/constrained_beam_in_solid.py +++ b/src/ansys/dyna/core/keywords/keyword_classes/auto/constrained_beam_in_solid.py @@ -32,7 +32,8 @@ class ConstrainedBeamInSolid(KeywordBase): keyword = "CONSTRAINED" subkeyword = "BEAM_IN_SOLID" option_specs = [ - OptionSpec("ID", 1, 0), + OptionSpec("ID", -1, 1), + OptionSpec("TITLE", -1, 1), ] def __init__(self, **kwargs): @@ -183,6 +184,30 @@ def __init__(self, **kwargs): ], **kwargs ), + OptionCardSet( + option_spec = ConstrainedBeamInSolid.option_specs[1], + cards = [ + Card( + [ + Field( + "coupid", + int, + 0, + 10, + kwargs.get("coupid") + ), + Field( + "title", + str, + 10, + 70, + kwargs.get("title") + ), + ], + ), + ], + **kwargs + ), ] @property @@ -336,3 +361,23 @@ def title(self) -> typing.Optional[str]: def title(self, value: str) -> None: self._cards[2].cards[0].set_value("title", value) + @property + def coupid(self) -> typing.Optional[int]: + """Get or set the Coupling card ID number + """ # nopep8 + return self._cards[3].cards[0].get_value("coupid") + + @coupid.setter + def coupid(self, value: int) -> None: + self._cards[3].cards[0].set_value("coupid", value) + + @property + def title(self) -> typing.Optional[str]: + """Get or set the A description of this coupling definition + """ # nopep8 + return self._cards[3].cards[0].get_value("title") + + @title.setter + def title(self, value: str) -> None: + self._cards[3].cards[0].set_value("title", value) + diff --git a/src/ansys/dyna/core/lib/option_card.py b/src/ansys/dyna/core/lib/option_card.py index f98ca20e0..de24c60a6 100644 --- a/src/ansys/dyna/core/lib/option_card.py +++ b/src/ansys/dyna/core/lib/option_card.py @@ -61,6 +61,10 @@ def title_order(self) -> int: def title_order(self, value: int) -> None: self._title_order = value + def __repr__(self) -> str: + return f"OptionSpec(name={self.name}, card_order={self.card_order}, title_order={self.title_order})" + + class OptionCardSet(CardInterface): def __init__( diff --git a/tests/test_keywords.py b/tests/test_keywords.py index 4bbf2ff78..d8506a655 100644 --- a/tests/test_keywords.py +++ b/tests/test_keywords.py @@ -215,6 +215,14 @@ def test_boundary_prescribed_motion_set(ref_string): def test_constrained_beam_in_solid(ref_string): b = kwd.ConstrainedBeamInSolid(ncoup=1) assert(b.write() == ref_string.test_constrained_beam_in_solid) + b.options["ID"].active = True + assert b.options["ID"].active == True + assert b.options["TITLE"].active == False + b.options["TITLE"].active = True + assert b.options["TITLE"].active == True + assert b.options["ID"].active == False + b_str = b.write() + assert b_str == ref_string.test_constrained_beam_in_solid_title @pytest.mark.keywords diff --git a/tests/testfiles/keywords/reference_string.py b/tests/testfiles/keywords/reference_string.py index 55898875d..d5d121f5b 100644 --- a/tests/testfiles/keywords/reference_string.py +++ b/tests/testfiles/keywords/reference_string.py @@ -160,6 +160,14 @@ 0 0 1.0 1e+28 0.0""" test_constrained_beam_in_solid = """*CONSTRAINED_BEAM_IN_SOLID +$# bside ssid bstyp sstyp unused unused ncoup cdir + 0 0 1 +$# start end unused axfor unused pssf unused xint + 0.0 1e+21 0.1 """ + +test_constrained_beam_in_solid_title = """*CONSTRAINED_BEAM_IN_SOLID_TITLE +$# coupid title + $# bside ssid bstyp sstyp unused unused ncoup cdir 0 0 1 $# start end unused axfor unused pssf unused xint From 660e22c635ad55f8ef89013b0c7f9a52d6b919f7 Mon Sep 17 00:00:00 2001 From: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> Date: Tue, 21 Jan 2025 21:28:06 +0000 Subject: [PATCH 05/10] chore: adding changelog file 671.miscellaneous.md [dependabot-skip] --- doc/changelog/671.miscellaneous.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 doc/changelog/671.miscellaneous.md diff --git a/doc/changelog/671.miscellaneous.md b/doc/changelog/671.miscellaneous.md new file mode 100644 index 000000000..993f4ea94 --- /dev/null +++ b/doc/changelog/671.miscellaneous.md @@ -0,0 +1 @@ +Options api rework \ No newline at end of file From 360153a41c8188954445f49abbcded7e3024a035 Mon Sep 17 00:00:00 2001 From: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> Date: Tue, 21 Jan 2025 21:28:45 +0000 Subject: [PATCH 06/10] chore: adding changelog file 671.miscellaneous.md [dependabot-skip] --- doc/changelog/671.miscellaneous.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/changelog/671.miscellaneous.md b/doc/changelog/671.miscellaneous.md index 993f4ea94..c58a181b9 100644 --- a/doc/changelog/671.miscellaneous.md +++ b/doc/changelog/671.miscellaneous.md @@ -1 +1 @@ -Options api rework \ No newline at end of file +fix: Options api rework \ No newline at end of file From c958433bfdb3049b43f06792c0188533eab02ced Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 21 Jan 2025 21:30:11 +0000 Subject: [PATCH 07/10] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/ansys/dyna/core/lib/option_card.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ansys/dyna/core/lib/option_card.py b/src/ansys/dyna/core/lib/option_card.py index de24c60a6..3caf7a40d 100644 --- a/src/ansys/dyna/core/lib/option_card.py +++ b/src/ansys/dyna/core/lib/option_card.py @@ -65,7 +65,6 @@ def __repr__(self) -> str: return f"OptionSpec(name={self.name}, card_order={self.card_order}, title_order={self.title_order})" - class OptionCardSet(CardInterface): def __init__( self, From ee4f8b867fe44f1cff379bd91fca0fca55a7e6b2 Mon Sep 17 00:00:00 2001 From: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> Date: Tue, 21 Jan 2025 21:30:53 +0000 Subject: [PATCH 08/10] chore: adding changelog file 671.documentation.md [dependabot-skip] --- doc/changelog/{671.miscellaneous.md => 671.documentation.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename doc/changelog/{671.miscellaneous.md => 671.documentation.md} (100%) diff --git a/doc/changelog/671.miscellaneous.md b/doc/changelog/671.documentation.md similarity index 100% rename from doc/changelog/671.miscellaneous.md rename to doc/changelog/671.documentation.md From adff0cc3302ea606e104d7ea1263270ade824e1b Mon Sep 17 00:00:00 2001 From: Mohamed Koubaa Date: Wed, 22 Jan 2025 07:13:32 -0600 Subject: [PATCH 09/10] implement shared field for options refactor generation system to use handler classes --- codegen/generate.py | 33 +++++---- codegen/keyword_generation/__init__.py | 0 .../handlers/handler_base.py | 15 +++++ .../handlers/shared_field.py | 67 +++++++++++++++++++ codegen/manifest.json | 13 +++- .../keyword/option_card_properties.j2 | 9 +++ .../auto/constrained_beam_in_solid.py | 20 +----- tests/test_keywords.py | 7 +- tests/testfiles/keywords/reference_string.py | 10 ++- 9 files changed, 132 insertions(+), 42 deletions(-) create mode 100644 codegen/keyword_generation/__init__.py create mode 100644 codegen/keyword_generation/handlers/handler_base.py create mode 100644 codegen/keyword_generation/handlers/shared_field.py diff --git a/codegen/generate.py b/codegen/generate.py index e27ca1a68..b02abe4c6 100644 --- a/codegen/generate.py +++ b/codegen/generate.py @@ -34,6 +34,10 @@ from jinja2 import Environment, FileSystemLoader +from keyword_generation.handlers.shared_field import SharedFieldHandler +from keyword_generation.handlers.handler_base import KeywordHandler + + SKIPPED_KEYWORDS = set( [ # defined manually because of the variable length text card @@ -288,6 +292,7 @@ def handle_override_field(kwd_data, settings): if "new-name" in setting: field["name"] = setting["new-name"] + def handle_rename_property(kwd_data, settings): for setting in settings: index = setting["index"] @@ -299,19 +304,6 @@ def handle_rename_property(kwd_data, settings): field["property_name"] = property_name -def handle_shared_field(kwd_data, settings): - for setting in settings: - fields = [] - for card in kwd_data["cards"]: - for field in card["fields"]: - if field["name"] == setting["name"]: - fields.append(field) - assert len(fields) > 1 - fields[0]["card_indices"] = setting["cards"] - for field in fields[1:]: - field["redundant"] = True - - def handle_override_subkeyword(kwd_data, settings) -> None: kwd_data["subkeyword"] = settings @@ -351,7 +343,7 @@ def expand(card): "rename-property": handle_rename_property, "skip-card": handle_skipped_cards, "duplicate-card-group": handle_duplicate_card_group, - "shared-field": handle_shared_field, + "shared-field": SharedFieldHandler(), "override-subkeyword": handle_override_subkeyword, } ) @@ -387,7 +379,6 @@ def add_option_indices(kwd_data): card["index"] = index index += 1 - def add_indices(kwd_data): # handlers might point to cards by a specific index. for index, card in enumerate(kwd_data["cards"]): @@ -435,6 +426,9 @@ def after_handle(kwd_data): do_insertions(kwd_data) delete_marked_indices(kwd_data) add_option_indices(kwd_data) + for handler_name, handler in HANDLERS.items(): + if isinstance(handler, KeywordHandler): + handler.post_process(kwd_data) def before_handle(kwd_data): @@ -446,11 +440,16 @@ def handle_keyword_data(kwd_data, settings): before_handle(kwd_data) # we have to iterate in the order of the handlers because right now the order still matters # right now this is only true for reorder_card - for handler_name, handler_func in HANDLERS.items(): + for handler_name, handler in HANDLERS.items(): handler_settings = settings.get(handler_name) if handler_settings == None: continue - handler_func(kwd_data, handler_settings) + # handler can be a KeywordHandler object or a function pointer + # TODO - change all handlers to objects + if isinstance(handler, KeywordHandler): + handler.handle(kwd_data, handler_settings) + else: + handler(kwd_data, handler_settings) after_handle(kwd_data) diff --git a/codegen/keyword_generation/__init__.py b/codegen/keyword_generation/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/codegen/keyword_generation/handlers/handler_base.py b/codegen/keyword_generation/handlers/handler_base.py new file mode 100644 index 000000000..03f3b1571 --- /dev/null +++ b/codegen/keyword_generation/handlers/handler_base.py @@ -0,0 +1,15 @@ +import abc +import typing + +class KeywordHandler(metaclass=abc.ABCMeta): + """Abstract base class for keyword handlers.""" + + @abc.abstractmethod + def handle(self, kwd_data: typing.Dict[str, typing.Any], settings: typing.Dict[str, typing.Any]) -> None: + """Transform `kwd_data` based on `settings`.""" + raise NotImplementedError + + @abc.abstractmethod + def post_process(self, kwd_data: typing.Dict[str, typing.Any]) -> None: + """Run after all handlers have run.""" + raise NotImplementedError diff --git a/codegen/keyword_generation/handlers/shared_field.py b/codegen/keyword_generation/handlers/shared_field.py new file mode 100644 index 000000000..7abba494c --- /dev/null +++ b/codegen/keyword_generation/handlers/shared_field.py @@ -0,0 +1,67 @@ +import typing + +import keyword_generation.handlers.handler_base + +def do_negative_shared_fields(kwd_data: typing.Dict): + negative_shared_fields = kwd_data.get("negative_shared_fields", []) + num_cards = len(kwd_data["cards"]) + options = kwd_data.get("options", []) + option_cards = [] + for options in kwd_data.get("options", []): + option_cards.extend(options["cards"]) + for setting in negative_shared_fields: + indices = [-i for i in setting["cards"]] + fields = [] + for index in indices: + if index >= num_cards: + for options in kwd_data.get("options", []): + for card in options["cards"]: + if card["index"] == index: + for field in card["fields"]: + if field["name"] == setting["name"]: + fields.append(field) + else: + assert False, "TODO - support negative indices for shared fields for non-options" + assert len(fields) > 1 + if not setting["applied_card_indices"]: + fields[0]["card_indices"] = indices + for field in fields[1:]: + field["redundant"] = True + + +def handle_shared_field(kwd_data, settings): + # positive card indices are applied in handler + # negative card indices are marked and handled after transformations (after_handle) + for setting in settings: + setting["applied_card_indices"] = False + cards = setting["cards"] + num_positive = len([c for c in cards if c > 0]) + + # either or - we cannot support some positive some negative in the same setting now + assert num_positive == 0 or num_positive == len(cards) + if num_positive > 0: + fields = [] + for card in kwd_data["cards"]: + for field in card["fields"]: + if field["name"] == setting["name"]: + fields.append(field) + assert len(fields) > 1 + fields[0]["card_indices"] = cards + setting["applied_card_indices"] = True + for field in fields[1:]: + field["redundant"] = True + else: + if "negative_shared_fields" not in kwd_data: + kwd_data["negative_shared_fields"] = [] + kwd_data["negative_shared_fields"].append(setting) + + +class SharedFieldHandler(keyword_generation.handlers.handler_base.KeywordHandler): + + def handle(self, kwd_data: typing.Dict[str, typing.Any], settings: typing.Dict[str, typing.Any]) -> None: + """Transform `kwd_data` based on `settings`.""" + return handle_shared_field(kwd_data, settings) + + def post_process(self, kwd_data: typing.Dict[str, typing.Any]) -> None: + """Run after all handlers have run.""" + return do_negative_shared_fields(kwd_data) \ No newline at end of file diff --git a/codegen/manifest.json b/codegen/manifest.json index d86a1e385..f4741ee8c 100644 --- a/codegen/manifest.json +++ b/codegen/manifest.json @@ -136,6 +136,16 @@ "option-name": "TITLE" } ], + "shared-field": [ + { + "name": "COUPID", + "cards": [-2, -3] + }, + { + "name": "TITLE", + "cards": [-2, -3] + } + ], "skip-card": 0, "override-field": [ { @@ -144,8 +154,7 @@ "new-name": "ncoup" } ] - }, - "comment": "TODO - the option name is either ID or TITLE, but there is no way in pydyna to have a union option" + } }, "DEFINE_TRANSFORMATION": { "generation-options": { diff --git a/codegen/templates/keyword/option_card_properties.j2 b/codegen/templates/keyword/option_card_properties.j2 index fa0923be2..36ea187ad 100644 --- a/codegen/templates/keyword/option_card_properties.j2 +++ b/codegen/templates/keyword/option_card_properties.j2 @@ -2,6 +2,7 @@ {% set card_loop = loop %} {% for field in card.fields %} {% if field.used %} +{% if not field.redundant %} @property def {{field.property_name}}(self) -> {%- if field.default is none %} typing.Optional[ {%- else %} {% endif %}{{field.property_type}}{%- if field.default is none %}]{%- endif %}: @@ -17,9 +18,17 @@ if value not in [{{ field.options|join(', ')}}]: raise Exception("""{{field.property_name}} must be one of {{openbrace}}{{ field.options|join(',')}}{{closebrace}}""") {% endif %} +{% if field.card_indices %} +{# This is COMPLETELY wrong but it works for CONSTRAINED_BEAM_IN_SOLID. REVISIT! #} +{% for card_index in field.card_indices %} + self._cards[{{card_index}}].cards[{{card_loop.index-1}}].set_value("{{field.name}}", value) +{% endfor %}{# card_index in field.card_indices #} +{% else %} self._cards[{{card.index}}].cards[{{card_loop.index-1}}].set_value("{{field.name}}", value) +{% endif %}{# card.indices #} {% endif %}{# not field.readonly #} +{% endif %}{# not field.redundant #} {% endif %}{# field.used #} {% endfor %}{# field in card.fields #} {% endfor %}{# card in option.cards #} \ No newline at end of file diff --git a/src/ansys/dyna/core/keywords/keyword_classes/auto/constrained_beam_in_solid.py b/src/ansys/dyna/core/keywords/keyword_classes/auto/constrained_beam_in_solid.py index 12bcad321..dc167d1fc 100644 --- a/src/ansys/dyna/core/keywords/keyword_classes/auto/constrained_beam_in_solid.py +++ b/src/ansys/dyna/core/keywords/keyword_classes/auto/constrained_beam_in_solid.py @@ -350,6 +350,7 @@ def coupid(self) -> typing.Optional[int]: @coupid.setter def coupid(self, value: int) -> None: self._cards[2].cards[0].set_value("coupid", value) + self._cards[3].cards[0].set_value("coupid", value) @property def title(self) -> typing.Optional[str]: @@ -360,24 +361,5 @@ def title(self) -> typing.Optional[str]: @title.setter def title(self, value: str) -> None: self._cards[2].cards[0].set_value("title", value) - - @property - def coupid(self) -> typing.Optional[int]: - """Get or set the Coupling card ID number - """ # nopep8 - return self._cards[3].cards[0].get_value("coupid") - - @coupid.setter - def coupid(self, value: int) -> None: - self._cards[3].cards[0].set_value("coupid", value) - - @property - def title(self) -> typing.Optional[str]: - """Get or set the A description of this coupling definition - """ # nopep8 - return self._cards[3].cards[0].get_value("title") - - @title.setter - def title(self, value: str) -> None: self._cards[3].cards[0].set_value("title", value) diff --git a/tests/test_keywords.py b/tests/test_keywords.py index d8506a655..9572ebf91 100644 --- a/tests/test_keywords.py +++ b/tests/test_keywords.py @@ -214,15 +214,16 @@ def test_boundary_prescribed_motion_set(ref_string): @pytest.mark.keywords def test_constrained_beam_in_solid(ref_string): b = kwd.ConstrainedBeamInSolid(ncoup=1) - assert(b.write() == ref_string.test_constrained_beam_in_solid) + b.coupid=12 + assert b.write() == ref_string.test_constrained_beam_in_solid b.options["ID"].active = True assert b.options["ID"].active == True assert b.options["TITLE"].active == False + assert b.write() == ref_string.test_constrained_beam_in_solid_id b.options["TITLE"].active = True assert b.options["TITLE"].active == True assert b.options["ID"].active == False - b_str = b.write() - assert b_str == ref_string.test_constrained_beam_in_solid_title + assert b.write() == ref_string.test_constrained_beam_in_solid_title @pytest.mark.keywords diff --git a/tests/testfiles/keywords/reference_string.py b/tests/testfiles/keywords/reference_string.py index d5d121f5b..c37fe5faa 100644 --- a/tests/testfiles/keywords/reference_string.py +++ b/tests/testfiles/keywords/reference_string.py @@ -167,7 +167,15 @@ test_constrained_beam_in_solid_title = """*CONSTRAINED_BEAM_IN_SOLID_TITLE $# coupid title - + 12 +$# bside ssid bstyp sstyp unused unused ncoup cdir + 0 0 1 +$# start end unused axfor unused pssf unused xint + 0.0 1e+21 0.1 """ + +test_constrained_beam_in_solid_id = """*CONSTRAINED_BEAM_IN_SOLID_ID +$# coupid title + 12 $# bside ssid bstyp sstyp unused unused ncoup cdir 0 0 1 $# start end unused axfor unused pssf unused xint From ce7cfb9c1dc9cc241d818b4d50fad8970adf32fd Mon Sep 17 00:00:00 2001 From: Mohamed Koubaa Date: Wed, 22 Jan 2025 07:15:08 -0600 Subject: [PATCH 10/10] style --- src/ansys/dyna/core/lib/option_card.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ansys/dyna/core/lib/option_card.py b/src/ansys/dyna/core/lib/option_card.py index de24c60a6..3caf7a40d 100644 --- a/src/ansys/dyna/core/lib/option_card.py +++ b/src/ansys/dyna/core/lib/option_card.py @@ -65,7 +65,6 @@ def __repr__(self) -> str: return f"OptionSpec(name={self.name}, card_order={self.card_order}, title_order={self.title_order})" - class OptionCardSet(CardInterface): def __init__( self,