setupEM 0.1.2__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.2
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.2"
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
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')))
@@ -123,31 +125,65 @@ def simulation_ports_to_struct (simulation_ports):
123
125
  return all_ports_list
124
126
 
125
127
 
126
- '''
127
- def add_struct_to_simulation_ports (all_ports_list):
128
- # import structure from JSON to all_simulation_ports
129
-
130
- for port in all_ports_list:
131
- portnumber = port.get(portnumber)
132
- source_layernum = port.get(source_layernum)
133
- target_layername = port.get(target_layername)
134
- from_layername = port.get(from_layername)
135
- to_layername = port.get(to_layername)
136
- direction = port.get(direction)
137
- port_Z0 = port.get(port_Z0)
138
- voltage = port.get(voltage)
139
-
140
- sim_port = simulation_setup.simulation_port(portnumber,
141
- voltage,
142
- port_Z0,
143
- source_layernum,
144
- target_layername=target_layername,
145
- from_layername=from_layername,
146
- to_layername=to_layername,
147
- direction=direction)
148
-
149
- simulation_ports.add_port(sim_port)
150
- '''
128
+
129
+ # ----------------------------------------
130
+
131
+ class FileDropLineEdit(QLineEdit):
132
+ def __init__(self, allowed_extensions=None, on_file_dropped=None):
133
+ super().__init__()
134
+ self.setAcceptDrops(True)
135
+
136
+ # Example: [".png", ".txt"]
137
+ self.allowed_extensions = allowed_extensions or []
138
+
139
+ # Function to execute after a successful drop
140
+ # Signature: callback(path: str)
141
+ self.on_file_dropped = on_file_dropped
142
+
143
+ def dragEnterEvent(self, event):
144
+ if event.mimeData().hasUrls() and self._contains_valid_files(event):
145
+ event.acceptProposedAction()
146
+ else:
147
+ event.ignore()
148
+
149
+ def dragMoveEvent(self, event):
150
+ if event.mimeData().hasUrls() and self._contains_valid_files(event):
151
+ event.acceptProposedAction()
152
+ else:
153
+ event.ignore()
154
+
155
+ def dropEvent(self, event):
156
+ if event.mimeData().hasUrls():
157
+ files = [url.toLocalFile() for url in event.mimeData().urls()]
158
+ valid_files = [f for f in files if self._is_valid(f)]
159
+
160
+ if valid_files:
161
+ file_path = valid_files[0]
162
+ self.setText(file_path)
163
+ event.acceptProposedAction()
164
+
165
+ # 🚀 Call the user function if set
166
+ if self.on_file_dropped:
167
+ self.on_file_dropped(file_path)
168
+
169
+ else:
170
+ self.setText("Invalid file type")
171
+ event.ignore()
172
+
173
+ def _contains_valid_files(self, event):
174
+ for url in event.mimeData().urls():
175
+ if self._is_valid(url.toLocalFile()):
176
+ return True
177
+ return False
178
+
179
+ def _is_valid(self, path):
180
+ if not self.allowed_extensions:
181
+ return True
182
+ ext = os.path.splitext(path)[1].lower()
183
+ return ext in self.allowed_extensions
184
+
185
+
186
+
151
187
 
152
188
  # ---------- FILE INPUT TAB ----------
153
189
  class FileInputTab(QWidget):
@@ -165,7 +201,8 @@ class FileInputTab(QWidget):
165
201
  self.gds_layout = QVBoxLayout()
166
202
 
167
203
  self.gds_file_layout = QHBoxLayout()
168
- self.gds_file_edit = QLineEdit("Please choose a file ===>")
204
+ self.gds_file_edit = FileDropLineEdit([".gds",".GDS"], self.set_gds_file)
205
+ self.gds_file_edit.setText( "Please choose a file ===>")
169
206
  self.gds_file_edit.setStyleSheet(EDIT_STYLE_REQUIRED)
170
207
 
171
208
  self.browse_gds_btn = QPushButton("Browse ...")
@@ -222,7 +259,8 @@ class FileInputTab(QWidget):
222
259
  self.XML_layout = QVBoxLayout()
223
260
 
224
261
  self.XML_file_layout = QHBoxLayout()
225
- self.XML_file_edit = QLineEdit("Please choose a file ===>")
262
+ self.XML_file_edit = FileDropLineEdit([".xml",".XML"], self.set_XML_file)
263
+ self.XML_file_edit.setText("Please choose a file ===>")
226
264
  self.XML_file_edit.setStyleSheet(EDIT_STYLE_REQUIRED)
227
265
 
228
266
  self.browse_XML_btn = QPushButton("Browse ...")
@@ -231,8 +269,16 @@ class FileInputTab(QWidget):
231
269
 
232
270
  self.XML_file_layout.addWidget(self.XML_file_edit)
233
271
  self.XML_file_layout.addWidget(self.browse_XML_btn)
234
-
235
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
+
236
282
  self.XML_group.setLayout(self.XML_layout)
237
283
 
238
284
  self.main_layout.addWidget(self.gds_group)
@@ -248,13 +294,18 @@ class FileInputTab(QWidget):
248
294
  previous_directory = os.path.dirname(previous_file)
249
295
  if not os.path.isdir(previous_directory):
250
296
  previous_directory = ""
297
+
298
+ filename, _ = QFileDialog.getOpenFileName(self, "Select GDSII File",previous_directory,"*.gds;;*.*")
299
+ if filename:
300
+ self.set_gds_file(filename)
251
301
 
252
- file_path, _ = QFileDialog.getOpenFileName(self, "Select GDSII File",previous_directory,"*.gds;;*.*")
253
- if file_path:
254
- self.gds_file_edit.setText(file_path)
255
- saved_values ["GdsFile"] = file_path.replace('\\','/')
256
302
 
257
-
303
+ def set_gds_file(self, filename):
304
+ # clear model name and target dir if they were auto-generated from previous model
305
+ self.MainWindow.clear_modelname_and_targetdir()
306
+ self.gds_file_edit.setText(filename)
307
+ saved_values ["GdsFile"] = filename.replace('\\','/')
308
+ # file is read when leaving the files tab
258
309
 
259
310
  def browse_XML_file(self):
260
311
  # start browsing from previous file location, if valid
@@ -265,9 +316,14 @@ class FileInputTab(QWidget):
265
316
 
266
317
  filename, _ = QFileDialog.getOpenFileName(self, "Select XML Stackup File",previous_directory,"*.xml;;*.*")
267
318
  if filename:
268
- self.XML_file_edit.setText(filename)
269
- saved_values ["SubstrateFile"] = filename.replace('\\','/')
270
- # file is read when leaving this tab
319
+ self.set_XML_file(filename)
320
+
321
+
322
+ def set_XML_file (self, filename):
323
+ self.XML_file_edit.setText(filename)
324
+ saved_values ["SubstrateFile"] = filename.replace('\\','/')
325
+ self.MainWindow.read_XML() # safe if invalid filename
326
+ # file is read when leaving the files tab
271
327
 
272
328
 
273
329
  def load_values(self):
@@ -285,22 +341,11 @@ class FileInputTab(QWidget):
285
341
  # read_XML(self.XML_file_edit.text()) # safe if invalid filename
286
342
 
287
343
 
288
- # load technology stackup data
289
- def read_XML(self, filename):
290
- if pathlib.Path(filename).exists():
291
- materials_list, dielectrics_list, metals_list = stackup_reader.read_substrate (filename)
292
- self.MainWindow.ports_tab.update_layers(metals_list)
293
-
294
-
295
-
296
344
  def save_values(self):
297
345
  saved_values ["GdsFile"] = self.gds_file_edit.text().replace('\\','/')
298
346
  saved_values ["SubstrateFile"] = self.XML_file_edit.text().replace('\\','/')
299
347
  saved_values ["preprocess_gds"] = self.preprocess_gds_checkbox.isChecked()
300
348
 
301
- # read Substrate file, which also updates port target layer choices
302
- self.read_XML(saved_values ["SubstrateFile"])
303
-
304
349
  try:
305
350
  merge_polygon_size = float(self.viamerge_edit.text())
306
351
  except Exception:
@@ -320,6 +365,9 @@ class FileInputTab(QWidget):
320
365
  # also trigger the load function of CreateModelTab, because that uses gds file info
321
366
  self.MainWindow.create_model_tab.load_values()
322
367
 
368
+ # read Substrate file, which also updates port target layer choices
369
+ self.MainWindow.read_XML()
370
+
323
371
  return True # Tab change only possible when returning True
324
372
 
325
373
 
@@ -1615,7 +1663,7 @@ class ModelEditorTab(QWidget):
1615
1663
  "\n\tpurposelist=settings['purpose'], "
1616
1664
  "\n\tmetals_list=metals_list, \n\tpreprocess=settings['preprocess_gds'], "
1617
1665
  "\n\tmerge_polygon_size=settings['merge_polygon_size'],"
1618
- "\n\tgds_boundary_layers=[],"
1666
+ "\n\tgds_boundary_layers=dielectrics_list.get_boundary_layers(),"
1619
1667
  "\n\tmirror=False, "
1620
1668
  "\n\toffset_x=0, offset_y=0,"
1621
1669
  "\n\tlayernumber_offset=0)")
@@ -1733,7 +1781,330 @@ class CodeEditor(QPlainTextEdit):
1733
1781
  self.setLineWrapMode(QPlainTextEdit.NoWrap)
1734
1782
 
1735
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
+
1736
2105
  # ---------- MAIN WINDOW ----------
2106
+
2107
+
1737
2108
  class MainWindow(QMainWindow):
1738
2109
  TAB_HEADER_COLORS = ["#FFCDD2", "#C8E6C9", "#BBDEFB", "#FFF9C4", "#D1C4E9"]
1739
2110
 
@@ -1775,6 +2146,11 @@ class MainWindow(QMainWindow):
1775
2146
  self.tabs_widget.currentChanged.connect(self.on_tab_change)
1776
2147
  main_layout.addWidget(self.tabs_widget)
1777
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
+
1778
2154
  # Do not auto-load default values at this early startup stage,
1779
2155
  # instead this is done from File menu
1780
2156
  # self.user_inputs_file = DEFAULT_SETTINGS_FILE
@@ -1823,6 +2199,18 @@ class MainWindow(QMainWindow):
1823
2199
  file_menu.addSeparator()
1824
2200
  file_menu.addAction(exit_action)
1825
2201
 
2202
+ help_menu = menu_bar.addMenu("&Help")
2203
+ self.web_gds2palace_action = QAction("gds2palace", self)
2204
+ self.web_manual_action = QAction("Documentation", self)
2205
+ self.web_examples_action = QAction("Examples", self)
2206
+ self.web_gds2palace_action.triggered.connect(lambda: webbrowser.open("https://github.com/VolkerMuehlhaus/gds2palace_ihp_sg13g2"))
2207
+ self.web_manual_action.triggered.connect(lambda: webbrowser.open("https://github.com/VolkerMuehlhaus/gds2palace_ihp_sg13g2/tree/main/doc"))
2208
+ self.web_examples_action.triggered.connect(lambda: webbrowser.open("https://github.com/VolkerMuehlhaus/gds2palace_ihp_sg13g2/tree/main/workflow"))
2209
+ help_menu.addAction(self.web_gds2palace_action)
2210
+ help_menu.addAction(self.web_manual_action)
2211
+ help_menu.addAction(self.web_examples_action)
2212
+
2213
+
1826
2214
  # ---------- Menu actions ----------
1827
2215
 
1828
2216
 
@@ -1891,7 +2279,6 @@ class MainWindow(QMainWindow):
1891
2279
  self.load_configuration_from_file(file_path)
1892
2280
 
1893
2281
 
1894
-
1895
2282
  def load_configuration_from_file(self, file_path):
1896
2283
  if file_path:
1897
2284
  extension = pathlib.Path(file_path).suffix
@@ -2021,7 +2408,6 @@ class MainWindow(QMainWindow):
2021
2408
  QMessageBox.information(self, f"Could not load file {file_path}")
2022
2409
 
2023
2410
 
2024
-
2025
2411
  def save_user_inputs_to_file(self, filename):
2026
2412
  # make sure all tabs save their values
2027
2413
  self.save_all_tabs()
@@ -2066,6 +2452,30 @@ class MainWindow(QMainWindow):
2066
2452
  QMessageBox.warning(self, "Error", f"Failed to export model code to {file_path}: {e}")
2067
2453
 
2068
2454
 
2455
+ def clear_modelname_and_targetdir(self):
2456
+ # clear model name in output settings, to avoid overwriting when changing data
2457
+ saved_values['model_basename']=''
2458
+ # clear target directory if is the gds directory, but keep if other value
2459
+ gds_dir = os.path.dirname(saved_values['GdsFile'])
2460
+ target_dir = saved_values['sim_path']
2461
+ if target_dir.upper()==gds_dir.upper():
2462
+ saved_values['sim_path']=''
2463
+
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
+
2069
2479
 
2070
2480
  def parse_assignments(file_path):
2071
2481
  # parse lines from a Python model code for variable assigments
@@ -2132,13 +2542,14 @@ def parse_python_ports_definitions (file_path):
2132
2542
 
2133
2543
  def main():
2134
2544
  app = QApplication(sys.argv)
2545
+
2135
2546
  if sys.platform.startswith("win"):
2136
- app.setStyle("Windows")
2137
- else:
2138
- # macOS + Linux
2139
- app.setStyle("Fusion")
2140
- win = MainWindow()
2547
+ app.setStyle(QStyleFactory.create("Windows"))
2548
+
2549
+ win = MainWindow()
2141
2550
  win.show()
2551
+
2552
+
2142
2553
  sys.exit(app.exec())
2143
2554
 
2144
2555
  if __name__ == "__main__":
@@ -1,12 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: setupEM
3
- Version: 0.1.2
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