From b6117a321a0f4e3c7d2eca4be1fe56429cb0678f Mon Sep 17 00:00:00 2001 From: Dominik Gresch Date: Fri, 18 Aug 2023 21:35:58 +0200 Subject: [PATCH 1/4] Add sensor class --- .../_grpc_helpers/property_helper.py | 27 ++++-- src/ansys/acp/core/_tree_objects/enums.py | 7 ++ src/ansys/acp/core/_tree_objects/model.py | 4 + src/ansys/acp/core/_tree_objects/sensor.py | 93 +++++++++++++++++++ tests/unittests/test_sensor.py | 68 ++++++++++++++ 5 files changed, 192 insertions(+), 7 deletions(-) create mode 100644 src/ansys/acp/core/_tree_objects/sensor.py create mode 100644 tests/unittests/test_sensor.py diff --git a/src/ansys/acp/core/_tree_objects/_grpc_helpers/property_helper.py b/src/ansys/acp/core/_tree_objects/_grpc_helpers/property_helper.py index 15de269a03..ff01d1bb1f 100644 --- a/src/ansys/acp/core/_tree_objects/_grpc_helpers/property_helper.py +++ b/src/ansys/acp/core/_tree_objects/_grpc_helpers/property_helper.py @@ -60,7 +60,9 @@ def inner(self: Readable) -> CreatableFromResourcePath | None: return inner -def grpc_data_getter(name: str, from_protobuf: _FROM_PROTOBUF_T) -> Callable[[Readable], Any]: +def grpc_data_getter( + name: str, from_protobuf: _FROM_PROTOBUF_T, check_optional: bool = False +) -> Callable[[Readable], Any]: """ Creates a getter method which obtains the server object via the gRPC Get endpoint. @@ -68,7 +70,10 @@ def grpc_data_getter(name: str, from_protobuf: _FROM_PROTOBUF_T) -> Callable[[Re def inner(self: Readable) -> Any: self._get_if_stored() - return from_protobuf(_get_data_attribute(self._pb_object, name)) + pb_attribute = _get_data_attribute(self._pb_object, name, check_optional=check_optional) + if check_optional and pb_attribute is None: + return None + return from_protobuf(pb_attribute) return inner @@ -94,8 +99,12 @@ def inner(self: Editable, value: Any) -> None: return inner -def _get_data_attribute(pb_obj: ObjectInfo, name: str) -> Any: +def _get_data_attribute(pb_obj: ObjectInfo, name: str, check_optional: bool = False) -> Any: name_parts = name.split(".") + if check_optional: + parent_obj = reduce(getattr, name_parts[:-1], pb_obj) + if hasattr(parent_obj, "HasField") and not parent_obj.HasField(name_parts[-1]): + return None return reduce(getattr, name_parts, pb_obj) @@ -122,6 +131,7 @@ def grpc_data_property( name: str, to_protobuf: _TO_PROTOBUF_T = lambda x: x, from_protobuf: _FROM_PROTOBUF_T = lambda x: x, + check_optional: bool = False, ) -> Any: """ Helper for defining properties accessed via gRPC. The property getter @@ -135,21 +145,24 @@ def grpc_data_property( # Protocol # See the discussion here on why it is hard to have typed properties: # https://github.com/python/typing/issues/985 - return _exposed_grpc_property(grpc_data_getter(name, from_protobuf=from_protobuf)).setter( - grpc_data_setter(name, to_protobuf=to_protobuf) - ) + return _exposed_grpc_property( + grpc_data_getter(name, from_protobuf=from_protobuf, check_optional=check_optional) + ).setter(grpc_data_setter(name, to_protobuf=to_protobuf)) def grpc_data_property_read_only( name: str, from_protobuf: _FROM_PROTOBUF_T = lambda x: x, + check_optional: bool = False, ) -> Any: """ Helper for defining properties accessed via gRPC. The property getter makes call to the gRPC Get endpoints to synchronize the local object with the remote backend. """ - return _exposed_grpc_property(grpc_data_getter(name, from_protobuf=from_protobuf)) + return _exposed_grpc_property( + grpc_data_getter(name, from_protobuf=from_protobuf, check_optional=check_optional) + ) def grpc_link_property(name: str) -> Any: diff --git a/src/ansys/acp/core/_tree_objects/enums.py b/src/ansys/acp/core/_tree_objects/enums.py index a45e723cd3..9bbf62dd0e 100644 --- a/src/ansys/acp/core/_tree_objects/enums.py +++ b/src/ansys/acp/core/_tree_objects/enums.py @@ -7,6 +7,7 @@ lookup_table_column_type_pb2, mesh_query_pb2, ply_material_pb2, + sensor_pb2, unit_system_pb2, ) @@ -25,6 +26,7 @@ "ElementalDataType", "NodalDataType", "LookUpTableColumnValueType", + "SensorType", ] (StatusType, status_type_to_pb, status_type_from_pb) = wrap_to_string_enum( @@ -155,3 +157,8 @@ lookup_table_3d_pb2.InterpolationAlgorithm, module=__name__, ) +( + SensorType, + sensor_type_to_pb, + sensor_type_from_pb, +) = wrap_to_string_enum("SensorType", sensor_pb2.SensorType, module=__name__) diff --git a/src/ansys/acp/core/_tree_objects/model.py b/src/ansys/acp/core/_tree_objects/model.py index 02d2130942..ca3bdeaa2d 100644 --- a/src/ansys/acp/core/_tree_objects/model.py +++ b/src/ansys/acp/core/_tree_objects/model.py @@ -26,6 +26,7 @@ oriented_selection_set_pb2_grpc, parallel_selection_rule_pb2_grpc, rosette_pb2_grpc, + sensor_pb2_grpc, spherical_selection_rule_pb2_grpc, stackup_pb2_grpc, sublaminate_pb2_grpc, @@ -60,6 +61,7 @@ from .oriented_selection_set import OrientedSelectionSet from .parallel_selection_rule import ParallelSelectionRule from .rosette import Rosette +from .sensor import Sensor from .spherical_selection_rule import SphericalSelectionRule from .stackup import Stackup from .sublaminate import SubLaminate @@ -339,6 +341,8 @@ def export_materials(self, path: _PATH) -> None: ModelingGroup, modeling_group_pb2_grpc.ObjectServiceStub ) + create_sensor, sensors = define_mutable_mapping(Sensor, sensor_pb2_grpc.ObjectServiceStub) + @property def mesh(self) -> MeshData: mesh_query_stub = mesh_query_pb2_grpc.MeshQueryServiceStub(self._channel) diff --git a/src/ansys/acp/core/_tree_objects/sensor.py b/src/ansys/acp/core/_tree_objects/sensor.py new file mode 100644 index 0000000000..d62acc101c --- /dev/null +++ b/src/ansys/acp/core/_tree_objects/sensor.py @@ -0,0 +1,93 @@ +from __future__ import annotations + +from typing import Iterable, Union, get_args + +from ansys.api.acp.v0 import sensor_pb2, sensor_pb2_grpc + +from .._utils.array_conversions import to_tuple_from_1D_array +from ._grpc_helpers.linked_object_list import define_polymorphic_linked_object_list +from ._grpc_helpers.property_helper import ( + grpc_data_property, + grpc_data_property_read_only, + mark_grpc_properties, +) +from .base import CreatableTreeObject, IdTreeObject +from .element_set import ElementSet +from .enums import SensorType, sensor_type_from_pb, sensor_type_to_pb, status_type_from_pb +from .fabric import Fabric +from .modeling_ply import ModelingPly +from .object_registry import register +from .oriented_selection_set import OrientedSelectionSet +from .stackup import Stackup +from .sublaminate import SubLaminate + +__all__ = ["Sensor"] + + +_LINKABLE_ENTITY_TYPES = Union[ + Fabric, Stackup, SubLaminate, ElementSet, OrientedSelectionSet, ModelingPly +] + + +@mark_grpc_properties +@register +class Sensor(CreatableTreeObject, IdTreeObject): + """Instantiate a Sensor. + + Parameters + ---------- + name : + Name of the sensor. + sensor_type : + Type of sensor: The sensor can be scoped by area, material, plies, + or solid model. + active : + Inactive sensors are not evaluated. + entities : + List of entities which define the sensor's scope. + """ + + __slots__: Iterable[str] = tuple() + + _COLLECTION_LABEL = "sensors" + OBJECT_INFO_TYPE = sensor_pb2.ObjectInfo + CREATE_REQUEST_TYPE = sensor_pb2.CreateRequest + + def __init__( + self, + name: str = "Sensor", + sensor_type: SensorType = SensorType.SENSOR_BY_AREA, + active: bool = True, + entities: Iterable[_LINKABLE_ENTITY_TYPES] = (), + ): + super().__init__(name=name) + self.active = active + self.entities = entities + self.sensor_type = sensor_type + + def _create_stub(self) -> sensor_pb2_grpc.ObjectServiceStub: + return sensor_pb2_grpc.ObjectServiceStub(self._channel) + + locked = grpc_data_property_read_only("properties.locked") + sensor_type = grpc_data_property( + "properties.sensor_type", from_protobuf=sensor_type_from_pb, to_protobuf=sensor_type_to_pb + ) + status = grpc_data_property_read_only("properties.status", from_protobuf=status_type_from_pb) + + active = grpc_data_property("properties.active") + entities = define_polymorphic_linked_object_list( + "properties.entities", allowed_types=get_args(_LINKABLE_ENTITY_TYPES) + ) + + covered_area = grpc_data_property_read_only("properties.covered_area", check_optional=True) + modeling_ply_area = grpc_data_property_read_only( + "properties.modeling_ply_area", check_optional=True + ) + production_ply_area = grpc_data_property_read_only( + "properties.production_ply_area", check_optional=True + ) + price = grpc_data_property_read_only("properties.price", check_optional=True) + weight = grpc_data_property_read_only("properties.weight", check_optional=True) + center_of_gravity = grpc_data_property_read_only( + "properties.center_of_gravity", from_protobuf=to_tuple_from_1D_array, check_optional=True + ) diff --git a/tests/unittests/test_sensor.py b/tests/unittests/test_sensor.py new file mode 100644 index 0000000000..5c28af73b6 --- /dev/null +++ b/tests/unittests/test_sensor.py @@ -0,0 +1,68 @@ +import pytest + +from ansys.acp.core._tree_objects.enums import SensorType + +from .common.tree_object_tester import NoLockedMixin, ObjectPropertiesToTest, TreeObjectTester + + +@pytest.fixture +def parent_object(load_model_from_tempfile): + with load_model_from_tempfile() as model: + yield model + + +@pytest.fixture +def tree_object(parent_object): + return parent_object.create_sensor() + + +class TestSensor(NoLockedMixin, TreeObjectTester): + COLLECTION_NAME = "sensors" + DEFAULT_PROPERTIES = { + "status": "NOTUPTODATE", + "active": True, + "entities": [], + "covered_area": None, + "modeling_ply_area": None, + "production_ply_area": None, + "price": None, + "weight": None, + "center_of_gravity": None, + } + + CREATE_METHOD_NAME = "create_sensor" + + @staticmethod + @pytest.fixture + def object_properties(parent_object): + model = parent_object + elset = model.create_element_set() + oriented_selection_set = model.create_oriented_selection_set() + modeling_ply = model.create_modeling_group().create_modeling_ply() + fabric = model.create_fabric() + stackup = model.create_stackup() + sublaminate = model.create_sublaminate() + return ObjectPropertiesToTest( + read_write=[ + ("name", "Sensor name"), + ("sensor_type", SensorType.SENSOR_BY_AREA), + ("entities", [elset, modeling_ply, oriented_selection_set]), + ("sensor_type", SensorType.SENSOR_BY_MATERIAL), + ("entities", [stackup, fabric, sublaminate]), + ("sensor_type", SensorType.SENSOR_BY_PLIES), + ("entities", [modeling_ply]), + ("sensor_type", SensorType.SENSOR_BY_SOLID_MODEL), + ("active", False), + ], + read_only=[ + ("id", "some_id"), + ("status", "UPTODATE"), + ("locked", True), + ("covered_area", 0.3), + ("modeling_ply_area", 0.3), + ("production_ply_area", 0.3), + ("price", 0.3), + ("weight", 0.3), + ("center_of_gravity", (0.1, 0.2, 0.3)), + ], + ) From 8eea033dcc2ee4b64400f7c1520ce38ecd67639d Mon Sep 17 00:00:00 2001 From: Dominik Gresch Date: Mon, 21 Aug 2023 08:38:52 +0200 Subject: [PATCH 2/4] Document check_optional on grpc_data_getter --- .../_tree_objects/_grpc_helpers/property_helper.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/ansys/acp/core/_tree_objects/_grpc_helpers/property_helper.py b/src/ansys/acp/core/_tree_objects/_grpc_helpers/property_helper.py index ff01d1bb1f..5768f4b472 100644 --- a/src/ansys/acp/core/_tree_objects/_grpc_helpers/property_helper.py +++ b/src/ansys/acp/core/_tree_objects/_grpc_helpers/property_helper.py @@ -66,6 +66,16 @@ def grpc_data_getter( """ Creates a getter method which obtains the server object via the gRPC Get endpoint. + + Parameters + ---------- + from_protobuf : + Function to convert the protobuf object to the type exposed by the + property. + check_optional : + If ``True``, the getter will return ``None`` if the property is not + set on the protobuf object. Otherwise, the default protobuf value + will be used. """ def inner(self: Readable) -> Any: From 9072e1a85199dea16c45b088d8600ed62f1489f8 Mon Sep 17 00:00:00 2001 From: Dominik Gresch Date: Mon, 21 Aug 2023 08:55:27 +0200 Subject: [PATCH 3/4] Document read-only properties of the Sensor --- doc/source/api/tree_objects.rst | 1 + src/ansys/acp/core/__init__.py | 1 + src/ansys/acp/core/_tree_objects/__init__.py | 2 + .../_grpc_helpers/property_helper.py | 56 +++++++++++++++++-- src/ansys/acp/core/_tree_objects/sensor.py | 36 ++++++++++-- 5 files changed, 84 insertions(+), 12 deletions(-) diff --git a/doc/source/api/tree_objects.rst b/doc/source/api/tree_objects.rst index ff9e529ec4..a4408442c1 100644 --- a/doc/source/api/tree_objects.rst +++ b/doc/source/api/tree_objects.rst @@ -28,3 +28,4 @@ ACP objects ModelingPly ProductionPly AnalysisPly + Sensor diff --git a/src/ansys/acp/core/__init__.py b/src/ansys/acp/core/__init__.py index 44c763e74f..b5980cdfcb 100644 --- a/src/ansys/acp/core/__init__.py +++ b/src/ansys/acp/core/__init__.py @@ -27,6 +27,7 @@ ParallelSelectionRule, ProductionPly, Rosette, + Sensor, SphericalSelectionRule, Stackup, SubLaminate, diff --git a/src/ansys/acp/core/_tree_objects/__init__.py b/src/ansys/acp/core/_tree_objects/__init__.py index 9cfedc0d92..ec3aaee5af 100644 --- a/src/ansys/acp/core/_tree_objects/__init__.py +++ b/src/ansys/acp/core/_tree_objects/__init__.py @@ -18,6 +18,7 @@ from .parallel_selection_rule import ParallelSelectionRule from .production_ply import ProductionPly from .rosette import Rosette +from .sensor import Sensor from .spherical_selection_rule import SphericalSelectionRule from .stackup import FabricWithAngle, Stackup from .sublaminate import Lamina, SubLaminate @@ -52,6 +53,7 @@ "ModelingPly", "ProductionPly", "AnalysisPly", + "Sensor", "UnitSystemType", "EdgeSetType", "ElementalDataType", diff --git a/src/ansys/acp/core/_tree_objects/_grpc_helpers/property_helper.py b/src/ansys/acp/core/_tree_objects/_grpc_helpers/property_helper.py index 5768f4b472..82dcf4f317 100644 --- a/src/ansys/acp/core/_tree_objects/_grpc_helpers/property_helper.py +++ b/src/ansys/acp/core/_tree_objects/_grpc_helpers/property_helper.py @@ -137,16 +137,39 @@ def _set_data_attribute(pb_obj: ObjectInfo, name: str, value: Any) -> None: target_object.add().CopyFrom(item) +def _wrap_doc(obj: Any, doc: str | None) -> Any: + if doc is not None: + obj.__doc__ = doc + return obj + + def grpc_data_property( name: str, to_protobuf: _TO_PROTOBUF_T = lambda x: x, from_protobuf: _FROM_PROTOBUF_T = lambda x: x, check_optional: bool = False, + doc: str | None = None, ) -> Any: """ Helper for defining properties accessed via gRPC. The property getter and setter make calls to the gRPC Get and Put endpoints to synchronize the local object with the remote backend. + + Parameters + ---------- + name : + Name of the property. + to_protobuf : + Function to convert the property value to the protobuf type. + from_protobuf : + Function to convert the protobuf object to the type exposed by the + property. + check_optional : + If ``True``, the getter will return ``None`` if the property is not + set on the protobuf object. Otherwise, the default protobuf value + will be used. + doc : + Docstring for the property. """ # Note jvonrick August 2023: We don't ensure with typechecks that the property returned here is # compatible with the class on which this property is created. For example: @@ -155,23 +178,44 @@ def grpc_data_property( # Protocol # See the discussion here on why it is hard to have typed properties: # https://github.com/python/typing/issues/985 - return _exposed_grpc_property( - grpc_data_getter(name, from_protobuf=from_protobuf, check_optional=check_optional) - ).setter(grpc_data_setter(name, to_protobuf=to_protobuf)) + return _wrap_doc( + _exposed_grpc_property( + grpc_data_getter(name, from_protobuf=from_protobuf, check_optional=check_optional) + ).setter(grpc_data_setter(name, to_protobuf=to_protobuf)), + doc=doc, + ) def grpc_data_property_read_only( name: str, from_protobuf: _FROM_PROTOBUF_T = lambda x: x, check_optional: bool = False, + doc: str | None = None, ) -> Any: """ Helper for defining properties accessed via gRPC. The property getter makes call to the gRPC Get endpoints to synchronize the local object with the remote backend. - """ - return _exposed_grpc_property( - grpc_data_getter(name, from_protobuf=from_protobuf, check_optional=check_optional) + + Parameters + ---------- + name : + Name of the property. + from_protobuf : + Function to convert the protobuf object to the type exposed by the + property. + check_optional : + If ``True``, the getter will return ``None`` if the property is not + set on the protobuf object. Otherwise, the default protobuf value + will be used. + doc : + Docstring for the property. + """ + return _wrap_doc( + _exposed_grpc_property( + grpc_data_getter(name, from_protobuf=from_protobuf, check_optional=check_optional) + ), + doc=doc, ) diff --git a/src/ansys/acp/core/_tree_objects/sensor.py b/src/ansys/acp/core/_tree_objects/sensor.py index d62acc101c..a11e35e776 100644 --- a/src/ansys/acp/core/_tree_objects/sensor.py +++ b/src/ansys/acp/core/_tree_objects/sensor.py @@ -79,15 +79,39 @@ def _create_stub(self) -> sensor_pb2_grpc.ObjectServiceStub: "properties.entities", allowed_types=get_args(_LINKABLE_ENTITY_TYPES) ) - covered_area = grpc_data_property_read_only("properties.covered_area", check_optional=True) + covered_area = grpc_data_property_read_only( + "properties.covered_area", + check_optional=True, + doc=( + "The surface area of a selected Element Set / Oriented Selection Set, " + "or the tooling surface area that is covered by the composite layup of " + "the selected Material or Modeling Ply." + ), + ) modeling_ply_area = grpc_data_property_read_only( - "properties.modeling_ply_area", check_optional=True + "properties.modeling_ply_area", + check_optional=True, + doc="The surface area of all Modeling Plies of the selected entity.", ) production_ply_area = grpc_data_property_read_only( - "properties.production_ply_area", check_optional=True + "properties.production_ply_area", + check_optional=True, + doc="The surface area of all production plies of the selected entity.", + ) + price = grpc_data_property_read_only( + "properties.price", + check_optional=True, + doc=( + "The price for the composite layup of the selected entity. The price " + "per area is defined on the Fabrics or Stackups." + ), + ) + weight = grpc_data_property_read_only( + "properties.weight", check_optional=True, doc="The mass of the selected entity." ) - price = grpc_data_property_read_only("properties.price", check_optional=True) - weight = grpc_data_property_read_only("properties.weight", check_optional=True) center_of_gravity = grpc_data_property_read_only( - "properties.center_of_gravity", from_protobuf=to_tuple_from_1D_array, check_optional=True + "properties.center_of_gravity", + from_protobuf=to_tuple_from_1D_array, + check_optional=True, + doc="The center of gravity of the selected entity in the global coordinate system.", ) From d772d2a7190f4985d9b93e9ac639e6b65b5c1603 Mon Sep 17 00:00:00 2001 From: Dominik Gresch Date: Mon, 21 Aug 2023 11:50:01 +0200 Subject: [PATCH 4/4] Update lockfile --- poetry.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/poetry.lock b/poetry.lock index 2350dc3526..7fa2dffd0b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -164,7 +164,7 @@ protobuf = ">=3.19,<5" type = "git" url = "https://github.com/ansys-internal/ansys-api-acp.git" reference = "main" -resolved_reference = "bbbedda5b51447e99ee739cc2e9fc1f14ae92246" +resolved_reference = "98feedcd67d263b081bcc0f51691c09f0af6a187" [[package]] name = "ansys-api-mapdl" @@ -416,13 +416,13 @@ tests = ["ansys-api-platform-instancemanagement (==1.0.0)", "grpcio-health-check [[package]] name = "ansys-sphinx-theme" -version = "0.10.3" +version = "0.10.4" description = "A theme devised by ANSYS, Inc. for Sphinx documentation." optional = false python-versions = ">=3.8,<4" files = [ - {file = "ansys_sphinx_theme-0.10.3-py3-none-any.whl", hash = "sha256:612057c257d776f9cbc14f3d188fcd80ee2e70bdb3725f2d336f98f3d2d1f9b7"}, - {file = "ansys_sphinx_theme-0.10.3.tar.gz", hash = "sha256:3d672eba39ab03506939e799ede49a70966ca389ec3d34d7e014aeca0d07b3b3"}, + {file = "ansys_sphinx_theme-0.10.4-py3-none-any.whl", hash = "sha256:fbf7e84b373fae06534f2b50b977354199aefa23a40c040ad5699cce2134728f"}, + {file = "ansys_sphinx_theme-0.10.4.tar.gz", hash = "sha256:e712d0df63d4d18b6e6927297d9bc0bf4d7a3cbaf11e60dfc4196568332dc123"}, ] [package.dependencies]