diff --git a/_unittest/conftest.py b/_unittest/conftest.py index 2c8ec686ba0..d3db08bf178 100644 --- a/_unittest/conftest.py +++ b/_unittest/conftest.py @@ -90,7 +90,7 @@ def my_teardown(self): oDesktop = sys.modules["__main__"].oDesktop except Exception as e: oDesktop = None - if oDesktop: + if oDesktop and not settings.non_graphical: oDesktop.ClearMessages("", "", 3) for edbapp in self.edbapps[::-1]: try: diff --git a/_unittest/example_models/Galileo_edb.aedb/edb.def b/_unittest/example_models/Galileo_edb.aedb/edb.def index 9c9156ae700..718ac2c803f 100644 Binary files a/_unittest/example_models/Galileo_edb.aedb/edb.def and b/_unittest/example_models/Galileo_edb.aedb/edb.def differ diff --git a/_unittest/test_00_EDB.py b/_unittest/test_00_EDB.py index 1709d116600..985881e9f86 100644 --- a/_unittest/test_00_EDB.py +++ b/_unittest/test_00_EDB.py @@ -15,7 +15,8 @@ try: import pytest -except ImportError: + import unittest.mock +except ImportError: # pragma: no cover import _unittest_ironpython.conf_unittest as pytest @@ -44,6 +45,17 @@ def test_00_export_ipc2581(self): self.edbapp.export_to_ipc2581(ipc_path, "mm") assert os.path.exists(ipc_path) + if not is_ironpython: + # Test the export_to_ipc2581 method when IPC8521.ExportIPC2581FromLayout raises an exception internally. + with unittest.mock.patch("pyaedt.Edb.edblib", new_callable=unittest.mock.PropertyMock) as edblib_mock: + Edb.edblib.IPC8521 = unittest.mock.Mock() + Edb.edblib.IPC8521.IPCExporter = unittest.mock.Mock() + Edb.edblib.IPC8521.IPCExporter.ExportIPC2581FromLayout = unittest.mock.Mock( + side_effect=Exception("Exception for testing raised in ExportIPC2581FromLayout.") + ) + + assert not self.edbapp.export_to_ipc2581(os.path.exists(ipc_path)) + def test_01_find_by_name(self): comp = self.edbapp.core_components.get_component_by_name("J1") assert comp is not None @@ -475,21 +487,17 @@ def test_55b_create_cutout(self): assert self.edbapp.create_cutout(["A0_N", "A0_P"], ["GND"], output_aedb_path=output, open_cutout_at_end=False) assert os.path.exists(os.path.join(output, "edb.def")) bounding = self.edbapp.get_bounding_box() - + cutout_line_x = 41 + cutout_line_y = 30 points = [[bounding[0][0], bounding[0][1]]] - points.append([bounding[0][0], bounding[0][1] + (bounding[1][1] - bounding[0][1]) / 10]) - points.append( - [ - bounding[0][0] + (bounding[0][1] - bounding[0][0]) / 10, - bounding[0][1] + (bounding[1][1] - bounding[0][1]) / 10, - ] - ) - points.append([bounding[0][0] + (bounding[0][1] - bounding[0][0]) / 10, bounding[0][1]]) + points.append([cutout_line_x, bounding[0][1]]) + points.append([cutout_line_x, cutout_line_y]) + points.append([bounding[0][0], cutout_line_y]) points.append([bounding[0][0], bounding[0][1]]) output = os.path.join(self.local_scratch.path, "cutout2.aedb") assert self.edbapp.create_cutout_on_point_list( - points, nets_to_include=["GND"], output_aedb_path=output, open_cutout_at_end=False + points, nets_to_include=["GND", "V3P3_S0"], output_aedb_path=output, open_cutout_at_end=False ) assert os.path.exists(os.path.join(output, "edb.def")) @@ -708,7 +716,7 @@ def test_79_get_placement_vector(self): hosting_component_pin2="A4", ) assert result - assert abs(rotation - math.pi / 2) < 1e-9 + assert abs(abs(rotation) - math.pi / 2) < 1e-9 assert solder_ball_height == 0.00033 assert len(vector) == 2 result, vector, rotation, solder_ball_height = self.edbapp.core_components.get_component_placement_vector( @@ -716,8 +724,9 @@ def test_79_get_placement_vector(self): hosting_component=hosting_cmp, mounted_component_pin1="A10", mounted_component_pin2="A12", - hosting_component_pin1="A4", - hosting_component_pin2="A2", + hosting_component_pin1="A2", + hosting_component_pin2="A4", + flipped=True, ) assert result assert abs(rotation + math.pi / 2) < 1e-9 diff --git a/_unittest/test_00_downloads.py b/_unittest/test_00_downloads.py index 6c7695ae520..8bac051ec15 100644 --- a/_unittest/test_00_downloads.py +++ b/_unittest/test_00_downloads.py @@ -32,3 +32,4 @@ def test_download_antenna_sherlock(self): def test_download_wfp(self): assert self.examples.download_edb_merge_utility() + assert self.examples.download_edb_merge_utility(True) diff --git a/_unittest/test_08_Primitives3D.py b/_unittest/test_08_Primitives3D.py index c51806e11f9..f52a635d295 100644 --- a/_unittest/test_08_Primitives3D.py +++ b/_unittest/test_08_Primitives3D.py @@ -1156,3 +1156,61 @@ def test_76_check_value_type(self): assert boolean2 assert isinstance(resolve3, float) assert not boolean3 + + @pyaedt_unittest_check_desktop_error + def test_77_create_helix(self): + + udp1 = [0, 0, 0] + udp2 = [5, 0, 0] + udp3 = [10, 5, 0] + udp4 = [15, 3, 0] + polyline = self.aedtapp.modeler.create_polyline( + [udp1, udp2, udp3, udp4], cover_surface=False, name="helix_polyline" + ) + + helix_right_turn = self.aedtapp.modeler.create_helix( + polyline_name="helix_polyline", + position=[0, 0, 0], + x_start_dir=0, + y_start_dir=1.0, + z_start_dir=1.0, + num_thread=1, + right_hand=True, + radius_increment=0.0, + thread=1.0, + ) + + assert helix_right_turn.object_units == "mm" + + # Test left turn without providing argument value for default parameters. + udp1 = [-45, 0, 0] + udp2 = [-50, 0, 0] + udp3 = [-105, 5, 0] + udp4 = [-110, 3, 0] + polyline_left = self.aedtapp.modeler.create_polyline( + [udp1, udp2, udp3, udp4], cover_surface=False, name="helix_polyline_left" + ) + + assert self.aedtapp.modeler.create_helix( + polyline_name="helix_polyline_left", + position=[0, 0, 0], + x_start_dir=1.0, + y_start_dir=1.0, + z_start_dir=1.0, + right_hand=False, + ) + + # Test that an exception is raised if the name of the polyline is not provided. + # We can't use with.pytest.raises pattern bellow because IronPython does not support pytest. + try: + self.aedtapp.modeler.create_helix( + polyline_name="", + position=[0, 0, 0], + x_start_dir=1.0, + y_start_dir=1.0, + z_start_dir=1.0, + ) + except ValueError as exc_info: + assert "The name of the polyline cannot be an empty string." in str(exc_info.args[0]) + else: + assert False diff --git a/examples/00-EDB/07_WPF_Merge_Utility.py b/examples/00-EDB/07_WPF_Merge_Utility.py index a8aa8e7c4be..16f1ce82ce0 100644 --- a/examples/00-EDB/07_WPF_Merge_Utility.py +++ b/examples/00-EDB/07_WPF_Merge_Utility.py @@ -15,8 +15,8 @@ from pyaedt.examples.downloads import download_edb_merge_utility -python_file = download_edb_merge_utility() - +python_file = download_edb_merge_utility(force_download=True) +desktop_version = "2022.1" ###################################################################### # Python Script execution @@ -35,3 +35,7 @@ # # The json file contains default settings that can be used in any other project to automatically # load all settings. +# The following command line can be unchecked and launched from python interpreter. + +# from pyaedt.generic.toolkit import launch +# launch(python_file, specified_version=desktop_version, new_desktop_session=False, autosave=False) diff --git a/pyaedt/desktop.py b/pyaedt/desktop.py index 1f97dcda1b5..68663e99830 100644 --- a/pyaedt/desktop.py +++ b/pyaedt/desktop.py @@ -284,21 +284,24 @@ def __init__( self._main.pyaedt_version = pyaedtversion self._main.interpreter_ver = _pythonver self._main.student_version = student_version + settings.non_graphical = non_graphical if is_ironpython: self._main.isoutsideDesktop = False else: self._main.isoutsideDesktop = True self.release_on_exit = True self.logfile = None - if "oDesktop" in dir(): + if "oDesktop" in dir(): # pragma: no cover self.release_on_exit = False self._main.oDesktop = oDesktop - elif "oDesktop" in dir(self._main) and self._main.oDesktop is not None: + settings.aedt_version = oDesktop.GetVersion()[0:6] + elif "oDesktop" in dir(self._main) and self._main.oDesktop is not None: # pragma: no cover self.release_on_exit = False else: if "oDesktop" in dir(self._main): del self._main.oDesktop self._main.student_version, version_key, version = self._set_version(specified_version, student_version) + settings.aedt_version = version if _com == "ironpython": # pragma: no cover print("Launching PyAEDT outside AEDT with IronPython.") self._init_ironpython(non_graphical, new_desktop_session, version) diff --git a/pyaedt/edb.py b/pyaedt/edb.py index a010f1c7735..3028aae0023 100644 --- a/pyaedt/edb.py +++ b/pyaedt/edb.py @@ -517,16 +517,19 @@ def export_to_ipc2581(self, ipc_path=None, units="millimeter"): ipc_path = self.edbpath[:-4] + "xml" self.logger.info("Export IPC 2581 is starting. This operation can take a while...") start = time.time() - result = self.edblib.IPC8521.IPCExporter.ExportIPC2581FromLayout( - self.active_layout, self.edbversion, ipc_path, units.lower() - ) - end = time.time() - start - if result: - self.logger.info("Export IPC 2581 completed in %s sec.", end) - self.logger.info("File saved as %s", ipc_path) - return ipc_path - self.logger.info("Error Exporting IPC 2581.") - return False + try: + result = self.edblib.IPC8521.IPCExporter.ExportIPC2581FromLayout( + self.active_layout, self.edbversion, ipc_path, units.lower() + ) + end = time.time() - start + if result: + self.logger.info("Export IPC 2581 completed in %s sec.", end) + self.logger.info("File saved as %s", ipc_path) + return ipc_path + except Exception as e: + self.logger.info("Error Exporting IPC 2581.") + self.logger.info(str(e)) + return False def edb_exception(self, ex_value, tb_data): """Write the trace stack to AEDT when a Python error occurs. @@ -1068,9 +1071,11 @@ def create_cutout_on_point_list( else: _ref_nets.append(self.core_nets.nets[_ref].net_object) # pragma: no cover # TODO check and insert via check on polygon intersection - circle_voids = [void_circle for void_circle in self.core_primitives.circles if void_circle.is_void] + voids = [p for p in self.core_primitives.circles if p.is_void] + voids2 = [p for p in self.core_primitives.polygons if p.is_void] + voids.extend(voids2) voids_to_add = [] - for circle in circle_voids: + for circle in voids: if polygonData.GetIntersectionType(circle.primitive_object.GetPolygonData()) >= 3: voids_to_add.append(circle) @@ -1078,23 +1083,27 @@ def create_cutout_on_point_list( net_signals = List[type(_ref_nets[0])]() # Create new cutout cell/design _cutout = self.active_cell.CutOut(net_signals, _netsClip, polygonData) - + layout = _cutout.GetLayout() for void_circle in voids_to_add: - layout = _cutout.GetLayout() - if is_ironpython: # pragma: no cover - res, center_x, center_y, radius = void_circle.primitive_object.GetParameters() - else: - res, center_x, center_y, radius = void_circle.primitive_object.GetParameters(0.0, 0.0, 0.0) - cloned_circle = self.edb.Cell.Primitive.Circle.Create( - layout, - void_circle.layer_name, - void_circle.net, - self.edb_value(center_x), - self.edb_value(center_y), - self.edb_value(radius), - ) - cloned_circle.SetIsNegative(True) - + if void_circle.type == "Circle": + if is_ironpython: # pragma: no cover + res, center_x, center_y, radius = void_circle.primitive_object.GetParameters() + else: + res, center_x, center_y, radius = void_circle.primitive_object.GetParameters(0.0, 0.0, 0.0) + cloned_circle = self.edb.Cell.Primitive.Circle.Create( + layout, + void_circle.layer_name, + void_circle.net, + self.edb_value(center_x), + self.edb_value(center_y), + self.edb_value(radius), + ) + cloned_circle.SetIsNegative(True) + elif void_circle.type == "Polygon": + cloned_polygon = self.edb.Cell.Primitive.Polygon.Create( + layout, void_circle.layer_name, void_circle.net, void_circle.primitive_object.GetPolygonData() + ) + cloned_polygon.SetIsNegative(True) layers = self.core_stackup.stackup_layers.signal_layers for layer in list(layers.keys()): layer_primitves = self.core_primitives.get_primitives(layer_name=layer) diff --git a/pyaedt/edb_core/EDB_Data.py b/pyaedt/edb_core/EDB_Data.py index 9da54afb172..bcd54da0c7f 100644 --- a/pyaedt/edb_core/EDB_Data.py +++ b/pyaedt/edb_core/EDB_Data.py @@ -193,6 +193,8 @@ def is_void(self): ------- bool """ + if not hasattr(self.primitive_object, "IsVoid"): + return False return self.primitive_object.IsVoid() @staticmethod diff --git a/pyaedt/edb_core/components.py b/pyaedt/edb_core/components.py index 1a41c97634c..29ee41b4126 100644 --- a/pyaedt/edb_core/components.py +++ b/pyaedt/edb_core/components.py @@ -406,6 +406,7 @@ def get_component_placement_vector( mounted_component_pin2, hosting_component_pin1, hosting_component_pin2, + flipped=False, ): """Get the placement vector between 2 components. @@ -423,6 +424,8 @@ def get_component_placement_vector( Hosted component Pin 1 name. hosting_component_pin2 : str Hosted component Pin 2 name. + flipped : bool, optional + Either if the mounted component will be flipped or not. Returns ------- @@ -454,43 +457,40 @@ def get_component_placement_vector( if mounted_component_pin1: m_pin1 = self._get_edb_pin_from_pin_name(mounted_component, mounted_component_pin1) m_pin1_pos = self.get_pin_position(m_pin1) - m_pin1_pos_3d = [m_pin1_pos[0], m_pin1_pos[1], 0] if mounted_component_pin2: m_pin2 = self._get_edb_pin_from_pin_name(mounted_component, mounted_component_pin2) m_pin2_pos = self.get_pin_position(m_pin2) - m_pin2_pos_3d = [m_pin2_pos[0], m_pin2_pos[1], 0] if hosting_component_pin1: h_pin1 = self._get_edb_pin_from_pin_name(hosting_component, hosting_component_pin1) h_pin1_pos = self.get_pin_position(h_pin1) - h_pin1_pos_3d = [h_pin1_pos[0], h_pin1_pos[1], 0] if hosting_component_pin2: h_pin2 = self._get_edb_pin_from_pin_name(hosting_component, hosting_component_pin2) h_pin2_pos = self.get_pin_position(h_pin2) - h_pin2_pos_3d = [h_pin2_pos[0], h_pin2_pos[1], 0] # vector = [h_pin1_pos[0] - m_pin1_pos[0], h_pin1_pos[1] - m_pin1_pos[1]] - vector1 = GeometryOperators.v_points(m_pin1_pos_3d, m_pin2_pos_3d) - vector2 = GeometryOperators.v_points(h_pin1_pos_3d, h_pin2_pos_3d) - - rotation = GeometryOperators.v_angle(vector1, vector2) - if abs(rotation - math.pi / 2) < 1e-9 and vector1[0] * vector2[1] + vector1[1] * vector2[0] < 0: - rotation = -1 * math.pi / 2 + vector1 = GeometryOperators.v_points(m_pin1_pos, m_pin2_pos) + vector2 = GeometryOperators.v_points(h_pin1_pos, h_pin2_pos) + multiplier = 1 + if flipped: + multiplier = -1 + vector1[1] = multiplier * vector1[1] + + rotation = GeometryOperators.v_angle_sign_2D(vector1, vector2, False) if rotation != 0.0: layinst = mounted_component.GetLayout().GetLayoutInstance() cmpinst = layinst.GetLayoutObjInstance(mounted_component, None) center = cmpinst.GetCenter() - center_double = [center.X.ToDouble(), center.Y.ToDouble(), 0] - vector_center = GeometryOperators.v_points(center_double, m_pin1_pos_3d) - x_v2 = vector_center[0] * math.cos(rotation) + vector_center[1] * math.sin(rotation) - y_v2 = -1 * vector_center[0] * math.sin(rotation) + vector_center[1] * math.cos(rotation) - new_vector = [x_v2 + center_double[0], y_v2 + center_double[1], 0] + center_double = [center.X.ToDouble(), center.Y.ToDouble()] + vector_center = GeometryOperators.v_points(center_double, m_pin1_pos) + x_v2 = vector_center[0] * math.cos(rotation) + multiplier * vector_center[1] * math.sin(rotation) + y_v2 = -1 * vector_center[0] * math.sin(rotation) + multiplier * vector_center[1] * math.cos(rotation) + new_vector = [x_v2 + center_double[0], y_v2 + center_double[1]] vector = [h_pin1_pos[0] - new_vector[0], h_pin1_pos[1] - new_vector[1]] if vector: solder_ball_height = self.get_solder_ball_height(mounted_component) - # self.set_solder_ball(component=mounted_component, sball_height=solder_ball_height) return True, vector, rotation, solder_ball_height self._logger.warning("Failed to compute vector.") return False, [0, 0], 0, 0 diff --git a/pyaedt/examples/downloads.py b/pyaedt/examples/downloads.py index 3452a87366d..9ef36f7ccee 100644 --- a/pyaedt/examples/downloads.py +++ b/pyaedt/examples/downloads.py @@ -122,12 +122,17 @@ def download_aedb(): return _download_file("edb/Galileo.aedb", "edb.def") -def download_edb_merge_utility(): +def download_edb_merge_utility(force_download=False): """Download an example of WPF Project which allows to merge 2aedb files. Examples files are downloaded to a persistent cache to avoid re-downloading the same file twice. + Parameters + ---------- + force_download : bool + Force to delete cache and download files again. + Returns ------- str @@ -137,10 +142,14 @@ def download_edb_merge_utility(): -------- Download an example result file and return the path of the file >>> from pyaedt import examples - >>> path = examples.download_edb_mergge_utility() + >>> path = examples.download_edb_merge_utility(force_download=True) >>> path - 'C:/Users/user/AppData/local/temp/Galileo.aedb' + 'C:/Users/user/AppData/local/temp/wpf_edb_merge/merge_wizard.py' """ + if force_download: + local_path = os.path.join(EXAMPLES_PATH, "wpf_edb_merge") + if os.path.exists(local_path): + shutil.rmtree(local_path, ignore_errors=True) _download_file("wpf_edb_merge/board.aedb", "edb.def") _download_file("wpf_edb_merge/package.aedb", "edb.def") _download_file("wpf_edb_merge", "merge_wizard_settings.json") diff --git a/pyaedt/generic/general_methods.py b/pyaedt/generic/general_methods.py index 7a96b6ddb77..438707456b9 100644 --- a/pyaedt/generic/general_methods.py +++ b/pyaedt/generic/general_methods.py @@ -641,6 +641,8 @@ def __init__(self): self._enable_debug_internal_methods_logger = False self._enable_debug_logger = False self._enable_error_handler = True + self.non_graphical = False + self.aedt_version = None @property def enable_error_handler(self): diff --git a/pyaedt/modeler/GeometryOperators.py b/pyaedt/modeler/GeometryOperators.py index da519ec6f98..6742daa06e5 100644 --- a/pyaedt/modeler/GeometryOperators.py +++ b/pyaedt/modeler/GeometryOperators.py @@ -431,7 +431,7 @@ def v_points(p1, p2): Returns ------- - list + List List of ``[vx, vy, vz]``coordinates for the vector from the first point to the second point. """ return GeometryOperators.v_sub(p2, p1) diff --git a/pyaedt/modeler/Object3d.py b/pyaedt/modeler/Object3d.py index 8373fd2b99c..55aef7265a9 100644 --- a/pyaedt/modeler/Object3d.py +++ b/pyaedt/modeler/Object3d.py @@ -24,6 +24,7 @@ from pyaedt.generic.constants import AEDT_UNITS from pyaedt.generic.constants import MILS2METER from pyaedt.generic.general_methods import is_ironpython +from pyaedt import settings from pyaedt.modeler.GeometryOperators import GeometryOperators clamp = lambda n, minn, maxn: max(min(maxn, n), minn) @@ -803,9 +804,10 @@ def _bounding_box_unmodel(self): self._odesign.Undo() if not modeled: self._odesign.Undo() - self._primitives._app.odesktop.ClearMessages( - self._primitives._app.project_name, self._primitives._app.design_name, 1 - ) + if not settings.non_graphical: + self._primitives._app.odesktop.ClearMessages( + self._primitives._app.project_name, self._primitives._app.design_name, 1 + ) return bounding @pyaedt_function_handler() diff --git a/pyaedt/modeler/Primitives3D.py b/pyaedt/modeler/Primitives3D.py index 9e829679d7c..bb3eb8c6060 100644 --- a/pyaedt/modeler/Primitives3D.py +++ b/pyaedt/modeler/Primitives3D.py @@ -876,13 +876,39 @@ def create_equationbased_curve( return self._create_object(new_name) @pyaedt_function_handler() - def create_helix(self, udphelixdefinition): - """Create an helix. + def create_helix( + self, + polyline_name, + position, + x_start_dir, + y_start_dir, + z_start_dir, + num_thread=1, + right_hand=True, + radius_increment=0.0, + thread=1, + ): + """Create an helix from a polyline. Parameters ---------- - udphelixdefinition : - + polyline_name : str + Name of the polyline used as the base for the helix. + position : list + List of ``[x, y, z]`` coordinates for the center point of the circle. + x_start_dir : float + Distance along x axis from the polyline. + y_start_dir : float + Distance along y axis from the polyline. + z_start_dir : float + Distance along z axis from the polyline. + num_thread : int, optional + Number of turns. The default value is ``1``. + right_hand : bool, optional + Whether the helix turning direction is right hand. The default value is ``True``. + radius_increment : float, optional + Radius change per turn. The default value is ``0.0``. + thread : float Returns ------- @@ -895,11 +921,36 @@ def create_helix(self, udphelixdefinition): >>> oEditor.CreateHelix """ + if not polyline_name or polyline_name == "": + raise ValueError("The name of the polyline cannot be an empty string.") + + x_center, y_center, z_center = self._pos_with_arg(position) + vArg1 = ["NAME:Selections"] - vArg1.append("Selections:="), vArg1.append(o.name) + vArg1.append("Selections:="), vArg1.append(polyline_name) vArg1.append("NewPartsModelFlag:="), vArg1.append("Model") - vArg2 = udphelixdefinition.toScript(self.model_units) + vArg2 = ["NAME:HelixParameters"] + vArg2.append("XCenter:=") + vArg2.append(x_center) + vArg2.append("YCenter:=") + vArg2.append(y_center) + vArg2.append("ZCenter:=") + vArg2.append(z_center) + vArg2.append("XStartDir:=") + vArg2.append(self._arg_with_dim(x_start_dir)) + vArg2.append("YStartDir:=") + vArg2.append(self._arg_with_dim(y_start_dir)) + vArg2.append("ZStartDir:=") + vArg2.append(self._arg_with_dim(z_start_dir)) + vArg2.append("NumThread:=") + vArg2.append(num_thread) + vArg2.append("RightHand:=") + vArg2.append(right_hand) + vArg2.append("RadiusIncrement:=") + vArg2.append(self._arg_with_dim(radius_increment)) + vArg2.append("Thread:=") + vArg2.append(self._arg_with_dim(thread)) new_name = self._oeditor.CreateHelix(vArg1, vArg2) return self._create_object(new_name)