diff --git a/pyproject.toml b/pyproject.toml index ac45564fa..27772df4b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ classifiers = [ ] dependencies = [ - "ansys-api-sherlock==0.1.29", + "ansys-api-sherlock==0.1.30", "grpcio>=1.17", "importlib-metadata>=4.0,<5; python_version<='3.8'", "protobuf~=3.20", diff --git a/src/ansys/sherlock/core/errors.py b/src/ansys/sherlock/core/errors.py index 08457e3a4..28e8b1162 100644 --- a/src/ansys/sherlock/core/errors.py +++ b/src/ansys/sherlock/core/errors.py @@ -1183,3 +1183,20 @@ def __init__(self, message): def __str__(self): """Format error message.""" return f"Export FEA model error: {self.message}" + + +class SherlockAddModelingRegionError(Exception): + """Contains the errors raised when modeling regions for a project cannot be added.""" + + def __init__(self, message=None, error_array=None): + """Initialize error message.""" + self.message = message + self.error_array = error_array + + def str_itr(self): + """Format error message.""" + if self.message is None: + return [f"Add modeling region error: {error}" for error in self.error_array] + + assert self.error_array is None + return [f"Add modeling region error: {self.message}"] diff --git a/src/ansys/sherlock/core/layer.py b/src/ansys/sherlock/core/layer.py index a7602eaa4..02d181bef 100644 --- a/src/ansys/sherlock/core/layer.py +++ b/src/ansys/sherlock/core/layer.py @@ -11,13 +11,18 @@ try: 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.SherlockLayerService_pb2 import ModelingRegion + +from typing import Dict, List, Union from ansys.sherlock.core import LOG from ansys.sherlock.core.errors import ( + SherlockAddModelingRegionError, SherlockAddPottingRegionError, SherlockDeleteAllICTFixturesError, SherlockDeleteAllMountPointsError, @@ -889,3 +894,267 @@ def export_all_mount_points( raise e return response.value + + def add_modeling_region( + self, + project: str, + modeling_regions: List[Dict[str, Union[str, float, bool, dict]]], + ): + """ + Add one or more modeling regions to a specific project. + + Parameters + ---------- + project : str + Name of the Sherlock project. + modeling_regions : list of dict + List of modeling regions to add. Each dictionary should contain: + + - cca_name : str + Name of the CCA. + - region_id : str + Unique region ID of the modeling region. + - region_units : str + Units of the modeling region. + - model_mode : str + Mode that specifies how the region is used. Valid values are ``Enabled``, + ``Disabled`` and ``Excluded``. + - shape: PolygonalShape|RectangularShape|SlotShape|CircularShape|PCBShape + The shape of the modeling region. + - pcb_model_props : list + List of the PCB model parameters consisting of these properties: + + - export_model_type : str + The type of model to be generated for a given modeling region. + Valid values are ``Default``, ``Sherlock``, ``Sweep`` and ``None``. + - elem_order: str + The type of 3D elements to be created for the PCB in the modeling region. + Valid values are ``First_Order``, ``Second_Order`` and ``Solid_Shell``. + - max_mesh_size : float + The maximum size of the mesh to be used in the region. + - max_mesh_size_units : str + Units for the maximum mesh size. + - quads_preferred : bool + Whether to generate quad-shaped elements when creating the mesh if true. + - trace_model_props : list + List of the trace model parameters consisting of these properties: + + - trace_model_type : str + The specification of whether trace modeling should be performed + within the region. Valid values are ``Default``, ``Enabled`` and + ``Disabled``. + - elem_order: str, optional + The type of 3D elements to be created for the PCB in the modeling region. + Valid values are ``First_Order``, ``Second_Order`` and ``Solid_Shell``. + - trace_mesh_size : float, optional + The maximum mesh size to be used in the region when trace modeling + is enabled. + - trace_mesh_size_units: str, optional + Units for the maximum mesh size when trace modeling is enabled. + + + Returns + ------- + int + Status code of the response. 0 for success. + + Examples + -------- + >>> from ansys.sherlock.core.launcher import launch_sherlock + >>> sherlock = launch_sherlock() + >>> sherlock.project.import_odb_archive( + "ODB++ Tutorial.tgz", + True, + True, + True, + True, + project="Tutorial Project", + cca_name="Card", + ) + >>> modeling_regions = [ + { + "cca_name": "Card", + "region_id": "Region001", + "region_units": "mm", + "model_mode": "Enabled", + "shape": PolygonalShape(points=[ + (0, 0), + (0, 6.35), + (9.77, 0) + ], rotation=87.8), + "pcb_model_props": { + "export_model_type": "Sherlock", + "elem_order": "First_Order", + "max_mesh_size": 0.5, + "max_mesh_size_units": "mm", + "quads_preferred": True + }, + "trace_model_props": { + "trace_model_type": "Enabled", + "elem_order": "Second_Order", + "trace_mesh_size": 0.3, + "trace_mesh_size_units": "mm" + } + } + ] + >>> result = sherlock.project.add_modeling_region("Tutorial Project", modeling_regions) + """ + try: + if not project: + raise SherlockAddModelingRegionError(message="Project name is invalid.") + + if not modeling_regions: + raise SherlockAddModelingRegionError(message="Modeling regions list is empty.") + + for region in modeling_regions: + if "cca_name" not in region or not region["cca_name"]: + raise SherlockAddModelingRegionError(message="CCA name is invalid.") + if "region_id" not in region or not region["region_id"]: + raise SherlockAddModelingRegionError(message="Region ID is invalid.") + if "region_units" not in region or not region["region_units"]: + raise SherlockAddModelingRegionError(message="Region units are invalid.") + if "shape" not in region: + raise SherlockAddModelingRegionError(message="Shape is missing.") + elif not isinstance( + region["shape"], + ( + PolygonalShape, + RectangularShape, + SlotShape, + CircularShape, + PCBShape, + ), + ): + raise SherlockAddModelingRegionError(message="Shape is not of a valid type.") + + pcb_model_props = region.get("pcb_model_props", {}) + if pcb_model_props: + if ( + "export_model_type" not in pcb_model_props + or pcb_model_props["export_model_type"] == "" + ): + raise SherlockAddModelingRegionError( + message="PCB model export type is invalid." + ) + if "elem_order" not in pcb_model_props or pcb_model_props["elem_order"] == "": + raise SherlockAddModelingRegionError( + message="PCB element order is invalid." + ) + if "max_mesh_size" not in pcb_model_props or not isinstance( + pcb_model_props["max_mesh_size"], float + ): + raise SherlockAddModelingRegionError( + message="PCB max mesh size is invalid." + ) + if "quads_preferred" not in pcb_model_props or not isinstance( + pcb_model_props["quads_preferred"], bool + ): + raise SherlockAddModelingRegionError( + message="PCB quads preferred is invalid." + ) + + trace_model_props = region.get("trace_model_props", {}) + if trace_model_props: + if ( + "trace_model_type" not in trace_model_props + or trace_model_props["trace_model_type"] == "" + ): + raise SherlockAddModelingRegionError(message="Trace model type is invalid.") + + if not self._is_connection_up(): + LOG.error("There is no connection to a gRPC service.") + return + + add_modeling_region_request = SherlockLayerService_pb2.AddModelingRegionRequest() + add_modeling_region_request.project = project + + for region_request in modeling_regions: + modeling_region = add_modeling_region_request.modelingRegions.add() + modeling_region.ccaName = region_request["cca_name"] + modeling_region.regionId = region_request["region_id"] + modeling_region.regionUnits = region_request["region_units"] + modeling_region.modelMode = ModelingRegion.ModelingMode.Value( + region_request["model_mode"] + ) + + shape = region_request["shape"] + if isinstance(shape, PolygonalShape): + polygonal_shape = modeling_region.polygonalShape + for point in shape.points: + polygonal_point = polygonal_shape.points.add() + polygonal_point.x = point[0] + polygonal_point.y = point[1] + polygonal_shape.rotation = shape.rotation + elif isinstance(shape, RectangularShape): + rectangular_shape = modeling_region.rectangularShape + for point in shape.points: + rectangular_point = rectangular_shape.points.add() + rectangular_point.x = point[0] + rectangular_point.y = point[1] + rectangular_shape.rotation = shape.rotation + elif isinstance(shape, SlotShape): + slot_shape = modeling_region.slotShape + for point in shape.points: + slot_point = slot_shape.points.add() + slot_point.x = point[0] + slot_point.y = point[1] + slot_shape.rotation = shape.rotation + elif isinstance(shape, CircularShape): + circular_shape = modeling_region.circularShape + for point in shape.points: + circular_point = circular_shape.points.add() + circular_point.x = point[0] + circular_point.y = point[1] + circular_shape.rotation = shape.rotation + else: + raise SherlockAddModelingRegionError( + message="Shape is not of a valid type." + ) + + ExportModelType = ModelingRegion.PCBModelingProperties.ExportModelType + pcb_model_props = region_request.get("pcb_model_props", {}) + modeling_region.pcbModelProps.exportModelType = getattr( + ExportModelType, + pcb_model_props["export_model_type"], + ) + modeling_region.pcbModelProps.elemOrder = getattr( + ModelingRegion.ElementOrder, + pcb_model_props["elem_order"], + ) + modeling_region.pcbModelProps.maxMeshSize = pcb_model_props["max_mesh_size"] + modeling_region.pcbModelProps.maxMeshSizeUnits = pcb_model_props[ + "max_mesh_size_units" + ] + modeling_region.pcbModelProps.quadsPreferred = pcb_model_props[ + "quads_preferred" + ] + + TraceModelingType = ModelingRegion.TraceModelingProperties.TraceModelingType + trace_model_props = region_request.get("trace_model_props", {}) + modeling_region.traceModelProps.traceModelType = getattr( + TraceModelingType, + trace_model_props["trace_model_type"], + ) + if "elem_order" in trace_model_props: + modeling_region.traceModelProps.elemOrder = getattr( + ModelingRegion.ElementOrder, + trace_model_props["elem_order"], + ) + if "trace_mesh_size" in trace_model_props: + modeling_region.traceModelProps.traceMeshSize = trace_model_props[ + "trace_mesh_size" + ] + if "trace_mesh_size_units" in trace_model_props: + modeling_region.traceModelProps.traceMeshSizeUnits = trace_model_props[ + "trace_mesh_size_units" + ] + + return_code = self.stub.addModelingRegion(add_modeling_region_request) + if return_code.value != 0: + raise SherlockAddModelingRegionError(message=return_code.message) + + return return_code.value + + except SherlockAddModelingRegionError as e: + LOG.error(str(e)) + raise e diff --git a/tests/test_layer.py b/tests/test_layer.py index ee0504277..d3e1b1f9c 100644 --- a/tests/test_layer.py +++ b/tests/test_layer.py @@ -1,4 +1,5 @@ # © 2023-2024 ANSYS, Inc. All rights reserved +import copy import os import platform import uuid @@ -7,6 +8,7 @@ import pytest from ansys.sherlock.core.errors import ( + SherlockAddModelingRegionError, SherlockAddPottingRegionError, SherlockDeleteAllICTFixturesError, SherlockDeleteAllMountPointsError, @@ -38,6 +40,7 @@ def test_all(): helper_test_export_all_mount_points(layer) helper_test_export_all_test_fixtures(layer) helper_test_export_all_test_points(layer) + helper_test_add_modeling_region(layer) def helper_test_add_potting_region(layer): @@ -718,5 +721,153 @@ def helper_test_export_all_mount_points(layer): assert type(e) == SherlockExportAllMountPoints +def helper_test_add_modeling_region(layer): + modeling_region_example = [ + { + "cca_name": "Card", + "region_id": "Region001", + "region_units": "mm", + "model_mode": "Enabled", + "shape": PolygonalShape(points=[(0, 0), (0, 6.35), (9.77, 0)], rotation=87.8), + "pcb_model_props": { + "export_model_type": "Sherlock", + "elem_order": "First_Order", + "max_mesh_size": 0.5, + "max_mesh_size_units": "mm", + "quads_preferred": True, + }, + "trace_model_props": { + "trace_model_type": "Enabled", + "elem_order": "Second_Order", + "trace_mesh_size": 0.3, + "trace_mesh_size_units": "mm", + }, + } + ] + + # Invalid project name + try: + layer.add_modeling_region("", modeling_region_example) + pytest.fail("No exception raised for invalid project name") + except SherlockAddModelingRegionError as e: + assert str(e.str_itr()) == "['Add modeling region error: Project name is invalid.']" + + # Empty modeling regions list + try: + layer.add_modeling_region("Tutorial Project", []) + pytest.fail("No exception raised for empty modeling regions list") + except SherlockAddModelingRegionError as e: + assert str(e.str_itr()) == "['Add modeling region error: Modeling regions list is empty.']" + + # Invalid CCA name + invalid_region = copy.deepcopy(modeling_region_example) + invalid_region[0]["cca_name"] = "" + try: + layer.add_modeling_region("Tutorial Project", invalid_region) + pytest.fail("No exception raised for invalid CCA name") + except SherlockAddModelingRegionError as e: + assert str(e.str_itr()) == "['Add modeling region error: CCA name is invalid.']" + + # Invalid region ID + invalid_region = copy.deepcopy(modeling_region_example) + invalid_region[0]["region_id"] = "" + try: + layer.add_modeling_region("Tutorial Project", invalid_region) + pytest.fail("No exception raised for invalid region ID") + except SherlockAddModelingRegionError as e: + assert str(e.str_itr()) == "['Add modeling region error: Region ID is invalid.']" + + # Invalid region units + invalid_region = copy.deepcopy(modeling_region_example) + invalid_region[0]["region_units"] = "" + try: + layer.add_modeling_region("Tutorial Project", invalid_region) + pytest.fail("No exception raised for invalid region units") + except SherlockAddModelingRegionError as e: + assert str(e.str_itr()) == "['Add modeling region error: Region units are invalid.']" + + # Missing shape + invalid_region = copy.deepcopy(modeling_region_example) + invalid_region[0].pop("shape") + try: + layer.add_modeling_region("Tutorial Project", invalid_region) + pytest.fail("No exception raised for missing shape") + except SherlockAddModelingRegionError as e: + assert str(e.str_itr()) == "['Add modeling region error: Shape is missing.']" + + # Invalid shape type + invalid_region = copy.deepcopy(modeling_region_example) + invalid_region[0]["shape"] = "InvalidShapeType" + try: + layer.add_modeling_region("Tutorial Project", invalid_region) + pytest.fail("No exception raised for invalid shape type") + except SherlockAddModelingRegionError as e: + assert str(e.str_itr()) == "['Add modeling region error: Shape is not of a valid type.']" + + # Invalid PCB model export type + invalid_region = copy.deepcopy(modeling_region_example) + invalid_region[0]["pcb_model_props"]["export_model_type"] = "" + try: + layer.add_modeling_region("Tutorial Project", invalid_region) + pytest.fail("No exception raised for invalid PCB model export type") + except SherlockAddModelingRegionError as e: + assert str(e.str_itr()) == ( + "['Add modeling region error: PCB model export type is invalid.']" + ) + + # Invalid PCB element order + invalid_region = copy.deepcopy(modeling_region_example) + invalid_region[0]["pcb_model_props"]["elem_order"] = "" + try: + layer.add_modeling_region("Tutorial Project", invalid_region) + pytest.fail("No exception raised for invalid PCB element order") + except SherlockAddModelingRegionError as e: + assert str(e.str_itr()) == "['Add modeling region error: PCB element order is invalid.']" + + # Invalid PCB max mesh size + invalid_region = copy.deepcopy(modeling_region_example) + invalid_region[0]["pcb_model_props"]["max_mesh_size"] = "not_a_float" + try: + layer.add_modeling_region("Tutorial Project", invalid_region) + pytest.fail("No exception raised for invalid PCB max mesh size") + except SherlockAddModelingRegionError as e: + assert str(e.str_itr()) == "['Add modeling region error: PCB max mesh size is invalid.']" + + # Invalid PCB quads preferred + invalid_region = copy.deepcopy(modeling_region_example) + invalid_region[0]["pcb_model_props"]["quads_preferred"] = "not_a_bool" + try: + layer.add_modeling_region("Tutorial Project", invalid_region) + pytest.fail("No exception raised for invalid PCB quads preferred") + except SherlockAddModelingRegionError as e: + assert str(e.str_itr()) == "['Add modeling region error: PCB quads preferred is invalid.']" + + # Invalid trace model type + invalid_region = copy.deepcopy(modeling_region_example) + invalid_region[0]["trace_model_props"]["trace_model_type"] = "" + try: + layer.add_modeling_region("Tutorial Project", invalid_region) + pytest.fail("No exception raised for invalid trace model type") + except SherlockAddModelingRegionError as e: + assert str(e.str_itr()) == "['Add modeling region error: Trace model type is invalid.']" + + if layer._is_connection_up(): + # Unhappy project name + try: + layer.add_modeling_region("Invalid Project", modeling_region_example) + pytest.fail("No exception raised for invalid project name") + except Exception as e: + assert type(e) == SherlockAddModelingRegionError + + # Valid request + try: + valid_region = copy.deepcopy(modeling_region_example) + valid_region[0]["cca_name"] = "Main Board" + result = layer.add_modeling_region("Tutorial Project", valid_region) + assert result == 0 + except SherlockAddModelingRegionError as e: + pytest.fail(str(e)) + + if __name__ == "__main__": test_all()