diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index 1328f44602..a470ad3854 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -26,13 +26,13 @@ concurrency: jobs: - check_title: + pr-title: if: github.event_name == 'pull_request' name: Check the title of the pull request runs-on: ubuntu-latest steps: - - name: Check commit name - uses: ansys/actions/commit-style@v6 + - name: Check the title of the pull request + uses: ansys/actions/check-pr-title@v8 with: token: ${{ secrets.GITHUB_TOKEN }} use-upper-case: true @@ -139,7 +139,7 @@ jobs: name: Test dotnet (linux) runs-on: [ Linux, self-hosted, toolkits ] env: - ANSYSEM_ROOT242: '/ansys_inc/AnsysEM/v242/Linux64' + ANSYSEM_ROOT242: '/opt/AnsysEM/v242/Linux64' ANS_NODEPCHECK: '1' steps: - name: "Install Git and clone project" @@ -153,7 +153,7 @@ jobs: - name: "Install os packages" run: | sudo apt update - sudo apt install libgl1-mesa-glx xvfb + sudo apt install libgl1-mesa-glx xvfb -y - name: Create Python venv run: | @@ -174,8 +174,9 @@ jobs: with: max_attempts: 3 retry_on: error - timeout_minutes: 20 + timeout_minutes: 40 command: | + export LD_LIBRARY_PATH=${{ env.ANSYSEM_ROOT242 }}/common/mono/Linux64/lib64:$LD_LIBRARY_PATH . .venv/bin/activate xvfb-run pytest -m "legacy" -n auto --dist loadfile -v --cov @@ -295,7 +296,7 @@ jobs: - name: Install OS packages run: | sudo apt update - sudo apt-get install tk + sudo apt-get install tk -y - name: Setup Python uses: actions/setup-python@v5 diff --git a/README.md b/README.md index ec9f0a39cd..0006a8f27a 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ ## What is PyEDB? -PyEDB is Python client library for processing complex and large layout designs in the +PyEDB is a Python client library for processing complex and large layout designs in the Ansys Electronics Database (EDB) format, which stores information describing designs for [Ansys Electronics Desktop](https://www.ansys.com/products/electronics) (AEDT). @@ -39,7 +39,7 @@ HFSS 3D Layout, Icepak, Maxwell, Q3D, and SIwave. EDB provides a proprietary database file format (AEDB) for efficient and fast layout design handling and processing for building ready-to-solve projects. EDB addresses signal integrity -(SI), power integrity (PI-DC), and electro-thermal work flows. You can import an AEDB file +(SI), power integrity (PI-DC), and electro-thermal workflows. You can import an AEDB file into AEDT to modify the layout, assign materials, and define ports, simulations, and constraints. You can then launch any of the Ansys electromagnetic simulators. @@ -49,7 +49,7 @@ memory, it provides the fastest and most efficient way to handle a large and com You can also parse an AEDB file from a command line in batch in an Ansys electromagnetic simulator like HFSS or SIwave. Thus, you can deploy completely non-graphical flows, from layout -translation through simulatiom results. +translation through simulation results. Additionally, you can use PyAEDT to import an AEDB file into AEDT to view a project, combine 3D designs, or perform simulation postprocessing. EDB also supports 3D component models. diff --git a/pyproject.toml b/pyproject.toml index e51e0f14b2..d8a46b036c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,7 +53,7 @@ tests = [ doc = [ "ansys-sphinx-theme>=0.10.0,<1.1", "imageio>=2.30.0,<2.37", - "ipython>=8.13.0,<8.31", + "ipython>=8.13.0,<8.32", "jupyterlab>=4.0.0,<4.4", "jupytext>=1.16.0,<1.17", "matplotlib>=3.5.0,<3.10", diff --git a/src/pyedb/configuration/cfg_components.py b/src/pyedb/configuration/cfg_components.py index 52c77c8fec..6b1be46121 100644 --- a/src/pyedb/configuration/cfg_components.py +++ b/src/pyedb/configuration/cfg_components.py @@ -210,10 +210,10 @@ def _set_port_properties_to_edb(self): self._pyedb_obj.component_property = cp def set_parameters_to_edb(self): - if self.enabled: - self._pyedb_obj.enabled = self.enabled if self.type: self._pyedb_obj.type = self.type + if self.enabled: + self._pyedb_obj.enabled = self.enabled self._set_model_properties_to_edb() if self._pyedb_obj.type.lower() == "ic": diff --git a/src/pyedb/configuration/cfg_padstacks.py b/src/pyedb/configuration/cfg_padstacks.py index 1885fd0eb3..c30c2901af 100644 --- a/src/pyedb/configuration/cfg_padstacks.py +++ b/src/pyedb/configuration/cfg_padstacks.py @@ -124,6 +124,15 @@ def set_parameters_to_edb(self): if self.solder_ball_parameters: self._set_solder_parameters_to_edb(self.solder_ball_parameters) + def retrieve_parameters_from_edb(self): + self.name = self._pyedb_obj.name + self.hole_plating_thickness = self._pyedb_obj.hole_plating_thickness + self.material = self._pyedb_obj.material + self.hole_range = self._pyedb_obj.hole_range + self.pad_parameters = self._get_pad_parameters_from_edb() + self.hole_parameters = self._get_hole_parameters_from_edb() + self.solder_ball_parameters = self._get_solder_parameters_from_edb() + def _set_solder_parameters_to_edb(self, parameters): pdef_data = self._pyedb_obj._padstack_def_data @@ -158,15 +167,6 @@ def _get_solder_parameters_from_edb(self): } return parameters - def retrieve_parameters_from_edb(self): - self.name = self._pyedb_obj.name - self.hole_plating_thickness = self._pyedb_obj.hole_plating_thickness - self.material = self._pyedb_obj.material - self.hole_range = self._pyedb_obj.hole_range - self.pad_parameters = self._get_pad_parameters_from_edb() - self.hole_parameters = self._get_hole_parameters_from_edb() - self.solder_ball_parameters = self._get_solder_parameters_from_edb() - def _get_pad_parameters_from_edb(self): """Pad parameters. diff --git a/src/pyedb/configuration/cfg_pin_groups.py b/src/pyedb/configuration/cfg_pin_groups.py index 05c8e3c224..c60cf44b1d 100644 --- a/src/pyedb/configuration/cfg_pin_groups.py +++ b/src/pyedb/configuration/cfg_pin_groups.py @@ -67,17 +67,16 @@ def __init__(self, pedb, **kwargs): def create(self): """Apply pin group on layout.""" if self.pins: - self._pedb.siwave.create_pin_group(self.reference_designator, list(self.pins), self.name) + pins = self.pins if isinstance(self.pins, list) else [self.pins] + self._pedb.siwave.create_pin_group(self.reference_designator, pins, self.name) elif self.net: - if self.reference_designator in self._pedb.components.instances: - comp = self._pedb.components.instances[self.reference_designator] - else: - raise f"Component not found for creating pin group {self.name}." - pins = [p for p, obj in comp.pins.items() if obj.net_name in self.net] + nets = self.net if isinstance(self.net, list) else [self.net] + comp = self._pedb.components.instances[self.reference_designator] + pins = [p for p, obj in comp.pins.items() if obj.net_name in nets] if not self._pedb.siwave.create_pin_group(self.reference_designator, pins, self.name): - self._pedb.logger.error(f"Failed to create pin group {self.name}") + raise RuntimeError(f"Failed to create pin group {self.name}") else: - self._pedb.logger.error(f"No net and pins defined for defining pin group {self.name}") + raise RuntimeError(f"No net and pins defined for defining pin group {self.name}") def export_properties(self): if self.pins: diff --git a/src/pyedb/configuration/cfg_ports_sources.py b/src/pyedb/configuration/cfg_ports_sources.py index 766345c0ff..b9bf3a6ff9 100644 --- a/src/pyedb/configuration/cfg_ports_sources.py +++ b/src/pyedb/configuration/cfg_ports_sources.py @@ -265,7 +265,7 @@ def _create_terminals(self): pins = {pos_value: self._pedb.components.instances[self.reference_designator].pins[pos_value]} pos_objs.update(pins) else: - raise f"Wrong positive terminal type {pos_type}" + raise Exception(f"Wrong positive terminal type {pos_type}.") self.pos_terminals = {i: j.create_terminal(i) for i, j in pos_objs.items()} self.pos_terminals.update(pos_coor_terminal) @@ -302,7 +302,7 @@ def _create_terminals(self): terminal_name: self._pedb.components.instances[self.reference_designator].pins[neg_value] } else: - raise f"Wrong negative terminal type {neg_type}" + raise Exception(f"Wrong negative terminal type {neg_type}.") self.neg_terminal = [ j.create_terminal(i) if not j.terminal else j.terminal for i, j in neg_obj.items() ][0] diff --git a/src/pyedb/configuration/configuration.py b/src/pyedb/configuration/configuration.py index 537e9cf068..a817bf990d 100644 --- a/src/pyedb/configuration/configuration.py +++ b/src/pyedb/configuration/configuration.py @@ -134,13 +134,17 @@ def run(self, **kwargs): # Configure stackup if kwargs.get("fix_padstack_def"): + from pyedb.configuration.cfg_padstacks import CfgPadstackDefinition + pedb_defs = self._pedb.padstacks.definitions - temp = {} - for name, pdef in pedb_defs.items(): - temp[name] = pdef.get_properties() + temp = [] + for _, pdef in pedb_defs.items(): + cfg_def = CfgPadstackDefinition(self._pedb, pdef) + cfg_def.retrieve_parameters_from_edb() + temp.append(cfg_def) self.cfg_data.stackup.apply() - for name, pdef_p in temp.items(): - pedb_defs[name].set_properties(**pdef_p) + for cfg_pdef in temp: + cfg_pdef.set_parameters_to_edb() else: self.cfg_data.stackup.apply() diff --git a/src/pyedb/dotnet/database/cell/hierarchy/pin_pair_model.py b/src/pyedb/dotnet/database/cell/hierarchy/pin_pair_model.py index d5f3473d8f..3ef8c6c165 100644 --- a/src/pyedb/dotnet/database/cell/hierarchy/pin_pair_model.py +++ b/src/pyedb/dotnet/database/cell/hierarchy/pin_pair_model.py @@ -66,7 +66,7 @@ def resistance(self): @resistance.setter def resistance(self, value): self._pin_pair_rlc.R = value - self._set_comp_prop(self._pin_pair_rlc) # pragma: no cover + self._set_comp_prop() # pragma: no cover @property def inductance(self): @@ -75,7 +75,7 @@ def inductance(self): @inductance.setter def inductance(self, value): self._pin_pair_rlc.L = value - self._set_comp_prop(self._pin_pair_rlc) # pragma: no cover + self._set_comp_prop() # pragma: no cover @property def capacitance(self): @@ -84,7 +84,7 @@ def capacitance(self): @capacitance.setter def capacitance(self, value): self._pin_pair_rlc.C = value - self._set_comp_prop(self._pin_pair_rlc) # pragma: no cover + self._set_comp_prop() # pragma: no cover @property def rlc_values(self): # pragma: no cover diff --git a/src/pyedb/dotnet/database/cell/terminal/padstack_instance_terminal.py b/src/pyedb/dotnet/database/cell/terminal/padstack_instance_terminal.py index 4be9681f09..b77ae28d63 100644 --- a/src/pyedb/dotnet/database/cell/terminal/padstack_instance_terminal.py +++ b/src/pyedb/dotnet/database/cell/terminal/padstack_instance_terminal.py @@ -101,3 +101,15 @@ def _get_parameters(self): def padstack_instance(self): p_inst, _ = self._get_parameters() return self._pedb.layout.find_object_by_id(p_inst.GetId()) + + @property + def layer(self): + """Get layer of the terminal.""" + _, _, layer = self._edb_object.GetParameters() + return self._pedb.stackup.all_layers[layer.GetName()] + + @layer.setter + def layer(self, value): + layer = self._pedb.stackup.layers[value]._edb_layer + point_data = self._pedb.point_data(*self.location) + self._edb_object.SetParameters(point_data, layer) diff --git a/src/pyedb/dotnet/database/cell/terminal/point_terminal.py b/src/pyedb/dotnet/database/cell/terminal/point_terminal.py index d1e715fe65..514661a6c7 100644 --- a/src/pyedb/dotnet/database/cell/terminal/point_terminal.py +++ b/src/pyedb/dotnet/database/cell/terminal/point_terminal.py @@ -66,3 +66,15 @@ def create(self, name, net, location, layer, is_ref=False): raise Exception(msg) else: return terminal + + @property + def layer(self): + """Get layer of the terminal.""" + _, _, layer = self._edb_object.GetParameters() + return self._pedb.stackup.all_layers[layer.GetName()] + + @layer.setter + def layer(self, value): + layer = self._pedb.stackup.layers[value]._edb_layer + point_data = self._pedb.point_data(*self.location) + self._edb_object.SetParameters(point_data, layer) diff --git a/src/pyedb/dotnet/database/cell/terminal/terminal.py b/src/pyedb/dotnet/database/cell/terminal/terminal.py index de71c99f7b..bf54114064 100644 --- a/src/pyedb/dotnet/database/cell/terminal/terminal.py +++ b/src/pyedb/dotnet/database/cell/terminal/terminal.py @@ -105,25 +105,16 @@ def hfss_type(self, value): @property def layer(self): """Get layer of the terminal.""" - point_data = self._pedb.point_data(0, 0) - layer = list(self._pedb.stackup.layers.values())[0]._edb_layer - if self._edb_object.GetParameters(point_data, layer): - return self._pedb.stackup.all_layers[layer.GetName()] - else: - self._pedb.logger.warning(f"No pad parameters found for terminal {self.name}") - - @layer.setter - def layer(self, value): - layer = self._pedb.stackup.layers[value]._edb_layer - point_data = self._pedb.point_data(*self.location) - self._edb_object.SetParameters(point_data, layer) + return self._pedb.logger.error("Cannot determine terminal layer") @property def location(self): """Location of the terminal.""" - layer = list(self._pedb.stackup.layers.values())[0]._edb_layer - _, point_data, _ = self._edb_object.GetParameters(None, layer) - return [point_data.X.ToDouble(), point_data.Y.ToDouble()] + try: + _, point_data, _ = self._edb_object.GetParameters() + return [point_data.X.ToDouble(), point_data.Y.ToDouble()] + except: + self._pedb.logger.error("Cannot determine terminal location") @location.setter def location(self, value): diff --git a/src/pyedb/dotnet/database/components.py b/src/pyedb/dotnet/database/components.py index 613c6a405a..d35634b09c 100644 --- a/src/pyedb/dotnet/database/components.py +++ b/src/pyedb/dotnet/database/components.py @@ -111,16 +111,9 @@ def __getitem__(self, name): def __init__(self, p_edb): self._pedb = p_edb - self._cmp = {} - self._res = {} - self._cap = {} - self._ind = {} - self._ios = {} - self._ics = {} - self._others = {} + self.refresh_components() self._pins = {} self._comps_by_part = {} - self._init_parts() self._padstack = EdbPadstacks(self._pedb) @property @@ -132,16 +125,6 @@ def _logger(self): def _edb(self): return self._pedb.edb_api - def _init_parts(self): - a = self.instances - a = self.resistors - a = self.ICs - a = self.Others - a = self.inductors - a = self.IOs - a = self.components_by_partname - return True - def _get_edb_value(self, value): return self._pedb.edb_value(value) @@ -205,8 +188,6 @@ def instances(self): >>> edbapp.components.instances """ - if not self._cmp: - self.refresh_components() return self._cmp @property @@ -310,10 +291,29 @@ def export_definition(self, file_path): def refresh_components(self): """Refresh the component dictionary.""" - # self._logger.info("Refreshing the Components dictionary.") self._cmp = {} + self._res = {} + self._ind = {} + self._cap = {} + self._ics = {} + self._ios = {} + self._others = {} for i in self._pedb.layout.groups: self._cmp[i.name] = i + if i.type == "Resistor": + self._res[i.name] = i + elif i.type == "Capacitor": + self._cap[i.name] = i + elif i.type == "Inductor": + self._ind[i.name] = i + elif i.type == "IC": + self._ics[i.name] = i + elif i.type == "IO": + self._ios[i.name] = i + elif i.type == "Other": + self._others[i.name] = i + else: + self._logger.warning(f"Unknown component type {i.name} found while refreshing components, will ignore") return True @property @@ -332,10 +332,6 @@ def resistors(self): >>> edbapp = Edb("myaedbfolder") >>> edbapp.components.resistors """ - self._res = {} - for el, val in self.instances.items(): - if val.type == "Resistor": - self._res[el] = val return self._res @property @@ -354,10 +350,6 @@ def capacitors(self): >>> edbapp = Edb("myaedbfolder") >>> edbapp.components.capacitors """ - self._cap = {} - for el, val in self.instances.items(): - if val.type == "Capacitor": - self._cap[el] = val return self._cap @property @@ -377,10 +369,6 @@ def inductors(self): >>> edbapp.components.inductors """ - self._ind = {} - for el, val in self.instances.items(): - if val.type == "Inductor": - self._ind[el] = val return self._ind @property @@ -400,10 +388,6 @@ def ICs(self): >>> edbapp.components.ICs """ - self._ics = {} - for el, val in self.instances.items(): - if val.type == "IC": - self._ics[el] = val return self._ics @property @@ -423,10 +407,6 @@ def IOs(self): >>> edbapp.components.IOs """ - self._ios = {} - for el, val in self.instances.items(): - if val.type == "IO": - self._ios[el] = val return self._ios @property @@ -446,10 +426,6 @@ def Others(self): >>> edbapp.components.others """ - self._others = {} - for el, val in self.instances.items(): - if val.type == "Other": - self._others[el] = val return self._others @property diff --git a/src/pyedb/dotnet/database/edb_data/layer_data.py b/src/pyedb/dotnet/database/edb_data/layer_data.py index a499bc9d69..6487cf7aec 100644 --- a/src/pyedb/dotnet/database/edb_data/layer_data.py +++ b/src/pyedb/dotnet/database/edb_data/layer_data.py @@ -794,13 +794,14 @@ def properties(self): data["thickness"] = self._edb_object.GetThicknessValue().ToString() data["color"] = self.color - etch_power_ground_nets = int(self._edb_layer.GetEtchNetClass()) - etch_power_ground_nets = False if etch_power_ground_nets else True data["etching"] = { "factor": self._edb_layer.GetEtchFactor().ToString(), "enabled": self._edb_layer.IsEtchFactorEnabled(), - "etch_power_ground_nets": etch_power_ground_nets, } + if self._pedb.edbversion >= "2024.2": + etch_power_ground_nets = int(self._edb_layer.GetEtchNetClass()) + etch_power_ground_nets = False if etch_power_ground_nets else True + data["etching"]["etch_power_ground_nets"] = etch_power_ground_nets roughness = {} for region in ["top", "bottom", "side"]: @@ -866,8 +867,9 @@ def properties(self, params): layer_clone = self._edb_layer layer_clone.SetEtchFactorEnabled(etching["enabled"]) layer_clone.SetEtchFactor(self._pedb.stackup._edb_value(float(etching["factor"]))) - if etching["etch_power_ground_nets"]: - layer_clone.SetEtchNetClass(self._pedb._edb.Cell.EtchNetClass.NoEtchPowerGroundNets) - else: - layer_clone.SetEtchNetClass(self._pedb._edb.Cell.EtchNetClass.EtchAllNets) + if self._pedb.edbversion >= "2024.2": + if etching["etch_power_ground_nets"]: + layer_clone.SetEtchNetClass(self._pedb._edb.Cell.EtchNetClass.NoEtchPowerGroundNets) + else: + layer_clone.SetEtchNetClass(self._pedb._edb.Cell.EtchNetClass.EtchAllNets) self._pedb.stackup._set_layout_stackup(layer_clone, "change_attribute") diff --git a/src/pyedb/dotnet/database/padstack.py b/src/pyedb/dotnet/database/padstack.py index a93e95785a..7e4211e1b6 100644 --- a/src/pyedb/dotnet/database/padstack.py +++ b/src/pyedb/dotnet/database/padstack.py @@ -1635,8 +1635,7 @@ def merge_via(self, contour_boxes, net_filter=None, start_layer=None, stop_layer """ merged_via_ids = [] if not contour_boxes: - self._pedb.logger.error("No contour box provided, you need to pass a nested list as argument.") - return False + raise Exception("No contour box provided, you need to pass a nested list as argument.") if not start_layer: start_layer = list(self._pedb.stackup.layers.values())[0].name if not stop_layer: @@ -1648,35 +1647,60 @@ def merge_via(self, contour_boxes, net_filter=None, start_layer=None, stop_layer instances = self.get_padstack_instances_id_intersecting_polygon( points=contour_box, padstack_instances_index=instances_index ) - if net_filter: - instances = [self.instances[id] for id in instances if not self.instances[id].net.name in net_filter] - net = self.instances[instances[0]].net.name - instances_pts = np.array([self.instances[id].position for id in instances]) - convex_hull_contour = ConvexHull(instances_pts) - contour_points = list(instances_pts[convex_hull_contour.vertices]) - layer = list(self._pedb.stackup.layers.values())[0].name - polygon = self._pedb.modeler.create_polygon(main_shape=contour_points, layer_name=layer) - polygon_data = polygon.polygon_data - polygon.delete() - new_padstack_def = generate_unique_name(self.instances[instances[0]].definition.name) - if not self.create( - padstackname=new_padstack_def, - pad_shape="Polygon", - antipad_shape="Polygon", - pad_polygon=polygon_data, - antipad_polygon=polygon_data, - polygon_hole=polygon_data, - start_layer=start_layer, - stop_layer=stop_layer, - ): - self._logger.error(f"Failed to create padstack definition {new_padstack_def}") - merged_instance = self.place(position=[0, 0], definition_name=new_padstack_def, net_name=net) - merged_via_ids.append(merged_instance.id) - [self.instances[id].delete() for id in instances] + if not instances: + raise Exception(f"No padstack instances found inside {contour_box}") + else: + if net_filter: + instances = [ + self.instances[id] for id in instances if not self.instances[id].net_name in net_filter + ] + + net = self.instances[instances[0]].net_name + x_values = [] + y_values = [] + for inst in instances: + pos = instances_index[inst] + x_values.append(pos[0]) + y_values.append(pos[1]) + x_values = list(set(x_values)) + y_values = list(set(y_values)) + if len(x_values) == 1 or len(y_values) == 1: + create_instances = self.merge_via_along_lines( + net_name=net, padstack_instances_id=instances, minimum_via_number=2 + ) + merged_via_ids.extend(create_instances) + else: + instances_pts = np.array([instances_index[id] for id in instances]) + convex_hull_contour = ConvexHull(instances_pts) + contour_points = list(instances_pts[convex_hull_contour.vertices]) + layer = list(self._pedb.stackup.layers.values())[0].name + polygon = self._pedb.modeler.create_polygon(main_shape=contour_points, layer_name=layer) + polygon_data = polygon.polygon_data + polygon.delete() + new_padstack_def = generate_unique_name(self.instances[instances[0]].definition.name) + if not self.create( + padstackname=new_padstack_def, + pad_shape="Polygon", + antipad_shape="Polygon", + pad_polygon=polygon_data, + antipad_polygon=polygon_data, + polygon_hole=polygon_data, + start_layer=start_layer, + stop_layer=stop_layer, + ): + raise Exception(f"Failed to create padstack definition {new_padstack_def}") + merged_instance = self.place(position=[0, 0], definition_name=new_padstack_def, net_name=net) + merged_via_ids.append(merged_instance.id) + [self.instances[id].delete() for id in instances] return merged_via_ids def merge_via_along_lines( - self, net_name="GND", distance_threshold=5e-3, minimum_via_number=6, selected_angles=None + self, + net_name="GND", + distance_threshold=5e-3, + minimum_via_number=6, + selected_angles=None, + padstack_instances_id=None, ): """Replace padstack instances along lines into a single polygon. @@ -1700,11 +1724,15 @@ def merge_via_along_lines( Specify angle in degrees to detected, for instance [0, 180] is only detecting horizontal and vertical lines. Other values can be assigned like 45 degrees. When `None` is provided all lines are detected. Default value is `None`. + padstack_instances_id : List[int] + List of padstack instances ID's to include. If `None`, the algorithm will scan all padstack instances belonging + to the specified net. Default value is `None`. + Returns ------- bool - ``True`` when succeeded ``False`` when failed. < + List[int], list of created padstack instances id. """ _def = list( @@ -1713,12 +1741,16 @@ def merge_via_along_lines( if not _def: self._logger.error(f"No padstack definition found for net {net_name}") return False + instances_created = [] _instances_to_delete = [] padstack_instances = [] - for pdstk_def in _def: - padstack_instances.append( - [inst for inst in self.definitions[pdstk_def].instances if inst.net_name == net_name] - ) + if padstack_instances_id: + padstack_instances = [[self.instances[id] for id in padstack_instances_id]] + else: + for pdstk_def in _def: + padstack_instances.append( + [inst for inst in self.definitions[pdstk_def].instances if inst.net_name == net_name] + ) for pdstk_series in padstack_instances: instances_location = [inst.position for inst in pdstk_series] lines, line_indexes = GeometryOperators.find_points_along_lines( @@ -1750,8 +1782,65 @@ def merge_via_along_lines( polygon_hole=polygon_data, ): self._logger.error(f"Failed to create padstack definition {new_padstack_def}") - if not self.place(position=[0, 0], definition_name=new_padstack_def, net_name=net_name): + new_instance = self.place(position=[0, 0], definition_name=new_padstack_def, net_name=net_name) + if not new_instance: self._logger.error(f"Failed to place padstack instance {new_padstack_def}") + else: + instances_created.append(new_instance.id) for inst in _instances_to_delete: inst.delete() - return True + return instances_created + + def reduce_via_in_bounding_box(self, bounding_box, x_samples, y_samples, nets=None): + """ + reduce the number of vias intersecting bounding box and nets by x and y samples. + + Parameters + ---------- + bounding_box : tuple or list. + bounding box, [x1, y1, x2, y2] + x_samples : int + y_samples : int + nets : str or list, optional + net name of list of nets name applying filtering on padstack instances selection. If ``None`` is provided + all instances are included in the index. Default value is ``None``. + + Returns + ------- + bool + ``True`` when succeeded ``False`` when failed. < + """ + + padstacks_inbox = self.get_padstack_instances_intersecting_bounding_box(bounding_box, nets) + if not padstacks_inbox: + self._logger.info("no padstack in bounding box") + return False + else: + if len(padstacks_inbox) <= (x_samples * y_samples): + self._logger.info(f"more samples {x_samples * y_samples} than existing {len(padstacks_inbox)}") + return False + else: + # extract ids and positions + vias = {item: self.instances[item].position for item in padstacks_inbox} + ids, positions = zip(*vias.items()) + pt_x, pt_y = zip(*positions) + + # meshgrid + _x_min, _x_max = min(pt_x), max(pt_x) + _y_min, _y_max = min(pt_y), max(pt_y) + + x_grid, y_grid = np.meshgrid( + np.linspace(_x_min, _x_max, x_samples), np.linspace(_y_min, _y_max, y_samples) + ) + + # mapping to meshgrid + to_keep = { + ids[np.argmin(np.square(_x - pt_x) + np.square(_y - pt_y))] + for _x, _y in zip(x_grid.ravel(), y_grid.ravel()) + } + + for item in padstacks_inbox: + if item not in to_keep: + self.instances[item].delete() + + return True diff --git a/src/pyedb/dotnet/database/siwave.py b/src/pyedb/dotnet/database/siwave.py index bd8b03efb3..8d05d46368 100644 --- a/src/pyedb/dotnet/database/siwave.py +++ b/src/pyedb/dotnet/database/siwave.py @@ -1242,8 +1242,7 @@ def create_pin_group(self, reference_designator, pin_numbers, group_name=None): ) if edb_pingroup.IsNull(): # pragma: no cover - self._logger.error(f"Failed to create pin group {group_name}.") - return False + raise RuntimeError(f"Failed to create pin group {group_name}.") else: names = [i for i in pins if i.GetNet().GetName()] edb_pingroup.SetNet(names[0].GetNet()) diff --git a/src/pyedb/dotnet/edb.py b/src/pyedb/dotnet/edb.py index 832709155a..a9e8db4a2c 100644 --- a/src/pyedb/dotnet/edb.py +++ b/src/pyedb/dotnet/edb.py @@ -4568,6 +4568,7 @@ def create_model_for_arbitrary_wave_ports( port_poly = cloned_edb.modeler.create_polygon( main_shape=void_info[0].polygon_data._edb_object, layer_name="ref", net_name="GND" ) + port_poly.scale(1.1) pec_poly = cloned_edb.modeler.create_polygon( main_shape=port_poly.polygon_data._edb_object, layer_name="port_pec", net_name="GND" ) diff --git a/src/pyedb/generic/process.py b/src/pyedb/generic/process.py index f6c2578cfe..813614881b 100644 --- a/src/pyedb/generic/process.py +++ b/src/pyedb/generic/process.py @@ -1,7 +1,7 @@ import os.path import subprocess -from pyedb.generic.general_methods import env_path, is_linux +from pyedb.generic.general_methods import env_path, is_linux, is_windows class SiwaveSolve(object): @@ -144,19 +144,19 @@ def export_3d_cad( f.write("oDoc.ScrExport3DModel('{}', q3d_filename)\n".format(format_3d)) f.write("oDoc.ScrCloseProject()\n") f.write("oApp.Quit()\n") - if is_linux: - _exe = '"' + os.path.join(self.installer_path, "siwave") + '"' - else: - _exe = '"' + os.path.join(self.installer_path, "siwave.exe") + '"' + _exe = os.path.join(self.installer_path, "siwave") + if is_windows: + _exe += ".exe" command = [_exe] if hidden: command.append("-embedding") - command.append("-RunScriptAndExit") - command.append(scriptname) + command += ["-RunScriptAndExit", scriptname] print(command) - os.system(" ".join(command)) - # p1 = subprocess.call(" ".join(command)) - # p1.wait() + try: + result = subprocess.run(command, check=True, capture_output=True) + print(result.stdout.decode()) + except subprocess.CalledProcessError as e: + print(f"Error occurred: {e.stderr.decode()}") return os.path.join(output_folder, aedt_file_name) def export_dc_report( diff --git a/tests/example_models/TEDB/vias_300.aedb/edb.def b/tests/example_models/TEDB/vias_300.aedb/edb.def new file mode 100644 index 0000000000..bb46d0e2b4 Binary files /dev/null and b/tests/example_models/TEDB/vias_300.aedb/edb.def differ diff --git a/tests/legacy/system/test_edb.py b/tests/legacy/system/test_edb.py index d5475e0ff3..6ec5c9238b 100644 --- a/tests/legacy/system/test_edb.py +++ b/tests/legacy/system/test_edb.py @@ -40,6 +40,8 @@ pytestmark = [pytest.mark.system, pytest.mark.legacy] +ON_CI = os.environ.get("CI", "false").lower() == "true" + class TestClass: @pytest.fixture(autouse=True) @@ -410,6 +412,9 @@ def test_create_custom_cutout_4(self): # assert edb.active_layout # edb.close() + @pytest.mark.skipif( + is_linux and ON_CI, reason="Test is slow due to software rendering fallback and lack of GPU acceleration." + ) def test_export_to_hfss(self): """Export EDB to HFSS.""" edb = Edb( @@ -419,10 +424,13 @@ def test_export_to_hfss(self): options_config = {"UNITE_NETS": 1, "LAUNCH_Q3D": 0} out = edb.write_export3d_option_config_file(self.local_scratch.path, options_config) assert os.path.exists(out) - out = edb.export_hfss(self.local_scratch.path) + out = edb.export_hfss(self.local_scratch.path, hidden=True) assert os.path.exists(out) edb.close() + @pytest.mark.skipif( + is_linux and ON_CI, reason="Test is slow due to software rendering fallback and lack of GPU acceleration." + ) def test_export_to_q3d(self): """Export EDB to Q3D.""" edb = Edb( @@ -436,7 +444,10 @@ def test_export_to_q3d(self): assert os.path.exists(out) edb.close() - def test_074_export_to_maxwell(self): + @pytest.mark.skipif( + is_linux and ON_CI, reason="Test is slow due to software rendering fallback and lack of GPU acceleration." + ) + def test_export_to_maxwell(self): """Export EDB to Maxwell 3D.""" edb = Edb( edbpath=os.path.join(local_path, "example_models", test_subfolder, "simple.aedb"), @@ -1712,7 +1723,7 @@ def test_workflow(self, edb_examples): assert path_bom.exists() edbapp.close() - def test_create_port_ob_component_no_ref_pins_in_component(self, edb_examples): + def test_create_port_on_component_no_ref_pins_in_component(self, edb_examples): from pyedb.generic.constants import SourceType edbapp = edb_examples.get_no_ref_pins_component() diff --git a/tests/legacy/system/test_edb_configuration_2p0.py b/tests/legacy/system/test_edb_configuration_2p0.py index 3d235fb609..c8f1f52655 100644 --- a/tests/legacy/system/test_edb_configuration_2p0.py +++ b/tests/legacy/system/test_edb_configuration_2p0.py @@ -626,7 +626,8 @@ def test_09_padstack_definition(self, edb_examples): data = {"padstacks": {"definitions": [DEFINITION], "instances": [INSTANCE]}} edbapp = edb_examples.get_si_verse() - assert edbapp.configuration.load(data, apply_file=True) + assert edbapp.configuration.load(data, apply_file=False) + edbapp.configuration.run() data_from_layout = edbapp.configuration.get_data_from_db(padstacks=True) pdef = [i for i in data_from_layout["padstacks"]["definitions"] if i["name"] == "v35h15"][0] diff --git a/tests/legacy/system/test_edb_padstacks.py b/tests/legacy/system/test_edb_padstacks.py index 59fdc9964e..14806b5359 100644 --- a/tests/legacy/system/test_edb_padstacks.py +++ b/tests/legacy/system/test_edb_padstacks.py @@ -23,6 +23,7 @@ """Tests related to Edb padstacks """ import os +from pathlib import Path import pytest @@ -452,16 +453,34 @@ def test_via_fence(self): assert "via_central" in edbapp.padstacks.definitions edbapp.close() edbapp = Edb(target_path2, edbversion=desktop_version) - assert edbapp.padstacks.merge_via_along_lines( - net_name="GND", distance_threshold=2e-3, minimum_via_number=6, selected_angles=[0, 180] - ) + assert edbapp.padstacks.merge_via_along_lines(net_name="GND", distance_threshold=2e-3, minimum_via_number=6) assert "main_via" in edbapp.padstacks.definitions assert "via_central" in edbapp.padstacks.definitions edbapp.close() + def test_reduce_via_in_bounding_box(self): + source_path = Path(__file__).parent.parent.parent / "example_models" / "TEDB" / "vias_300.aedb" + edbapp = Edb(edbpath=source_path) + assert len(edbapp.padstacks.instances) == 301 + # empty bounding box + assert edbapp.padstacks.reduce_via_in_bounding_box([-16e-3, -7e-3, -13e-3, -6e-3], 10, 10) is False + # over sampling + assert edbapp.padstacks.reduce_via_in_bounding_box([-20e-3, -10e-3, 20e-3, 10e-3], 20, 20) is False + + assert edbapp.padstacks.reduce_via_in_bounding_box([-20e-3, -10e-3, 20e-3, 10e-3], 10, 10) is True + assert len(edbapp.padstacks.instances) == 96 + edbapp.close_edb() + def test_via_merge(self, edb_examples): edbapp = edb_examples.get_si_verse() polygon = [[[118e-3, 60e-3], [125e-3, 60e-3], [124e-3, 56e-3], [118e-3, 56e-3]]] result = edbapp.padstacks.merge_via(contour_boxes=polygon, start_layer="1_Top", stop_layer="16_Bottom") assert len(result) == 1 edbapp.close() + + def test_via_merge2(self, edb_examples): + edbapp = edb_examples.get_si_verse() + polygon = [[[123.37e-3, 69.5e-3], [124.83e-3, 69.25e-3], [124.573e-3, 60.23e-3], [123e-3, 60.5e-3]]] + result = edbapp.padstacks.merge_via(contour_boxes=polygon, start_layer="1_Top", stop_layer="16_Bottom") + assert len(result) == 1 + edbapp.close() diff --git a/tests/legacy/unit/test_edbsiwave.py b/tests/legacy/unit/test_edbsiwave.py index 0ac34c9607..28bd21beed 100644 --- a/tests/legacy/unit/test_edbsiwave.py +++ b/tests/legacy/unit/test_edbsiwave.py @@ -32,9 +32,9 @@ class TestClass: @pytest.fixture(autouse=True) - def init(self): + def init(self, tmpdir): self.edb = Mock() - self.edb.edbpath = os.path.join(os.path.expanduser("~"), "fake_edb.aedb") + self.edb.edbpath = os.path.join(tmpdir, "fake_edb.aedb") self.siwave = EdbSiwave(self.edb) def test_siwave_add_syz_analsyis(self): diff --git a/tests/legacy/unit/test_padstack.py b/tests/legacy/unit/test_padstack.py index fd7f3ba56a..8aa4ac5cb2 100644 --- a/tests/legacy/unit/test_padstack.py +++ b/tests/legacy/unit/test_padstack.py @@ -20,6 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. + from mock import MagicMock, PropertyMock, patch import pytest