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.
- {setupem-0.1.2 → setupem-0.1.4}/PKG-INFO +3 -2
- {setupem-0.1.2 → setupem-0.1.4}/pyproject.toml +4 -3
- {setupem-0.1.2 → setupem-0.1.4}/src/setupEM/setupEM.py +470 -59
- {setupem-0.1.2 → setupem-0.1.4}/src/setupEM.egg-info/PKG-INFO +3 -2
- setupem-0.1.4/src/setupEM.egg-info/requires.txt +3 -0
- setupem-0.1.2/src/setupEM.egg-info/requires.txt +0 -2
- {setupem-0.1.2 → setupem-0.1.4}/README.md +0 -0
- {setupem-0.1.2 → setupem-0.1.4}/setup.cfg +0 -0
- {setupem-0.1.2 → setupem-0.1.4}/src/examples/pcb_lowpass/palace_pcb_lowpass.py +0 -0
- {setupem-0.1.2 → setupem-0.1.4}/src/examples/sg13_balun_140-170/palace_balun_mesh2.py +0 -0
- {setupem-0.1.2 → setupem-0.1.4}/src/examples/sg13_microstrip_fast/palace_demo_line.py +0 -0
- {setupem-0.1.2 → setupem-0.1.4}/src/examples/sg13_rfcmim/palace_rfcmim.py +0 -0
- {setupem-0.1.2 → setupem-0.1.4}/src/setupEM/__init__.py +0 -0
- {setupem-0.1.2 → setupem-0.1.4}/src/setupEM.egg-info/SOURCES.txt +0 -0
- {setupem-0.1.2 → setupem-0.1.4}/src/setupEM.egg-info/dependency_links.txt +0 -0
- {setupem-0.1.2 → setupem-0.1.4}/src/setupEM.egg-info/entry_points.txt +0 -0
- {setupem-0.1.2 → setupem-0.1.4}/src/setupEM.egg-info/top_level.txt +0 -0
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: setupEM
|
|
3
|
-
Version: 0.1.
|
|
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.
|
|
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.
|
|
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.
|
|
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,
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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 =
|
|
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 =
|
|
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.
|
|
269
|
-
|
|
270
|
-
|
|
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
|
-
|
|
2138
|
-
|
|
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.
|
|
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.
|
|
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
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|