diff --git a/doc/changelog.d/672.miscellaneous.md b/doc/changelog.d/672.miscellaneous.md new file mode 100644 index 000000000..1decbc579 --- /dev/null +++ b/doc/changelog.d/672.miscellaneous.md @@ -0,0 +1 @@ +Feat: added get_mount_point_props and update_mount_points methods. diff --git a/doc/source/api/layer_types.rst b/doc/source/api/layer_types.rst index 323d874df..32350bdcf 100644 --- a/doc/source/api/layer_types.rst +++ b/doc/source/api/layer_types.rst @@ -14,8 +14,10 @@ Classes used for the Layer API. CopyPottingRegionRequest DeletePottingRegionRequest GetICTFixturesPropertiesRequest + GetMountPointsPropertiesRequest GetTestPointPropertiesRequest ICTFixtureProperties + MountPointProperties PolygonalShape RectangularShape SlotShape @@ -27,6 +29,7 @@ Classes used for the Layer API. PottingRegionUpdateData TestPointProperties UpdateICTFixturesRequest + UpdateMountPointsRequest UpdatePottingRegionRequest UpdateTestPointsRequest diff --git a/src/ansys/sherlock/core/layer.py b/src/ansys/sherlock/core/layer.py index 83abf416e..517881aa5 100644 --- a/src/ansys/sherlock/core/layer.py +++ b/src/ansys/sherlock/core/layer.py @@ -8,12 +8,14 @@ CopyPottingRegionRequest, DeletePottingRegionRequest, GetICTFixturesPropertiesRequest, + GetMountPointsPropertiesRequest, GetTestPointPropertiesRequest, PCBShape, PolygonalShape, RectangularShape, SlotShape, UpdateICTFixturesRequest, + UpdateMountPointsRequest, UpdatePottingRegionRequest, UpdateTestPointsRequest, ) @@ -2239,3 +2241,97 @@ def update_ict_fixtures( update_request = request._convert_to_grpc() response = self.stub.updateICTFixtures(update_request) return response + + @require_version(261) + def get_mount_point_props( + self, request: GetMountPointsPropertiesRequest + ) -> SherlockLayerService_pb2.GetMountPointsPropertiesResponse: + """ + Return the properties for each mount point given a comma-separated list of mount point IDs. + + Available Since: 2026R1 + + Parameters + ---------- + request: GetmountPointsPropertiesRequest + Contains all the information needed to return the properties for one or more mount + points. + + Returns + ------- + SherlockCommonService_pb2.GetMountPointsPropertiesResponse + Properties for each mount point that correspond to the reference designators. + + Examples + -------- + >>> from ansys.sherlock.core.launcher import launch_sherlock + >>> from ansys.sherlock.core.types.layer_types import GetMountPointsPropertiesRequest + >>> sherlock = launch_sherlock() + >>> request = layer_types.GetMountPointsPropertiesRequest( + >>> project = "Tutorial Project" + >>> cca_name = "Main Board" + >>> mount_point_ids = "MP1,MP2" + >>> ) + >>> response = layer.get_mount_point_props(request) + """ + if not self._is_connection_up(): + raise SherlockNoGrpcConnectionException() + + return self.stub.getMountPointsProperties(request._convert_to_grpc()) + + @require_version(261) + def update_mount_points( + self, request: UpdateMountPointsRequest + ) -> SherlockLayerService_pb2.UpdateMountPointsResponse: + """Update mount point properties of a CCA from input parameters. + + Available Since: 2026R1 + + Parameters + ---------- + request: UpdateMountPointsRequest + Contains all the information needed to update the properties for one or more + mount points. + + Returns + ------- + SherlockCommonService_pb2.UpdateMountPointsResponse + A status code and message for the update mount points request. + + Examples + -------- + >>> from ansys.sherlock.core.launcher import launch_sherlock + >>> from ansys.sherlock.core.types.layer_types import UpdateMountPointsRequest, + >>> MountPointProperties + >>> sherlock = connect() + >>> mount_point = MountPointProperties( + >>> id="MP1", + >>> type="Mount Pad", + >>> shape="Rectangular", + >>> units="mm", + >>> side="BOTTOM", + >>> height=1.0, + >>> material="GOLD", + >>> state="DISABLED", + >>> x=0.3, + >>> y=-0.4, + >>> length=1.0, + >>> width=0.2, + >>> diameter=0.0, + >>> nodes="", + >>> rotation=45, + >>> polygon="", + >>> boundary="Outline", + >>> constraints="X-axis translation|Z-axis translation", + >>> chassis_material="SILVER", + >>> ) + >>> response = sherlock.layer.update_mount_points(UpdateMountPointsRequest( + >>> project="Tutorial Project", + >>> cca_name="Main Board", + >>> update_mount_points=[mount_point], + >>> )) + """ + if not self._is_connection_up(): + raise SherlockNoGrpcConnectionException() + + return self.stub.updateMountPoints(request._convert_to_grpc()) diff --git a/src/ansys/sherlock/core/types/layer_types.py b/src/ansys/sherlock/core/types/layer_types.py index ea9547d3a..da3aba4e2 100644 --- a/src/ansys/sherlock/core/types/layer_types.py +++ b/src/ansys/sherlock/core/types/layer_types.py @@ -574,3 +574,148 @@ def _convert_to_grpc(self) -> SherlockLayerService_pb2.UpdateICTFixturesRequest: for update_fixture in self.update_fixtures: request.ICTFixtureProperties.append(update_fixture._convert_to_grpc()) return request + + +class MountPointProperties(BaseModel): + """Contains the properties of a mount point.""" + + id: str + """ID""" + type: str + """Type""" + shape: str + """Shape type""" + units: str + """Units""" + x: float + """Center X""" + y: float + """Center Y""" + length: float + """Length""" + width: float + """Width""" + diameter: float + """Diameter""" + nodes: str + """Number of nodes""" + rotation: float + """Degrees of rotation""" + side: str + """Side""" + height: float + """Height""" + material: str + """Material""" + boundary: str + """Boundary point(s)""" + constraints: str + """FEA constraints""" + polygon: str + """Coordinates of points""" + state: str + """State""" + chassis_material: str + """Chassis material""" + + def _convert_to_grpc(self) -> SherlockLayerService_pb2.MountPointProperties: + grpc_mount_point_data = SherlockLayerService_pb2.MountPointProperties() + + grpc_mount_point_data.ID = self.id + grpc_mount_point_data.type = self.type + grpc_mount_point_data.shape = self.shape + grpc_mount_point_data.units = self.units + grpc_mount_point_data.x = str(self.x) + grpc_mount_point_data.y = str(self.y) + grpc_mount_point_data.length = str(self.length) + grpc_mount_point_data.width = str(self.width) + grpc_mount_point_data.diameter = str(self.diameter) + grpc_mount_point_data.nodes = self.nodes + grpc_mount_point_data.rotation = str(self.rotation) + grpc_mount_point_data.side = self.side + grpc_mount_point_data.height = str(self.height) + grpc_mount_point_data.material = self.material + grpc_mount_point_data.boundary = self.boundary + grpc_mount_point_data.constraints = self.constraints + grpc_mount_point_data.polygon = self.polygon + grpc_mount_point_data.state = self.state + grpc_mount_point_data.chassisMaterial = self.chassis_material + + return grpc_mount_point_data + + @field_validator("type", "shape", "units", "side", "state") + @classmethod + def str_validation(cls, value: str, info: ValidationInfo): + """Validate string fields listed.""" + return basic_str_validator(value, info.field_name) + + +class GetMountPointsPropertiesRequest(BaseModel): + """Return the properties for each mount point given a comma-separated list of mount point ids.""" # noqa: E501 + + project: str + """Name of the project.""" + cca_name: str + """Name of the CCA containing the mount point properties to return.""" + mount_point_ids: Optional[str] = None + """Optional Param: Comma-separated list of mount point ids representing one or more mount + points. If this parameter is not included, then the entire list of mount points + for a given CCA will have their properties returned. + """ + + @field_validator("project", "cca_name", "mount_point_ids") + @classmethod + def str_validation(cls, value: str, info: ValidationInfo): + """Validate string fields listed.""" + return basic_str_validator(value, info.field_name) + + @field_validator("mount_point_ids") + @classmethod + def optional_str_validation(cls, value: Optional[str], info): + """Allow the mount_point_ids to not be set, i.e., None.""" + return optional_str_validator(value, info.field_name) + + def _convert_to_grpc(self) -> SherlockLayerService_pb2.GetMountPointsPropertiesRequest: + request = SherlockLayerService_pb2.GetMountPointsPropertiesRequest() + request.project = self.project + request.ccaName = self.cca_name + if self.mount_point_ids is not None: + request.mountPointIDs = self.mount_point_ids + return request + + +class UpdateMountPointsRequest(BaseModel): + """Contains the properties of a mount point update per project.""" + + project: str + """Name of the Sherlock project.""" + cca_name: str + """Name of the Sherlock CCA.""" + mount_points: list[MountPointProperties] + """List of mount points with their properties to update""" + + @field_validator("project", "cca_name") + @classmethod + def str_validation(cls, value: str, info: ValidationInfo): + """Validate string fields listed.""" + return basic_str_validator(value, info.field_name) + + @field_validator("mount_points") + @classmethod + def list_validation(cls, value: list, info: ValidationInfo): + """Validate that mount_points is not empty.""" + if not value: + raise ValueError(f"{info.field_name} must contain at least one item.") + return value + + def _convert_to_grpc(self) -> SherlockLayerService_pb2.UpdateMountPointsRequest: + request = SherlockLayerService_pb2.UpdateMountPointsRequest() + request.project = self.project + request.ccaName = self.cca_name + if self.mount_points is not None: + for mount_point in self.mount_points: + request.mountPointsProperties.append(mount_point._convert_to_grpc()) + else: + raise ValueError("mount_points is invalid because it is None or empty.") + + return request diff --git a/tests/test_layer.py b/tests/test_layer.py index 56c8dee33..4dab0fbbe 100644 --- a/tests/test_layer.py +++ b/tests/test_layer.py @@ -34,8 +34,10 @@ CopyPottingRegionRequest, DeletePottingRegionRequest, GetICTFixturesPropertiesRequest, + GetMountPointsPropertiesRequest, GetTestPointPropertiesRequest, ICTFixtureProperties, + MountPointProperties, PCBShape, PolygonalShape, PottingRegion, @@ -46,6 +48,7 @@ SlotShape, TestPointProperties, UpdateICTFixturesRequest, + UpdateMountPointsRequest, UpdatePottingRegionRequest, UpdateTestPointsRequest, ) @@ -72,6 +75,8 @@ def test_all(): # Update APIs must be called after properties APIs so all pass helper_test_update_ict_fixtures(layer) + helper_test_update_mount_points(layer) + helper_test_get_mount_point_props(layer) helper_test_update_mount_points_by_file(layer) helper_test_update_potting_region(layer) helper_test_update_test_fixtures_by_file(layer) @@ -2117,6 +2122,396 @@ def helper_test_update_ict_fixtures(layer): assert properties_response.ICTFixtureProperties[1].chassisMaterial == "NYLON" +def helper_test_update_mount_points(layer): + """Test update_mount_points API""" + + project = "Tutorial Project" + cca_name = "Main Board" + + mount_point_1 = MountPointProperties( + id="MP1", + type="Mount Pad", + shape="Rectangular", + units="mm", + x=1.0, + y=-2.0, + length=2.0, + width=1.0, + diameter=2.0, + nodes="4", + rotation=45, + side="BOTTOM", + height=1.0, + material="GOLD", + boundary="Outline", + constraints="X-axis translation|Z-axis translation", + polygon="", + state="DISABLED", + chassis_material="SILVER", + ) + + mount_point_without_id = MountPointProperties( + id="", + type="Standoff", + shape="Circular", + units="mil", + x=100, + y=50, + length=200, + width=200, + diameter=200, + nodes="6", + rotation=0, + side="BOTTOM", + height=10, + material="FERRITE", + boundary="Center", + constraints="Y-axis translation", + polygon="", + state="ENABLED", + chassis_material="NYLON", + ) + + try: + UpdateMountPointsRequest(project="", cca_name=cca_name, mount_points=[mount_point_1]) + pytest.fail("No exception thrown when using missing project") + except pydantic.ValidationError as e: + assert isinstance(e, pydantic.ValidationError) + assert ( + str(e.errors()[0]["msg"]) + == "Value error, project is invalid because it is None or empty." + ) + + try: + UpdateMountPointsRequest(project=project, cca_name="", mount_points=[mount_point_1]) + pytest.fail("No exception thrown when using missing cca_name") + except pydantic.ValidationError as e: + assert isinstance(e, pydantic.ValidationError) + assert ( + str(e.errors()[0]["msg"]) + == "Value error, cca_name is invalid because it is None or empty." + ) + + try: + UpdateMountPointsRequest(project=project, cca_name=cca_name, mount_points=[]) + pytest.fail("No exception thrown when using missing mount_points") + except pydantic.ValidationError as e: + assert isinstance(e, pydantic.ValidationError) + assert ( + str(e.errors()[0]["msg"]) == "Value error, mount_points must contain at least one item." + ) + + try: + layer.update_mount_points( + UpdateMountPointsRequest( + project=project, + cca_name=cca_name, + mount_points=[ + MountPointProperties( + id="MP1", + type="", + shape="Rectangular", + units="mm", + x=1.0, + y=-2.0, + length=2.0, + width=1.0, + diameter=0.0, + nodes="10", + rotation=45, + side="BOTTOM", + height=1.0, + material="GOLD", + boundary="Outline", + constraints="X-axis translation|Z-axis translation", + polygon="", + state="DISABLED", + chassis_material="SILVER", + ) + ], + ) + ) + pytest.fail("No exception thrown when using missing type") + except pydantic.ValidationError as e: + assert isinstance(e, pydantic.ValidationError) + assert ( + str(e.errors()[0]["msg"]) == "Value error, type is invalid because it is None or empty." + ) + + try: + layer.update_mount_points( + UpdateMountPointsRequest( + project=project, + cca_name=cca_name, + mount_points=[ + MountPointProperties( + id="MP1", + type="Mount Pad", + shape="", + units="mm", + x=1.0, + y=-2.0, + length=2.0, + width=1.0, + diameter=0.0, + nodes="10", + rotation=45, + side="BOTTOM", + height=1.0, + material="GOLD", + boundary="Outline", + constraints="X-axis translation|Z-axis translation", + polygon="", + state="DISABLED", + chassis_material="SILVER", + ) + ], + ) + ) + pytest.fail("No exception thrown when using missing shape") + except pydantic.ValidationError as e: + assert isinstance(e, pydantic.ValidationError) + assert ( + str(e.errors()[0]["msg"]) + == "Value error, shape is invalid because it is None or empty." + ) + + try: + layer.update_mount_points( + UpdateMountPointsRequest( + project=project, + cca_name=cca_name, + mount_points=[ + MountPointProperties( + id="MP1", + type="Mount Pad", + shape="Rectangular", + units="", + x=1.0, + y=-2.0, + length=2.0, + width=1.0, + diameter=0.0, + nodes="10", + rotation=45, + side="BOTTOM", + height=1.0, + material="GOLD", + boundary="Outline", + constraints="X-axis translation|Z-axis translation", + polygon="", + state="DISABLED", + chassis_material="SILVER", + ) + ], + ) + ) + pytest.fail("No exception thrown when using missing units") + except pydantic.ValidationError as e: + assert isinstance(e, pydantic.ValidationError) + assert ( + str(e.errors()[0]["msg"]) + == "Value error, units is invalid because it is None or empty." + ) + + try: + layer.update_mount_points( + UpdateMountPointsRequest( + project=project, + cca_name=cca_name, + mount_points=[ + MountPointProperties( + id="MP1", + type="Mount Pad", + shape="Rectangular", + units="mm", + x=1.0, + y=-2.0, + length=2.0, + width=1.0, + diameter=0.0, + nodes="10", + rotation=45, + side="", + height=1.0, + material="GOLD", + boundary="Outline", + constraints="X-axis translation|Z-axis translation", + polygon="", + state="DISABLED", + chassis_material="SILVER", + ) + ], + ) + ) + pytest.fail("No exception thrown when using missing side") + except pydantic.ValidationError as e: + assert isinstance(e, pydantic.ValidationError) + assert ( + str(e.errors()[0]["msg"]) == "Value error, side is invalid because it is None or empty." + ) + + try: + layer.update_mount_points( + UpdateMountPointsRequest( + project=project, + cca_name=cca_name, + mount_points=[ + MountPointProperties( + id="MP1", + type="Mount Pad", + shape="Rectangular", + units="mm", + x=1.0, + y=-2.0, + length=2.0, + width=1.0, + diameter=0.0, + nodes="10", + rotation=45, + side="BOTTOM", + height=1.0, + material="GOLD", + boundary="Outline", + constraints="X-axis translation|Z-axis translation", + polygon="", + state="", + chassis_material="SILVER", + ) + ], + ) + ) + pytest.fail("No exception thrown when using missing state") + except pydantic.ValidationError as e: + assert isinstance(e, pydantic.ValidationError) + assert ( + str(e.errors()[0]["msg"]) + == "Value error, state is invalid because it is None or empty." + ) + + if layer._is_connection_up(): + # Happy Path + try: + successful_response = layer.update_mount_points( + UpdateMountPointsRequest( + project=project, + cca_name=cca_name, + mount_points=[mount_point_1, mount_point_without_id], + ) + ) + assert successful_response.returnCode.value == 0 + except Exception as e: + pytest.fail(f"Exception thrown during successful mount point update: {e}") + + +def helper_test_get_mount_point_props(layer): + """Test get_mount_point_props API""" + + project = "Tutorial Project" + cca_name = "Main Board" + + try: + GetMountPointsPropertiesRequest( + project="", + cca_name=cca_name, + mount_point_ids="MP1", + ) + pytest.fail("No exception raised when using missing project") + except Exception as e: + assert isinstance(e, pydantic.ValidationError) + assert ( + str(e.errors()[0]["msg"]) + == "Value error, project is invalid because it is None or empty." + ) + + try: + GetMountPointsPropertiesRequest( + project=project, + cca_name="", + mount_point_ids="MP1", + ) + pytest.fail("No exception raised when using missing cca_name") + except Exception as e: + assert isinstance(e, pydantic.ValidationError) + assert ( + str(e.errors()[0]["msg"]) + == "Value error, cca_name is invalid because it is None or empty." + ) + + try: + GetMountPointsPropertiesRequest( + project=project, + cca_name=cca_name, + mount_point_ids="", + ) + pytest.fail("No exception raised when using an invalid mount_point_ids parameter") + except Exception as e: + assert isinstance(e, pydantic.ValidationError) + assert ( + str(e.errors()[0]["msg"]) + == "Value error, mount_point_ids is invalid because it is None or empty." + ) + + if layer._is_connection_up(): + # Dependent on helper_test_update_mount_points to set the mount point properties + + properties_request = GetMountPointsPropertiesRequest( + project=project, + cca_name=cca_name, + mount_point_ids="MP0, MP5", + ) + + properties_request = GetMountPointsPropertiesRequest( + project=project, + cca_name=cca_name, + mount_point_ids="MP1, MP5", + ) + + properties_response = layer.get_mount_point_props(properties_request) + + assert properties_response.mountPointsProperties[0].ID == "MP1" + assert properties_response.mountPointsProperties[0].type == "Mount Pad" + assert properties_response.mountPointsProperties[0].shape == "Rectangular" + assert properties_response.mountPointsProperties[0].units == "mm" + assert properties_response.mountPointsProperties[0].x == "1" + assert properties_response.mountPointsProperties[0].y == "-2" + assert properties_response.mountPointsProperties[0].length == "2" + assert properties_response.mountPointsProperties[0].width == "1" + assert properties_response.mountPointsProperties[0].diameter == "2" + assert properties_response.mountPointsProperties[0].nodes == "4" + assert properties_response.mountPointsProperties[0].rotation == "45.0" + assert properties_response.mountPointsProperties[0].side == "BOTTOM" + assert properties_response.mountPointsProperties[0].height == "-1.0" + assert properties_response.mountPointsProperties[0].material == "GOLD" + assert properties_response.mountPointsProperties[0].boundary == "Outline" + assert properties_response.mountPointsProperties[0].constraints == ( + "X-axis translation|" "Z-axis translation" + ) + assert properties_response.mountPointsProperties[0].polygon == "" + assert properties_response.mountPointsProperties[0].state == "DISABLED" + assert properties_response.mountPointsProperties[0].chassisMaterial == "SILVER" + + assert properties_response.mountPointsProperties[1].ID == "MP5" + assert properties_response.mountPointsProperties[1].type == "Standoff" + assert properties_response.mountPointsProperties[1].shape == "Circular" + assert properties_response.mountPointsProperties[1].units == "mil" + assert properties_response.mountPointsProperties[1].x == "100" + assert properties_response.mountPointsProperties[1].y == "50" + assert properties_response.mountPointsProperties[1].length == "200" + assert properties_response.mountPointsProperties[1].width == "200" + assert properties_response.mountPointsProperties[1].diameter == "200" + assert properties_response.mountPointsProperties[1].nodes == "6" + assert properties_response.mountPointsProperties[1].rotation == "0.0" + assert properties_response.mountPointsProperties[1].side == "BOTTOM" + assert properties_response.mountPointsProperties[1].height == "-10.0" + assert properties_response.mountPointsProperties[1].material == "FERRITE" + assert properties_response.mountPointsProperties[1].boundary == "Center" + assert properties_response.mountPointsProperties[1].constraints == "Y-axis translation" + assert properties_response.mountPointsProperties[1].polygon == "" + assert properties_response.mountPointsProperties[1].state == "ENABLED" + assert properties_response.mountPointsProperties[1].chassisMaterial == "NYLON" + + def helper_test_export_layer_image(layer): """Test export_layer_image API"""