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.
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()