diff --git a/doc/changelog.d/428.documentation.md b/doc/changelog.d/428.documentation.md new file mode 100644 index 00000000..3d332772 --- /dev/null +++ b/doc/changelog.d/428.documentation.md @@ -0,0 +1 @@ +Update unit test for HV strain map analysis. \ No newline at end of file diff --git a/doc/changelog.d/435.documentation.md b/doc/changelog.d/435.documentation.md new file mode 100644 index 00000000..b468b0a2 --- /dev/null +++ b/doc/changelog.d/435.documentation.md @@ -0,0 +1 @@ +Add update potting region \ No newline at end of file diff --git a/src/ansys/sherlock/core/layer.py b/src/ansys/sherlock/core/layer.py index 03b92740..9e4987fc 100644 --- a/src/ansys/sherlock/core/layer.py +++ b/src/ansys/sherlock/core/layer.py @@ -5,17 +5,20 @@ CircularShape, PCBShape, PolygonalShape, + PottingRegionUpdateData, RectangularShape, SlotShape, ) try: + import SherlockCommonService_pb2 import SherlockLayerService_pb2 from SherlockLayerService_pb2 import ModelingRegion import SherlockLayerService_pb2_grpc except ModuleNotFoundError: from ansys.api.sherlock.v0 import SherlockLayerService_pb2 from ansys.api.sherlock.v0 import SherlockLayerService_pb2_grpc + from ansys.api.sherlock.v0 import SherlockCommonService_pb2 from ansys.api.sherlock.v0.SherlockLayerService_pb2 import ModelingRegion from typing import Dict, List, Union @@ -235,6 +238,81 @@ def add_potting_region( LOG.error(str(e)) raise e + def update_potting_region( + self, project: str, update_potting_region_requests: list[PottingRegionUpdateData] + ) -> list[SherlockCommonService_pb2.ReturnCode]: + """Update one or more potting regions in a specific project. + + Parameters: + ---------- + project: str + Name of the Sherlock project. + update_potting_region_requests: list[PottingRegionUpdateData] + List of data used to update potting regions. + + Returns + ------- + list[SherlockCommonService_pb2.ReturnCode] + Return codes for each request + + Examples + -------- + >>> from ansys.sherlock.core.launcher import launch_sherlock + >>> from ansys.sherlock.core.types.layer_types import PolygonalShape + >>> from ansys.sherlock.core.types.layer_types import PottingRegionUpdateData + >>> from ansys.sherlock.core.types.layer_types import PottingRegionData + >>> sherlock = launch_sherlock() + >>> + >>> update_request1 = PottingRegionUpdateData( + potting_region_id_to_update=potting_id, + potting_region=PottingRegionData( + cca_name=cca_name, + potting_id=potting_id, + potting_side=potting_side, + potting_material=potting_material, + potting_units=potting_units, + potting_thickness=potting_thickness, + potting_standoff=potting_standoff, + shape=PolygonalShape( + points=[(0, 1), (5, 1), (5, 5), (1, 5)], + rotation=45.0 + ) + ) + ) + >>> update_request2 = PottingRegionUpdateData( + potting_region_id_to_update=potting_id, + potting_region=PottingRegionData( + cca_name=cca_name, + potting_id=potting_id, + potting_side=potting_side, + potting_material=potting_material, + potting_units=potting_units, + potting_thickness=potting_thickness, + potting_standoff=potting_standoff, + shape=PolygonalShape( + points=[(0, 1), (5, 1), (5, 5), (1, 5)], + rotation=0.0 + ) + ) + ) + >>> potting_region_requests = [ + update_request1, + update_request2 + ] + >>> responses = sherlock.layer.update_potting_region(project, potting_region_requests) + """ + update_request = SherlockLayerService_pb2.UpdatePottingRegionRequest() + update_request.project = project + + for update_potting_region_request in update_potting_region_requests: + update_request.updatePottingRegions.append( + update_potting_region_request._convert_to_grpc() + ) + responses = [] + for grpc_return_code in self.stub.updatePottingRegion(update_request): + responses.append(grpc_return_code) + return responses + def update_mount_points_by_file( self, project, diff --git a/src/ansys/sherlock/core/types/common_types.py b/src/ansys/sherlock/core/types/common_types.py index 20ead8c4..2bea1b6b 100644 --- a/src/ansys/sherlock/core/types/common_types.py +++ b/src/ansys/sherlock/core/types/common_types.py @@ -8,6 +8,13 @@ from ansys.api.sherlock.v0 import SherlockCommonService_pb2 +def basic_str_validator(value: str, field_name: str): + """Apply basic string validation.""" + if value is None or value == "": + raise ValueError(field_name + " is invalid because it is None or empty.") + return value + + class ListUnitsRequestUnitType: """Constants for Unit Type in the List Units request.""" diff --git a/src/ansys/sherlock/core/types/layer_types.py b/src/ansys/sherlock/core/types/layer_types.py index 8468f441..ffb1d767 100644 --- a/src/ansys/sherlock/core/types/layer_types.py +++ b/src/ansys/sherlock/core/types/layer_types.py @@ -1,71 +1,190 @@ # © 2023 ANSYS, Inc. All rights reserved. - """Module containing types for the Layer Service.""" -class PolygonalShape: +from typing import Union + +from ansys.api.sherlock.v0 import SherlockLayerService_pb2 +from pydantic import BaseModel, ValidationInfo, field_validator + +from ansys.sherlock.core.types.common_types import basic_str_validator + + +class PolygonalShape(BaseModel): """Contains the properties for a polygonal shape.""" - def __init__(self, points, rotation): - """Initialize the shape properties.""" - self.points = points - """points (length two tuples of the form (x, y)) : list[tuple[float, float]]""" - self.rotation = rotation - """rotation (in degrees) : float""" + points: list[tuple[float, float]] + """points (length two tuples of the form (x, y)) : list[tuple[float, float]]""" + rotation: float + """rotation (in degrees) : float""" + def _convert_to_grpc(self) -> SherlockLayerService_pb2.PolygonalShape: + grpc_polygonal_shape = SherlockLayerService_pb2.PolygonalShape() + for x, y in self.points: + p = grpc_polygonal_shape.points.add() + p.x = x + p.y = y + grpc_polygonal_shape.rotation = self.rotation + return grpc_polygonal_shape -class RectangularShape: + +class RectangularShape(BaseModel): """Contains the properties for a rectangular shape.""" - def __init__(self, length, width, center_x, center_y, rotation): - """Initialize the shape properties.""" - self.length = length - """length : float""" - self.width = width - """width : float""" - self.center_x = center_x - """x coordinate of center : float""" - self.center_y = center_y - """y coordinate of center : float""" - self.rotation = rotation - """rotation (in degrees) : float""" - - -class SlotShape: + length: float + """length : float""" + width: float + """width : float""" + center_x: float + """x coordinate of center : float""" + center_y: float + """y coordinate of center : float""" + rotation: float + """rotation (in degrees) : float""" + + def _convert_to_grpc(self) -> SherlockLayerService_pb2.RectangularShape: + grpc_rectangular_shape = SherlockLayerService_pb2.RectangularShape() + grpc_rectangular_shape.length = self.length + grpc_rectangular_shape.width = self.width + grpc_rectangular_shape.centerX = self.center_x + grpc_rectangular_shape.centerY = self.center_y + grpc_rectangular_shape.rotation = self.rotation + return grpc_rectangular_shape + + +class SlotShape(BaseModel): """Contains the properties for a slot shape.""" - def __init__(self, length, width, node_count, center_x, center_y, rotation): - """Initialize the shape properties.""" - self.length = length - """length : float""" - self.width = width - """width : float""" - self.node_count = node_count - """node count : int""" - self.center_x = center_x - """x coordinate of center : float""" - self.center_y = center_y - """y coordinate of center : float""" - self.rotation = rotation - """rotation (in degrees) : float""" - - -class CircularShape: + length: float + """length : float""" + width: float + """width : float""" + node_count: int + """node count : int""" + center_x: float + """x coordinate of center : float""" + center_y: float + """y coordinate of center : float""" + rotation: float + """rotation (in degrees) : float""" + + def _convert_to_grpc(self) -> SherlockLayerService_pb2.SlotShape: + grpc_slot_shape = SherlockLayerService_pb2.SlotShape + + grpc_slot_shape.length = self.length + grpc_slot_shape.width = self.width + grpc_slot_shape.nodeCount = self.node_count + grpc_slot_shape.centerX = self.center_x + grpc_slot_shape.centerY = self.center_y + grpc_slot_shape.rotation = self.rotation + + return grpc_slot_shape + + +class CircularShape(BaseModel): """Contains the properties for a circular shape.""" - def __init__(self, diameter, node_count, center_x, center_y, rotation): - """Initialize the shape properties.""" - self.diameter = diameter - """diameter : float""" - self.node_count = node_count - """node count : int""" - self.center_x = center_x - """x coordinate of center : float""" - self.center_y = center_y - """y coordinate of center : float""" - self.rotation = rotation - """rotation (in degrees) : float""" - - -class PCBShape: + diameter: float + """diameter : float""" + node_count: int + """node count : int""" + center_x: float + """x coordinate of center : float""" + center_y: float + """y coordinate of center : float""" + rotation: float + """rotation (in degrees) : float""" + + def _convert_to_grpc(self) -> SherlockLayerService_pb2.CircularShape: + grpc_circular_shape = SherlockLayerService_pb2.CircularShape() + grpc_circular_shape.diameter = self.diameter + grpc_circular_shape.nodeCount = self.node_count + grpc_circular_shape.centerX = self.center_x + grpc_circular_shape.centerY = self.center_y + grpc_circular_shape.rotation = self.rotation + return grpc_circular_shape + + +class PCBShape(BaseModel): """Contains the properties for a PCB shape.""" + + def _convert_to_grpc(self) -> SherlockLayerService_pb2.PCBShape: + return SherlockLayerService_pb2.PCBShape() + + +class PottingRegionData(BaseModel): + """Contains the properties of a Potting Region add or update request.""" + + cca_name: str + """The name of the CCA.""" + potting_id: str + """The potting ID.""" + potting_side: str + """The potting side, options are "TOP", "BOT", or "BOTTOM".""" + potting_material: str + """The potting material.""" + potting_units: str + """The units to use for the potting region.""" + potting_thickness: float + """The potting thickness.""" + potting_standoff: float + """The potting standoff.""" + shape: Union[CircularShape, PCBShape, PolygonalShape, RectangularShape, SlotShape] + """The shape of the potting region.""" + + def _convert_to_grpc(self) -> SherlockLayerService_pb2.PottingRegion: + grpc_potting_region_data = SherlockLayerService_pb2.PottingRegion() + + grpc_potting_region_data.ccaName = self.cca_name + grpc_potting_region_data.pottingID = self.potting_id + grpc_potting_region_data.pottingSide = self.potting_side + grpc_potting_region_data.pottingMaterial = self.potting_material + grpc_potting_region_data.pottingUnits = self.potting_units + grpc_potting_region_data.pottingThickness = self.potting_thickness + grpc_potting_region_data.pottingStandoff = self.potting_standoff + + if isinstance(self.shape, CircularShape): + grpc_potting_region_data.circularShape.CopyFrom(self.shape._convert_to_grpc()) + elif isinstance(self.shape, PCBShape): + grpc_potting_region_data.pcbShape.CopyFrom(self.shape._convert_to_grpc()) + elif isinstance(self.shape, PolygonalShape): + grpc_potting_region_data.polygonalShape.CopyFrom(self.shape._convert_to_grpc()) + elif isinstance(self.shape, RectangularShape): + grpc_potting_region_data.rectangularShape.CopyFrom(self.shape._convert_to_grpc()) + elif isinstance(self.shape, SlotShape): + grpc_potting_region_data.slotShape.CopyFrom(self.shape._convert_to_grpc()) + else: + raise ValueError("Unsupported shape given '" + type(self.shape).__name__ + "'") + + return grpc_potting_region_data + + @field_validator("cca_name", "potting_id", "potting_side", "potting_material", "potting_units") + @classmethod + def str_validation(cls, value: str, info: ValidationInfo): + """Validate string fields listed.""" + return basic_str_validator(value, info.field_name) + + +class PottingRegionUpdateData(BaseModel): + """Contains the properties of a potting region update request.""" + + potting_region_id_to_update: str + potting_region: PottingRegionData + + def _convert_to_grpc( + self, + ) -> SherlockLayerService_pb2.UpdatePottingRegionRequest.PottingRegionUpdateData: + + grpc_potting_update_data = ( + SherlockLayerService_pb2.UpdatePottingRegionRequest.PottingRegionUpdateData() + ) + + grpc_potting_update_data.pottingRegionIDToUpdate = self.potting_region_id_to_update + grpc_potting_update_data.pottingRegion.CopyFrom(self.potting_region._convert_to_grpc()) + return grpc_potting_update_data + + @field_validator("potting_region_id_to_update") + @classmethod + def str_validation(cls, value: str, info: ValidationInfo): + """Validate string fields listed.""" + return basic_str_validator(value, info.field_name) diff --git a/tests/test_analysis.py b/tests/test_analysis.py index 25a54be0..13ed0fff 100644 --- a/tests/test_analysis.py +++ b/tests/test_analysis.py @@ -178,7 +178,7 @@ def helper_test_run_strain_map_analysis(analysis): "Main Board", [ [ - analysis_type_enum.RandomVibe, + analysis_type_enum.HarmonicVibe, [ ["Phase 1", "Harmonic Vibe", "TOP", "MainBoardStrain - Top"], ["Phase 1", "Harmonic Vibe", "BOTTOM", "MainBoardStrain - Bottom"], diff --git a/tests/test_layer.py b/tests/test_layer.py index 57290a0f..79dba31d 100644 --- a/tests/test_layer.py +++ b/tests/test_layer.py @@ -28,6 +28,8 @@ CircularShape, PCBShape, PolygonalShape, + PottingRegionData, + PottingRegionUpdateData, RectangularShape, SlotShape, ) @@ -44,6 +46,7 @@ def test_all(): helper_test_delete_all_mount_points(layer) helper_test_delete_all_test_points(layer) helper_test_add_potting_region(layer) + helper_test_update_potting_region(layer) helper_test_update_test_fixtures_by_file(layer) helper_test_update_test_points_by_file(layer) helper_test_export_all_mount_points(layer) @@ -179,71 +182,6 @@ def helper_test_add_potting_region(layer): except SherlockAddPottingRegionError as e: assert str(e) == "Add potting region error: Shape invalid for potting region 0." - try: - shape = PolygonalShape(points="INVALID", rotation=123.4) - layer.add_potting_region( - "Test", - [ - { - "cca_name": "Test Card", - "potting_id": "Test Region", - "side": "TOP", - "material": "epoxyencapsulant", - "potting_units": "in", - "thickness": 0.1, - "standoff": 0.2, - "shape": shape, - }, - ], - ) - pytest.fail("No exception thrown when polygonal points list is invalid.") - except SherlockAddPottingRegionError as e: - assert str(e) == "Add potting region error: Invalid points argument for potting region 0." - - try: - invalid_point = "INVALID" - shape = PolygonalShape(points=[(1, 2), (4.4, 5.5), invalid_point], rotation=123.4) - layer.add_potting_region( - "Test", - [ - { - "cca_name": "Test Card", - "potting_id": "Test Region", - "side": "TOP", - "material": "epoxyencapsulant", - "potting_units": "in", - "thickness": 0.1, - "standoff": 0.2, - "shape": shape, - }, - ], - ) - pytest.fail("No exception thrown when polygonal points list element is incorrect type.") - except SherlockAddPottingRegionError as e: - assert str(e) == "Add potting region error: Point 2 invalid for potting region 0." - - try: - invalid_point = (4.4, 5.5, 10) - shape = PolygonalShape(points=[(1, 2), invalid_point, (1, 6)], rotation=123.4) - layer.add_potting_region( - "Test", - [ - { - "cca_name": "Test Card", - "potting_id": "Test Region", - "side": "TOP", - "material": "epoxyencapsulant", - "potting_units": "in", - "thickness": 0.1, - "standoff": 0.2, - "shape": shape, - }, - ], - ) - pytest.fail("No exception thrown when polygonal points list element is incorrect length.") - except SherlockAddPottingRegionError as e: - assert str(e) == "Add potting region error: Point 1 invalid for potting region 0." - if not layer._is_connection_up(): return @@ -292,6 +230,73 @@ def helper_test_add_potting_region(layer): pytest.fail(str(e)) +def helper_test_update_potting_region(layer): + """Test update potting region API.""" + + if layer._is_connection_up(): + project = "Tutorial Project" + # Add Potting region to update + potting_id = f"Test Region {uuid.uuid4()}" + cca_name = "Main Board" + potting_side = "TOP" + potting_material = "epoxyencapsulant" + potting_units = "mm" + potting_thickness = 0.1 + potting_standoff = 0.2 + potting_shape = PolygonalShape(points=[(1, 2), (4.4, 5.5), (1, 6)], rotation=0.0) + + layer.add_potting_region( + project, + [ + { + "cca_name": cca_name, + "potting_id": potting_id, + "side": potting_side, + "material": potting_material, + "potting_units": potting_units, + "thickness": potting_thickness, + "standoff": potting_standoff, + "shape": potting_shape, + }, + ], + ) + + # Update potting region that was added above + + update_request1 = PottingRegionUpdateData( + potting_region_id_to_update=potting_id, + potting_region=PottingRegionData( + cca_name=cca_name, + potting_id=potting_id, + potting_side=potting_side, + potting_material=potting_material, + potting_units=potting_units, + potting_thickness=potting_thickness, + potting_standoff=potting_standoff, + shape=PolygonalShape(points=[(0, 1), (5, 1), (5, 5), (1, 5)], rotation=45.0), + ), + ) + update_request2 = PottingRegionUpdateData( + potting_region_id_to_update=potting_id, + potting_region=PottingRegionData( + cca_name=cca_name, + potting_id=potting_id, + potting_side=potting_side, + potting_material=potting_material, + potting_units=potting_units, + potting_thickness=potting_thickness, + potting_standoff=potting_standoff, + shape=PolygonalShape(points=[(0, 1), (5, 1), (5, 5), (1, 5)], rotation=0.0), + ), + ) + potting_region_requests = [update_request1, update_request2] + responses = layer.update_potting_region(project, potting_region_requests) + + for return_code in responses: + assert return_code.value == 0 + assert return_code.message == "" + + def helper_test_update_mount_points_by_file(layer): """Test update_mount_points_by_file API.""" try: