setupEM 0.1.3__tar.gz → 0.1.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,12 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: setupEM
3
- Version: 0.1.3
3
+ Version: 0.1.4
4
4
  Summary: Python tool for configuration of gds2palace workflow with GUI.
5
5
  Author-email: Volker Muehlhaus <volker@muehlhaus.com>
6
6
  Requires-Python: >=3.9
7
7
  Description-Content-Type: text/markdown
8
- Requires-Dist: gds2palace>=0.1.2
8
+ Requires-Dist: gds2palace>=0.1.4
9
9
  Requires-Dist: PySide6
10
+ Requires-Dist: scipy
10
11
 
11
12
  # Create EM Simulation model from GDSII layout files
12
13
 
@@ -4,15 +4,16 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "setupEM"
7
- version = "0.1.3"
7
+ version = "0.1.4"
8
8
  description = "Python tool for configuration of gds2palace workflow with GUI."
9
9
  readme = "README.md"
10
10
  authors = [
11
11
  { name = "Volker Muehlhaus", email = "volker@muehlhaus.com" }
12
12
  ]
13
13
  dependencies = [
14
- "gds2palace>=0.1.2",
15
- "PySide6"
14
+ "gds2palace>=0.1.4",
15
+ "PySide6",
16
+ "scipy"
16
17
  ]
17
18
  requires-python = ">=3.9"
18
19
 
@@ -17,15 +17,17 @@
17
17
  ########################################################################
18
18
 
19
19
 
20
- import sys, json, os, tempfile, pathlib, ast, webbrowser
20
+ import sys, json, os, pathlib, ast, webbrowser
21
+ import numpy as np
22
+ from scipy.interpolate import interp1d
21
23
  from PySide6.QtWidgets import (
22
24
  QApplication, QMainWindow, QWidget, QHBoxLayout, QVBoxLayout,
23
25
  QLabel, QLineEdit,QComboBox,QTableWidget,QHeaderView,
24
26
  QPushButton, QFileDialog, QTabWidget, QMessageBox, QGroupBox,
25
- QCheckBox, QAbstractItemView,QStyleFactory,QTableWidgetItem, QPlainTextEdit
27
+ QCheckBox, QAbstractItemView,QStyleFactory,QTableWidgetItem, QPlainTextEdit, QDialog
26
28
  )
27
- from PySide6.QtGui import QAction, QColor, QTextCharFormat, QFont, QSyntaxHighlighter
28
- from PySide6.QtCore import Qt, QRegularExpression, QProcess
29
+ from PySide6.QtGui import QAction, QColor, QTextCharFormat, QFont, QSyntaxHighlighter, QPainter, QPen
30
+ from PySide6.QtCore import Qt, QRegularExpression, QProcess, QRect
29
31
 
30
32
  # we expect gds2palace in the same directory as this code, or installed as module
31
33
  sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), 'gds2palace')))
@@ -267,8 +269,16 @@ class FileInputTab(QWidget):
267
269
 
268
270
  self.XML_file_layout.addWidget(self.XML_file_edit)
269
271
  self.XML_file_layout.addWidget(self.browse_XML_btn)
270
-
271
272
  self.XML_layout.addLayout(self.XML_file_layout)
273
+
274
+ self.XML_show_layout = QHBoxLayout()
275
+ self.XML_show_layout.setAlignment(Qt.AlignRight)
276
+ self.show_XML_btn = QPushButton("Show Stackup")
277
+ self.show_XML_btn.setFixedWidth(150)
278
+ self.show_XML_btn.clicked.connect(self.MainWindow.open_popup)
279
+ self.XML_show_layout.addWidget(self.show_XML_btn)
280
+ self.XML_layout.addLayout(self.XML_show_layout)
281
+
272
282
  self.XML_group.setLayout(self.XML_layout)
273
283
 
274
284
  self.main_layout.addWidget(self.gds_group)
@@ -312,6 +322,7 @@ class FileInputTab(QWidget):
312
322
  def set_XML_file (self, filename):
313
323
  self.XML_file_edit.setText(filename)
314
324
  saved_values ["SubstrateFile"] = filename.replace('\\','/')
325
+ self.MainWindow.read_XML() # safe if invalid filename
315
326
  # file is read when leaving the files tab
316
327
 
317
328
 
@@ -330,22 +341,11 @@ class FileInputTab(QWidget):
330
341
  # read_XML(self.XML_file_edit.text()) # safe if invalid filename
331
342
 
332
343
 
333
- # load technology stackup data
334
- def read_XML(self, filename):
335
- if pathlib.Path(filename).exists():
336
- materials_list, dielectrics_list, metals_list = stackup_reader.read_substrate (filename)
337
- self.MainWindow.ports_tab.update_layers(metals_list)
338
-
339
-
340
-
341
344
  def save_values(self):
342
345
  saved_values ["GdsFile"] = self.gds_file_edit.text().replace('\\','/')
343
346
  saved_values ["SubstrateFile"] = self.XML_file_edit.text().replace('\\','/')
344
347
  saved_values ["preprocess_gds"] = self.preprocess_gds_checkbox.isChecked()
345
348
 
346
- # read Substrate file, which also updates port target layer choices
347
- self.read_XML(saved_values ["SubstrateFile"])
348
-
349
349
  try:
350
350
  merge_polygon_size = float(self.viamerge_edit.text())
351
351
  except Exception:
@@ -365,6 +365,9 @@ class FileInputTab(QWidget):
365
365
  # also trigger the load function of CreateModelTab, because that uses gds file info
366
366
  self.MainWindow.create_model_tab.load_values()
367
367
 
368
+ # read Substrate file, which also updates port target layer choices
369
+ self.MainWindow.read_XML()
370
+
368
371
  return True # Tab change only possible when returning True
369
372
 
370
373
 
@@ -1660,7 +1663,7 @@ class ModelEditorTab(QWidget):
1660
1663
  "\n\tpurposelist=settings['purpose'], "
1661
1664
  "\n\tmetals_list=metals_list, \n\tpreprocess=settings['preprocess_gds'], "
1662
1665
  "\n\tmerge_polygon_size=settings['merge_polygon_size'],"
1663
- "\n\tgds_boundary_layers=[],"
1666
+ "\n\tgds_boundary_layers=dielectrics_list.get_boundary_layers(),"
1664
1667
  "\n\tmirror=False, "
1665
1668
  "\n\toffset_x=0, offset_y=0,"
1666
1669
  "\n\tlayernumber_offset=0)")
@@ -1778,6 +1781,327 @@ class CodeEditor(QPlainTextEdit):
1778
1781
  self.setLineWrapMode(QPlainTextEdit.NoWrap)
1779
1782
 
1780
1783
 
1784
+ # ---------- POP UP WINDOW TO SHOW STACKUP ------------------
1785
+
1786
+ def epsilon_to_color(erel, transparency):
1787
+ # Compute raw float components
1788
+ red = 250 - 30 * (erel - 1)
1789
+ green = 255 - 20 * (erel - 1) + (20 / erel) + 10 * erel
1790
+ blue = 100 + 15 * erel + (250 / erel)
1791
+
1792
+ # Extra adjustment
1793
+ if 3.8 < erel < 4.5:
1794
+ red += 50 * (erel - 3.8)
1795
+ green -= 100 * (erel - 3.8)
1796
+
1797
+ # Clamp to range 0–255
1798
+ red = min(max(red, 0), 255)
1799
+ green = min(max(green, 0), 255)
1800
+ blue = min(max(blue, 0), 255)
1801
+
1802
+ # Convert to integer RGB
1803
+ r = int(round(red))
1804
+ g = int(round(green))
1805
+ b = int(round(blue))
1806
+
1807
+ return QColor(r, g, b, transparency)
1808
+
1809
+
1810
+ class VectorWidget(QWidget):
1811
+ """This widget actually draws the stackup preview"""
1812
+
1813
+ def __init__(self, materials_list,dielectrics_list,metals_list):
1814
+ super().__init__()
1815
+ self.materials_list = materials_list
1816
+ self.dielectrics_list = dielectrics_list
1817
+ self.metals_list = metals_list
1818
+
1819
+
1820
+ def paintEvent(self, event):
1821
+
1822
+ # utility: flip y to have y=0 at bottom
1823
+ def flipy(y):
1824
+ return self.height()-y
1825
+
1826
+ # utility to draw text with alignment on right side
1827
+ def drawText_right (x, y, w, h, text):
1828
+ rect = QRect(x, y-h, w, h)
1829
+ painter.drawText(rect, Qt.AlignVCenter | Qt.AlignRight, text)
1830
+
1831
+ def drawText_left (x, y, w, h, text):
1832
+ rect = QRect(x, y-h, w, h)
1833
+ painter.drawText(rect, Qt.AlignVCenter | Qt.AlignLeft, text)
1834
+
1835
+ painter = QPainter(self)
1836
+ painter.fillRect(self.rect(), Qt.white)
1837
+ painter.setRenderHint(QPainter.Antialiasing)
1838
+
1839
+ xmin = int(self.width()*0.02)
1840
+ xmax = int(self.width()*0.98)
1841
+
1842
+ ymin = int(self.height()*0.025)
1843
+ ymax = int(self.height()*0.975)
1844
+
1845
+ penBlack = QPen(Qt.black, 1)
1846
+ penGray = QPen(QColor(134, 132, 130))
1847
+ penDarkGray = QPen(QColor(53, 50, 47))
1848
+
1849
+ # get total dielectric parts, where each metal in a dielectric adds one part
1850
+ dielectric_shapes = []
1851
+ total_parts = 0
1852
+ dielectrics_bottom_up = self.dielectrics_list.dielectrics[::-1]
1853
+ for dielectric in dielectrics_bottom_up: # bottom up
1854
+ painter.setPen(penBlack)
1855
+
1856
+ metals_inside = dielectric.get_planar_metals_inside()
1857
+ # get number of unique zmin values in that list
1858
+ zmin_list = []
1859
+ for metal in metals_inside:
1860
+ if not metal.zmin in zmin_list:
1861
+ zmin_list.append(metal.zmin)
1862
+ metals_count = len(zmin_list)
1863
+
1864
+ # first metal not aligned with dielectric?
1865
+ if len(metals_inside)>0:
1866
+ if metals_inside[0].zmin > dielectric.zmin:
1867
+ metals_count = metals_count+ 0.5
1868
+
1869
+ parts = max (1,metals_count)
1870
+ dielectric_shape = {}
1871
+ dielectric_shape['name'] = dielectric.name
1872
+ dielectric_shape['dielectric'] = dielectric
1873
+ dielectric_shape['numparts'] = parts
1874
+
1875
+ # dielectric color is calculated from permittivity
1876
+ materialname = dielectric.material
1877
+ material = self.materials_list.get_by_name(materialname)
1878
+ eps = material.eps
1879
+ dielectric_shape['color'] = epsilon_to_color(eps, 95) # QColor(Qt.white)
1880
+ dielectric_shape['material'] = material
1881
+
1882
+ total_parts = total_parts + parts
1883
+ dielectric_shapes.append(dielectric_shape)
1884
+
1885
+ # calculate height of one dielectric shape
1886
+ total_parts = max(total_parts, 1)
1887
+ part_height = int((ymax-ymin)/(total_parts))
1888
+
1889
+ y = ymin
1890
+ w = xmax-ymin
1891
+
1892
+ # we need to store data for original z position and the displayed y position
1893
+ stored_z = np.array([0])
1894
+ stored_y = np.array([0])
1895
+
1896
+
1897
+ for dielectric_shape in dielectric_shapes:
1898
+ h = part_height * dielectric_shape['numparts']
1899
+ dielectric = dielectric_shape['dielectric']
1900
+ color = dielectric_shape['color']
1901
+ material = dielectric_shape['material']
1902
+
1903
+ material_string = f'εr={material.eps:.1f}'
1904
+ if material.sigma > 1e-3:
1905
+ material_string = material_string + f' σ={material.sigma:.1f}'
1906
+ material_string = material_string + f'\n{dielectric.thickness:.2f}µm'
1907
+
1908
+ painter.setPen(penBlack)
1909
+ painter.setBrush(color)
1910
+ painter.drawRect(xmin, flipy(y), w, -h)
1911
+ drawText_left (xmin+5, flipy(y), w, h, dielectric.name)
1912
+ drawText_right (xmin, flipy(y), w-5, h, material_string)
1913
+
1914
+ if not dielectric.zmax in stored_z:
1915
+ stored_z = np.append(stored_z, dielectric.zmax)
1916
+ stored_y = np.append(stored_y, y+h)
1917
+
1918
+
1919
+ # get metals inside this dielectric
1920
+ metals_inside = dielectric.get_planar_metals_inside()
1921
+ # height for one dielectric segment including one metal is part_height
1922
+ if len(metals_inside) > 0:
1923
+
1924
+ # there could be multiple metals starting at the same zmin
1925
+
1926
+ # draw planar metals, one after another
1927
+ ymetal = y
1928
+ for n, metal in enumerate(metals_inside):
1929
+
1930
+ painter.setPen(penBlack)
1931
+
1932
+ # check if metal is aligned with dielectric zmin
1933
+ elevation = metal.zmin-dielectric.zmin
1934
+ if n==0 and (abs(elevation) > 0.001):
1935
+ # draw some vertical offset, not aligned with dielectric
1936
+ ymetal = ymetal + part_height*0.5 # slight offset
1937
+
1938
+ # check if next metal is at same zmin
1939
+ next_at_same_zmin = False
1940
+ previous_at_same_zmin = False
1941
+ xmetal = xmin+120
1942
+ wmetal = w-200
1943
+
1944
+ if n < len(metals_inside)-1:
1945
+ next_metal = metals_inside[n+1]
1946
+ if abs(next_metal.zmin - metal.zmin) < 0.001:
1947
+ next_at_same_zmin = True
1948
+ xmetal = xmin+120
1949
+ wmetal = int(w/2)-100
1950
+ else:
1951
+ next_metal = None
1952
+
1953
+ if n > 0:
1954
+ previous_metal = metals_inside[n-1]
1955
+ if abs(previous_metal.zmin - metal.zmin) < 0.001:
1956
+ xmetal = xmin+int(w/2)+20
1957
+ wmetal = int(w/2)-100
1958
+ previous_at_same_zmin = True
1959
+
1960
+ material = self.materials_list.get_by_name(metal.material)
1961
+
1962
+ if metal.is_sheet:
1963
+ # sheet metal that is simulated with zero extrusion
1964
+ height = 3
1965
+ resistance_string = f'Rs={material.Rs*1e3:.1f}mΩ'
1966
+ else:
1967
+ # regular extruded metal
1968
+ height = part_height/2
1969
+ if (material.sigma > 0) and (metal.thickness > 0):
1970
+ Rs = 1 / (material.sigma*metal.thickness*1e-6)
1971
+ if Rs < 1:
1972
+ resistance_string = f'Rs={Rs*1e3:.1f} mΩ'
1973
+ else:
1974
+ resistance_string = f'Rs={Rs:.2f} Ω'
1975
+ else:
1976
+ resistance_string = '? ' + material.type + ' ?'
1977
+
1978
+ # the box for this metal
1979
+ if material.type.upper() == "CONDUCTOR":
1980
+ painter.setBrush(QColor(230,230,230, 90))
1981
+ painter.drawRect(xmetal, flipy(ymetal), wmetal, -int(height))
1982
+ else:
1983
+ painter.setBrush(QColor(230,130,130, 90))
1984
+ painter.drawRect(xmetal, flipy(ymetal), wmetal, -int(height))
1985
+
1986
+ painter.setPen(penBlack)
1987
+ drawText_left(xmetal+10, flipy(ymetal), wmetal, part_height/2, metal.name)
1988
+ painter.setPen(penGray)
1989
+ drawText_right(xmetal, flipy(ymetal), wmetal-10, part_height/2, resistance_string)
1990
+ # store the drawing position, because vias will refer to that
1991
+ if not metal.zmin in stored_z:
1992
+ stored_z = np.append(stored_z, metal.zmin)
1993
+ stored_y = np.append(stored_y, ymetal)
1994
+ if not metal.zmax in stored_z:
1995
+ stored_z = np.append(stored_z, metal.zmax)
1996
+ stored_y = np.append(stored_y, ymetal+height)
1997
+
1998
+ painter.setPen(penGray)
1999
+ painter.drawLine(xmetal-60, flipy(ymetal), xmetal-10, flipy(ymetal))
2000
+ # draw line at top side of metal
2001
+ if not metal.is_sheet:
2002
+ painter.drawLine(xmetal-60, flipy(ymetal+height), xmetal-10, flipy(ymetal+height))
2003
+ heightstring = f'{metal.thickness:.3f}µm'
2004
+ painter.setPen(penDarkGray)
2005
+ drawText_left(xmetal-60, flipy(ymetal), 50, height, heightstring)
2006
+
2007
+
2008
+ if not previous_at_same_zmin:
2009
+ # draw height to metal above
2010
+ if next_metal is not None:
2011
+ dz = abs(next_metal.zmin - metal.zmax)
2012
+ heightstring = f'{dz:.3f}µm'
2013
+ painter.setPen(penGray)
2014
+ drawText_left(xmetal-60, flipy(ymetal+height), 50, height, heightstring)
2015
+
2016
+ if n==len(metals_inside)-1:
2017
+ # last metal (top metal)
2018
+ # place text for distance to dielectric boundary
2019
+
2020
+ painter.setPen(penBlack)
2021
+ dz = dielectric.zmax - metal.zmax
2022
+ if dz > 10:
2023
+ heightstring = f'{dz:.1f}µm'
2024
+ else:
2025
+ heightstring = f'{dz:.3f}µm'
2026
+ painter.setPen(penGray)
2027
+ painter.drawText(xmetal-60, flipy(ymetal+height+5), heightstring)
2028
+
2029
+ if n==0 and elevation > 0.001:
2030
+ # metal not aligned with bottom of dielectric, add a label for offset value
2031
+ heightstring = f'{elevation:.3f}µm'
2032
+ painter.setPen(penGray)
2033
+ painter.drawText(xmetal-60, flipy(ymetal-10), heightstring)
2034
+
2035
+
2036
+ if not next_at_same_zmin:
2037
+ # increase screen y for next metal
2038
+ ymetal = ymetal + part_height
2039
+
2040
+ y = y + h
2041
+
2042
+ # sort stored positions
2043
+ if len(stored_z) > 2:
2044
+ idx = np.argsort(stored_z)
2045
+ y_sorted = stored_y[idx]
2046
+ z_sorted = stored_z[idx]
2047
+ z_to_y = interp1d(z_sorted, y_sorted, kind='cubic', fill_value='extrapolate')
2048
+
2049
+ # next we draw the vias, based on the screen position of metals that we have stored
2050
+ # via position alternates between 3 positions along x axis
2051
+ pos = 1
2052
+ w = (xmax-xmin)/10
2053
+
2054
+ painter.setBrush(QColor(136,192,200,80))
2055
+ for metal in self.metals_list.metals:
2056
+ if metal.is_via:
2057
+ y1 = z_to_y(metal.zmin)
2058
+ y2 = z_to_y(metal.zmax)
2059
+ h = abs(y2-y1)
2060
+
2061
+ if pos == 1:
2062
+ xvia = (xmax+xmin)/2-4*w/2
2063
+ pos = 2
2064
+ elif pos == 2:
2065
+ xvia = (xmax+xmin)/2-w/2
2066
+ pos = 3
2067
+ else:
2068
+ xvia = (xmax+xmin)/2+w
2069
+ pos = 1
2070
+
2071
+ painter.setPen(penBlack)
2072
+ painter.drawRect(xvia, flipy(y1), w, -h)
2073
+ painter.drawText(xvia+10, flipy(y1+5), metal.name)
2074
+
2075
+ painter.end()
2076
+
2077
+
2078
+ class PopUpWindow(QDialog):
2079
+ """This window shows the substrate stackup preview"""
2080
+ def __init__(self, MainWindow):
2081
+ super().__init__()
2082
+ self.setAttribute(Qt.WA_DeleteOnClose)
2083
+ self.setWindowTitle("Stackup Preview")
2084
+ self.resize(700, 800)
2085
+ self.MainWindow = MainWindow
2086
+
2087
+ layout = QVBoxLayout()
2088
+
2089
+ # Add the custom painting widget
2090
+ self.vector_widget = VectorWidget(self.MainWindow.materials_list,
2091
+ self.MainWindow.dielectrics_list,
2092
+ self.MainWindow.metals_list)
2093
+ layout.addWidget(self.vector_widget)
2094
+
2095
+ # Close button
2096
+ close_button = QPushButton("Close")
2097
+ close_button.clicked.connect(self.close)
2098
+ layout.addWidget(close_button)
2099
+
2100
+ self.setLayout(layout)
2101
+ self.setModal(True)
2102
+
2103
+
2104
+
1781
2105
  # ---------- MAIN WINDOW ----------
1782
2106
 
1783
2107
 
@@ -1822,6 +2146,11 @@ class MainWindow(QMainWindow):
1822
2146
  self.tabs_widget.currentChanged.connect(self.on_tab_change)
1823
2147
  main_layout.addWidget(self.tabs_widget)
1824
2148
 
2149
+ # Stackup data from XML, used when showing stackup viewer
2150
+ self.materials_list = None
2151
+ self.dielectrics_list = None
2152
+ self.metals_list = None
2153
+
1825
2154
  # Do not auto-load default values at this early startup stage,
1826
2155
  # instead this is done from File menu
1827
2156
  # self.user_inputs_file = DEFAULT_SETTINGS_FILE
@@ -1950,7 +2279,6 @@ class MainWindow(QMainWindow):
1950
2279
  self.load_configuration_from_file(file_path)
1951
2280
 
1952
2281
 
1953
-
1954
2282
  def load_configuration_from_file(self, file_path):
1955
2283
  if file_path:
1956
2284
  extension = pathlib.Path(file_path).suffix
@@ -2134,6 +2462,20 @@ class MainWindow(QMainWindow):
2134
2462
  saved_values['sim_path']=''
2135
2463
 
2136
2464
 
2465
+ # load technology stackup data
2466
+ def read_XML(self):
2467
+ filename = saved_values ["SubstrateFile"]
2468
+ if pathlib.Path(filename).exists():
2469
+ self.materials_list, self.dielectrics_list, self.metals_list = stackup_reader.read_substrate (filename)
2470
+ self.ports_tab.update_layers(self.metals_list)
2471
+
2472
+
2473
+ def open_popup(self):
2474
+ # self.read_XML(saved_values ["SubstrateFile"])
2475
+ self.popup = PopUpWindow(self)
2476
+ self.popup.show()
2477
+
2478
+
2137
2479
 
2138
2480
  def parse_assignments(file_path):
2139
2481
  # parse lines from a Python model code for variable assigments
@@ -2206,6 +2548,8 @@ def main():
2206
2548
 
2207
2549
  win = MainWindow()
2208
2550
  win.show()
2551
+
2552
+
2209
2553
  sys.exit(app.exec())
2210
2554
 
2211
2555
  if __name__ == "__main__":
@@ -1,12 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: setupEM
3
- Version: 0.1.3
3
+ Version: 0.1.4
4
4
  Summary: Python tool for configuration of gds2palace workflow with GUI.
5
5
  Author-email: Volker Muehlhaus <volker@muehlhaus.com>
6
6
  Requires-Python: >=3.9
7
7
  Description-Content-Type: text/markdown
8
- Requires-Dist: gds2palace>=0.1.2
8
+ Requires-Dist: gds2palace>=0.1.4
9
9
  Requires-Dist: PySide6
10
+ Requires-Dist: scipy
10
11
 
11
12
  # Create EM Simulation model from GDSII layout files
12
13
 
@@ -0,0 +1,3 @@
1
+ gds2palace>=0.1.4
2
+ PySide6
3
+ scipy
@@ -1,2 +0,0 @@
1
- gds2palace>=0.1.2
2
- PySide6
File without changes
File without changes
File without changes