From 0dcd83498d21b634d3a4add1c3b8a25f62a3701d Mon Sep 17 00:00:00 2001 From: Dominik Gresch Date: Wed, 23 Oct 2024 14:41:27 +0200 Subject: [PATCH] Add ButtJointSequence exposure (#606) Add the `ButtJointSequence` class, and a `PrimaryPly` class for the edge properties of the `primary_plies` attribute. Other changes: - Add an `allowed_types_getter` parameter to `define_polymorphic_linked_object_list` with the same purpose as the existing `allowed_types`, except the types are evaluated only inside the getter. This was needed to avoid a circular import with the `ModelingGroup`, since `define_polymorphic_linked_object_list` is called at the module top-level. --- doc/source/api/linked_object_definitions.rst | 5 +- doc/source/api/tree_objects.rst | 1 + src/ansys/acp/core/__init__.py | 4 + src/ansys/acp/core/_tree_objects/__init__.py | 3 + .../_grpc_helpers/linked_object_list.py | 12 +- .../core/_tree_objects/butt_joint_sequence.py | 225 ++++++++++++++++++ .../acp/core/_tree_objects/modeling_group.py | 20 +- tests/unittests/test_butt_joint_sequence.py | 121 ++++++++++ 8 files changed, 383 insertions(+), 8 deletions(-) create mode 100644 src/ansys/acp/core/_tree_objects/butt_joint_sequence.py create mode 100644 tests/unittests/test_butt_joint_sequence.py diff --git a/doc/source/api/linked_object_definitions.rst b/doc/source/api/linked_object_definitions.rst index 6f2168eca1..1383ae6641 100644 --- a/doc/source/api/linked_object_definitions.rst +++ b/doc/source/api/linked_object_definitions.rst @@ -7,7 +7,8 @@ Linked object definitions :toctree: _autosummary FabricWithAngle + Lamina LinkedSelectionRule - TaperEdge + PrimaryPly SubShape - Lamina + TaperEdge diff --git a/doc/source/api/tree_objects.rst b/doc/source/api/tree_objects.rst index 861e42be11..d95ad469ac 100644 --- a/doc/source/api/tree_objects.rst +++ b/doc/source/api/tree_objects.rst @@ -8,6 +8,7 @@ ACP objects AnalysisPly BooleanSelectionRule + ButtJointSequence CADComponent CADGeometry CutoffSelectionRule diff --git a/src/ansys/acp/core/__init__.py b/src/ansys/acp/core/__init__.py index 8e3566465b..2401fe59b6 100644 --- a/src/ansys/acp/core/__init__.py +++ b/src/ansys/acp/core/__init__.py @@ -48,6 +48,7 @@ BooleanSelectionRule, BooleanSelectionRuleElementalData, BooleanSelectionRuleNodalData, + ButtJointSequence, CADComponent, CADGeometry, CutoffMaterialType, @@ -105,6 +106,7 @@ PlyCutoffType, PlyGeometryExportFormat, PlyType, + PrimaryPly, ProductionPly, ProductionPlyElementalData, ProductionPlyNodalData, @@ -154,6 +156,7 @@ "BooleanSelectionRule", "BooleanSelectionRuleElementalData", "BooleanSelectionRuleNodalData", + "ButtJointSequence", "CADComponent", "CADGeometry", "ConnectLaunchConfig", @@ -223,6 +226,7 @@ "PlyCutoffType", "PlyGeometryExportFormat", "PlyType", + "PrimaryPly", "print_model", "ProductionPly", "ProductionPlyElementalData", diff --git a/src/ansys/acp/core/_tree_objects/__init__.py b/src/ansys/acp/core/_tree_objects/__init__.py index 2c7b24b34b..0b86c423bb 100644 --- a/src/ansys/acp/core/_tree_objects/__init__.py +++ b/src/ansys/acp/core/_tree_objects/__init__.py @@ -27,6 +27,7 @@ BooleanSelectionRuleElementalData, BooleanSelectionRuleNodalData, ) +from .butt_joint_sequence import ButtJointSequence, PrimaryPly from .cad_component import CADComponent from .cad_geometry import CADGeometry, TriangleMesh from .cutoff_selection_rule import ( @@ -127,6 +128,7 @@ "BooleanSelectionRule", "BooleanSelectionRuleElementalData", "BooleanSelectionRuleNodalData", + "ButtJointSequence", "CADComponent", "CADGeometry", "CutoffMaterialType", @@ -186,6 +188,7 @@ "PlyCutoffType", "PlyGeometryExportFormat", "PlyType", + "PrimaryPly", "ProductionPly", "ProductionPlyElementalData", "ProductionPlyNodalData", diff --git a/src/ansys/acp/core/_tree_objects/_grpc_helpers/linked_object_list.py b/src/ansys/acp/core/_tree_objects/_grpc_helpers/linked_object_list.py index 4bd4b5d7f2..2fdbff54e5 100644 --- a/src/ansys/acp/core/_tree_objects/_grpc_helpers/linked_object_list.py +++ b/src/ansys/acp/core/_tree_objects/_grpc_helpers/linked_object_list.py @@ -41,7 +41,7 @@ ValueT = TypeVar("ValueT", bound=CreatableTreeObject) -__all__ = ["LinkedObjectList", "define_linked_object_list"] +__all__ = ["LinkedObjectList", "define_linked_object_list", "define_polymorphic_linked_object_list"] class LinkedObjectList(ObjectCacheMixin, MutableSequence[ValueT]): @@ -302,11 +302,19 @@ def setter(self: ValueT, value: list[ChildT]) -> None: def define_polymorphic_linked_object_list( - attribute_name: str, allowed_types: tuple[Any, ...] + attribute_name: str, + allowed_types: tuple[Any, ...] | None = None, + allowed_types_getter: Callable[[], tuple[Any, ...]] | None = None, ) -> Any: """Define a list of linked tree objects with polymorphic types.""" + if allowed_types is None != allowed_types_getter is None: + raise ValueError("Exactly one of allowed_types and allowed_types_getter must be provided.") def getter(self: ValueT) -> LinkedObjectList[Any]: + nonlocal allowed_types + if allowed_types_getter is not None: + allowed_types = allowed_types_getter() + return LinkedObjectList( _parent_object=self, _attribute_name=attribute_name, diff --git a/src/ansys/acp/core/_tree_objects/butt_joint_sequence.py b/src/ansys/acp/core/_tree_objects/butt_joint_sequence.py new file mode 100644 index 0000000000..c45d19a2a2 --- /dev/null +++ b/src/ansys/acp/core/_tree_objects/butt_joint_sequence.py @@ -0,0 +1,225 @@ +# Copyright (C) 2022 - 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 __future__ import annotations + +from collections.abc import Callable, Iterable, Sequence +from typing import TYPE_CHECKING, Any, Union, cast + +from typing_extensions import Self + +from ansys.api.acp.v0 import butt_joint_sequence_pb2, butt_joint_sequence_pb2_grpc + +from .._utils.property_protocols import ReadWriteProperty +from ._grpc_helpers.edge_property_list import ( + GenericEdgePropertyType, + define_add_method, + define_edge_property_list, +) +from ._grpc_helpers.linked_object_list import define_polymorphic_linked_object_list +from ._grpc_helpers.polymorphic_from_pb import tree_object_from_resource_path +from ._grpc_helpers.property_helper import ( + _exposed_grpc_property, + grpc_data_property, + grpc_data_property_read_only, + mark_grpc_properties, +) +from .base import CreatableTreeObject, IdTreeObject +from .enums import status_type_from_pb +from .modeling_ply import ModelingPly +from .object_registry import register + +if TYPE_CHECKING: # pragma: no cover + # Creates a circular import if imported at the top-level, since the ButtJointSequence + # is a direct child of the ModelingGroup. + from .modeling_group import ModelingGroup + +__all__ = ["ButtJointSequence", "PrimaryPly"] + + +@mark_grpc_properties +class PrimaryPly(GenericEdgePropertyType): + """Defines a primary ply of a butt joint sequence. + + Parameters + ---------- + sequence : + Modeling group or modeling ply defining the primary ply. + level : + Level of the primary ply. Plies with a higher level inherit the thickness + from adjacent plies with a lower level. + + """ + + _SUPPORTED_SINCE = "25.1" + + def __init__(self, sequence: ModelingGroup | ModelingPly, level: int = 1): + self._callback_apply_changes: Callable[[], None] | None = None + self.sequence = sequence + self.level = level + + @_exposed_grpc_property + def sequence(self) -> ModelingGroup | ModelingPly: + """Linked sequence.""" + return self._sequence + + @sequence.setter + def sequence(self, value: ModelingGroup | ModelingPly) -> None: + from .modeling_group import ModelingGroup + + if not isinstance(value, (ModelingGroup, ModelingPly)): + raise TypeError(f"Expected a ModelingGroup or ModelingPly, got {type(value)}") + self._sequence = value + if self._callback_apply_changes: + self._callback_apply_changes() + + @_exposed_grpc_property + def level(self) -> int: + """Level of the primary ply. + + Plies with a higher level inherit the thickness from adjacent plies with a lower level. + """ + return self._level + + @level.setter + def level(self, value: int) -> None: + self._level = value + if self._callback_apply_changes: + self._callback_apply_changes() + + def _set_callback_apply_changes(self, callback_apply_changes: Callable[[], None]) -> None: + self._callback_apply_changes = callback_apply_changes + + @classmethod + def _from_pb_object( + cls, + parent_object: CreatableTreeObject, + message: butt_joint_sequence_pb2.PrimaryPly, + apply_changes: Callable[[], None], + ) -> Self: + from .modeling_group import ModelingGroup # imported here to avoid circular import + + new_obj = cls( + sequence=cast( + Union["ModelingGroup", ModelingPly], + tree_object_from_resource_path( + message.sequence, + server_wrapper=parent_object._server_wrapper, + allowed_types=(ModelingGroup, ModelingPly), + ), + ), + level=message.level, + ) + new_obj._set_callback_apply_changes(apply_changes) + return new_obj + + def _to_pb_object(self) -> butt_joint_sequence_pb2.PrimaryPly: + return butt_joint_sequence_pb2.PrimaryPly( + sequence=self.sequence._resource_path, level=self.level + ) + + def _check(self) -> bool: + # Check for empty resource paths + return bool(self.sequence._resource_path.value) + + def __eq__(self, other: Any) -> bool: + if isinstance(other, self.__class__): + return ( + self.sequence._resource_path == other.sequence._resource_path + and self.level == other.level + ) + + return False + + def __repr__(self) -> str: + return f"PrimaryPly(sequence={self.sequence.__repr__()}, level={self.level})" + + def clone(self) -> Self: + """Create a new unstored PrimaryPly with the same properties.""" + return type(self)(sequence=self.sequence, level=self.level) + + +def _get_allowed_secondary_ply_types() -> tuple[type, ...]: + from .modeling_group import ModelingGroup + + return (ModelingGroup, ModelingPly) + + +@mark_grpc_properties +@register +class ButtJointSequence(CreatableTreeObject, IdTreeObject): + """Instantiate a ButtJointSequence. + + Parameters + ---------- + name : + Name of the butt joint sequence. + primary_plies : + Primary plies are the source of a butt joint and they pass the thickness to + adjacent plies. Plies with a higher level inherit the thickness from those + with a lower level. + secondary_plies : + Secondary plies are butt-joined to adjacent primary plies and they inherit + the thickness. + """ + + __slots__: Iterable[str] = tuple() + + _COLLECTION_LABEL = "butt_joint_sequences" + _OBJECT_INFO_TYPE = butt_joint_sequence_pb2.ObjectInfo + _CREATE_REQUEST_TYPE = butt_joint_sequence_pb2.CreateRequest + _SUPPORTED_SINCE = "25.1" + + def __init__( + self, + *, + name: str = "ButtJointSequence", + active: bool = True, + global_ply_nr: int = 0, + primary_plies: Sequence[PrimaryPly] = (), + secondary_plies: Sequence[ModelingGroup | ModelingPly] = (), + ): + super().__init__(name=name) + self.active = active + self.global_ply_nr = global_ply_nr + self.primary_plies = primary_plies + self.secondary_plies = secondary_plies + + def _create_stub(self) -> butt_joint_sequence_pb2_grpc.ObjectServiceStub: + return butt_joint_sequence_pb2_grpc.ObjectServiceStub(self._channel) + + status = grpc_data_property_read_only("properties.status", from_protobuf=status_type_from_pb) + active: ReadWriteProperty[bool, bool] = grpc_data_property("properties.active") + global_ply_nr: ReadWriteProperty[int, int] = grpc_data_property("properties.global_ply_nr") + + primary_plies = define_edge_property_list("properties.primary_plies", PrimaryPly) + add_primary_ply = define_add_method( + PrimaryPly, + attribute_name="primary_plies", + func_name="add_primary_ply", + parent_class_name="ButtJointSequence", + module_name=__module__, + ) + + secondary_plies = define_polymorphic_linked_object_list( + "properties.secondary_plies", allowed_types_getter=_get_allowed_secondary_ply_types + ) diff --git a/src/ansys/acp/core/_tree_objects/modeling_group.py b/src/ansys/acp/core/_tree_objects/modeling_group.py index bb0058b669..5cc118a7ca 100644 --- a/src/ansys/acp/core/_tree_objects/modeling_group.py +++ b/src/ansys/acp/core/_tree_objects/modeling_group.py @@ -26,6 +26,7 @@ import dataclasses from ansys.api.acp.v0 import ( + butt_joint_sequence_pb2_grpc, interface_layer_pb2_grpc, modeling_group_pb2, modeling_group_pb2_grpc, @@ -42,6 +43,7 @@ nodal_data_property, ) from .base import CreatableTreeObject, IdTreeObject +from .butt_joint_sequence import ButtJointSequence from .interface_layer import InterfaceLayer from .modeling_ply import ModelingPly from .object_registry import register @@ -68,7 +70,7 @@ class ModelingGroup(CreatableTreeObject, IdTreeObject): Parameters ---------- - name + name : Name of the modeling group. """ @@ -92,15 +94,25 @@ def _create_stub(self) -> modeling_group_pb2_grpc.ObjectServiceStub: module_name=__module__, ) modeling_plies = define_mutable_mapping(ModelingPly, modeling_ply_pb2_grpc.ObjectServiceStub) - interface_layers = define_mutable_mapping( - InterfaceLayer, interface_layer_pb2_grpc.ObjectServiceStub - ) create_interface_layer = define_create_method( InterfaceLayer, func_name="create_interface_layer", parent_class_name="ModelingGroup", module_name=__module__, ) + interface_layers = define_mutable_mapping( + InterfaceLayer, interface_layer_pb2_grpc.ObjectServiceStub + ) + + create_butt_joint_sequence = define_create_method( + ButtJointSequence, + func_name="create_butt_joint_sequence", + parent_class_name="ModelingGroup", + module_name=__module__, + ) + butt_joint_sequences = define_mutable_mapping( + ButtJointSequence, butt_joint_sequence_pb2_grpc.ObjectServiceStub + ) elemental_data = elemental_data_property(ModelingGroupElementalData) nodal_data = nodal_data_property(ModelingGroupNodalData) diff --git a/tests/unittests/test_butt_joint_sequence.py b/tests/unittests/test_butt_joint_sequence.py new file mode 100644 index 0000000000..9608100dd4 --- /dev/null +++ b/tests/unittests/test_butt_joint_sequence.py @@ -0,0 +1,121 @@ +# Copyright (C) 2022 - 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 packaging.version import parse as parse_version +import pytest + +from ansys.acp.core import ButtJointSequence, PrimaryPly + +from .common.tree_object_tester import NoLockedMixin, ObjectPropertiesToTest, TreeObjectTester +from .common.utils import AnyThing + + +@pytest.fixture(autouse=True) +def skip_if_unsupported_version(acp_instance): + if parse_version(acp_instance.server_version) < parse_version( + ButtJointSequence._SUPPORTED_SINCE + ): + pytest.skip("ButtJointSequence is not supported on this version of the server.") + + +@pytest.fixture +def parent_model(load_model_from_tempfile): + with load_model_from_tempfile() as model: + yield model + + +@pytest.fixture +def parent_object(parent_model): + return parent_model.modeling_groups["ModelingGroup.1"] + + +@pytest.fixture +def tree_object(parent_object): + return parent_object.create_butt_joint_sequence() + + +class TestButtJointSequence(NoLockedMixin, TreeObjectTester): + COLLECTION_NAME = "butt_joint_sequences" + + @staticmethod + @pytest.fixture + def default_properties(): + return { + "status": "NOTUPTODATE", + "active": True, + "global_ply_nr": AnyThing(), + "primary_plies": [], + "secondary_plies": [], + } + + CREATE_METHOD_NAME = "create_butt_joint_sequence" + + @staticmethod + @pytest.fixture + def object_properties(parent_model): + mg1 = parent_model.create_modeling_group() + mg2 = parent_model.create_modeling_group() + mp1 = mg1.create_modeling_ply() + mp2 = mg1.create_modeling_ply() + return ObjectPropertiesToTest( + read_write=[ + ("name", "ButtJointSequence name"), + ("active", False), + ("global_ply_nr", 3), + ( + "primary_plies", + [ + PrimaryPly(sequence=mg1, level=1), + PrimaryPly(sequence=mp2, level=3), + ], + ), + ("secondary_plies", [mg2, mp1]), + ], + read_only=[ + ("id", "some_id"), + ("status", "UPTODATE"), + ], + ) + + +def test_wrong_primary_ply_type_error_message(tree_object, parent_model): + butt_joint_sequence = tree_object + fabric = parent_model.create_fabric() + with pytest.raises(TypeError) as exc: + butt_joint_sequence.primary_plies = [fabric] + assert "PrimaryPly" in str(exc.value) + assert "Fabric" in str(exc.value) + + +def test_add_primary_ply(parent_object): + """Verify add method for primary plies.""" + modeling_ply_1 = parent_object.create_modeling_ply() + + butt_joint_sequence = parent_object.create_butt_joint_sequence() + butt_joint_sequence.add_primary_ply(modeling_ply_1) + assert butt_joint_sequence.primary_plies[-1].sequence == modeling_ply_1 + assert butt_joint_sequence.primary_plies[-1].level == 1 + modeling_ply_2 = modeling_ply_1.clone() + modeling_ply_2.store(parent=parent_object) + butt_joint_sequence.add_primary_ply(modeling_ply_2, level=3) + assert butt_joint_sequence.primary_plies[-1].sequence == modeling_ply_2 + assert butt_joint_sequence.primary_plies[-1].level == 3