setupEM 0.1.1__py3-none-any.whl
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.
- examples/pcb_lowpass/palace_pcb_lowpass.py +120 -0
- examples/sg13_balun_140-170/palace_balun_mesh2.py +120 -0
- examples/sg13_microstrip_fast/palace_demo_line.py +117 -0
- examples/sg13_rfcmim/palace_rfcmim.py +114 -0
- setupEM/__init__.py +9 -0
- setupEM/setupEM.py +2141 -0
- setupem-0.1.1.dist-info/METADATA +57 -0
- setupem-0.1.1.dist-info/RECORD +11 -0
- setupem-0.1.1.dist-info/WHEEL +5 -0
- setupem-0.1.1.dist-info/entry_points.txt +2 -0
- setupem-0.1.1.dist-info/top_level.txt +2 -0
setupEM/setupEM.py
ADDED
|
@@ -0,0 +1,2141 @@
|
|
|
1
|
+
########################################################################
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2025 Volker Muehlhaus and IHP PDK Authors
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the GNU General Public License, Version 3.0 (the "License");
|
|
6
|
+
# you may not use this file except in compliance with the License.
|
|
7
|
+
# You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# https://www.gnu.org/licenses/gpl-3.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
# See the License for the specific language governing permissions and
|
|
15
|
+
# limitations under the License.
|
|
16
|
+
#
|
|
17
|
+
########################################################################
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
import sys, json, os, tempfile, pathlib, ast
|
|
21
|
+
from PySide6.QtWidgets import (
|
|
22
|
+
QApplication, QMainWindow, QWidget, QHBoxLayout, QVBoxLayout,
|
|
23
|
+
QLabel, QLineEdit,QComboBox,QTableWidget,QHeaderView,
|
|
24
|
+
QPushButton, QFileDialog, QTabWidget, QMessageBox, QGroupBox,
|
|
25
|
+
QCheckBox, QAbstractItemView,QStyleFactory,QTableWidgetItem, QPlainTextEdit
|
|
26
|
+
)
|
|
27
|
+
from PySide6.QtGui import QAction, QColor, QTextCharFormat, QFont, QSyntaxHighlighter
|
|
28
|
+
from PySide6.QtCore import Qt, QRegularExpression, QProcess
|
|
29
|
+
|
|
30
|
+
# we expect gds2palace in the same directory as this code, or installed as module
|
|
31
|
+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), 'gds2palace')))
|
|
32
|
+
from gds2palace import *
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
'''
|
|
36
|
+
Ubuntu 24.04 Notice:
|
|
37
|
+
|
|
38
|
+
Error message:
|
|
39
|
+
qt.qpa.plugin: From 6.5.0, xcb-cursor0 or libxcb-cursor0 is needed to load the Qt xcb platform plugin. qt.qpa.plugin: Could not load the Qt platform plugin "xcb" in "" even though it was found.
|
|
40
|
+
|
|
41
|
+
Solution:
|
|
42
|
+
sudo apt update
|
|
43
|
+
sudo apt install libxcb-cursor0 libxcb-xinerama0 libxcb-xkb1 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-render0 libxcb-shape0 libxcb-shm0 libxcb-sync1 libxcb-xfixes0 libxcb-xinput0 libxcb-xv0 libxcb-util1 libxkbcommon-x11-0
|
|
44
|
+
|
|
45
|
+
'''
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
CONFIG_SUFFIX = "simcfg" # file suffix for native config file used here
|
|
49
|
+
APP_NAME = "setupEM" # name of this application
|
|
50
|
+
|
|
51
|
+
DEFAULT_SETTINGS_FILE = os.path.join(os.path.expanduser("~"),"default." + CONFIG_SUFFIX)
|
|
52
|
+
|
|
53
|
+
EDIT_STYLE_OPTIONAL = """
|
|
54
|
+
QLineEdit {
|
|
55
|
+
background-color: white;
|
|
56
|
+
border: 1px solid gray;
|
|
57
|
+
border-radius: 4px;
|
|
58
|
+
padding: 4px;
|
|
59
|
+
}
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
EDIT_STYLE_REQUIRED = """
|
|
63
|
+
QLineEdit {
|
|
64
|
+
background-color: lightyellow;
|
|
65
|
+
border: 1px solid gray;
|
|
66
|
+
border-radius: 4px;
|
|
67
|
+
padding: 4px;
|
|
68
|
+
}
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
COMBO_STYLE_REQUIRED = """
|
|
72
|
+
QComboBox {
|
|
73
|
+
background-color: lightyellow;
|
|
74
|
+
border: 1px solid gray;
|
|
75
|
+
border-radius: 4px;
|
|
76
|
+
padding: 4px;
|
|
77
|
+
combobox-popup: 0;
|
|
78
|
+
}
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
COMBO_STYLE_OPTIONAL = """
|
|
82
|
+
QComboBox {
|
|
83
|
+
background-color: white;
|
|
84
|
+
border: 1px solid gray;
|
|
85
|
+
border-radius: 4px;
|
|
86
|
+
padding: 4px;
|
|
87
|
+
combobox-popup: 0;
|
|
88
|
+
}
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
saved_values = {} # dictionary of user input in this application
|
|
93
|
+
simulation_ports = simulation_setup.all_simulation_ports() # store port settings
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def get_saved_value (key, default):
|
|
97
|
+
data = saved_values.get('saved_values',None)
|
|
98
|
+
if data is not None:
|
|
99
|
+
if key in data.keys():
|
|
100
|
+
return data [key]
|
|
101
|
+
else:
|
|
102
|
+
if key in saved_values.keys():
|
|
103
|
+
return saved_values [key]
|
|
104
|
+
else:
|
|
105
|
+
return default
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def simulation_ports_to_struct (simulation_ports):
|
|
110
|
+
# convert simulation ports to a strcuture that can be serialized to JSON output
|
|
111
|
+
all_ports_list = []
|
|
112
|
+
for sim_port in simulation_ports.ports:
|
|
113
|
+
port = {}
|
|
114
|
+
port['portnumber'] = sim_port.portnumber
|
|
115
|
+
port['source_layernum'] = sim_port.source_layernum
|
|
116
|
+
port['target_layername'] = sim_port.target_layername
|
|
117
|
+
port['from_layername'] = sim_port.from_layername
|
|
118
|
+
port['to_layername'] = sim_port.to_layername
|
|
119
|
+
port['direction'] = sim_port.direction
|
|
120
|
+
port['port_Z0'] = sim_port.port_Z0
|
|
121
|
+
port['voltage'] = sim_port.voltage
|
|
122
|
+
all_ports_list.append(port)
|
|
123
|
+
return all_ports_list
|
|
124
|
+
|
|
125
|
+
|
|
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
|
+
'''
|
|
151
|
+
|
|
152
|
+
# ---------- FILE INPUT TAB ----------
|
|
153
|
+
class FileInputTab(QWidget):
|
|
154
|
+
# File definitions go here
|
|
155
|
+
def __init__(self, MainWindow):
|
|
156
|
+
super().__init__()
|
|
157
|
+
|
|
158
|
+
self.MainWindow = MainWindow # parent = MainWindow
|
|
159
|
+
|
|
160
|
+
self.main_layout = QVBoxLayout()
|
|
161
|
+
self.main_layout.setAlignment(Qt.AlignTop)
|
|
162
|
+
|
|
163
|
+
# ---------- GDSII FILE GROUP ----------
|
|
164
|
+
self.gds_group = QGroupBox("GDSII Layout File")
|
|
165
|
+
self.gds_layout = QVBoxLayout()
|
|
166
|
+
|
|
167
|
+
self.gds_file_layout = QHBoxLayout()
|
|
168
|
+
self.gds_file_edit = QLineEdit("Please choose a file ===>")
|
|
169
|
+
self.gds_file_edit.setStyleSheet(EDIT_STYLE_REQUIRED)
|
|
170
|
+
|
|
171
|
+
self.browse_gds_btn = QPushButton("Browse ...")
|
|
172
|
+
self.browse_gds_btn.setFixedWidth(150) # narrower
|
|
173
|
+
self.browse_gds_btn.clicked.connect(self.browse_gds_file)
|
|
174
|
+
|
|
175
|
+
self.gds_file_layout.addWidget(self.gds_file_edit)
|
|
176
|
+
self.gds_file_layout.addWidget(self.browse_gds_btn)
|
|
177
|
+
|
|
178
|
+
self.gds_layout.addLayout(self.gds_file_layout)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
self.purpose_layout = QHBoxLayout()
|
|
182
|
+
self.purpose_label1 = QLabel("Read this datatype (purpose): ")
|
|
183
|
+
self.purpose_edit = QLineEdit("0")
|
|
184
|
+
self.purpose_edit.setStyleSheet(EDIT_STYLE_OPTIONAL)
|
|
185
|
+
self.purpose_edit.setFixedWidth(70)
|
|
186
|
+
self.purpose_label2 = QLabel(" (default=0, multiple values can be separated by comma)")
|
|
187
|
+
self.purpose_layout.addWidget(self.purpose_label1)
|
|
188
|
+
self.purpose_layout.addWidget(self.purpose_edit)
|
|
189
|
+
self.purpose_layout.addWidget(self.purpose_label2)
|
|
190
|
+
self.purpose_layout.addStretch()
|
|
191
|
+
self.gds_layout.addLayout(self.purpose_layout)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
self.viamerge_layout = QHBoxLayout()
|
|
196
|
+
self.viamerge_label1 = QLabel("Merge via arrays with spacing ")
|
|
197
|
+
self.viamerge_edit = QLineEdit("0")
|
|
198
|
+
self.viamerge_edit.setStyleSheet(EDIT_STYLE_OPTIONAL)
|
|
199
|
+
self.viamerge_edit.setFixedWidth(70)
|
|
200
|
+
self.viamerge_label2 = QLabel(" micron or more, value 0 disables via array merging")
|
|
201
|
+
self.viamerge_layout.addWidget(self.viamerge_label1)
|
|
202
|
+
self.viamerge_layout.addWidget(self.viamerge_edit)
|
|
203
|
+
self.viamerge_layout.addWidget(self.viamerge_label2)
|
|
204
|
+
self.viamerge_layout.addStretch()
|
|
205
|
+
self.gds_layout.addLayout(self.viamerge_layout)
|
|
206
|
+
|
|
207
|
+
self.preprocess_layout = QHBoxLayout()
|
|
208
|
+
self.preprocess_gds_checkbox = QCheckBox()
|
|
209
|
+
self.preprocess_gds_checkbox.setFixedWidth(20)
|
|
210
|
+
self.preprocess_gds_label = QLabel("Preprocess GDSII file (required for polygons with holes/cutouts)")
|
|
211
|
+
self.preprocess_layout.addWidget(self.preprocess_gds_checkbox)
|
|
212
|
+
self.preprocess_layout.addWidget(self.preprocess_gds_label)
|
|
213
|
+
self.gds_layout.addLayout(self.preprocess_layout)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
self.gds_group.setLayout(self.gds_layout)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
# ---------- XML FILE GROUP ----------
|
|
220
|
+
|
|
221
|
+
self.XML_group = QGroupBox("XML Stackup File")
|
|
222
|
+
self.XML_layout = QVBoxLayout()
|
|
223
|
+
|
|
224
|
+
self.XML_file_layout = QHBoxLayout()
|
|
225
|
+
self.XML_file_edit = QLineEdit("Please choose a file ===>")
|
|
226
|
+
self.XML_file_edit.setStyleSheet(EDIT_STYLE_REQUIRED)
|
|
227
|
+
|
|
228
|
+
self.browse_XML_btn = QPushButton("Browse ...")
|
|
229
|
+
self.browse_XML_btn.setFixedWidth(150) # narrower
|
|
230
|
+
self.browse_XML_btn.clicked.connect(self.browse_XML_file)
|
|
231
|
+
|
|
232
|
+
self.XML_file_layout.addWidget(self.XML_file_edit)
|
|
233
|
+
self.XML_file_layout.addWidget(self.browse_XML_btn)
|
|
234
|
+
|
|
235
|
+
self.XML_layout.addLayout(self.XML_file_layout)
|
|
236
|
+
self.XML_group.setLayout(self.XML_layout)
|
|
237
|
+
|
|
238
|
+
self.main_layout.addWidget(self.gds_group)
|
|
239
|
+
self.main_layout.addSpacing(20)
|
|
240
|
+
self.main_layout.addWidget(self.XML_group)
|
|
241
|
+
self.main_layout.addStretch()
|
|
242
|
+
self.setLayout(self.main_layout)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def browse_gds_file(self):
|
|
246
|
+
# start browsing from previous file location, if valid
|
|
247
|
+
previous_file = self.gds_file_edit.text()
|
|
248
|
+
previous_directory = os.path.dirname(previous_file)
|
|
249
|
+
if not os.path.isdir(previous_directory):
|
|
250
|
+
previous_directory = ""
|
|
251
|
+
|
|
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
|
+
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def browse_XML_file(self):
|
|
260
|
+
# start browsing from previous file location, if valid
|
|
261
|
+
previous_file = self.XML_file_edit.text()
|
|
262
|
+
previous_directory = os.path.dirname(previous_file)
|
|
263
|
+
if not os.path.isdir(previous_directory):
|
|
264
|
+
previous_directory = ""
|
|
265
|
+
|
|
266
|
+
filename, _ = QFileDialog.getOpenFileName(self, "Select XML Stackup File",previous_directory,"*.xml;;*.*")
|
|
267
|
+
if filename:
|
|
268
|
+
self.XML_file_edit.setText(filename)
|
|
269
|
+
saved_values ["SubstrateFile"] = filename.replace('\\','/')
|
|
270
|
+
# file is read when leaving this tab
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def load_values(self):
|
|
274
|
+
self.gds_file_edit.setText(get_saved_value("GdsFile","Please choose a file ===>"))
|
|
275
|
+
XML = get_saved_value("SubstrateFile","Please choose a file ===>")
|
|
276
|
+
self.XML_file_edit.setText(XML)
|
|
277
|
+
self.viamerge_edit.setText(str(get_saved_value("merge_polygon_size","0")))
|
|
278
|
+
self.preprocess_gds_checkbox.setChecked(bool(get_saved_value("preprocess_gds",False)))
|
|
279
|
+
|
|
280
|
+
int_list = saved_values.get("purpose","0")
|
|
281
|
+
purpose_string = str(int_list).replace('[','').replace(']','')
|
|
282
|
+
self.purpose_edit.setText(purpose_string)
|
|
283
|
+
# self.purpose_edit.setText(','.join(map(str, int_list)))
|
|
284
|
+
|
|
285
|
+
# read_XML(self.XML_file_edit.text()) # safe if invalid filename
|
|
286
|
+
|
|
287
|
+
|
|
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
|
+
def save_values(self):
|
|
297
|
+
saved_values ["GdsFile"] = self.gds_file_edit.text().replace('\\','/')
|
|
298
|
+
saved_values ["SubstrateFile"] = self.XML_file_edit.text().replace('\\','/')
|
|
299
|
+
saved_values ["preprocess_gds"] = self.preprocess_gds_checkbox.isChecked()
|
|
300
|
+
|
|
301
|
+
# read Substrate file, which also updates port target layer choices
|
|
302
|
+
self.read_XML(saved_values ["SubstrateFile"])
|
|
303
|
+
|
|
304
|
+
try:
|
|
305
|
+
merge_polygon_size = float(self.viamerge_edit.text())
|
|
306
|
+
except Exception:
|
|
307
|
+
QMessageBox.warning(self, "Error", f"Not a valid value for via array merging")
|
|
308
|
+
self.viamerge_edit.setText("0")
|
|
309
|
+
return False
|
|
310
|
+
saved_values ["merge_polygon_size"] = float(merge_polygon_size)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
text = self.purpose_edit.text()
|
|
314
|
+
if text != "":
|
|
315
|
+
# save as list of comma separated values
|
|
316
|
+
saved_values ["purpose"] = ast.literal_eval('['+text+']')
|
|
317
|
+
else:
|
|
318
|
+
saved_values ["purpose"] = [0] # safe default
|
|
319
|
+
|
|
320
|
+
# also trigger the load function of CreateModelTab, because that uses gds file info
|
|
321
|
+
self.MainWindow.create_model_tab.load_values()
|
|
322
|
+
|
|
323
|
+
return True # Tab change only possible when returning True
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
# ---------- OTHER TABS ----------
|
|
327
|
+
class FrequenciesTab(QWidget):
|
|
328
|
+
def __init__(self, MainWindow):
|
|
329
|
+
super().__init__()
|
|
330
|
+
|
|
331
|
+
self.MainWindow = MainWindow # parent = MainWindow
|
|
332
|
+
|
|
333
|
+
self.main_layout = QVBoxLayout()
|
|
334
|
+
self.main_layout.setAlignment(Qt.AlignTop)
|
|
335
|
+
|
|
336
|
+
# ---------- SWEEP GROUP ----------
|
|
337
|
+
self.sweep_group = QGroupBox("Adaptive frequency sweep")
|
|
338
|
+
self.sweep_layout = QHBoxLayout()
|
|
339
|
+
|
|
340
|
+
self.start_layout = QVBoxLayout()
|
|
341
|
+
self.start_layout.addWidget(QLabel("fstart [GHz]"))
|
|
342
|
+
self.start_edit = QLineEdit("0.1")
|
|
343
|
+
self.start_edit.setStyleSheet(EDIT_STYLE_REQUIRED)
|
|
344
|
+
self.start_layout.addWidget(self.start_edit)
|
|
345
|
+
self.sweep_layout.addLayout(self.start_layout)
|
|
346
|
+
|
|
347
|
+
self.stop_layout = QVBoxLayout()
|
|
348
|
+
self.stop_layout.addWidget(QLabel("fstop [GHz]"))
|
|
349
|
+
self.stop_edit = QLineEdit("50")
|
|
350
|
+
self.stop_edit.setStyleSheet(EDIT_STYLE_REQUIRED)
|
|
351
|
+
self.stop_layout.addWidget(self.stop_edit)
|
|
352
|
+
self.sweep_layout.addLayout(self.stop_layout)
|
|
353
|
+
|
|
354
|
+
self.step_layout = QVBoxLayout()
|
|
355
|
+
self.step_layout.addWidget(QLabel("fstep [GHz], optional"))
|
|
356
|
+
self.step_edit = QLineEdit("")
|
|
357
|
+
self.step_edit.setStyleSheet(EDIT_STYLE_OPTIONAL)
|
|
358
|
+
self.step_layout.addWidget(self.step_edit)
|
|
359
|
+
self.sweep_layout.addLayout(self.step_layout)
|
|
360
|
+
|
|
361
|
+
self.sweep_group.setLayout(self.sweep_layout)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
# ---------- DISCRETE GROUP ----------
|
|
365
|
+
self.discrete_group = QGroupBox("Optional list of fixed frequencies")
|
|
366
|
+
self.discrete_layout = QHBoxLayout()
|
|
367
|
+
|
|
368
|
+
self.fpoint_layout = QVBoxLayout()
|
|
369
|
+
self.fpoint_layout.addWidget(QLabel("fpoint [GHz], values separated by comma"))
|
|
370
|
+
self.fpoint_edit = QLineEdit("")
|
|
371
|
+
self.fpoint_edit.setStyleSheet(EDIT_STYLE_OPTIONAL)
|
|
372
|
+
self.fpoint_layout.addWidget(self.fpoint_edit)
|
|
373
|
+
self.discrete_layout.addLayout(self.fpoint_layout)
|
|
374
|
+
|
|
375
|
+
self.discrete_group.setLayout(self.discrete_layout)
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
# ---------- DUMP GROUP ----------
|
|
379
|
+
self.dump_group = QGroupBox("Optional list of fixed frequencies creating field dump data for visualization (Paraview files))")
|
|
380
|
+
self.dump_layout = QHBoxLayout()
|
|
381
|
+
|
|
382
|
+
self.fdump_layout = QVBoxLayout()
|
|
383
|
+
self.fdump_layout.addWidget(QLabel("fdump [GHz], values separated by comma"))
|
|
384
|
+
self.fdump_edit = QLineEdit("")
|
|
385
|
+
self.fdump_edit.setStyleSheet(EDIT_STYLE_OPTIONAL)
|
|
386
|
+
self.fdump_layout.addWidget(self.fdump_edit)
|
|
387
|
+
self.dump_layout.addLayout(self.fdump_layout)
|
|
388
|
+
self.dump_group.setLayout(self.dump_layout)
|
|
389
|
+
|
|
390
|
+
self.main_layout.addWidget(self.sweep_group)
|
|
391
|
+
self.main_layout.addSpacing(20)
|
|
392
|
+
self.main_layout.addWidget(self.discrete_group)
|
|
393
|
+
self.main_layout.addSpacing(20)
|
|
394
|
+
self.main_layout.addWidget(self.dump_group)
|
|
395
|
+
self.main_layout.addStretch()
|
|
396
|
+
self.setLayout(self.main_layout)
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def save_values(self):
|
|
400
|
+
try:
|
|
401
|
+
fstart = float(self.start_edit.text())
|
|
402
|
+
except Exception:
|
|
403
|
+
QMessageBox.warning(self, "Error", "Not a valid value for fstart")
|
|
404
|
+
self.start_edit.setText("0")
|
|
405
|
+
return False
|
|
406
|
+
saved_values ["fstart"] = float(fstart)
|
|
407
|
+
|
|
408
|
+
try:
|
|
409
|
+
fstop = float(self.stop_edit.text())
|
|
410
|
+
except Exception:
|
|
411
|
+
QMessageBox.warning(self, "Error", "Not a valid value for fstop")
|
|
412
|
+
self.stop_edit.setText("50")
|
|
413
|
+
return False
|
|
414
|
+
saved_values ["fstop"] = float(fstop)
|
|
415
|
+
|
|
416
|
+
if self.step_edit.text() != "":
|
|
417
|
+
try:
|
|
418
|
+
fstep = float(self.step_edit.text())
|
|
419
|
+
zerocheck = 1 / fstep # raise an exception if zero
|
|
420
|
+
except Exception:
|
|
421
|
+
QMessageBox.warning(self, "Error", "Not a valid value for fstep")
|
|
422
|
+
self.step_edit.setText("")
|
|
423
|
+
return False
|
|
424
|
+
saved_values ["fstep"] = float(fstep)
|
|
425
|
+
else:
|
|
426
|
+
saved_values.pop("fstep",None) # delete key
|
|
427
|
+
|
|
428
|
+
text = self.fpoint_edit.text()
|
|
429
|
+
if text != "":
|
|
430
|
+
# save as list of comma separated values
|
|
431
|
+
saved_values ["fpoint"] = ast.literal_eval('['+text+']')
|
|
432
|
+
else:
|
|
433
|
+
saved_values.pop("fpoint",None) # delete key
|
|
434
|
+
|
|
435
|
+
text = self.fdump_edit.text()
|
|
436
|
+
if text != "":
|
|
437
|
+
saved_values ["fdump"] = ast.literal_eval('['+text+']')
|
|
438
|
+
else:
|
|
439
|
+
saved_values.pop("fdump",None)
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
return True # Tab change only possible when returning True
|
|
443
|
+
|
|
444
|
+
def load_values(self):
|
|
445
|
+
self.start_edit.setText(str(saved_values.get("fstart","0.1")))
|
|
446
|
+
self.stop_edit.setText(str(saved_values.get("fstop","50")))
|
|
447
|
+
self.step_edit.setText(str(saved_values.get("fstep","")))
|
|
448
|
+
|
|
449
|
+
float_list = saved_values.get("fpoint","")
|
|
450
|
+
self.fpoint_edit.setText(','.join(map(str, float_list)))
|
|
451
|
+
|
|
452
|
+
float_list = saved_values.get("fdump","")
|
|
453
|
+
self.fdump_edit.setText(','.join(map(str, float_list)))
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
class PortsTab(QWidget):
|
|
457
|
+
def __init__(self, MainWindow):
|
|
458
|
+
super().__init__()
|
|
459
|
+
|
|
460
|
+
self.MainWindow = MainWindow # parent = MainWindow
|
|
461
|
+
self.main_layout = QVBoxLayout()
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
self.top_group = QGroupBox("Port settings")
|
|
465
|
+
self.top_layout = QVBoxLayout()
|
|
466
|
+
|
|
467
|
+
self.bottom_group = QGroupBox("Port overview")
|
|
468
|
+
# self.left_group.setFixedWidth(100)
|
|
469
|
+
self.bottom_layout = QVBoxLayout()
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
# port list is bottom group
|
|
473
|
+
|
|
474
|
+
self.portslist = QTableWidget()
|
|
475
|
+
self.portslist.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows)
|
|
476
|
+
self.portslist.setSelectionMode(QTableWidget.SelectionMode.SingleSelection)
|
|
477
|
+
self.portslist.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
|
|
478
|
+
self.bottom_layout.addWidget(self.portslist)
|
|
479
|
+
|
|
480
|
+
self.portslist.setColumnCount(8)
|
|
481
|
+
self.portslist.setHorizontalHeaderLabels(["Z0", "Voltage", "Source layer", "Target layer", "From layer", "To layer","Direction",""])
|
|
482
|
+
header = self.portslist.horizontalHeader()
|
|
483
|
+
for col in range(self.portslist.columnCount()-1):
|
|
484
|
+
header.setSectionResizeMode(col, QHeaderView.ResizeToContents)
|
|
485
|
+
header.setStretchLastSection(True)
|
|
486
|
+
|
|
487
|
+
self.portslist.setRowCount(32)
|
|
488
|
+
self.portslist.selectRow(0)
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
# details rigis top group
|
|
492
|
+
self.details_layout = QVBoxLayout()
|
|
493
|
+
|
|
494
|
+
left_label_width = 200
|
|
495
|
+
|
|
496
|
+
self.sourcelayer_layout = QHBoxLayout()
|
|
497
|
+
label = QLabel("Port geometry on layer number")
|
|
498
|
+
self.sourcelayer_layout.addWidget(label)
|
|
499
|
+
label.setFixedWidth(left_label_width)
|
|
500
|
+
self.sourcelayer_edit = QLineEdit("201")
|
|
501
|
+
self.sourcelayer_edit.setFixedWidth(80)
|
|
502
|
+
self.sourcelayer_edit.setStyleSheet(EDIT_STYLE_REQUIRED)
|
|
503
|
+
self.sourcelayer_layout.addWidget(self.sourcelayer_edit)
|
|
504
|
+
label = QLabel(" in GDSII file")
|
|
505
|
+
self.sourcelayer_layout.addWidget(label)
|
|
506
|
+
# label.setFixedWidth(200)
|
|
507
|
+
self.sourcelayer_layout.addStretch()
|
|
508
|
+
self.details_layout.addLayout(self.sourcelayer_layout)
|
|
509
|
+
|
|
510
|
+
self.direction_layout = QHBoxLayout()
|
|
511
|
+
label = QLabel("Port direction")
|
|
512
|
+
self.direction_layout.addWidget(label)
|
|
513
|
+
label.setFixedWidth(left_label_width)
|
|
514
|
+
self.direction_box = QComboBox()
|
|
515
|
+
self.direction_box.setFixedWidth(80)
|
|
516
|
+
self.direction_box.setStyleSheet(COMBO_STYLE_REQUIRED)
|
|
517
|
+
self.direction_box.addItems(["X", "Y", "Z", "-X", "-Y", "-Z"])
|
|
518
|
+
self.direction_layout.addWidget(self.direction_box)
|
|
519
|
+
label2 = QLabel(" (negative for reversed polarity)")
|
|
520
|
+
self.direction_layout.addWidget(label2)
|
|
521
|
+
# label2.setFixedWidth(200)
|
|
522
|
+
self.direction_layout.addStretch()
|
|
523
|
+
self.details_layout.addLayout(self.direction_layout)
|
|
524
|
+
|
|
525
|
+
self.targetlayer_layout = QHBoxLayout()
|
|
526
|
+
self.target_label = QLabel("Target layer for in-plane port ")
|
|
527
|
+
self.targetlayer_layout.addWidget(self.target_label)
|
|
528
|
+
self.target_label.setFixedWidth(left_label_width)
|
|
529
|
+
self.target_box = QComboBox()
|
|
530
|
+
self.target_box.setFixedWidth(150)
|
|
531
|
+
self.target_box.setStyleSheet(COMBO_STYLE_REQUIRED)
|
|
532
|
+
self.target_box.addItems(["XML stackup missing"])
|
|
533
|
+
self.targetlayer_layout.addWidget(self.target_box)
|
|
534
|
+
|
|
535
|
+
self.targetlayer_layout.addStretch()
|
|
536
|
+
self.details_layout.addLayout(self.targetlayer_layout)
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
self.viaport_layout = QHBoxLayout()
|
|
540
|
+
self.from_label = QLabel("Via port from layer")
|
|
541
|
+
self.viaport_layout.addWidget(self.from_label)
|
|
542
|
+
self.from_label.setFixedWidth(left_label_width)
|
|
543
|
+
self.from_box = QComboBox()
|
|
544
|
+
self.from_box.setFixedWidth(150)
|
|
545
|
+
self.from_box.setStyleSheet(COMBO_STYLE_REQUIRED)
|
|
546
|
+
self.from_box.addItems(["XML stackup missing"])
|
|
547
|
+
self.viaport_layout.addWidget(self.from_box)
|
|
548
|
+
self.to_label = QLabel(" to layer")
|
|
549
|
+
self.viaport_layout.addWidget(self.to_label)
|
|
550
|
+
# label2.setFixedWidth(200)
|
|
551
|
+
self.to_box = QComboBox()
|
|
552
|
+
self.to_box.setFixedWidth(150)
|
|
553
|
+
self.to_box.setStyleSheet(COMBO_STYLE_REQUIRED)
|
|
554
|
+
self.viaport_layout.addWidget(self.to_box)
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
self.viaport_layout.addStretch()
|
|
558
|
+
self.details_layout.addLayout(self.viaport_layout)
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
self.impedance_layout = QHBoxLayout()
|
|
562
|
+
label = QLabel("Port impedance")
|
|
563
|
+
self.impedance_layout.addWidget(label)
|
|
564
|
+
label.setFixedWidth(left_label_width)
|
|
565
|
+
self.impedance_edit = QLineEdit("50")
|
|
566
|
+
self.impedance_edit.setFixedWidth(80)
|
|
567
|
+
self.impedance_edit.setStyleSheet(EDIT_STYLE_OPTIONAL)
|
|
568
|
+
self.impedance_layout.addWidget(self.impedance_edit)
|
|
569
|
+
|
|
570
|
+
self.impedance_layout.addStretch()
|
|
571
|
+
self.details_layout.addLayout(self.impedance_layout)
|
|
572
|
+
|
|
573
|
+
self.voltage_layout = QHBoxLayout()
|
|
574
|
+
label = QLabel("Port voltage")
|
|
575
|
+
self.voltage_layout.addWidget(label)
|
|
576
|
+
label.setFixedWidth(left_label_width)
|
|
577
|
+
self.voltage_edit = QLineEdit("1")
|
|
578
|
+
self.voltage_edit.setFixedWidth(80)
|
|
579
|
+
self.voltage_edit.setStyleSheet(EDIT_STYLE_OPTIONAL)
|
|
580
|
+
self.voltage_layout.addWidget(self.voltage_edit)
|
|
581
|
+
|
|
582
|
+
self.voltage_layout.addWidget(QLabel(" (1=active, 0=passive)"))
|
|
583
|
+
self.details_layout.addLayout(self.voltage_layout)
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
self.buttons_layout = QHBoxLayout()
|
|
587
|
+
|
|
588
|
+
button_width = 100
|
|
589
|
+
|
|
590
|
+
self.apply_button = QPushButton(text="Apply ↓")
|
|
591
|
+
self.buttons_layout.addWidget(self.apply_button)
|
|
592
|
+
self.buttons_layout.setAlignment(Qt.AlignRight)
|
|
593
|
+
self.apply_button.setFixedWidth(button_width)
|
|
594
|
+
self.apply_button.clicked.connect(self.apply_port_values_to_table)
|
|
595
|
+
|
|
596
|
+
self.remove_button = QPushButton(text="Remove")
|
|
597
|
+
self.buttons_layout.addWidget(self.remove_button)
|
|
598
|
+
self.remove_button.setFixedWidth(button_width)
|
|
599
|
+
self.remove_button.clicked.connect(self.remove_port_values_from_table)
|
|
600
|
+
|
|
601
|
+
# self.buttons_layout.addStretch()
|
|
602
|
+
# self.details_layout.addSpacing(20)
|
|
603
|
+
self.details_layout.addLayout(self.buttons_layout)
|
|
604
|
+
|
|
605
|
+
# self.details_layout.addStretch()
|
|
606
|
+
self.top_layout.addLayout(self.details_layout)
|
|
607
|
+
|
|
608
|
+
self.bottom_group.setLayout(self.bottom_layout)
|
|
609
|
+
self.top_group.setLayout(self.top_layout)
|
|
610
|
+
|
|
611
|
+
self.main_layout.addWidget(self.top_group)
|
|
612
|
+
self.main_layout.addSpacing(20)
|
|
613
|
+
|
|
614
|
+
self.main_layout.addWidget(self.bottom_group)
|
|
615
|
+
# self.main_layout.addStretch()
|
|
616
|
+
self.setLayout(self.main_layout)
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
# callback when direction changed, so that we can show/hide layer choices
|
|
620
|
+
def on_direction_changed(direction):
|
|
621
|
+
if "Z" in direction:
|
|
622
|
+
# hide target layer label and edit
|
|
623
|
+
self.target_label.hide()
|
|
624
|
+
self.target_box.hide()
|
|
625
|
+
self.from_label.show()
|
|
626
|
+
self.from_box.show()
|
|
627
|
+
self.to_label.show()
|
|
628
|
+
self.to_box.show()
|
|
629
|
+
else:
|
|
630
|
+
# show target layer label and edit
|
|
631
|
+
self.target_label.show()
|
|
632
|
+
self.target_box.show()
|
|
633
|
+
self.from_label.hide()
|
|
634
|
+
self.from_box.hide()
|
|
635
|
+
self.to_label.hide()
|
|
636
|
+
self.to_box.hide()
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
self.direction_box.currentTextChanged.connect(on_direction_changed)
|
|
640
|
+
self.direction_box.setCurrentIndex(2)
|
|
641
|
+
|
|
642
|
+
# self.apply_port_values_to_table()
|
|
643
|
+
|
|
644
|
+
# set this AFTER apply_port_values_to_table(), not earlier!
|
|
645
|
+
self.portslist.itemSelectionChanged.connect(self.portslist_selection_changed)
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
# callback when applying changes to the selected port
|
|
649
|
+
def apply_port_values_to_table(self):
|
|
650
|
+
selected_indexes = self.portslist.selectedIndexes()
|
|
651
|
+
if selected_indexes:
|
|
652
|
+
selected_row = selected_indexes[0].row() # row of the first selected cell
|
|
653
|
+
# "Z0", "Voltage", "Source layer", "Target layer", "From layer", "To layer","Direction"
|
|
654
|
+
|
|
655
|
+
if "Z" in self.direction_box.currentText():
|
|
656
|
+
target_layer = ""
|
|
657
|
+
from_layer = self.from_box.currentText()
|
|
658
|
+
to_layer = self.to_box.currentText()
|
|
659
|
+
else:
|
|
660
|
+
target_layer = self.target_box.currentText()
|
|
661
|
+
from_layer = ""
|
|
662
|
+
to_layer = ""
|
|
663
|
+
|
|
664
|
+
data = [self.impedance_edit.text(),
|
|
665
|
+
self.voltage_edit.text(),
|
|
666
|
+
self.sourcelayer_edit.text(),
|
|
667
|
+
target_layer,
|
|
668
|
+
from_layer,
|
|
669
|
+
to_layer,
|
|
670
|
+
self.direction_box.currentText()]
|
|
671
|
+
|
|
672
|
+
for col, value in enumerate(data):
|
|
673
|
+
self.portslist.setItem(selected_row, col, QTableWidgetItem(str(value)))
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
# callback when applying changes to the selected port
|
|
677
|
+
def get_port_values_from_table(self):
|
|
678
|
+
selected_indexes = self.portslist.selectedIndexes()
|
|
679
|
+
if selected_indexes:
|
|
680
|
+
selected_row = selected_indexes[0].row() # row of the first selected cell
|
|
681
|
+
# "Z0", "Voltage", "Source layer", "Target layer", "From layer", "To layer","Direction"
|
|
682
|
+
|
|
683
|
+
def safe_get_for_lineedit (index, target, default):
|
|
684
|
+
item = self.portslist.item(selected_row, index)
|
|
685
|
+
if item is None:
|
|
686
|
+
itemvalue = default
|
|
687
|
+
else:
|
|
688
|
+
itemvalue = item.text()
|
|
689
|
+
target.setText(itemvalue)
|
|
690
|
+
|
|
691
|
+
def safe_get_for_combobox (index, target, default):
|
|
692
|
+
item = self.portslist.item(selected_row, index)
|
|
693
|
+
if item is None:
|
|
694
|
+
index = default
|
|
695
|
+
else:
|
|
696
|
+
index = target.findText(item.text())
|
|
697
|
+
if not index >= 0:
|
|
698
|
+
index = default
|
|
699
|
+
target.setCurrentIndex(index)
|
|
700
|
+
|
|
701
|
+
safe_get_for_lineedit(0,self.impedance_edit,"50")
|
|
702
|
+
safe_get_for_lineedit(1,self.voltage_edit,"1")
|
|
703
|
+
safe_get_for_lineedit(2,self.sourcelayer_edit,"")
|
|
704
|
+
|
|
705
|
+
safe_get_for_combobox(3,self.target_box,0)
|
|
706
|
+
safe_get_for_combobox(4,self.from_box,0)
|
|
707
|
+
safe_get_for_combobox(5,self.to_box,0)
|
|
708
|
+
safe_get_for_combobox(6,self.direction_box,2)
|
|
709
|
+
|
|
710
|
+
# self.apply_port_values_to_table()
|
|
711
|
+
|
|
712
|
+
|
|
713
|
+
# callback when removing selected port settings
|
|
714
|
+
def remove_port_values_from_table(self):
|
|
715
|
+
selected_indexes = self.portslist.selectedIndexes()
|
|
716
|
+
if selected_indexes:
|
|
717
|
+
selected_row = selected_indexes[0].row() # row of the first selected cell
|
|
718
|
+
|
|
719
|
+
data = ["","","","","","",""]
|
|
720
|
+
|
|
721
|
+
for col, value in enumerate(data):
|
|
722
|
+
# self.portslist.setItem(selected_row, col, QTableWidgetItem(str(value)))
|
|
723
|
+
self.portslist.setItem(selected_row, col, None)
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
def portslist_selection_changed(self):
|
|
727
|
+
selected_indexes = self.portslist.selectedIndexes()
|
|
728
|
+
if selected_indexes:
|
|
729
|
+
selected_row = selected_indexes[0].row() # row of the first selected cell
|
|
730
|
+
item = self.portslist.item(selected_row, 0) # first item is Z0
|
|
731
|
+
if item is not None:
|
|
732
|
+
# assume that we have an line that is not empty, so get port details from this line
|
|
733
|
+
if item.text() != "":
|
|
734
|
+
self.get_port_values_from_table()
|
|
735
|
+
else:
|
|
736
|
+
self.sourcelayer_edit.setText(str(201+selected_row))
|
|
737
|
+
|
|
738
|
+
|
|
739
|
+
def save_values(self):
|
|
740
|
+
# clear previous port data in simulation_ports instance
|
|
741
|
+
simulation_ports.ports.clear()
|
|
742
|
+
# loop over rows in ports table
|
|
743
|
+
for row in range(self.portslist.rowCount()):
|
|
744
|
+
portnumber = row+1
|
|
745
|
+
testvalue = self.portslist.item(row, 0)
|
|
746
|
+
if testvalue != None: # not an empty port line
|
|
747
|
+
if testvalue.text != "":
|
|
748
|
+
try:
|
|
749
|
+
Z0 = float(testvalue.text())
|
|
750
|
+
voltage = float(self.portslist.item(row, 1).text())
|
|
751
|
+
source_layer_num = int(self.portslist.item(row, 2).text())
|
|
752
|
+
target_name = self.portslist.item(row, 3).text()
|
|
753
|
+
from_name = self.portslist.item(row, 4).text()
|
|
754
|
+
to_name = self.portslist.item(row, 5).text()
|
|
755
|
+
direction = self.portslist.item(row, 6).text()
|
|
756
|
+
except Exception:
|
|
757
|
+
QMessageBox.warning(self, "Error", "Invalid input for port " + str(portnumber))
|
|
758
|
+
return False
|
|
759
|
+
# create port
|
|
760
|
+
if "Z" in direction:
|
|
761
|
+
# via port
|
|
762
|
+
simulation_ports.add_port(simulation_setup.simulation_port(portnumber=portnumber,
|
|
763
|
+
voltage=voltage,
|
|
764
|
+
port_Z0=Z0,
|
|
765
|
+
source_layernum=source_layer_num,
|
|
766
|
+
from_layername=from_name,
|
|
767
|
+
to_layername=to_name,
|
|
768
|
+
direction=direction))
|
|
769
|
+
else:
|
|
770
|
+
# in-plane port
|
|
771
|
+
simulation_ports.add_port(simulation_setup.simulation_port(portnumber=portnumber,
|
|
772
|
+
voltage=voltage,
|
|
773
|
+
port_Z0=Z0,
|
|
774
|
+
source_layernum=source_layer_num,
|
|
775
|
+
target_layername=target_name,
|
|
776
|
+
direction=direction))
|
|
777
|
+
|
|
778
|
+
return True
|
|
779
|
+
|
|
780
|
+
def load_values(self):
|
|
781
|
+
...
|
|
782
|
+
# self.log.setText(data.get("log", ""))
|
|
783
|
+
|
|
784
|
+
def update_layers(self, metals_list):
|
|
785
|
+
self.target_box.clear()
|
|
786
|
+
self.from_box.clear()
|
|
787
|
+
self.to_box.clear()
|
|
788
|
+
for metal in metals_list.metals:
|
|
789
|
+
self.target_box.addItems([metal.name])
|
|
790
|
+
self.from_box.addItems([metal.name])
|
|
791
|
+
self.to_box.addItems([metal.name])
|
|
792
|
+
# try to preset useful values for SG13G2 technology
|
|
793
|
+
index = self.target_box.findText('TopMetal2') # returns -1 if not found
|
|
794
|
+
if index != -1:
|
|
795
|
+
self.target_box.setCurrentIndex(index)
|
|
796
|
+
self.to_box.setCurrentIndex(index)
|
|
797
|
+
index = self.from_box.findText('Metal1') # returns -1 if not found
|
|
798
|
+
if index != -1:
|
|
799
|
+
self.from_box.setCurrentIndex(index)
|
|
800
|
+
|
|
801
|
+
|
|
802
|
+
def update_port_from_import (self, ports):
|
|
803
|
+
# update ports from imported model code in our native JSON format
|
|
804
|
+
|
|
805
|
+
self.portslist.clearContents()
|
|
806
|
+
for port in ports:
|
|
807
|
+
# each port is a dictionary
|
|
808
|
+
portnum = port.get("portnumber", None)
|
|
809
|
+
if portnum is not None:
|
|
810
|
+
portnum = int(portnum)
|
|
811
|
+
self.portslist.setItem(portnum-1, 0, QTableWidgetItem(str(port.get("port_Z0",50))))
|
|
812
|
+
self.portslist.setItem(portnum-1, 1, QTableWidgetItem(str(port.get("voltage",1.0))))
|
|
813
|
+
self.portslist.setItem(portnum-1, 2, QTableWidgetItem(str(port.get("source_layernum",""))))
|
|
814
|
+
self.portslist.setItem(portnum-1, 3, QTableWidgetItem(str(port.get("target_layername",""))))
|
|
815
|
+
self.portslist.setItem(portnum-1, 4, QTableWidgetItem(str(port.get("from_layername",""))))
|
|
816
|
+
self.portslist.setItem(portnum-1, 5, QTableWidgetItem(str(port.get("to_layername",""))))
|
|
817
|
+
self.portslist.setItem(portnum-1, 6, QTableWidgetItem(str(port.get("direction","")).upper()))
|
|
818
|
+
|
|
819
|
+
self.portslist.selectRow(1)
|
|
820
|
+
self.portslist.selectRow(0)
|
|
821
|
+
|
|
822
|
+
|
|
823
|
+
def update_port_from_EMStudio (self, ports):
|
|
824
|
+
# update ports from imported EM Studio data, which uses other keywords
|
|
825
|
+
|
|
826
|
+
self.portslist.clearContents()
|
|
827
|
+
for port in ports:
|
|
828
|
+
# each port is a dictionary
|
|
829
|
+
portnum = port.get("Num", None)
|
|
830
|
+
if portnum is not None:
|
|
831
|
+
portnum = int(portnum)
|
|
832
|
+
self.portslist.setItem(portnum-1, 0, QTableWidgetItem(str(port.get("Z0",50))))
|
|
833
|
+
self.portslist.setItem(portnum-1, 1, QTableWidgetItem(str(port.get("Voltage",1.0))))
|
|
834
|
+
self.portslist.setItem(portnum-1, 2, QTableWidgetItem(str(port.get("Source Layer",""))))
|
|
835
|
+
self.portslist.setItem(portnum-1, 3, QTableWidgetItem(str(port.get("Target Layername",""))))
|
|
836
|
+
self.portslist.setItem(portnum-1, 4, QTableWidgetItem(str(port.get("From Layer",""))))
|
|
837
|
+
self.portslist.setItem(portnum-1, 5, QTableWidgetItem(str(port.get("To Layer",""))))
|
|
838
|
+
self.portslist.setItem(portnum-1, 6, QTableWidgetItem(str(port.get("Direction","")).upper()))
|
|
839
|
+
|
|
840
|
+
self.portslist.selectRow(1)
|
|
841
|
+
self.portslist.selectRow(0)
|
|
842
|
+
|
|
843
|
+
|
|
844
|
+
|
|
845
|
+
|
|
846
|
+
class MeshTab(QWidget):
|
|
847
|
+
def __init__(self, MainWindow):
|
|
848
|
+
super().__init__()
|
|
849
|
+
|
|
850
|
+
self.MainWindow = MainWindow # parent = MainWindow
|
|
851
|
+
|
|
852
|
+
self.main_layout = QVBoxLayout()
|
|
853
|
+
self.main_layout.setAlignment(Qt.AlignTop)
|
|
854
|
+
|
|
855
|
+
label_width = 250
|
|
856
|
+
edit_width = 150
|
|
857
|
+
|
|
858
|
+
|
|
859
|
+
# ---------- MESH GROUP ----------
|
|
860
|
+
self.mesh_group = QGroupBox("Mesh settings")
|
|
861
|
+
self.mesh_layout = QVBoxLayout()
|
|
862
|
+
|
|
863
|
+
self.metalloss_layout = QHBoxLayout()
|
|
864
|
+
self.label1 = QLabel("Metal loss model")
|
|
865
|
+
self.label1.setFixedWidth(label_width)
|
|
866
|
+
self.metalloss_layout.addWidget(self.label1)
|
|
867
|
+
|
|
868
|
+
self.surface_impedance_box = QComboBox()
|
|
869
|
+
self.surface_impedance_box.setFixedWidth(edit_width)
|
|
870
|
+
self.surface_impedance_box.setStyleSheet(COMBO_STYLE_OPTIONAL)
|
|
871
|
+
self.surface_impedance_box.addItems(["Surface impedance"])
|
|
872
|
+
self.metalloss_layout.addWidget(self.surface_impedance_box)
|
|
873
|
+
self.metalloss_layout.addStretch()
|
|
874
|
+
self.mesh_layout.addLayout(self.metalloss_layout)
|
|
875
|
+
|
|
876
|
+
self.refinement_layout = QHBoxLayout()
|
|
877
|
+
self.label2 = QLabel("Mesh refinement at metal edges")
|
|
878
|
+
self.label2.setFixedWidth(label_width)
|
|
879
|
+
self.refinement_layout.addWidget(self.label2)
|
|
880
|
+
self.refinement_edit = QLineEdit("5")
|
|
881
|
+
self.refinement_edit.setFixedWidth(edit_width)
|
|
882
|
+
self.refinement_edit.setStyleSheet(EDIT_STYLE_REQUIRED)
|
|
883
|
+
self.refinement_layout.addWidget(self.refinement_edit)
|
|
884
|
+
self.label3 = QLabel(" µm")
|
|
885
|
+
self.refinement_layout.addWidget(self.label3)
|
|
886
|
+
self.refinement_layout.addStretch()
|
|
887
|
+
self.mesh_layout.addLayout(self.refinement_layout)
|
|
888
|
+
|
|
889
|
+
self.cells_lambda_layout = QHBoxLayout()
|
|
890
|
+
self.label4 = QLabel("Mesh cells per wavelength")
|
|
891
|
+
self.label4.setFixedWidth(label_width)
|
|
892
|
+
self.cells_lambda_layout.addWidget(self.label4)
|
|
893
|
+
self.cells_lambda_edit = QLineEdit("10")
|
|
894
|
+
self.cells_lambda_edit.setFixedWidth(edit_width)
|
|
895
|
+
self.cells_lambda_edit.setStyleSheet(EDIT_STYLE_OPTIONAL)
|
|
896
|
+
self.cells_lambda_layout.addWidget(self.cells_lambda_edit)
|
|
897
|
+
self.label5 = QLabel(" (min 10)")
|
|
898
|
+
self.cells_lambda_layout.addWidget(self.label5)
|
|
899
|
+
self.cells_lambda_layout.addStretch()
|
|
900
|
+
self.mesh_layout.addLayout(self.cells_lambda_layout)
|
|
901
|
+
|
|
902
|
+
|
|
903
|
+
self.cells_maxsize_layout = QHBoxLayout()
|
|
904
|
+
self.label6 = QLabel("Mesh cell maximum size absolute")
|
|
905
|
+
self.label6.setFixedWidth(label_width)
|
|
906
|
+
self.cells_maxsize_layout.addWidget(self.label6)
|
|
907
|
+
self.cells_maxsize_edit = QLineEdit("100")
|
|
908
|
+
self.cells_maxsize_edit.setFixedWidth(edit_width)
|
|
909
|
+
self.cells_maxsize_edit.setStyleSheet(EDIT_STYLE_OPTIONAL)
|
|
910
|
+
self.cells_maxsize_layout.addWidget(self.cells_maxsize_edit)
|
|
911
|
+
self.label5 = QLabel(" µm ")
|
|
912
|
+
self.cells_maxsize_layout.addWidget(self.label5)
|
|
913
|
+
self.cells_maxsize_layout.addStretch()
|
|
914
|
+
self.mesh_layout.addLayout(self.cells_maxsize_layout)
|
|
915
|
+
|
|
916
|
+
|
|
917
|
+
self.mesh_group.setLayout(self.mesh_layout)
|
|
918
|
+
self.main_layout.addWidget(self.mesh_group)
|
|
919
|
+
self.main_layout.addSpacing(20)
|
|
920
|
+
|
|
921
|
+
|
|
922
|
+
# ---------- MESH GROUP ----------
|
|
923
|
+
self.AMR_group = QGroupBox("Adaptive mesh refinement (AMR)")
|
|
924
|
+
self.AMR_layout = QVBoxLayout()
|
|
925
|
+
|
|
926
|
+
self.cells_AMRiterations_layout = QHBoxLayout()
|
|
927
|
+
self.labelAMR1 = QLabel("Adaptive mesh iterations")
|
|
928
|
+
self.labelAMR1.setFixedWidth(label_width)
|
|
929
|
+
self.cells_AMRiterations_layout.addWidget(self.labelAMR1)
|
|
930
|
+
self.AMR_iterations_edit = QLineEdit("0")
|
|
931
|
+
self.AMR_iterations_edit.setFixedWidth(edit_width)
|
|
932
|
+
self.AMR_iterations_edit.setStyleSheet(EDIT_STYLE_OPTIONAL)
|
|
933
|
+
self.cells_AMRiterations_layout.addWidget(self.AMR_iterations_edit)
|
|
934
|
+
self.labelAMR2 = QLabel(" (default is 0, no adaptive mesh refinement)")
|
|
935
|
+
self.cells_AMRiterations_layout.addWidget(self.labelAMR2)
|
|
936
|
+
self.cells_AMRiterations_layout.addStretch()
|
|
937
|
+
self.AMR_layout.addLayout(self.cells_AMRiterations_layout)
|
|
938
|
+
|
|
939
|
+
self.AMR_group.setLayout(self.AMR_layout)
|
|
940
|
+
self.main_layout.addWidget(self.AMR_group)
|
|
941
|
+
self.main_layout.addSpacing(20)
|
|
942
|
+
|
|
943
|
+
|
|
944
|
+
# ---------- BOUNDARY GROUP ----------
|
|
945
|
+
|
|
946
|
+
self.mesh_group = QGroupBox("Boundary settings")
|
|
947
|
+
self.mesh_layout = QVBoxLayout()
|
|
948
|
+
|
|
949
|
+
self.boundary_layout = QHBoxLayout()
|
|
950
|
+
self.label6 = QLabel("Boundary conditions")
|
|
951
|
+
self.label6.setFixedWidth(label_width)
|
|
952
|
+
self.boundary_layout.addWidget(self.label6)
|
|
953
|
+
self.boundary_box = QComboBox()
|
|
954
|
+
self.boundary_box.setFixedWidth(edit_width)
|
|
955
|
+
self.boundary_box.setStyleSheet(COMBO_STYLE_OPTIONAL)
|
|
956
|
+
self.boundary_box.addItems(["Absorbing","PEC","PMC"])
|
|
957
|
+
self.boundary_layout.addWidget(self.boundary_box)
|
|
958
|
+
self.boundary_layout.addStretch()
|
|
959
|
+
self.mesh_layout.addLayout(self.boundary_layout)
|
|
960
|
+
|
|
961
|
+
self.margins_layout = QHBoxLayout()
|
|
962
|
+
self.label7 = QLabel("Dielectric stackup: oversize by")
|
|
963
|
+
self.label7.setFixedWidth(label_width)
|
|
964
|
+
self.margins_layout.addWidget(self.label7)
|
|
965
|
+
self.margins_edit = QLineEdit("200")
|
|
966
|
+
self.margins_edit.setFixedWidth(edit_width)
|
|
967
|
+
self.margins_edit.setStyleSheet(EDIT_STYLE_REQUIRED)
|
|
968
|
+
self.margins_layout.addWidget(self.margins_edit)
|
|
969
|
+
self.label8 = QLabel(" µm from metal drawing")
|
|
970
|
+
self.margins_layout.addWidget(self.label8)
|
|
971
|
+
self.margins_layout.addStretch()
|
|
972
|
+
self.mesh_layout.addLayout(self.margins_layout)
|
|
973
|
+
|
|
974
|
+
|
|
975
|
+
self.airaround_layout = QHBoxLayout()
|
|
976
|
+
self.label9 = QLabel("Air layer thickness around stackup is")
|
|
977
|
+
self.label9.setFixedWidth(label_width)
|
|
978
|
+
self.airaround_layout.addWidget(self.label9)
|
|
979
|
+
|
|
980
|
+
self.airaround_box = QComboBox()
|
|
981
|
+
self.airaround_box.setFixedWidth(edit_width)
|
|
982
|
+
self.airaround_box.setStyleSheet(COMBO_STYLE_OPTIONAL)
|
|
983
|
+
self.airaround_box.addItems(["same on all sides","different per side"])
|
|
984
|
+
self.airaround_layout.addWidget(self.airaround_box)
|
|
985
|
+
|
|
986
|
+
air_edit_width = 100
|
|
987
|
+
|
|
988
|
+
self.airaround_edit = QLineEdit("200")
|
|
989
|
+
self.airaround_edit.setFixedWidth(air_edit_width)
|
|
990
|
+
self.airaround_edit.setStyleSheet(EDIT_STYLE_REQUIRED)
|
|
991
|
+
self.airaround_layout.addWidget(self.airaround_edit)
|
|
992
|
+
self.label10 = QLabel(" µm")
|
|
993
|
+
self.airaround_layout.addWidget(self.label10)
|
|
994
|
+
self.airaround_layout.addStretch()
|
|
995
|
+
self.mesh_layout.addLayout(self.airaround_layout)
|
|
996
|
+
|
|
997
|
+
self.airx_layout = QHBoxLayout()
|
|
998
|
+
self.label11 = QLabel("at xmin, xmax")
|
|
999
|
+
self.label11.setFixedWidth(label_width)
|
|
1000
|
+
self.label11.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
|
|
1001
|
+
self.airx_layout.addWidget(self.label11)
|
|
1002
|
+
self.airxmin_edit = QLineEdit("200")
|
|
1003
|
+
self.airxmin_edit.setFixedWidth(air_edit_width)
|
|
1004
|
+
self.airxmin_edit.setStyleSheet(EDIT_STYLE_REQUIRED)
|
|
1005
|
+
self.airx_layout.addWidget(self.airxmin_edit)
|
|
1006
|
+
self.airxmax_edit = QLineEdit("200")
|
|
1007
|
+
self.airxmax_edit.setFixedWidth(air_edit_width)
|
|
1008
|
+
self.airxmax_edit.setStyleSheet(EDIT_STYLE_REQUIRED)
|
|
1009
|
+
self.airx_layout.addWidget(self.airxmax_edit)
|
|
1010
|
+
self.label12 = QLabel(" µm")
|
|
1011
|
+
self.airx_layout.addWidget(self.label12)
|
|
1012
|
+
self.airx_layout.addStretch()
|
|
1013
|
+
self.mesh_layout.addLayout(self.airx_layout)
|
|
1014
|
+
|
|
1015
|
+
self.airy_layout = QHBoxLayout()
|
|
1016
|
+
self.label13 = QLabel("at ymin, ymax")
|
|
1017
|
+
self.label13.setFixedWidth(label_width)
|
|
1018
|
+
self.label13.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
|
|
1019
|
+
self.airy_layout.addWidget(self.label13)
|
|
1020
|
+
self.airymin_edit = QLineEdit("200")
|
|
1021
|
+
self.airymin_edit.setFixedWidth(air_edit_width)
|
|
1022
|
+
self.airymin_edit.setStyleSheet(EDIT_STYLE_REQUIRED)
|
|
1023
|
+
self.airy_layout.addWidget(self.airymin_edit)
|
|
1024
|
+
self.airymax_edit = QLineEdit("200")
|
|
1025
|
+
self.airymax_edit.setFixedWidth(air_edit_width)
|
|
1026
|
+
self.airymax_edit.setStyleSheet(EDIT_STYLE_REQUIRED)
|
|
1027
|
+
self.airy_layout.addWidget(self.airymax_edit)
|
|
1028
|
+
self.label14 = QLabel(" µm")
|
|
1029
|
+
self.airy_layout.addWidget(self.label14)
|
|
1030
|
+
self.airy_layout.addStretch()
|
|
1031
|
+
self.mesh_layout.addLayout(self.airy_layout)
|
|
1032
|
+
|
|
1033
|
+
self.airz_layout = QHBoxLayout()
|
|
1034
|
+
self.label15 = QLabel("at zmin, zmax")
|
|
1035
|
+
self.label15.setFixedWidth(label_width)
|
|
1036
|
+
self.label15.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
|
|
1037
|
+
self.airz_layout.addWidget(self.label15)
|
|
1038
|
+
self.airzmin_edit = QLineEdit("200")
|
|
1039
|
+
self.airzmin_edit.setFixedWidth(air_edit_width)
|
|
1040
|
+
self.airzmin_edit.setStyleSheet(EDIT_STYLE_REQUIRED)
|
|
1041
|
+
self.airz_layout.addWidget(self.airzmin_edit)
|
|
1042
|
+
self.airzmax_edit = QLineEdit("200")
|
|
1043
|
+
self.airzmax_edit.setFixedWidth(air_edit_width)
|
|
1044
|
+
self.airzmax_edit.setStyleSheet(EDIT_STYLE_REQUIRED)
|
|
1045
|
+
self.airz_layout.addWidget(self.airzmax_edit)
|
|
1046
|
+
self.label16 = QLabel(" µm")
|
|
1047
|
+
self.airz_layout.addWidget(self.label16)
|
|
1048
|
+
self.airz_layout.addStretch()
|
|
1049
|
+
self.mesh_layout.addLayout(self.airz_layout)
|
|
1050
|
+
|
|
1051
|
+
# callback when air_around dropdown changed, so that we can show/hide edit fields
|
|
1052
|
+
def on_airaround_changed(value):
|
|
1053
|
+
if "different" in value:
|
|
1054
|
+
# hide target layer label and edit
|
|
1055
|
+
for item in [self.label11,self.label12,self.label13,self.label14,self.label15,self.label16,self.airxmin_edit, self.airxmax_edit, self.airymin_edit, self.airymax_edit, self.airzmin_edit, self.airzmax_edit]:
|
|
1056
|
+
item.show()
|
|
1057
|
+
self.airaround_edit.hide()
|
|
1058
|
+
self.label10.hide()
|
|
1059
|
+
else:
|
|
1060
|
+
for item in [self.label11,self.label12,self.label13,self.label14,self.label15,self.label16,self.airxmin_edit, self.airxmax_edit, self.airymin_edit, self.airymax_edit, self.airzmin_edit, self.airzmax_edit]:
|
|
1061
|
+
item.hide()
|
|
1062
|
+
self.airaround_edit.show()
|
|
1063
|
+
self.label10.show()
|
|
1064
|
+
|
|
1065
|
+
self.airaround_box.currentTextChanged.connect(on_airaround_changed)
|
|
1066
|
+
self.airaround_box.setCurrentIndex(1)
|
|
1067
|
+
self.airaround_box.setCurrentIndex(0)
|
|
1068
|
+
|
|
1069
|
+
self.mesh_group.setLayout(self.mesh_layout)
|
|
1070
|
+
self.main_layout.addWidget(self.mesh_group)
|
|
1071
|
+
|
|
1072
|
+
|
|
1073
|
+
self.setLayout(self.main_layout)
|
|
1074
|
+
|
|
1075
|
+
def save_values(self):
|
|
1076
|
+
try:
|
|
1077
|
+
value = float(self.refinement_edit.text())
|
|
1078
|
+
except Exception:
|
|
1079
|
+
QMessageBox.warning(self, "Error", "Not a valid value for mesh refinement")
|
|
1080
|
+
self.refinement_edit.setText("5")
|
|
1081
|
+
return False
|
|
1082
|
+
saved_values ["refined_cellsize"] = float(value)
|
|
1083
|
+
|
|
1084
|
+
try:
|
|
1085
|
+
value = float(self.cells_lambda_edit.text())
|
|
1086
|
+
except Exception:
|
|
1087
|
+
QMessageBox.warning(self, "Error", "Not a valid value for cells/wavelength")
|
|
1088
|
+
self.cells_lambda_edit.setText("10")
|
|
1089
|
+
return False
|
|
1090
|
+
saved_values ["cells_per_wavelength"] = float(value)
|
|
1091
|
+
|
|
1092
|
+
try:
|
|
1093
|
+
value = float(self.cells_maxsize_edit.text())
|
|
1094
|
+
except Exception:
|
|
1095
|
+
QMessageBox.warning(self, "Error", "Not a valid value for max. meshsize")
|
|
1096
|
+
self.cells_maxsize_edit.setText("100")
|
|
1097
|
+
return False
|
|
1098
|
+
saved_values ["meshsize_max"] = float(value)
|
|
1099
|
+
|
|
1100
|
+
try:
|
|
1101
|
+
value = int(self.AMR_iterations_edit.text())
|
|
1102
|
+
except Exception:
|
|
1103
|
+
QMessageBox.warning(self, "Error", "Not a valid value for AMR iterations")
|
|
1104
|
+
self.AMR_iterations_edit.setText("0")
|
|
1105
|
+
return False
|
|
1106
|
+
saved_values ["adaptive_mesh_iterations"] = int(value)
|
|
1107
|
+
|
|
1108
|
+
BC = self.boundary_box.currentText()
|
|
1109
|
+
if BC=="PEC":
|
|
1110
|
+
saved_values ["boundary"] = ['PEC','PEC','PEC','PEC','PEC','PEC']
|
|
1111
|
+
elif BC=="PMC":
|
|
1112
|
+
saved_values ["boundary"] = ['PMC','PMC','PMC','PMC','PMC','PMC']
|
|
1113
|
+
else: # Absorbing
|
|
1114
|
+
saved_values ["boundary"] = ['ABC','ABC','ABC','ABC','ABC','ABC']
|
|
1115
|
+
|
|
1116
|
+
|
|
1117
|
+
try:
|
|
1118
|
+
value = float(self.margins_edit.text())
|
|
1119
|
+
except Exception:
|
|
1120
|
+
QMessageBox.warning(self, "Error", "Not a valid value for dielectric oversize margin")
|
|
1121
|
+
self.margins_edit.setText("200")
|
|
1122
|
+
return False
|
|
1123
|
+
saved_values ["margin"] = float(value)
|
|
1124
|
+
|
|
1125
|
+
airsides = self.airaround_box.currentText()
|
|
1126
|
+
if not "different" in airsides:
|
|
1127
|
+
# one air margin for all
|
|
1128
|
+
try:
|
|
1129
|
+
value = float(self.airaround_edit.text())
|
|
1130
|
+
except Exception:
|
|
1131
|
+
QMessageBox.warning(self, "Error", "Not a valid value for AMR iterations")
|
|
1132
|
+
# default air thickness = margins
|
|
1133
|
+
self.airaround_edit.setText(self.margins_edit.text())
|
|
1134
|
+
return False
|
|
1135
|
+
saved_values ["air_around"] = float(value)
|
|
1136
|
+
else:
|
|
1137
|
+
# one air margin per side, total 6 values
|
|
1138
|
+
try:
|
|
1139
|
+
xmin = float(self.airxmin_edit.text())
|
|
1140
|
+
xmax = float(self.airxmax_edit.text())
|
|
1141
|
+
ymin = float(self.airymin_edit.text())
|
|
1142
|
+
ymax = float(self.airymax_edit.text())
|
|
1143
|
+
zmin = float(self.airzmin_edit.text())
|
|
1144
|
+
zmax = float(self.airzmax_edit.text())
|
|
1145
|
+
except Exception:
|
|
1146
|
+
QMessageBox.warning(self, "Error", "Not a valid value for air margins, reset to default")
|
|
1147
|
+
# default air thickness = margins
|
|
1148
|
+
self.airxmin_edit.setText(self.margins_edit.text())
|
|
1149
|
+
self.airxmax_edit.setText(self.margins_edit.text())
|
|
1150
|
+
self.airymin_edit.setText(self.margins_edit.text())
|
|
1151
|
+
self.airymax_edit.setText(self.margins_edit.text())
|
|
1152
|
+
self.airzmin_edit.setText(self.margins_edit.text())
|
|
1153
|
+
self.airzmax_edit.setText(self.margins_edit.text())
|
|
1154
|
+
return False
|
|
1155
|
+
saved_values ["air_around"] = [xmin, xmax, ymin, ymax, zmin, zmax]
|
|
1156
|
+
|
|
1157
|
+
# all saved
|
|
1158
|
+
return True
|
|
1159
|
+
|
|
1160
|
+
|
|
1161
|
+
def load_values(self):
|
|
1162
|
+
self.refinement_edit.setText(str(saved_values.get("refined_cellsize","5")))
|
|
1163
|
+
self.cells_lambda_edit.setText(str(saved_values.get("cells_per_wavelength","10")))
|
|
1164
|
+
self.cells_maxsize_edit.setText(str(saved_values.get("meshsize_max","100")))
|
|
1165
|
+
self.AMR_iterations_edit.setText(str(saved_values.get("adaptive_mesh_iterations","0")))
|
|
1166
|
+
self.margins_edit.setText(str(saved_values.get("margin","200")))
|
|
1167
|
+
|
|
1168
|
+
if 'PEC' in saved_values.get("boundary",""):
|
|
1169
|
+
self.boundary_box.setCurrentIndex(1)
|
|
1170
|
+
elif 'PMC' in saved_values.get("boundary",""):
|
|
1171
|
+
self.boundary_box.setCurrentIndex(2)
|
|
1172
|
+
else:
|
|
1173
|
+
# default is absorbing
|
|
1174
|
+
self.boundary_box.setCurrentIndex(0)
|
|
1175
|
+
|
|
1176
|
+
# check if air layer is defined at all, or single value or list
|
|
1177
|
+
air = saved_values.get("air_around","")
|
|
1178
|
+
if air == "":
|
|
1179
|
+
# no value defined, use same value as dielectric margins
|
|
1180
|
+
self.airaround_edit.setText(saved_values.get("margin","200"))
|
|
1181
|
+
self.airaround_box.setCurrentIndex(0)
|
|
1182
|
+
else:
|
|
1183
|
+
if "," in str(air):
|
|
1184
|
+
# we have a list of 6 values
|
|
1185
|
+
air_as_list = air.split(',')
|
|
1186
|
+
if len(air_as_list) == 6:
|
|
1187
|
+
self.airaround_box.setCurrentIndex(1)
|
|
1188
|
+
self.airxmin_edit.setText(air_as_list[0])
|
|
1189
|
+
self.airxmax_edit.setText(air_as_list[1])
|
|
1190
|
+
self.airymin_edit.setText(air_as_list[2])
|
|
1191
|
+
self.airymax_edit.setText(air_as_list[3])
|
|
1192
|
+
self.airzmin_edit.setText(air_as_list[4])
|
|
1193
|
+
self.airzmax_edit.setText(air_as_list[5])
|
|
1194
|
+
else:
|
|
1195
|
+
# we have air_around defined as a single value
|
|
1196
|
+
self.airaround_box.setCurrentIndex(0)
|
|
1197
|
+
self.airaround_edit.setText(str(air))
|
|
1198
|
+
|
|
1199
|
+
|
|
1200
|
+
|
|
1201
|
+
class CreateModelTab(QWidget):
|
|
1202
|
+
def __init__(self, MainWindow):
|
|
1203
|
+
super().__init__()
|
|
1204
|
+
|
|
1205
|
+
self.MainWindow = MainWindow # parent = MainWindow
|
|
1206
|
+
|
|
1207
|
+
self.main_layout = QVBoxLayout()
|
|
1208
|
+
self.main_layout.setAlignment(Qt.AlignTop)
|
|
1209
|
+
|
|
1210
|
+
# File group
|
|
1211
|
+
|
|
1212
|
+
self.file_group = QGroupBox("Output files for Palace model")
|
|
1213
|
+
self.file_layout = QVBoxLayout()
|
|
1214
|
+
|
|
1215
|
+
self.targetdir_layout = QHBoxLayout()
|
|
1216
|
+
self.label2 = QLabel("Target directory:")
|
|
1217
|
+
self.label2.setFixedWidth(120)
|
|
1218
|
+
self.targetdir_layout.addWidget(self.label2)
|
|
1219
|
+
self.targetdir_edit = QLineEdit("")
|
|
1220
|
+
self.targetdir_edit.setStyleSheet(EDIT_STYLE_REQUIRED)
|
|
1221
|
+
self.targetdir_layout.addWidget(self.targetdir_edit)
|
|
1222
|
+
self.targetdir_btn = QPushButton("Browse ...")
|
|
1223
|
+
self.targetdir_btn.setFixedWidth(150) # narrower
|
|
1224
|
+
self.targetdir_btn.clicked.connect(self.browse_directory)
|
|
1225
|
+
self.targetdir_layout.addWidget(self.targetdir_btn)
|
|
1226
|
+
self.file_layout.addLayout(self.targetdir_layout)
|
|
1227
|
+
|
|
1228
|
+
|
|
1229
|
+
self.modelname_layout = QHBoxLayout()
|
|
1230
|
+
self.label1 = QLabel("Model name:")
|
|
1231
|
+
self.label1.setFixedWidth(120)
|
|
1232
|
+
self.modelname_layout.addWidget(self.label1)
|
|
1233
|
+
self.modelname_edit = QLineEdit("")
|
|
1234
|
+
self.modelname_edit.setFixedWidth(250)
|
|
1235
|
+
self.modelname_edit.setStyleSheet(EDIT_STYLE_OPTIONAL)
|
|
1236
|
+
# install event filter, so we capture when edit looses focus
|
|
1237
|
+
self.modelname_edit.editingFinished.connect(self.on_modelname_edit_done)
|
|
1238
|
+
|
|
1239
|
+
self.modelname_layout.addWidget(self.modelname_edit)
|
|
1240
|
+
self.modelname_layout.addStretch()
|
|
1241
|
+
self.file_layout.addLayout(self.modelname_layout)
|
|
1242
|
+
|
|
1243
|
+
self.preview_model_btn = QPushButton("⚙️ Preview model geometry in gmsh")
|
|
1244
|
+
self.preview_model_btn.clicked.connect(self.preview_model)
|
|
1245
|
+
self.file_layout.addWidget(self.preview_model_btn)
|
|
1246
|
+
|
|
1247
|
+
self.create_model_btn = QPushButton("⚙️ Create mesh and Palace model file")
|
|
1248
|
+
self.create_model_btn.clicked.connect(self.create_mesh)
|
|
1249
|
+
self.file_layout.addWidget(self.create_model_btn)
|
|
1250
|
+
|
|
1251
|
+
self.run_layout = QHBoxLayout()
|
|
1252
|
+
self.create_run_btn = QPushButton("▶️ Run Palace")
|
|
1253
|
+
self.create_run_btn.clicked.connect(self.run_model)
|
|
1254
|
+
self.run_layout.addWidget(self.create_run_btn)
|
|
1255
|
+
self.kill_btn = QPushButton("🛑 Terminate ")
|
|
1256
|
+
self.kill_btn.clicked.connect(self.terminate_run)
|
|
1257
|
+
self.run_layout.addWidget(self.kill_btn)
|
|
1258
|
+
self.run_layout.setStretch(0, 5) # index 0 = Run
|
|
1259
|
+
self.run_layout.setStretch(1, 1) # index 1 = Terminate
|
|
1260
|
+
|
|
1261
|
+
self.file_layout.addLayout(self.run_layout)
|
|
1262
|
+
|
|
1263
|
+
self.file_group.setLayout(self.file_layout)
|
|
1264
|
+
|
|
1265
|
+
|
|
1266
|
+
# Log group
|
|
1267
|
+
|
|
1268
|
+
self.log_group = QGroupBox("Log file")
|
|
1269
|
+
self.log_layout = QVBoxLayout()
|
|
1270
|
+
self.log_group.setLayout(self.log_layout)
|
|
1271
|
+
self.log_area = QPlainTextEdit()
|
|
1272
|
+
self.log_area.setReadOnly(True)
|
|
1273
|
+
self.log_layout.addWidget(self.log_area)
|
|
1274
|
+
|
|
1275
|
+
|
|
1276
|
+
self.main_layout.addWidget(self.file_group)
|
|
1277
|
+
self.main_layout.addSpacing(20)
|
|
1278
|
+
self.main_layout.addWidget(self.log_group)
|
|
1279
|
+
self.setLayout(self.main_layout)
|
|
1280
|
+
|
|
1281
|
+
|
|
1282
|
+
# --- QProcess setup ---
|
|
1283
|
+
self.process = QProcess(self)
|
|
1284
|
+
self.process.readyReadStandardOutput.connect(self.on_stdout)
|
|
1285
|
+
self.process.readyReadStandardError.connect(self.on_stderr)
|
|
1286
|
+
self.process.finished.connect(self.on_finished)
|
|
1287
|
+
|
|
1288
|
+
def on_modelname_edit_done(self):
|
|
1289
|
+
# Model name edit field has changed
|
|
1290
|
+
saved_values['model_basename']= self.modelname_edit.text()
|
|
1291
|
+
|
|
1292
|
+
|
|
1293
|
+
def on_stdout(self):
|
|
1294
|
+
data = self.process.readAllStandardOutput().data().decode()
|
|
1295
|
+
for line in data.splitlines():
|
|
1296
|
+
if line.strip(): # Skip empty lines
|
|
1297
|
+
self.log_area.appendPlainText(line)
|
|
1298
|
+
|
|
1299
|
+
def on_stderr(self):
|
|
1300
|
+
data = self.process.readAllStandardError().data().decode()
|
|
1301
|
+
for line in data.splitlines():
|
|
1302
|
+
if line.strip(): # Skip empty lines
|
|
1303
|
+
self.log_area.appendPlainText(f"[Error] {line}")
|
|
1304
|
+
|
|
1305
|
+
|
|
1306
|
+
def on_finished(self, exit_code, exit_status):
|
|
1307
|
+
"""Handle process completion."""
|
|
1308
|
+
self.log_area.appendPlainText(f"\n--- Process finished with exit code {exit_code} ---\n")
|
|
1309
|
+
|
|
1310
|
+
|
|
1311
|
+
def browse_directory(self):
|
|
1312
|
+
directory = QFileDialog.getExistingDirectory(
|
|
1313
|
+
self,
|
|
1314
|
+
"Select Target Directory",
|
|
1315
|
+
"", # Starting directory ("" = current)
|
|
1316
|
+
QFileDialog.ShowDirsOnly | QFileDialog.DontResolveSymlinks
|
|
1317
|
+
)
|
|
1318
|
+
if directory:
|
|
1319
|
+
self.targetdir_edit.setText(str(directory))
|
|
1320
|
+
saved_values['sim_path'] = str(directory)
|
|
1321
|
+
|
|
1322
|
+
|
|
1323
|
+
def save_values(self):
|
|
1324
|
+
saved_values['model_basename'] = self.modelname_edit.text()
|
|
1325
|
+
saved_values['sim_path'] = self.targetdir_edit.text().replace('\\','/')
|
|
1326
|
+
return True # Tab change only possible when returning True
|
|
1327
|
+
|
|
1328
|
+
|
|
1329
|
+
def load_values(self):
|
|
1330
|
+
# set target dir to GDSII directory by default
|
|
1331
|
+
gdsfile = saved_values.get("GdsFile","")
|
|
1332
|
+
|
|
1333
|
+
model_basename = saved_values.get('model_basename','')
|
|
1334
|
+
if model_basename == "":
|
|
1335
|
+
if gdsfile != "":
|
|
1336
|
+
model_basename = 'run_' + os.path.basename(gdsfile).replace('.gds', '')
|
|
1337
|
+
if "===" in model_basename:
|
|
1338
|
+
model_basename = ""
|
|
1339
|
+
self.modelname_edit.setText(model_basename)
|
|
1340
|
+
|
|
1341
|
+
sim_path = saved_values.get('sim_path','')
|
|
1342
|
+
if sim_path == "":
|
|
1343
|
+
if gdsfile != "":
|
|
1344
|
+
gds_dir = os.path.normcase(os.path.dirname(gdsfile))
|
|
1345
|
+
else:
|
|
1346
|
+
gds_dir = os.getcwd()
|
|
1347
|
+
if os.path.isdir(gds_dir):
|
|
1348
|
+
self.targetdir_edit.setText(gds_dir)
|
|
1349
|
+
else:
|
|
1350
|
+
self.targetdir_edit.setText("")
|
|
1351
|
+
else:
|
|
1352
|
+
if os.path.exists(sim_path):
|
|
1353
|
+
self.targetdir_edit.setText(sim_path)
|
|
1354
|
+
|
|
1355
|
+
|
|
1356
|
+
|
|
1357
|
+
def preview_model(self):
|
|
1358
|
+
# create model and run gmsh, but skip the final mesh and output file creation
|
|
1359
|
+
|
|
1360
|
+
# check if filenames are valid, maybe they are from different machine
|
|
1361
|
+
gdsfile = saved_values.get("GdsFile")
|
|
1362
|
+
XMLfile = saved_values.get("SubstrateFile")
|
|
1363
|
+
if os.path.isfile(gdsfile):
|
|
1364
|
+
if os.path.isfile(XMLfile):
|
|
1365
|
+
saved_values['preview_only'] = True
|
|
1366
|
+
saved_values['no_preview'] = False
|
|
1367
|
+
self.create_model()
|
|
1368
|
+
del saved_values['preview_only']
|
|
1369
|
+
del saved_values['no_preview']
|
|
1370
|
+
else:
|
|
1371
|
+
self.log_area.appendPlainText("⚠️ Cannot load XML stackup file!\n"+saved_values.get("SubstrateFile")+"\n")
|
|
1372
|
+
else:
|
|
1373
|
+
self.log_area.appendPlainText("⚠️ Cannot load GDSII layout stackup file!\n"+saved_values.get("GdsFile")+"\n")
|
|
1374
|
+
|
|
1375
|
+
|
|
1376
|
+
|
|
1377
|
+
def create_mesh(self):
|
|
1378
|
+
# create model and run gmsh, but skip the final mesh and output file creation
|
|
1379
|
+
|
|
1380
|
+
# check if filenames are valid, maybe they are from different machine
|
|
1381
|
+
gdsfile = saved_values.get("GdsFile")
|
|
1382
|
+
XMLfile = saved_values.get("SubstrateFile")
|
|
1383
|
+
if os.path.isfile(gdsfile):
|
|
1384
|
+
if os.path.isfile(XMLfile):
|
|
1385
|
+
saved_values['preview_only'] = False
|
|
1386
|
+
saved_values['no_preview'] = True
|
|
1387
|
+
self.create_model()
|
|
1388
|
+
del saved_values['preview_only']
|
|
1389
|
+
del saved_values['no_preview']
|
|
1390
|
+
else:
|
|
1391
|
+
self.log_area.appendPlainText("⚠️ Cannot load XML stackup file!\n"+saved_values.get("SubstrateFile")+"\n")
|
|
1392
|
+
else:
|
|
1393
|
+
self.log_area.appendPlainText("⚠️ Cannot load GDSII layout stackup file!\n"+saved_values.get("GdsFile")+"\n")
|
|
1394
|
+
|
|
1395
|
+
|
|
1396
|
+
|
|
1397
|
+
def create_model(self):
|
|
1398
|
+
# Request all tabs to save values again,
|
|
1399
|
+
# which can do some update to saved_values
|
|
1400
|
+
self.MainWindow.save_all_tabs()
|
|
1401
|
+
|
|
1402
|
+
if simulation_ports.portcount > 0 or saved_values['preview_only']:
|
|
1403
|
+
# save settings on this page to internal data structure
|
|
1404
|
+
self.save_values()
|
|
1405
|
+
# clear log
|
|
1406
|
+
self.log_area.clear()
|
|
1407
|
+
|
|
1408
|
+
# get code from model editor tab
|
|
1409
|
+
self.MainWindow.modeleditor_tab.create_model_text()
|
|
1410
|
+
code = self.MainWindow.modeleditor_tab.model_edit.toPlainText().strip()
|
|
1411
|
+
if not code:
|
|
1412
|
+
self.log_area.appendPlainText("⚠️ No code to run.\n")
|
|
1413
|
+
return
|
|
1414
|
+
|
|
1415
|
+
# Write code to Python file
|
|
1416
|
+
pymodel_filename = os.path.abspath(os.path.join(saved_values['sim_path'], saved_values['model_basename']+'.py'))
|
|
1417
|
+
with open(pymodel_filename, "w", encoding="utf-8") as f:
|
|
1418
|
+
f.write(code)
|
|
1419
|
+
f.close()
|
|
1420
|
+
|
|
1421
|
+
# Run Python interpreter on that file
|
|
1422
|
+
python_exe = sys.executable # Use the same Python interpreter
|
|
1423
|
+
self.process.start(python_exe, [pymodel_filename])
|
|
1424
|
+
|
|
1425
|
+
|
|
1426
|
+
else:
|
|
1427
|
+
QMessageBox.warning(self, "Error", "Model incomplete, there are no simulation ports defined!")
|
|
1428
|
+
|
|
1429
|
+
|
|
1430
|
+
|
|
1431
|
+
def terminate_run(self):
|
|
1432
|
+
if self.process.state() == QProcess.Running:
|
|
1433
|
+
self.process.terminate()
|
|
1434
|
+
if not self.process.waitForFinished(2000):
|
|
1435
|
+
self.process.kill()
|
|
1436
|
+
|
|
1437
|
+
|
|
1438
|
+
|
|
1439
|
+
def run_model(self):
|
|
1440
|
+
# Run model that we created before
|
|
1441
|
+
|
|
1442
|
+
# clear log
|
|
1443
|
+
self.log_area.clear()
|
|
1444
|
+
self.log_area.appendPlainText("Trying to start Palace using script ./run_sim now")
|
|
1445
|
+
|
|
1446
|
+
run_path = saved_values['sim_path'] + "/palace_model/" + saved_values['model_basename'] + "_data"
|
|
1447
|
+
|
|
1448
|
+
if os.name == "nt":
|
|
1449
|
+
# Windows
|
|
1450
|
+
|
|
1451
|
+
def windows_to_wsl_path(win_path: str) -> str:
|
|
1452
|
+
"""
|
|
1453
|
+
Convert a Windows-style path like:
|
|
1454
|
+
C:\\Users\\Volker\\Projects\\SimApp
|
|
1455
|
+
into a WSL-style path like:
|
|
1456
|
+
/mnt/c/Users/Volker/Projects/SimApp
|
|
1457
|
+
"""
|
|
1458
|
+
win_path = win_path.strip()
|
|
1459
|
+
if not win_path or ":" not in win_path:
|
|
1460
|
+
return win_path # Already looks like a Linux path or invalid
|
|
1461
|
+
drive, rest = win_path.split(":", 1)
|
|
1462
|
+
drive = drive.lower()
|
|
1463
|
+
rest = rest.replace("\\", "/").lstrip("/")
|
|
1464
|
+
return f"/mnt/{drive}/{rest}"
|
|
1465
|
+
|
|
1466
|
+
|
|
1467
|
+
wsl_run_path = windows_to_wsl_path(run_path)
|
|
1468
|
+
# tell user what to do, we can open WSL in the right place but not start simulation
|
|
1469
|
+
self.log_area.appendPlainText("Running on Windows with WSL, you need to type ./run_sim in the terminal yourself!")
|
|
1470
|
+
self.log_area.appendPlainText("Note that this works for LOCAL drives only, we can't open WSL on network drive.")
|
|
1471
|
+
self.process.start("cmd.exe", ["/c", "start", "wt.exe", "wsl", "--cd", wsl_run_path])
|
|
1472
|
+
else:
|
|
1473
|
+
# Linux
|
|
1474
|
+
self.log_area.appendPlainText('Setting work directory ' + run_path)
|
|
1475
|
+
# make file executable
|
|
1476
|
+
run_file = os.path.join(run_path, 'run_sim')
|
|
1477
|
+
os.chmod(run_file, 0o755)
|
|
1478
|
+
|
|
1479
|
+
self.process.setWorkingDirectory(run_path)
|
|
1480
|
+
# start simulation
|
|
1481
|
+
self.process.start(".//run_sim")
|
|
1482
|
+
|
|
1483
|
+
|
|
1484
|
+
|
|
1485
|
+
class ModelEditorTab(QWidget):
|
|
1486
|
+
def __init__(self, MainWindow):
|
|
1487
|
+
super().__init__()
|
|
1488
|
+
|
|
1489
|
+
self.MainWindow = MainWindow # parent = MainWindow
|
|
1490
|
+
|
|
1491
|
+
self.main_layout = QVBoxLayout()
|
|
1492
|
+
self.main_layout.setAlignment(Qt.AlignTop)
|
|
1493
|
+
|
|
1494
|
+
self.model_edit = CodeEditor()
|
|
1495
|
+
self.main_layout.addWidget(self.model_edit)
|
|
1496
|
+
self.model_edit.setReadOnly(True)
|
|
1497
|
+
self.setLayout(self.main_layout)
|
|
1498
|
+
|
|
1499
|
+
def create_model_text(self, forExport=False):
|
|
1500
|
+
# Create model text in editor from in-memory data
|
|
1501
|
+
# This can look different from imported Python model code
|
|
1502
|
+
|
|
1503
|
+
def add_text(text):
|
|
1504
|
+
self.model_edit.appendPlainText(text)
|
|
1505
|
+
|
|
1506
|
+
def add_key (key):
|
|
1507
|
+
value = str(saved_values[key])
|
|
1508
|
+
if '\\' in value:
|
|
1509
|
+
# make sure we don't run into escape character issue with Windows paths
|
|
1510
|
+
value = value.replace('\\','/')
|
|
1511
|
+
|
|
1512
|
+
if key in ['fstart','fstop','fstep']:
|
|
1513
|
+
# special case: we have unit Hz in Python code but unit GHz in this GUI program internally
|
|
1514
|
+
value = str(value) + 'e9'
|
|
1515
|
+
|
|
1516
|
+
if key in ['fdump','fpoint']:
|
|
1517
|
+
# special case: we have unit Hz in Python code but unit GHz in this GUI program internally
|
|
1518
|
+
# value is string representation of a list, so make it a list now
|
|
1519
|
+
flist = ast.literal_eval(value)
|
|
1520
|
+
new = "["
|
|
1521
|
+
for n,f in enumerate(flist):
|
|
1522
|
+
if n>0:
|
|
1523
|
+
new = new + ","
|
|
1524
|
+
new = new + str(f) + "e9"
|
|
1525
|
+
new = new + "]"
|
|
1526
|
+
value = new
|
|
1527
|
+
|
|
1528
|
+
|
|
1529
|
+
if isinstance(saved_values[key],str):
|
|
1530
|
+
# value is a string, enclose in quotes and check for backslash (Windows path!)
|
|
1531
|
+
add_text("settings['" + key + "'] = '" + value+ "'")
|
|
1532
|
+
else:
|
|
1533
|
+
add_text("settings['" + key + "'] = " + str(value) )
|
|
1534
|
+
|
|
1535
|
+
|
|
1536
|
+
# get folder where this GUI application is running and assume we have gds2palace modules there
|
|
1537
|
+
# this_app_path = os.path.abspath(os.path.join(os.path.dirname(__file__))).replace('\\','/')
|
|
1538
|
+
|
|
1539
|
+
# for port tab to update data
|
|
1540
|
+
self.MainWindow.save_all_tabs()
|
|
1541
|
+
|
|
1542
|
+
self.model_edit.clear()
|
|
1543
|
+
add_text("# Model for IHP OpenPDK EM workflow created using " + APP_NAME)
|
|
1544
|
+
add_text("import os, sys, subprocess")
|
|
1545
|
+
|
|
1546
|
+
add_text("\n# We expect gds2palace in PYTHONPATH *or* in the same directory as this model file")
|
|
1547
|
+
add_text("# sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), 'gds2palace')))")
|
|
1548
|
+
# add_text(f"sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname('{this_app_path}'), 'gds2palace')))")
|
|
1549
|
+
add_text("from gds2palace import *")
|
|
1550
|
+
|
|
1551
|
+
add_text("\n# get path for this simulation file")
|
|
1552
|
+
add_text("script_path = utilities.get_script_path(__file__)")
|
|
1553
|
+
add_text("# use script filename as model basename")
|
|
1554
|
+
add_text("model_basename = utilities.get_basename(__file__)")
|
|
1555
|
+
add_text("# set and create directory for simulation output")
|
|
1556
|
+
add_text("sim_path = utilities.create_sim_path (script_path,model_basename)")
|
|
1557
|
+
|
|
1558
|
+
add_text("\n# ========================= workflow settings ==========================")
|
|
1559
|
+
add_text("# preview model/mesh only, without running solver?")
|
|
1560
|
+
add_text("start_simulation = False")
|
|
1561
|
+
|
|
1562
|
+
if forExport:
|
|
1563
|
+
add_text("\n# Command to start simulation")
|
|
1564
|
+
add_text("# run_command = ['start', 'wsl.exe'] # Windows Subsystem for Linux")
|
|
1565
|
+
add_text("run_command = ['./run_sim'] # Linux")
|
|
1566
|
+
|
|
1567
|
+
add_text("\n# ===================== input files and settings =======================")
|
|
1568
|
+
add_text("settings={}")
|
|
1569
|
+
|
|
1570
|
+
# List of keys that must be included in Python code AFTER reading stackups and GDSII, not before
|
|
1571
|
+
special_keylist = ['simulation_ports','materials_list','dielectrics_list','metals_list',
|
|
1572
|
+
'layernumbers','allpolygons']
|
|
1573
|
+
# List of keys that we don't write to Python model code editor
|
|
1574
|
+
ignore_list = ['model_basename','sim_path']
|
|
1575
|
+
|
|
1576
|
+
if forExport:
|
|
1577
|
+
# these commands are only used within this GUI application to control gmsh
|
|
1578
|
+
ignore_list.append(['preview_only','no_preview'])
|
|
1579
|
+
|
|
1580
|
+
|
|
1581
|
+
for key in saved_values.keys():
|
|
1582
|
+
if not key in special_keylist:
|
|
1583
|
+
if not key in ignore_list:
|
|
1584
|
+
add_key(key)
|
|
1585
|
+
|
|
1586
|
+
|
|
1587
|
+
add_text("\n# ===================== port definitions =======================")
|
|
1588
|
+
add_text("simulation_ports = simulation_setup.all_simulation_ports()")
|
|
1589
|
+
for sim_port in simulation_ports.ports:
|
|
1590
|
+
if "Z" in sim_port.direction.upper():
|
|
1591
|
+
add_text(f"simulation_ports.add_port(simulation_setup.simulation_port("
|
|
1592
|
+
f"portnumber={str(sim_port.portnumber)}, "
|
|
1593
|
+
f"voltage={str(sim_port.voltage)}, "
|
|
1594
|
+
f"port_Z0={str(sim_port.port_Z0)}, "
|
|
1595
|
+
f"source_layernum={str(sim_port.source_layernum)}, "
|
|
1596
|
+
f"from_layername='{sim_port.from_layername}', "
|
|
1597
|
+
f"to_layername='{sim_port.to_layername}', "
|
|
1598
|
+
f"direction='{sim_port.direction}'))")
|
|
1599
|
+
else:
|
|
1600
|
+
add_text(f"simulation_ports.add_port(simulation_setup.simulation_port("
|
|
1601
|
+
f"portnumber={str(sim_port.portnumber)}, "
|
|
1602
|
+
f"voltage={str(sim_port.voltage)}, "
|
|
1603
|
+
f"port_Z0={str(sim_port.port_Z0)}, "
|
|
1604
|
+
f"source_layernum={str(sim_port.source_layernum)}, "
|
|
1605
|
+
f"target_layername='{sim_port.target_layername}', "
|
|
1606
|
+
f"direction='{sim_port.direction}'))")
|
|
1607
|
+
|
|
1608
|
+
add_text("\n# ================= read stackup and geometries =================")
|
|
1609
|
+
add_text("materials_list, dielectrics_list, metals_list = stackup_reader.read_substrate (settings['SubstrateFile'])")
|
|
1610
|
+
add_text("layernumbers = metals_list.getlayernumbers()")
|
|
1611
|
+
add_text("layernumbers.extend(simulation_ports.portlayers)")
|
|
1612
|
+
add_text("\n# read geometries from GDSII")
|
|
1613
|
+
add_text("allpolygons = gds_reader.read_gds(settings['GdsFile'], "
|
|
1614
|
+
"\n\tlayernumbers,"
|
|
1615
|
+
"\n\tpurposelist=settings['purpose'], "
|
|
1616
|
+
"\n\tmetals_list=metals_list, \n\tpreprocess=settings['preprocess_gds'], "
|
|
1617
|
+
"\n\tmerge_polygon_size=settings['merge_polygon_size'],"
|
|
1618
|
+
"\n\tgds_boundary_layers=[],"
|
|
1619
|
+
"\n\tmirror=False, "
|
|
1620
|
+
"\n\toffset_x=0, offset_y=0,"
|
|
1621
|
+
"\n\tlayernumber_offset=0)")
|
|
1622
|
+
add_text("\n")
|
|
1623
|
+
|
|
1624
|
+
|
|
1625
|
+
# Now do the special keys that we skipped before
|
|
1626
|
+
for key in special_keylist:
|
|
1627
|
+
add_text("settings['" + key + "'] = " + key)
|
|
1628
|
+
add_text("settings['sim_path'] = sim_path")
|
|
1629
|
+
add_text("settings['model_basename'] = model_basename")
|
|
1630
|
+
|
|
1631
|
+
|
|
1632
|
+
# now create the Palace files
|
|
1633
|
+
add_text("\n# list of ports that are excited (set voltage to zero in port excitation to skip an excitation!)")
|
|
1634
|
+
add_text("excite_ports = simulation_ports.all_active_excitations()")
|
|
1635
|
+
add_text("config_name, data_dir = simulation_setup.create_palace (excite_ports, settings)")
|
|
1636
|
+
|
|
1637
|
+
add_text("\n# for convenience, write run script to model directory")
|
|
1638
|
+
add_text("utilities.create_run_script(settings['sim_path'])")
|
|
1639
|
+
|
|
1640
|
+
if forExport:
|
|
1641
|
+
add_text("\n# run after crerating mesh and Palace config.json ")
|
|
1642
|
+
add_text("if start_simulation:")
|
|
1643
|
+
add_text(" try:")
|
|
1644
|
+
add_text(" os.chdir(sim_path)")
|
|
1645
|
+
add_text(" subprocess.run(run_command, shell=True)")
|
|
1646
|
+
add_text(" except:")
|
|
1647
|
+
add_text(" print(f'Unable to run Palace using command ',run_command)\n")
|
|
1648
|
+
|
|
1649
|
+
|
|
1650
|
+
def save_values(self):
|
|
1651
|
+
self.create_model_text(forExport=True) # show "external" code including run from Python model
|
|
1652
|
+
return True
|
|
1653
|
+
|
|
1654
|
+
def load_values(self):
|
|
1655
|
+
self.create_model_text(forExport=True) # show "external" code including run from Python model
|
|
1656
|
+
|
|
1657
|
+
|
|
1658
|
+
# ---- Python Syntax Highlighter ----
|
|
1659
|
+
class PythonHighlighter(QSyntaxHighlighter):
|
|
1660
|
+
def __init__(self, parent):
|
|
1661
|
+
super().__init__(parent)
|
|
1662
|
+
self.highlighting_rules = []
|
|
1663
|
+
|
|
1664
|
+
# --- Keyword format ---
|
|
1665
|
+
keyword_format = QTextCharFormat()
|
|
1666
|
+
keyword_format.setForeground(QColor("blue"))
|
|
1667
|
+
keyword_format.setFontWeight(QFont.Bold)
|
|
1668
|
+
keywords = [
|
|
1669
|
+
"and", "as", "assert", "break", "class", "continue", "def",
|
|
1670
|
+
"del", "elif", "else", "except", "False", "finally", "for",
|
|
1671
|
+
"from", "global", "if", "import", "in", "is", "lambda", "None",
|
|
1672
|
+
"nonlocal", "not", "or", "pass", "raise", "return", "True",
|
|
1673
|
+
"try", "while", "with", "yield"
|
|
1674
|
+
]
|
|
1675
|
+
for keyword in keywords:
|
|
1676
|
+
pattern = QRegularExpression(rf"\b{keyword}\b")
|
|
1677
|
+
self.highlighting_rules.append((pattern, keyword_format))
|
|
1678
|
+
|
|
1679
|
+
# --- Strings format (Blue) ---
|
|
1680
|
+
string_format = QTextCharFormat()
|
|
1681
|
+
string_format.setForeground(QColor("darkGreen"))
|
|
1682
|
+
# Single and double quoted strings
|
|
1683
|
+
self.highlighting_rules.append((QRegularExpression(r'".*?"'), string_format))
|
|
1684
|
+
self.highlighting_rules.append((QRegularExpression(r"'.*?'"), string_format))
|
|
1685
|
+
# Multi-line triple-quoted strings (both """ and ''')
|
|
1686
|
+
self.highlighting_rules.append((QRegularExpression(r'""".*?"""', QRegularExpression.DotMatchesEverythingOption), string_format))
|
|
1687
|
+
self.highlighting_rules.append((QRegularExpression(r"'''.*?'''", QRegularExpression.DotMatchesEverythingOption), string_format))
|
|
1688
|
+
|
|
1689
|
+
# --- Comments format (Green, Italic) ---
|
|
1690
|
+
comment_format = QTextCharFormat()
|
|
1691
|
+
comment_format.setForeground(QColor("gray"))
|
|
1692
|
+
comment_format.setFontItalic(True)
|
|
1693
|
+
self.highlighting_rules.append((QRegularExpression(r"#.*"), comment_format))
|
|
1694
|
+
|
|
1695
|
+
# --- Numbers format (Orange) ---
|
|
1696
|
+
number_format = QTextCharFormat()
|
|
1697
|
+
number_format.setForeground(QColor("darkOrange"))
|
|
1698
|
+
number_regex = QRegularExpression(r"\b\d+(?:\.\d+)?(?:[eE][+-]?\d+)?\b")
|
|
1699
|
+
self.highlighting_rules.append((number_regex, number_format))
|
|
1700
|
+
|
|
1701
|
+
# --- Class name format (Cyan) ---
|
|
1702
|
+
class_format = QTextCharFormat()
|
|
1703
|
+
class_format.setForeground(QColor("darkCyan"))
|
|
1704
|
+
class_format.setFontWeight(QFont.Bold)
|
|
1705
|
+
self.highlighting_rules.append((QRegularExpression(r"\bclass\s+([A-Z]\w*)"), class_format))
|
|
1706
|
+
|
|
1707
|
+
# --- Function name format (Yellow) ---
|
|
1708
|
+
function_format = QTextCharFormat()
|
|
1709
|
+
function_format.setForeground(QColor("darkGoldenrod"))
|
|
1710
|
+
function_format.setFontItalic(True)
|
|
1711
|
+
self.highlighting_rules.append((QRegularExpression(r"\bdef\s+([a-zA-Z_]\w*)"), function_format))
|
|
1712
|
+
|
|
1713
|
+
def highlightBlock(self, text):
|
|
1714
|
+
for pattern, fmt in self.highlighting_rules:
|
|
1715
|
+
it = pattern.globalMatch(text)
|
|
1716
|
+
while it.hasNext():
|
|
1717
|
+
match = it.next()
|
|
1718
|
+
# If there's a capture group, highlight only it (for class/function names)
|
|
1719
|
+
if match.lastCapturedIndex() > 0:
|
|
1720
|
+
start = match.capturedStart(1)
|
|
1721
|
+
length = match.capturedLength(1)
|
|
1722
|
+
else:
|
|
1723
|
+
start = match.capturedStart()
|
|
1724
|
+
length = match.capturedLength()
|
|
1725
|
+
self.setFormat(start, length, fmt)
|
|
1726
|
+
|
|
1727
|
+
# Editor widget
|
|
1728
|
+
class CodeEditor(QPlainTextEdit):
|
|
1729
|
+
def __init__(self):
|
|
1730
|
+
super().__init__()
|
|
1731
|
+
# self.setFont(QFont("Courier", 12))
|
|
1732
|
+
self.highlighter = PythonHighlighter(self.document())
|
|
1733
|
+
self.setLineWrapMode(QPlainTextEdit.NoWrap)
|
|
1734
|
+
|
|
1735
|
+
|
|
1736
|
+
# ---------- MAIN WINDOW ----------
|
|
1737
|
+
class MainWindow(QMainWindow):
|
|
1738
|
+
TAB_HEADER_COLORS = ["#FFCDD2", "#C8E6C9", "#BBDEFB", "#FFF9C4", "#D1C4E9"]
|
|
1739
|
+
|
|
1740
|
+
def __init__(self):
|
|
1741
|
+
super().__init__()
|
|
1742
|
+
self.setWindowTitle(APP_NAME)
|
|
1743
|
+
self.setGeometry(100, 100, 750, 700)
|
|
1744
|
+
|
|
1745
|
+
# --- Menu Bar ---
|
|
1746
|
+
self.create_menu_bar()
|
|
1747
|
+
|
|
1748
|
+
# --- Central Widget ---
|
|
1749
|
+
central_widget = QWidget()
|
|
1750
|
+
self.setCentralWidget(central_widget)
|
|
1751
|
+
main_layout = QHBoxLayout(central_widget)
|
|
1752
|
+
|
|
1753
|
+
self.tabs_widget = QTabWidget()
|
|
1754
|
+
self.tabs_widget.setTabPosition(QTabWidget.North)
|
|
1755
|
+
self.tabs_widget.setMovable(False)
|
|
1756
|
+
|
|
1757
|
+
# Tabs
|
|
1758
|
+
self.file_tab = FileInputTab(self)
|
|
1759
|
+
self.frequencies_tab = FrequenciesTab(self)
|
|
1760
|
+
self.ports_tab = PortsTab(self)
|
|
1761
|
+
self.mesh_tab = MeshTab(self)
|
|
1762
|
+
self.create_model_tab = CreateModelTab(self)
|
|
1763
|
+
self.modeleditor_tab = ModelEditorTab(self)
|
|
1764
|
+
|
|
1765
|
+
# Add tabs
|
|
1766
|
+
self.tabs_widget.addTab(self.file_tab, "Input Files")
|
|
1767
|
+
self.tabs_widget.addTab(self.frequencies_tab, "Frequencies")
|
|
1768
|
+
self.tabs_widget.addTab(self.ports_tab, "Ports")
|
|
1769
|
+
self.tabs_widget.addTab(self.mesh_tab, "Mesh and Boundaries")
|
|
1770
|
+
self.tabs_widget.addTab(self.create_model_tab, "Create Model")
|
|
1771
|
+
self.tabs_widget.addTab(self.modeleditor_tab, "Code")
|
|
1772
|
+
|
|
1773
|
+
self.apply_tab_header_colors()
|
|
1774
|
+
self._previous_index = 0
|
|
1775
|
+
self.tabs_widget.currentChanged.connect(self.on_tab_change)
|
|
1776
|
+
main_layout.addWidget(self.tabs_widget)
|
|
1777
|
+
|
|
1778
|
+
# Do not auto-load default values at this early startup stage,
|
|
1779
|
+
# instead this is done from File menu
|
|
1780
|
+
# self.user_inputs_file = DEFAULT_SETTINGS_FILE
|
|
1781
|
+
# self.user_inputs = self.load_user_inputs(DEFAULT_SETTINGS_FILE)
|
|
1782
|
+
# saved_values.update(self.user_inputs)
|
|
1783
|
+
|
|
1784
|
+
# Load all saved data into tabs
|
|
1785
|
+
self.load_all_tabs()
|
|
1786
|
+
|
|
1787
|
+
# ---------- Menu Bar ----------
|
|
1788
|
+
def create_menu_bar(self):
|
|
1789
|
+
menu_bar = self.menuBar()
|
|
1790
|
+
file_menu = menu_bar.addMenu("&File")
|
|
1791
|
+
|
|
1792
|
+
# browse_action = QAction("Browse Settings File...", self)
|
|
1793
|
+
self.load_settings_action = QAction("Load Settings ...", self)
|
|
1794
|
+
self.save_action = QAction("Save Settings ...", self)
|
|
1795
|
+
self.load_default_action = QAction("Load Default Settings", self)
|
|
1796
|
+
self.savedefault_action = QAction("Save as Default Settings", self)
|
|
1797
|
+
self.import_model_action = QAction("Import from *.py model ...", self)
|
|
1798
|
+
self.export_model_action = QAction("Export to *.py model ...", self)
|
|
1799
|
+
exit_action = QAction("Exit", self)
|
|
1800
|
+
|
|
1801
|
+
# disable export by default, onyl enables when on Code tab
|
|
1802
|
+
self.export_model_action.setEnabled(False)
|
|
1803
|
+
|
|
1804
|
+
# browse_action.triggered.connect(self.browse_settings_file)
|
|
1805
|
+
self.load_settings_action.triggered.connect(lambda: self.load_configuration_dialog())
|
|
1806
|
+
self.load_default_action.triggered.connect(lambda: self.load_configuration_from_file(DEFAULT_SETTINGS_FILE))
|
|
1807
|
+
self.save_action.triggered.connect(lambda: self.save_ask_filenamefile())
|
|
1808
|
+
self.savedefault_action.triggered.connect(lambda: self.save_user_inputs_to_file(DEFAULT_SETTINGS_FILE))
|
|
1809
|
+
|
|
1810
|
+
self.import_model_action.triggered.connect(lambda: self.import_from_python())
|
|
1811
|
+
self.export_model_action.triggered.connect(lambda: self.export_to_python())
|
|
1812
|
+
exit_action.triggered.connect(self.close)
|
|
1813
|
+
|
|
1814
|
+
# file_menu.addAction(browse_action)
|
|
1815
|
+
file_menu.addAction(self.load_settings_action)
|
|
1816
|
+
file_menu.addAction(self.save_action)
|
|
1817
|
+
file_menu.addSeparator()
|
|
1818
|
+
file_menu.addAction(self.import_model_action)
|
|
1819
|
+
file_menu.addAction(self.export_model_action)
|
|
1820
|
+
file_menu.addSeparator()
|
|
1821
|
+
file_menu.addAction(self.load_default_action)
|
|
1822
|
+
file_menu.addAction(self.savedefault_action)
|
|
1823
|
+
file_menu.addSeparator()
|
|
1824
|
+
file_menu.addAction(exit_action)
|
|
1825
|
+
|
|
1826
|
+
# ---------- Menu actions ----------
|
|
1827
|
+
|
|
1828
|
+
|
|
1829
|
+
# ---------- Tab header coloring ----------
|
|
1830
|
+
def apply_tab_header_colors(self):
|
|
1831
|
+
style = "QTabBar::tab { color: black; font-weight: bold; padding: 10px; }\n"
|
|
1832
|
+
for i, color in enumerate(self.TAB_HEADER_COLORS, start=1):
|
|
1833
|
+
style += f"QTabBar::tab:nth-child({i}) {{ background: {color}; }}\n"
|
|
1834
|
+
self.tabs_widget.setStyleSheet(style)
|
|
1835
|
+
|
|
1836
|
+
# ---------- Tab change handling ----------
|
|
1837
|
+
def on_tab_change(self, index):
|
|
1838
|
+
# check if we are ready to leave the tab, i.e. all values are valid
|
|
1839
|
+
previous_widget = self.tabs_widget.widget(self._previous_index)
|
|
1840
|
+
if hasattr(previous_widget, "save_values"):
|
|
1841
|
+
if not previous_widget.save_values():
|
|
1842
|
+
self.tabs_widget.blockSignals(True)
|
|
1843
|
+
self.tabs_widget.setCurrentIndex(self._previous_index)
|
|
1844
|
+
self.tabs_widget.blockSignals(False)
|
|
1845
|
+
return
|
|
1846
|
+
self._previous_index = index
|
|
1847
|
+
|
|
1848
|
+
# check if we switch to the Model editor tab, in that case store all other tabs
|
|
1849
|
+
if index == 5:
|
|
1850
|
+
self.save_all_tabs()
|
|
1851
|
+
|
|
1852
|
+
# Save model code only when model tab active
|
|
1853
|
+
self.export_model_action.setEnabled(index == 5)
|
|
1854
|
+
|
|
1855
|
+
|
|
1856
|
+
|
|
1857
|
+
# ---------- User input persistence ----------
|
|
1858
|
+
|
|
1859
|
+
def load_all_tabs(self):
|
|
1860
|
+
self.file_tab.load_values()
|
|
1861
|
+
self.frequencies_tab.load_values()
|
|
1862
|
+
self.ports_tab.load_values()
|
|
1863
|
+
self.mesh_tab.load_values()
|
|
1864
|
+
self.create_model_tab.load_values()
|
|
1865
|
+
self.modeleditor_tab.load_values()
|
|
1866
|
+
|
|
1867
|
+
def save_all_tabs(self):
|
|
1868
|
+
self.file_tab.save_values()
|
|
1869
|
+
self.frequencies_tab.save_values()
|
|
1870
|
+
self.ports_tab.save_values()
|
|
1871
|
+
self.mesh_tab.save_values()
|
|
1872
|
+
self.create_model_tab.save_values()
|
|
1873
|
+
|
|
1874
|
+
|
|
1875
|
+
def load_user_inputs(self, filename):
|
|
1876
|
+
# load of native configuration file
|
|
1877
|
+
if os.path.exists(filename):
|
|
1878
|
+
try:
|
|
1879
|
+
with open(filename, "r") as f:
|
|
1880
|
+
return json.load(f)
|
|
1881
|
+
except Exception:
|
|
1882
|
+
QMessageBox.warning(self, "Error", f"Failed to load settings from {filename}")
|
|
1883
|
+
return {}
|
|
1884
|
+
return {}
|
|
1885
|
+
|
|
1886
|
+
|
|
1887
|
+
def load_configuration_dialog(self):
|
|
1888
|
+
file_path, _ = QFileDialog.getOpenFileName(self, "Select Settings File", filter=f"*.{CONFIG_SUFFIX};;EM Studio *.json;;Python model code *.py")
|
|
1889
|
+
# we can load JSON or Python models, decide which suffix we have
|
|
1890
|
+
if file_path:
|
|
1891
|
+
self.load_configuration_from_file(file_path)
|
|
1892
|
+
|
|
1893
|
+
|
|
1894
|
+
|
|
1895
|
+
def load_configuration_from_file(self, file_path):
|
|
1896
|
+
if file_path:
|
|
1897
|
+
extension = pathlib.Path(file_path).suffix
|
|
1898
|
+
if CONFIG_SUFFIX.upper() in extension.upper():
|
|
1899
|
+
# regular data storage
|
|
1900
|
+
self.user_inputs_file = file_path
|
|
1901
|
+
# call the native config file loading function
|
|
1902
|
+
data = self.load_user_inputs(file_path)
|
|
1903
|
+
if data.get("application","") == "setupEM":
|
|
1904
|
+
# update internal data structure
|
|
1905
|
+
saved_values.clear()
|
|
1906
|
+
saved_values.update(data.get("saved_values"))
|
|
1907
|
+
# update ports, there are separate from the other internal data
|
|
1908
|
+
self.ports_tab.update_port_from_import (data.get("ports"))
|
|
1909
|
+
self.load_all_tabs()
|
|
1910
|
+
QMessageBox.information(self, "Loaded", f"Settings loaded from {file_path}")
|
|
1911
|
+
self.create_model_tab.log_area.clear()
|
|
1912
|
+
else:
|
|
1913
|
+
QMessageBox.information(self, "Failed", "Unknown data format")
|
|
1914
|
+
elif extension.upper() == ".JSON":
|
|
1915
|
+
# try to extract variable values from EMStudio JSON
|
|
1916
|
+
self.user_inputs_file = file_path
|
|
1917
|
+
# call the native config file loading function
|
|
1918
|
+
data = self.load_user_inputs(file_path)
|
|
1919
|
+
|
|
1920
|
+
# clear existing settings
|
|
1921
|
+
saved_values.clear()
|
|
1922
|
+
|
|
1923
|
+
# get GDSII and XML filenames
|
|
1924
|
+
for key in ["GdsFile","SubstrateFile"]:
|
|
1925
|
+
value = data.get(key)
|
|
1926
|
+
if os.path.isfile(value):
|
|
1927
|
+
saved_values[key]=value
|
|
1928
|
+
|
|
1929
|
+
# get frequencies
|
|
1930
|
+
for key in ["fstart","fstop"]:
|
|
1931
|
+
value = data.get(key)
|
|
1932
|
+
saved_values[key]=value/1e9
|
|
1933
|
+
|
|
1934
|
+
# get Boundaries
|
|
1935
|
+
boundaries = data.get("Boundaries", None)
|
|
1936
|
+
if boundaries is not None:
|
|
1937
|
+
boundary_list = []
|
|
1938
|
+
for key in ["X-","X+","Y-","Y+","Z-","Z+"]:
|
|
1939
|
+
boundary_list.append(boundaries.get(key,'??'))
|
|
1940
|
+
saved_values["boundary"] = boundary_list
|
|
1941
|
+
|
|
1942
|
+
# get Ports from EMStudio format (same structure, but other keywords)
|
|
1943
|
+
self.ports_tab.update_port_from_EMStudio (data.get("Ports", []))
|
|
1944
|
+
|
|
1945
|
+
self.load_all_tabs()
|
|
1946
|
+
QMessageBox.information(self, "Loaded", f"Settings loaded from {file_path}")
|
|
1947
|
+
self.create_model_tab.log_area.clear()
|
|
1948
|
+
elif extension.upper() == ".PY":
|
|
1949
|
+
import_mapping = {
|
|
1950
|
+
"gds_filename":"GdsFile",
|
|
1951
|
+
"XML_filename":"SubstrateFile",
|
|
1952
|
+
"GdsFile":"GdsFile",
|
|
1953
|
+
"SubstrateFile":"SubstrateFile",
|
|
1954
|
+
"merge_polygon_size":"merge_polygon_size",
|
|
1955
|
+
"preprocess_gds":"preprocess_gds",
|
|
1956
|
+
"purpose":"purpose",
|
|
1957
|
+
"margin":"margin",
|
|
1958
|
+
"air_around":"air_around",
|
|
1959
|
+
"boundary":"boundary",
|
|
1960
|
+
"fstart":"fstart",
|
|
1961
|
+
"fstop":"fstop",
|
|
1962
|
+
"fstep":"fstep",
|
|
1963
|
+
"fpoint":"fpoint",
|
|
1964
|
+
"fdump":"fdump",
|
|
1965
|
+
"refined_cellsize":"refined_cellsize",
|
|
1966
|
+
"cells_per_wavelength":"cells_per_wavelength",
|
|
1967
|
+
"meshsize_max":"meshsize_max",
|
|
1968
|
+
"adaptive_mesh_iterations":"adaptive_mesh_iterations"
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
# remove old settings, so that we don't keep old values that don't exist in loaded file
|
|
1972
|
+
saved_values.clear()
|
|
1973
|
+
# set values that are not included in import
|
|
1974
|
+
saved_values["unit"] = 1e-6
|
|
1975
|
+
saved_values["purpose"] = 0
|
|
1976
|
+
|
|
1977
|
+
# check what directory the Python code is in, we might use that to prefix gdsfile and XML file
|
|
1978
|
+
modelcode_path = os.path.dirname(file_path)
|
|
1979
|
+
|
|
1980
|
+
# variable assignments
|
|
1981
|
+
imported_parameters = parse_assignments(file_path)
|
|
1982
|
+
for import_key, import_value in imported_parameters.items():
|
|
1983
|
+
if import_key in import_mapping.keys():
|
|
1984
|
+
if import_key not in import_value: # skip the section where key might appear in different context
|
|
1985
|
+
# get the internal name for this variable
|
|
1986
|
+
varname = import_mapping.get(import_key, '')
|
|
1987
|
+
if varname == "fpoint" or varname == "fdump":
|
|
1988
|
+
saved_values [varname] = ast.literal_eval(import_value)
|
|
1989
|
+
elif varname in ["gds_filename","XML_filename","GdsFile","SubstrateFile"]:
|
|
1990
|
+
# check if we have full path for files in imported Python script,
|
|
1991
|
+
# otherwise prefix from *.py path assuming that it was local to the *.py model script
|
|
1992
|
+
value_path = os.path.dirname(import_value)
|
|
1993
|
+
if value_path == '':
|
|
1994
|
+
import_value = os.path.join(modelcode_path,import_value)
|
|
1995
|
+
saved_values [varname] = import_value
|
|
1996
|
+
elif varname != '':
|
|
1997
|
+
raw = import_value.strip("[]")
|
|
1998
|
+
if varname in ['fstart','fstop','fstep']:
|
|
1999
|
+
saved_values [varname] = float(raw)/1e9
|
|
2000
|
+
else:
|
|
2001
|
+
saved_values [varname] = raw
|
|
2002
|
+
|
|
2003
|
+
|
|
2004
|
+
# read port assignments in workflow syntax for gds2palace Python code
|
|
2005
|
+
ports = parse_python_ports_definitions(file_path)
|
|
2006
|
+
self.ports_tab.update_port_from_import (ports)
|
|
2007
|
+
|
|
2008
|
+
self.load_all_tabs()
|
|
2009
|
+
QMessageBox.information(self, "Loaded", f"Settings loaded from {file_path}")
|
|
2010
|
+
self.create_model_tab.log_area.clear()
|
|
2011
|
+
else:
|
|
2012
|
+
QMessageBox.information(self, "Error", f"Could not load file {file_path}")
|
|
2013
|
+
|
|
2014
|
+
|
|
2015
|
+
def import_from_python(self):
|
|
2016
|
+
file_path, _ = QFileDialog.getOpenFileName(self, "Select Settings File", filter=f"*.py model code")
|
|
2017
|
+
# we can load JSON or Python models, decide which suffix we have
|
|
2018
|
+
if file_path:
|
|
2019
|
+
self.load_configuration_from_file(file_path)
|
|
2020
|
+
else:
|
|
2021
|
+
QMessageBox.information(self, f"Could not load file {file_path}")
|
|
2022
|
+
|
|
2023
|
+
|
|
2024
|
+
|
|
2025
|
+
def save_user_inputs_to_file(self, filename):
|
|
2026
|
+
# make sure all tabs save their values
|
|
2027
|
+
self.save_all_tabs()
|
|
2028
|
+
|
|
2029
|
+
try:
|
|
2030
|
+
struct = {"application":"setupEM",
|
|
2031
|
+
"data_format":"1.0"}
|
|
2032
|
+
struct["saved_values"] = saved_values
|
|
2033
|
+
struct["ports"] = simulation_ports_to_struct (simulation_ports)
|
|
2034
|
+
|
|
2035
|
+
with open(filename, "w") as f:
|
|
2036
|
+
json.dump(struct, f, indent=4)
|
|
2037
|
+
QMessageBox.information(self, "Saved", f"Settings saved to {filename}")
|
|
2038
|
+
except Exception as e:
|
|
2039
|
+
QMessageBox.warning(self, "Error", f"Failed to save settings to {filename}: {e}")
|
|
2040
|
+
|
|
2041
|
+
|
|
2042
|
+
def save_ask_filenamefile(self):
|
|
2043
|
+
# make sure all tabs save their values
|
|
2044
|
+
file_path, _ = QFileDialog.getSaveFileName(self, "Select Settings File", filter=f"{APP_NAME} (*.{CONFIG_SUFFIX})")
|
|
2045
|
+
# Ensure filename ends with CONFIG_SUFFIX
|
|
2046
|
+
if file_path:
|
|
2047
|
+
if not file_path.lower().endswith('.' + CONFIG_SUFFIX):
|
|
2048
|
+
file_path = file_path + '.' + CONFIG_SUFFIX
|
|
2049
|
+
self.save_user_inputs_to_file(file_path)
|
|
2050
|
+
|
|
2051
|
+
|
|
2052
|
+
|
|
2053
|
+
def export_to_python(self):
|
|
2054
|
+
# make sure all tabs save their values
|
|
2055
|
+
self.save_all_tabs()
|
|
2056
|
+
self.modeleditor_tab.create_model_text(forExport=True)
|
|
2057
|
+
|
|
2058
|
+
file_path, _ = QFileDialog.getSaveFileName(self, "Select Python Model", filter="Python model (*.py)")
|
|
2059
|
+
if file_path:
|
|
2060
|
+
try:
|
|
2061
|
+
code = self.modeleditor_tab.model_edit.toPlainText()
|
|
2062
|
+
with open(file_path, 'w', encoding='utf-8') as f:
|
|
2063
|
+
f.write(code)
|
|
2064
|
+
QMessageBox.information(self, "Saved", f"Model code saved to {file_path}")
|
|
2065
|
+
except Exception as e:
|
|
2066
|
+
QMessageBox.warning(self, "Error", f"Failed to export model code to {file_path}: {e}")
|
|
2067
|
+
|
|
2068
|
+
|
|
2069
|
+
|
|
2070
|
+
def parse_assignments(file_path):
|
|
2071
|
+
# parse lines from a Python model code for variable assigments
|
|
2072
|
+
parameters = {}
|
|
2073
|
+
|
|
2074
|
+
for line in pathlib.Path(file_path).read_text().splitlines():
|
|
2075
|
+
# Remove comments (anything after # or //)
|
|
2076
|
+
line = line.split('#', 1)[0].split('//', 1)[0].strip()
|
|
2077
|
+
|
|
2078
|
+
# Skip blank lines
|
|
2079
|
+
if not line:
|
|
2080
|
+
continue
|
|
2081
|
+
|
|
2082
|
+
# Split only if '=' exists
|
|
2083
|
+
if '=' in line:
|
|
2084
|
+
param, value = map(str.strip, line.split('=', 1))
|
|
2085
|
+
param = param.replace('settings','')
|
|
2086
|
+
param = param.strip("[]'").strip('"')
|
|
2087
|
+
value = value.strip("'").strip('"')
|
|
2088
|
+
parameters[param] = value
|
|
2089
|
+
|
|
2090
|
+
return parameters
|
|
2091
|
+
|
|
2092
|
+
def parse_python_ports_definitions (file_path):
|
|
2093
|
+
# parse the port assignment from Python code for Palace or openEMS workflow
|
|
2094
|
+
#
|
|
2095
|
+
# example input:
|
|
2096
|
+
# simulation_ports = simulation_setup.all_simulation_ports()
|
|
2097
|
+
# simulation_ports.add_port(simulation_setup.simulation_port(portnumber=1, voltage=1, port_Z0=50, source_layernum=201, from_layername='Metal3', to_layername='TopMetal2', direction='z'))
|
|
2098
|
+
# simulation_ports.add_port(simulation_setup.simulation_port(portnumber=2, voltage=0, port_Z0=50, source_layernum=202, from_layername='Metal3', to_layername='TopMetal2', direction='z'))
|
|
2099
|
+
#
|
|
2100
|
+
# return value is a list of dictionaries, one dict for each port
|
|
2101
|
+
# [{'portnumber': 1, 'voltage': 1, 'port_Z0': 50, 'source_layernum': 201,
|
|
2102
|
+
# 'from_layername': 'Metal3', 'to_layername': 'TopMetal2', 'direction': 'z'},
|
|
2103
|
+
# {'portnumber': 2, 'voltage': 0, 'port_Z0': 50, 'source_layernum': 202,
|
|
2104
|
+
# 'from_layername': 'Metal3', 'to_layername': 'TopMetal2', 'direction': 'z'}, ... ]
|
|
2105
|
+
|
|
2106
|
+
# Function to parse the arguments inside simulation_port(...)
|
|
2107
|
+
def parse_port_args(arg_str):
|
|
2108
|
+
args = {}
|
|
2109
|
+
# Wrap the arguments into a fake function call so AST can parse it
|
|
2110
|
+
expr = ast.parse(f"f({arg_str})", mode='eval')
|
|
2111
|
+
for kw in expr.body.keywords:
|
|
2112
|
+
args[kw.arg] = ast.literal_eval(kw.value) # safely evaluate literals
|
|
2113
|
+
return args
|
|
2114
|
+
|
|
2115
|
+
# List to store parsed ports
|
|
2116
|
+
ports = []
|
|
2117
|
+
|
|
2118
|
+
# Read your input file line by line
|
|
2119
|
+
with open(file_path) as f:
|
|
2120
|
+
for line in f:
|
|
2121
|
+
if "simulation_port(" in line:
|
|
2122
|
+
start = line.index("simulation_port(") + len("simulation_port(")
|
|
2123
|
+
inside = line[start:].rstrip(") \n") # remove trailing ')'
|
|
2124
|
+
ports.append(parse_port_args(inside))
|
|
2125
|
+
|
|
2126
|
+
return ports
|
|
2127
|
+
|
|
2128
|
+
|
|
2129
|
+
|
|
2130
|
+
|
|
2131
|
+
# ---------- RUN APP ----------
|
|
2132
|
+
|
|
2133
|
+
def main():
|
|
2134
|
+
app = QApplication(sys.argv)
|
|
2135
|
+
app.setStyle(QStyleFactory.create('Windows'))
|
|
2136
|
+
win = MainWindow()
|
|
2137
|
+
win.show()
|
|
2138
|
+
sys.exit(app.exec())
|
|
2139
|
+
|
|
2140
|
+
if __name__ == "__main__":
|
|
2141
|
+
main()
|