Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/changelog.d/2217.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Issue #2213 fixed
11 changes: 3 additions & 8 deletions src/pyedb/configuration/cfg_components.py
Original file line number Diff line number Diff line change
Expand Up @@ -302,8 +302,7 @@ def _set_solder_ball_properties_to_edb(self):
if self._pedb.grpc:
if not shape:
raise ValueError("Solderball shape must be either cylinder or spheroid")
cp = self.pyedb_obj.component_property
sbp = cp.solder_ball_property
sbp = self.pyedb_obj.component_property.solder_ball_property
shape_lower = shape.lower()
if shape_lower == "cylinder":
sbp.set_diameter(self._pedb.value(diameter))
Expand Down Expand Up @@ -336,12 +335,8 @@ def _retrieve_ic_die_properties_from_edb(self):
die_props = self.pyedb_obj.ic_die_properties
die_type = die_props.die_type
temp = {"type": "no_die" if die_type in _NO_DIE_TYPES else die_type}
if die_type in _NO_DIE_TYPES:
if self._pedb and self._pedb.grpc:
orientation = die_props.die_orientation
temp["orientation"] = orientation if orientation is not None else "chip_up"
else:
temp["orientation"] = die_props.die_orientation
temp["orientation"] = die_props.die_orientation
if die_type not in _NO_DIE_TYPES:
if die_type == "wire_bond":
temp["height"] = str(die_props.height)
self.ic_die_properties = temp
Expand Down
6 changes: 5 additions & 1 deletion src/pyedb/configuration/cfg_s_parameter_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,11 @@ def apply(self):
for s_param in self.models:
fpath = s_param.file_path
if not Path(fpath).anchor:
fpath = str(Path(self.path_libraries) / fpath)
if self.path_libraries:
base = Path(self.path_libraries)
else:
base = Path(self._pedb.edbpath).parent
fpath = str(base / fpath)
comp_def = self._pedb.definitions.component[s_param.component_definition]
if s_param.pin_order:
comp_def.set_properties(pin_order=s_param.pin_order)
Expand Down
6 changes: 5 additions & 1 deletion src/pyedb/configuration/cfg_spice_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,11 @@ def apply(self):
if self._pedb is None:
return self.model_dump(exclude_none=True)
if not Path(self.file_path).anchor:
fpath = str(Path(self._path_libraries or "") / self.file_path)
if self._path_libraries:
base = Path(self._path_libraries)
else:
base = Path(self._pedb.edbpath).parent
fpath = str(base / self.file_path)
else:
fpath = self.file_path

Expand Down
2 changes: 1 addition & 1 deletion src/pyedb/grpc/database/hierarchy/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -1389,7 +1389,7 @@ def assign_spice_model(
pname, pnumber = pair
if pname not in pin_names_sp: # pragma: no cover
raise ValueError(f"Pin name {pname} doesn't exist in {file_path}.")
model.core.add_terminal(str(pnumber), pname)
model.core.add_terminal(pname, str(pnumber))
Comment thread
svandenb-dev marked this conversation as resolved.
else:
for idx, pname in enumerate(pin_names_sp):
model.core.add_terminal(pname, str(idx + 1))
Expand Down
6 changes: 1 addition & 5 deletions tests/system/test_edb_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -1911,11 +1911,7 @@ def test_17_ic_die_properties(self):
comps_edb = db.configuration.get_data_from_db(components=True)["components"]
component = [i for i in comps_edb if i["reference_designator"] == "U8"][0]
assert component["ic_die_properties"]["type"] in ["none", "no_die", "flip_chip", "flipchip"]
if db.grpc:
# grpc does have this property not DotNet.
assert "orientation" in component["ic_die_properties"]
else:
assert "orientation" not in component["ic_die_properties"]
assert "orientation" in component["ic_die_properties"]
assert "height" not in component["ic_die_properties"]
db.configuration.load(U8_IC_DIE_PROPERTIES, apply_file=True)
comps_edb = db.configuration.get_data_from_db(components=True)["components"]
Expand Down
161 changes: 120 additions & 41 deletions tests/system/test_edb_stackup.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import math
import os
import platform
import time

import pytest

Expand All @@ -36,6 +37,43 @@

@pytest.mark.usefixtures("close_rpc_session")
class TestClass(BaseTestClass):
@staticmethod
def _wait_until_ready(path, attempts=20, delay=0.25):
"""Wait until a file path exists and is non-empty."""
for _ in range(attempts):
if os.path.exists(path) and os.path.getsize(path) > 0:
return True
time.sleep(delay)
return os.path.exists(path) and os.path.getsize(path) > 0

def _load_stackup_with_retries(self, source_path, stackup_path, **kwargs):
"""Open an EDB and import stackup with bounded retries for CI filesystem/server races."""
last_error = None
for _ in range(3):
edbapp = None
try:
edbapp = self.edb_examples.load_edb(source_path)
assert edbapp.stackup.load(stackup_path, **kwargs)
return
except Exception as e:
last_error = e
time.sleep(0.5)
finally:
if edbapp:
edbapp.close(terminate_rpc_session=False)
raise AssertionError(f"Failed to load stackup after retries: {last_error}")

def _get_si_verse_with_retries(self):
"""Open the SI-Verse example with retries to reduce transient CI failures."""
last_error = None
for _ in range(3):
try:
return self.edb_examples.get_si_verse()
except Exception as e:
last_error = e
time.sleep(0.5)
raise AssertionError(f"Failed to open SI-Verse example after retries: {last_error}")

def test_stackup_get_signal_layers(self):
"""Report residual copper area per layer."""
edbapp = self.edb_examples.get_si_verse()
Expand Down Expand Up @@ -287,18 +325,21 @@ def test_stackup_properties_0(self):
assert edbapp.stackup.add_layer("new_bottom", "1_Top", "add_at_elevation", "dielectric", elevation=0.0003)
edbapp.close(terminate_rpc_session=False)

@pytest.mark.skipif(config["use_grpc"] and platform.system() == "Linux", reason="random fails on Linux.")
def test_stackup_properties_1(self):
"""Evaluate various stackup properties."""
edbapp = self.edb_examples.create_empty_edb()
import_method = edbapp.stackup.load
export_method = edbapp.stackup.export

csv_path = self.edb_examples.copy_test_files_into_local_folder("TEDB/ansys_pcb_stackup.csv")[0]
# Verify the file is fully written before importing (guards against
# Linux filesystem flush races that caused intermittent failures).
assert os.path.exists(csv_path) and os.path.getsize(csv_path) > 0, f"CSV file not ready: {csv_path}"
assert import_method(csv_path)
assert self._wait_until_ready(csv_path), f"CSV file not ready: {csv_path}"
import_ok = False
for _ in range(3):
import_ok = import_method(csv_path)
if import_ok:
break
time.sleep(0.5)
assert import_ok
assert "18_Bottom" in edbapp.stackup.layers.keys()
assert edbapp.stackup.add_layer("19_Bottom", None, "add_on_top", material="iron")
export_stackup_path = os.path.join(self.edb_examples.test_folder, "export_stackup.csv")
Expand All @@ -307,16 +348,21 @@ def test_stackup_properties_1(self):

edbapp.close(terminate_rpc_session=False)

@pytest.mark.skipif(config["use_grpc"] and platform.system() == "Linux", reason="random fails on Linux.")
def test_stackup_properties_2(self):
"""Evaluate various stackup properties (JSON export round-trip)."""
edbapp = self.edb_examples.create_empty_edb()
import_method = edbapp.stackup.load
export_method = edbapp.stackup.export

csv_path = self.edb_examples.copy_test_files_into_local_folder("TEDB/ansys_pcb_stackup.csv")[0]
assert os.path.exists(csv_path) and os.path.getsize(csv_path) > 0, f"CSV file not ready: {csv_path}"
assert import_method(csv_path)
assert self._wait_until_ready(csv_path), f"CSV file not ready: {csv_path}"
import_ok = False
for _ in range(3):
import_ok = import_method(csv_path)
if import_ok:
break
time.sleep(0.5)
assert import_ok
assert "18_Bottom" in edbapp.stackup.layers.keys()
assert edbapp.stackup.add_layer("19_Bottom", None, "add_on_top", material="iron")
export_stackup_path = os.path.join(self.edb_examples.test_folder, "export_stackup_2.csv")
Expand All @@ -329,13 +375,10 @@ def test_stackup_load_json(self):
source_path, fpath = self.edb_examples.copy_test_files_into_local_folder(
["TEDB/ANSYS-HSD_V1.aedb", "TEDB/stackup.json"]
)
# On Linux, shutil.copytree may return before the OS has fully flushed
# all inodes; verify both paths are accessible before opening.
assert os.path.exists(source_path), f"EDB source not ready: {source_path}"
assert os.path.exists(fpath) and os.path.getsize(fpath) > 0, f"JSON not ready: {fpath}"
edbapp = self.edb_examples.load_edb(source_path)
assert edbapp.stackup.load(fpath)
edbapp.close(terminate_rpc_session=False)
edb_def = os.path.join(source_path, "edb.def")
assert self._wait_until_ready(edb_def), f"EDB source not ready: {edb_def}"
assert self._wait_until_ready(fpath), f"JSON not ready: {fpath}"
self._load_stackup_with_retries(source_path, fpath)

def test_stackup_export_json(self):
"""Export stackup into a JSON file."""
Expand Down Expand Up @@ -406,6 +449,7 @@ def test_stackup_load_xml(self):
edbapp = self.edb_examples.get_si_verse()
except Exception as e:
pytest.skip(f"Skipping test due to file access failure (possible CI instability): {e}")
assert self._wait_until_ready(file_path), f"XML file not ready: {file_path}"
assert edbapp.stackup.load(file_path)
assert "Inner1" in list(edbapp.stackup.layers.keys()) # Renamed layer
assert "DE1" not in edbapp.stackup.layers.keys() # Removed layer
Expand All @@ -419,13 +463,27 @@ def test_stackup_load_layer_renamed(self):
source_path, fpath = self.edb_examples.copy_test_files_into_local_folder(
["TEDB/ANSYS-HSD_V1.aedb", "TEDB/stackup_renamed.json"]
)

edbapp = self.edb_examples.load_edb(source_path)
edbapp.stackup.load(fpath, rename=True)
assert "1_Top_renamed" in edbapp.stackup.layers
assert "DE1_renamed" in edbapp.stackup.layers
assert "16_Bottom_renamed" in edbapp.stackup.layers
edbapp.close(terminate_rpc_session=False)
edb_def = os.path.join(source_path, "edb.def")
assert self._wait_until_ready(edb_def), f"EDB source not ready: {edb_def}"
assert self._wait_until_ready(fpath), f"JSON not ready: {fpath}"

last_error = None
for _ in range(3):
edbapp = None
try:
edbapp = self.edb_examples.load_edb(source_path)
assert edbapp.stackup.load(fpath, rename=True)
assert "1_Top_renamed" in edbapp.stackup.layers
assert "DE1_renamed" in edbapp.stackup.layers
assert "16_Bottom_renamed" in edbapp.stackup.layers
return
except Exception as e:
last_error = e
time.sleep(0.5)
finally:
if edbapp:
edbapp.close(terminate_rpc_session=False)
raise AssertionError(f"Failed to load renamed stackup after retries: {last_error}")

def test_stackup_place_in_3d_with_flipped_stackup(self):
"""Place into another cell using 3d placement method with and
Expand Down Expand Up @@ -1369,14 +1427,21 @@ def test_stackup_limits_only_metals(self):
edbapp.close(terminate_rpc_session=False)

@pytest.mark.skipif(not config["use_grpc"], reason="gRPC only.")
@pytest.mark.skipif(platform.system() == "Linux", reason="random fails on Linux.")
def test_stackup_export_csv(self):
"""Export stackup to CSV format."""
edbapp = self.edb_examples.get_si_verse()
edbapp = self._get_si_verse_with_retries()
csv_path = os.path.join(self.edb_examples.test_folder, "test_export_stackup.csv")
assert edbapp.stackup.export(csv_path)
assert os.path.exists(csv_path)
edbapp.close(terminate_rpc_session=False)
try:
export_ok = False
for _ in range(3):
export_ok = edbapp.stackup.export(csv_path)
if export_ok and self._wait_until_ready(csv_path):
break
time.sleep(0.5)
assert export_ok
assert self._wait_until_ready(csv_path), f"CSV export not ready: {csv_path}"
finally:
edbapp.close(terminate_rpc_session=False)

@pytest.mark.skipif(not config["use_grpc"], reason="gRPC only.")
def test_stackup_export_unsupported_format(self):
Expand Down Expand Up @@ -1473,20 +1538,28 @@ def test_stackup_adjust_solder_dielectrics(self):
edbapp.close(terminate_rpc_session=False)

@pytest.mark.skipif(not config["use_grpc"], reason="gRPC only.")
@pytest.mark.skipif(platform.system() == "Linux", reason="random fails on Linux.")
def test_stackup_export_json_with_material_in_layer(self):
"""Export stackup JSON with include_material_with_layer=True."""
import json

edbapp = self.edb_examples.get_si_verse()
edbapp = self._get_si_verse_with_retries()
json_path = os.path.join(self.edb_examples.test_folder, "stackup_with_material.json")
assert edbapp.stackup.export(json_path, include_material_with_layer=True)
with open(json_path, "r") as f:
data = json.load(f)
# When include_material_with_layer=True, there should be no top-level 'materials' key.
assert "materials" not in data
assert "layers" in data
edbapp.close(terminate_rpc_session=False)
try:
export_ok = False
for _ in range(3):
export_ok = edbapp.stackup.export(json_path, include_material_with_layer=True)
if export_ok and self._wait_until_ready(json_path):
break
time.sleep(0.5)
assert export_ok
assert self._wait_until_ready(json_path), f"JSON export not ready: {json_path}"
with open(json_path, "r") as f:
data = json.load(f)
# When include_material_with_layer=True, there should be no top-level 'materials' key.
assert "materials" not in data
assert "layers" in data
finally:
edbapp.close(terminate_rpc_session=False)

@pytest.mark.skipif(not config["use_grpc"], reason="gRPC only.")
def test_stackup_place_in_layout(self):
Expand All @@ -1510,10 +1583,16 @@ def test_stackup_place_in_layout(self):
edb1.close(terminate_rpc_session=False)

@pytest.mark.skipif(not config["use_grpc"], reason="gRPC only.")
@pytest.mark.skipif(platform.system() == "Linux", reason="random fails on Linux.")
def test_stackup_export_json_no_output_file(self):
"""_export_layer_stackup_to_json returns False when output_file is None."""
edbapp = self.edb_examples.get_si_verse()
result = edbapp.stackup._export_layer_stackup_to_json(output_file=None)
assert result is False
edbapp.close(terminate_rpc_session=False)
edbapp = self._get_si_verse_with_retries()
try:
last_result = None
for _ in range(3):
last_result = edbapp.stackup._export_layer_stackup_to_json(output_file=None)
if last_result is False:
break
time.sleep(0.5)
assert last_result is False
finally:
edbapp.close(terminate_rpc_session=False)
Loading
Loading