setupEM 0.10.2__tar.gz → 0.11.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- setupem-0.11.1/PKG-INFO +26 -0
- {setupem-0.10.2 → setupem-0.11.1}/README.md +1 -1
- {setupem-0.10.2 → setupem-0.11.1}/pyproject.toml +1 -1
- {setupem-0.10.2 → setupem-0.11.1}/src/setupEM/__init__.py +1 -1
- {setupem-0.10.2 → setupem-0.11.1}/src/setupEM/field_viewer.py +162 -35
- {setupem-0.10.2 → setupem-0.11.1}/src/setupEM/setupEM.py +1 -1
- {setupem-0.10.2 → setupem-0.11.1}/src/setupEM/setupThermal.py +1 -1
- {setupem-0.10.2 → setupem-0.11.1}/src/setupEM/setup_common.py +52 -17
- {setupem-0.10.2 → setupem-0.11.1}/src/setupEM/simplify_gds.py +96 -15
- setupem-0.11.1/src/setupEM.egg-info/PKG-INFO +26 -0
- {setupem-0.10.2 → setupem-0.11.1}/src/setupEM.egg-info/SOURCES.txt +0 -1
- {setupem-0.10.2 → setupem-0.11.1}/src/setupEM.egg-info/requires.txt +1 -1
- setupem-0.10.2/PKG-INFO +0 -103
- setupem-0.10.2/README_pypi.md +0 -76
- setupem-0.10.2/src/setupEM.egg-info/PKG-INFO +0 -103
- {setupem-0.10.2 → setupem-0.11.1}/LICENSE +0 -0
- {setupem-0.10.2 → setupem-0.11.1}/setup.cfg +0 -0
- {setupem-0.10.2 → setupem-0.11.1}/src/setupEM/__main__.py +0 -0
- {setupem-0.10.2 → setupem-0.11.1}/src/setupEM/data/SG13CMOS5L_200um.xml +0 -0
- {setupem-0.10.2 → setupem-0.11.1}/src/setupEM/data/SG13G2_100um.xml +0 -0
- {setupem-0.10.2 → setupem-0.11.1}/src/setupEM/data/SG13G2_200um.xml +0 -0
- {setupem-0.10.2 → setupem-0.11.1}/src/setupEM/data/SG13G2_FEM_200um.xml +0 -0
- {setupem-0.10.2 → setupem-0.11.1}/src/setupEM/data/SG13G2_FEM_200um_passi3D.xml +0 -0
- {setupem-0.10.2 → setupem-0.11.1}/src/setupEM/data/SG13G2_nosub.xml +0 -0
- {setupem-0.10.2 → setupem-0.11.1}/src/setupEM/gds_hierarchy_scan.py +0 -0
- {setupem-0.10.2 → setupem-0.11.1}/src/setupEM/layout_preview.py +0 -0
- {setupem-0.10.2 → setupem-0.11.1}/src/setupEM/momentum_import.py +0 -0
- {setupem-0.10.2 → setupem-0.11.1}/src/setupEM/palace_results.py +0 -0
- {setupem-0.10.2 → setupem-0.11.1}/src/setupEM/result_viewer.py +0 -0
- {setupem-0.10.2 → setupem-0.11.1}/src/setupEM/stackupEditor.py +0 -0
- {setupem-0.10.2 → setupem-0.11.1}/src/setupEM/stackup_writer.py +0 -0
- {setupem-0.10.2 → setupem-0.11.1}/src/setupEM/thermal_results.py +0 -0
- {setupem-0.10.2 → setupem-0.11.1}/src/setupEM.egg-info/dependency_links.txt +0 -0
- {setupem-0.10.2 → setupem-0.11.1}/src/setupEM.egg-info/entry_points.txt +0 -0
- {setupem-0.10.2 → setupem-0.11.1}/src/setupEM.egg-info/top_level.txt +0 -0
setupem-0.11.1/PKG-INFO
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: setupEM
|
|
3
|
+
Version: 0.11.1
|
|
4
|
+
Summary: Python tool for configuration of gds2palace workflow with GUI.
|
|
5
|
+
Author-email: Volker Muehlhaus <volker@muehlhaus.com>
|
|
6
|
+
License-Expression: GPL-3.0-or-later
|
|
7
|
+
Project-URL: Homepage, https://github.com/VolkerMuehlhaus/setupEM
|
|
8
|
+
Project-URL: Repository, https://github.com/VolkerMuehlhaus/setupEM
|
|
9
|
+
Project-URL: Changelog, https://github.com/VolkerMuehlhaus/setupEM/blob/main/doc/CHANGES.md
|
|
10
|
+
Requires-Python: >=3.9
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: gds2palace>=0.8.0
|
|
14
|
+
Requires-Dist: gds_prepare_for_EM>=1.3.0
|
|
15
|
+
Requires-Dist: PySide6
|
|
16
|
+
Requires-Dist: shiboken6
|
|
17
|
+
Requires-Dist: scipy
|
|
18
|
+
Requires-Dist: requests
|
|
19
|
+
Requires-Dist: scikit-rf
|
|
20
|
+
Requires-Dist: matplotlib
|
|
21
|
+
Requires-Dist: numpy
|
|
22
|
+
Requires-Dist: gdspy
|
|
23
|
+
Requires-Dist: meshio
|
|
24
|
+
Requires-Dist: pyvista
|
|
25
|
+
Requires-Dist: pyvistaqt
|
|
26
|
+
Dynamic: license-file
|
|
@@ -251,7 +251,7 @@ Once field-dump results are available (Palace: set `fdump`; Elmer: enable field
|
|
|
251
251
|
|
|
252
252
|
The built-in viewer has a single, axis-aligned clip plane (X/Y/Z + a position slider, shown in µm) to see a cross-section through the model, rather than a free-orientation drag-widget - **Find max.** jumps the plane straight to the largest value of the currently selected field along that axis. Standard CAD/ParaView-style **+X/-X/+Y/-Y/+Z/-Z** buttons snap the camera to look straight down each axis; the clip plane's kept side follows whichever of these you last used for its axis, so the exposed cut face always faces the camera instead of occasionally showing the model's untouched exterior surface.
|
|
253
253
|
|
|
254
|
-
The **Field** panel picks which array to color by, defaulting to E-field magnitude (log color scale) for Palace and Elmer-as-EM-solver mode, or temperature (linear) for Elmer thermal - the Min/Max fields let you override the color range manually, with a button to reset back to the data's own range. **Display** controls opacity (to see a hotspot through the surrounding material without losing the outer shape as context) and a mesh-edge overlay. If more than one equally-valid result file exists (e.g. Palace's main "driven" field dump and its separate "driven_boundary" one), a **Result File** picker lets you choose between them instead of guessing.
|
|
254
|
+
The **Field** panel picks which array to color by, defaulting to E-field magnitude (log color scale) for Palace and Elmer-as-EM-solver mode, or temperature (linear) for Elmer thermal - the Min/Max fields let you override the color range manually, with a button to reset back to the data's own range. With the log color scale, a Range dropdown (-10 to -70 dB below Max, default -70 dB) replaces the Min field. **Display** controls opacity (to see a hotspot through the surrounding material without losing the outer shape as context) and a mesh-edge overlay. If more than one equally-valid result file exists (e.g. Palace's main "driven" field dump and its separate "driven_boundary" one), a **Result File** picker lets you choose between them instead of guessing.
|
|
255
255
|
|
|
256
256
|
For a vector array (E/B-field, Poynting vector `S`, ...), **Show arrows** overlays direction arrows on top of the color, auto-scaled from the field's own magnitude on a log scale so both weak and strong regions stay visible instead of only the single hottest point - the **Arrow size** slider (0.5% steps) scales them to taste, and also controls how densely they're packed in, since smaller arrows can sit closer together than large ones without turning into a solid block.
|
|
257
257
|
|
|
@@ -63,7 +63,7 @@ from pyvistaqt import QtInteractor
|
|
|
63
63
|
from PySide6.QtWidgets import (
|
|
64
64
|
QApplication, QDialog, QVBoxLayout, QHBoxLayout, QGridLayout, QGroupBox,
|
|
65
65
|
QLabel, QPushButton, QRadioButton, QButtonGroup, QCheckBox,
|
|
66
|
-
QSlider, QComboBox, QLineEdit, QStyleFactory, QColorDialog, QMenu,
|
|
66
|
+
QSlider, QComboBox, QLineEdit, QStyleFactory, QColorDialog, QMenu, QWidget,
|
|
67
67
|
)
|
|
68
68
|
from PySide6.QtCore import Qt, QThread, QTimer, Signal
|
|
69
69
|
from PySide6.QtGui import QShortcut, QKeySequence, QColor
|
|
@@ -527,6 +527,25 @@ def _is_field_amplitude_array(array_name):
|
|
|
527
527
|
return array_name in _FIELD_AMPLITUDE_ARRAYS
|
|
528
528
|
|
|
529
529
|
|
|
530
|
+
# Log color scale range below the maximum, offered in the "Range" dropdown -
|
|
531
|
+
# see FieldViewerWindow._get_clim(). The last entry is the default.
|
|
532
|
+
_LOG_RANGE_DB_CHOICES = (10, 20, 30, 40, 50, 60, 70)
|
|
533
|
+
_LOG_RANGE_DB_DEFAULT = 70
|
|
534
|
+
|
|
535
|
+
# Two meshes with bounding boxes this close (fraction of the diagonal) count
|
|
536
|
+
# as the same geometry when switching result files: camera and clip position
|
|
537
|
+
# are kept - see FieldViewerWindow._switch_to_file().
|
|
538
|
+
_SAME_BOUNDS_TOLERANCE = 1e-3
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
def _db_floor(array_name, clim_max, db_range):
|
|
542
|
+
"""Lower color limit db_range dB below clim_max: 20*log10 convention for
|
|
543
|
+
E/B-field arrays, 10*log10 for everything else - see
|
|
544
|
+
_is_field_amplitude_array()."""
|
|
545
|
+
db_per_decade = 20.0 if _is_field_amplitude_array(array_name) else 10.0
|
|
546
|
+
return clim_max / 10.0 ** (db_range / db_per_decade)
|
|
547
|
+
|
|
548
|
+
|
|
530
549
|
def _db_range_to_clim(mesh, array_name, db_range):
|
|
531
550
|
"""(floor, data_max) for a log color scale spanning db_range dB below
|
|
532
551
|
array_name's own maximum in mesh, using the field-amplitude convention
|
|
@@ -539,9 +558,7 @@ def _db_range_to_clim(mesh, array_name, db_range):
|
|
|
539
558
|
if positive.size == 0:
|
|
540
559
|
return None
|
|
541
560
|
data_max = positive.max()
|
|
542
|
-
|
|
543
|
-
floor = data_max / 10.0 ** (db_range / db_per_decade)
|
|
544
|
-
return floor, data_max
|
|
561
|
+
return _db_floor(array_name, data_max, db_range), data_max
|
|
545
562
|
|
|
546
563
|
|
|
547
564
|
# ------------------------------------------------------------------
|
|
@@ -623,6 +640,13 @@ class FieldViewerWindow(QDialog):
|
|
|
623
640
|
# leaves the scene briefly actor-less, and PyVista's own "reset camera if
|
|
624
641
|
# this looks like the first mesh" heuristic was firing on every redraw.
|
|
625
642
|
self._camera_needs_reset = True
|
|
643
|
+
# Field type the user picked in the Field combo (and its log scale /
|
|
644
|
+
# colormap), restored on a cycle or file switch whenever the new data
|
|
645
|
+
# has an array of that name - even after a cycle or file in between
|
|
646
|
+
# lacked it and fell back to a default. See _load_mesh().
|
|
647
|
+
self._preferred_array = None
|
|
648
|
+
self._preferred_log = None
|
|
649
|
+
self._preferred_cmap = None
|
|
626
650
|
|
|
627
651
|
# Colors for mesh edges / vector arrows / scalar-bar (legend) text,
|
|
628
652
|
# each changeable via a small swatch button next to its checkbox in
|
|
@@ -689,6 +713,7 @@ class FieldViewerWindow(QDialog):
|
|
|
689
713
|
self._redraw_scheduled = False
|
|
690
714
|
|
|
691
715
|
self._build_ui()
|
|
716
|
+
self._update_range_controls()
|
|
692
717
|
self._load_mesh()
|
|
693
718
|
# Same generic starting view for every source: full mesh, no clip - the
|
|
694
719
|
# "Find max." button (see _move_slider_to_max()) is one click away for
|
|
@@ -902,9 +927,12 @@ class FieldViewerWindow(QDialog):
|
|
|
902
927
|
field_layout = QVBoxLayout()
|
|
903
928
|
self.array_combo = QComboBox()
|
|
904
929
|
self.array_combo.currentTextChanged.connect(self._on_array_changed)
|
|
930
|
+
# user selection only (not programmatic changes): remembered as the preferred field type
|
|
931
|
+
self.array_combo.textActivated.connect(self._remember_field_choice)
|
|
905
932
|
field_layout.addWidget(self.array_combo)
|
|
906
933
|
self.log_scale_cb = QCheckBox("Log color scale")
|
|
907
|
-
self.log_scale_cb.toggled.connect(self.
|
|
934
|
+
self.log_scale_cb.toggled.connect(self._on_log_scale_toggled)
|
|
935
|
+
self.log_scale_cb.clicked.connect(self._remember_field_choice)
|
|
908
936
|
field_layout.addWidget(self.log_scale_cb)
|
|
909
937
|
|
|
910
938
|
# Color range (Min/Max/Reset) directly below the array/log-scale
|
|
@@ -913,12 +941,32 @@ class FieldViewerWindow(QDialog):
|
|
|
913
941
|
# Min/Max stacked one per row (not side by side) - two QLineEdits
|
|
914
942
|
# sharing one row left each too narrow to read its own value
|
|
915
943
|
# comfortably, given the whole Field group's fixed column width.
|
|
916
|
-
|
|
944
|
+
# Linear scale: Min field. Log scale: "Range" dropdown instead, the
|
|
945
|
+
# lower limit is that many dB below Max - see _get_clim(). Only one
|
|
946
|
+
# of the two rows is visible, see _update_range_controls().
|
|
947
|
+
self.clim_min_row = QWidget()
|
|
948
|
+
min_layout = QHBoxLayout(self.clim_min_row)
|
|
949
|
+
min_layout.setContentsMargins(0, 0, 0, 0)
|
|
917
950
|
min_layout.addWidget(QLabel("Min:"))
|
|
918
951
|
self.clim_min_edit = QLineEdit()
|
|
919
952
|
self.clim_min_edit.editingFinished.connect(self._on_redraw_needed)
|
|
920
953
|
min_layout.addWidget(self.clim_min_edit)
|
|
921
|
-
field_layout.
|
|
954
|
+
field_layout.addWidget(self.clim_min_row)
|
|
955
|
+
|
|
956
|
+
self.log_range_row = QWidget()
|
|
957
|
+
log_range_layout = QHBoxLayout(self.log_range_row)
|
|
958
|
+
log_range_layout.setContentsMargins(0, 0, 0, 0)
|
|
959
|
+
log_range_layout.addWidget(QLabel("Range:"))
|
|
960
|
+
self.log_range_combo = QComboBox()
|
|
961
|
+
for db in _LOG_RANGE_DB_CHOICES:
|
|
962
|
+
self.log_range_combo.addItem(f"-{db} dB", db)
|
|
963
|
+
self.log_range_combo.setCurrentIndex(_LOG_RANGE_DB_CHOICES.index(_LOG_RANGE_DB_DEFAULT))
|
|
964
|
+
self.log_range_combo.setToolTip(
|
|
965
|
+
"Log color scale: show values down to this many dB below Max "
|
|
966
|
+
"(20*log10 for E/B fields, 10*log10 for power-like arrays such as S)")
|
|
967
|
+
self.log_range_combo.currentIndexChanged.connect(self._on_redraw_needed)
|
|
968
|
+
log_range_layout.addWidget(self.log_range_combo)
|
|
969
|
+
field_layout.addWidget(self.log_range_row)
|
|
922
970
|
|
|
923
971
|
max_layout = QHBoxLayout()
|
|
924
972
|
max_layout.addWidget(QLabel("Max:"))
|
|
@@ -1124,22 +1172,34 @@ class FieldViewerWindow(QDialog):
|
|
|
1124
1172
|
hidden again)."""
|
|
1125
1173
|
if path == self.file_path:
|
|
1126
1174
|
return
|
|
1175
|
+
old_bounds = np.array(self._full_mesh.bounds) if self._full_mesh is not None else None
|
|
1127
1176
|
self.file_path = path
|
|
1128
1177
|
self._load_error = None
|
|
1129
1178
|
self.warning_label.setText("")
|
|
1130
|
-
# A different result file can have entirely different geometry/bounds
|
|
1131
|
-
# (e.g. Palace's "driven" vs. "driven_boundary") - unlike every other
|
|
1132
|
-
# control in this window, this is a good reason to re-fit the camera
|
|
1133
|
-
# rather than keep the previous file's pan/zoom/rotation.
|
|
1134
|
-
self._camera_needs_reset = True
|
|
1135
1179
|
# Default to the NEW file's own first cycle - a cycle index picked on
|
|
1136
1180
|
# the old file has no guaranteed correspondence here (different files
|
|
1137
1181
|
# can have different cycle counts), see __init__'s self._cycle_index.
|
|
1138
1182
|
self._cycle_index = None
|
|
1139
1183
|
self._cycles_without_data = set()
|
|
1140
1184
|
self._cycle_labels = None
|
|
1141
|
-
|
|
1142
|
-
self.
|
|
1185
|
+
# keep the field type if the new file has the same array, see _load_mesh()
|
|
1186
|
+
self._load_mesh(preserve_selection=True, keep_range=False)
|
|
1187
|
+
# Same geometry (e.g. another AMR iteration or frequency file of the
|
|
1188
|
+
# same model): keep the camera (view direction, pan, zoom) and the clip
|
|
1189
|
+
# position. Different bounds (e.g. Palace's "driven" vs.
|
|
1190
|
+
# "driven_boundary" of a different domain): re-fit the camera and
|
|
1191
|
+
# re-center the clip slider for the new mesh.
|
|
1192
|
+
new_bounds = np.array(self._full_mesh.bounds) if self._full_mesh is not None else None
|
|
1193
|
+
same_geometry = (
|
|
1194
|
+
old_bounds is not None and new_bounds is not None
|
|
1195
|
+
and np.allclose(old_bounds, new_bounds,
|
|
1196
|
+
atol=_SAME_BOUNDS_TOLERANCE * max(self._full_mesh.length, 1e-30))
|
|
1197
|
+
)
|
|
1198
|
+
if same_geometry:
|
|
1199
|
+
self._schedule_redraw()
|
|
1200
|
+
else:
|
|
1201
|
+
self._camera_needs_reset = True
|
|
1202
|
+
self._on_axis_changed() # resets the clip slider for the new mesh's bounds, redraws
|
|
1143
1203
|
|
|
1144
1204
|
def _on_cycle_changed(self, index):
|
|
1145
1205
|
"""Switch which solved cycle (e.g. frequency, for a multi-frequency
|
|
@@ -1151,7 +1211,7 @@ class FieldViewerWindow(QDialog):
|
|
|
1151
1211
|
if index < 0 or index == self._cycle_index:
|
|
1152
1212
|
return
|
|
1153
1213
|
self._cycle_index = index
|
|
1154
|
-
self._load_mesh(preserve_selection=True)
|
|
1214
|
+
self._load_mesh(preserve_selection=True, keep_range=True)
|
|
1155
1215
|
self._schedule_redraw()
|
|
1156
1216
|
|
|
1157
1217
|
def _on_include_iterations_toggled(self, checked):
|
|
@@ -1169,15 +1229,18 @@ class FieldViewerWindow(QDialog):
|
|
|
1169
1229
|
|
|
1170
1230
|
# ---------- Mesh loading ----------
|
|
1171
1231
|
|
|
1172
|
-
def _load_mesh(self, preserve_selection=False):
|
|
1232
|
+
def _load_mesh(self, preserve_selection=False, keep_range=True):
|
|
1173
1233
|
"""Load self._full_mesh from self.file_path/self._cycle_index.
|
|
1174
1234
|
|
|
1175
|
-
preserve_selection=True (
|
|
1176
|
-
the current array/color-by selection
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1235
|
+
preserve_selection=True (passed by _on_cycle_changed() and
|
|
1236
|
+
_switch_to_file()) keeps the current array/color-by selection and log
|
|
1237
|
+
scale setting if the newly loaded data has an array of the same name,
|
|
1238
|
+
instead of recomputing a default. The initial load leaves this False.
|
|
1239
|
+
|
|
1240
|
+
keep_range=True (cycle switch) keeps a linear scale's manually entered
|
|
1241
|
+
Min/Max; keep_range=False (file switch) resets them to the new data.
|
|
1242
|
+
A log scale's Max is always reset to the new data's maximum, since its
|
|
1243
|
+
dB range is defined relative to the maximum.
|
|
1181
1244
|
"""
|
|
1182
1245
|
self.setWindowTitle(f"Field Viewer - {self._file_labels[self.file_path]}")
|
|
1183
1246
|
# Bump the generation before anything else below - see __init__'s
|
|
@@ -1245,16 +1308,35 @@ class FieldViewerWindow(QDialog):
|
|
|
1245
1308
|
self.array_combo.addItems(available)
|
|
1246
1309
|
self.array_combo.blockSignals(False)
|
|
1247
1310
|
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1311
|
+
# The user's own field choice first, then whatever is shown now (e.g. a
|
|
1312
|
+
# default the user kept), then the default for this data.
|
|
1313
|
+
if preserve_selection and self._preferred_array in available:
|
|
1314
|
+
target_array = self._preferred_array
|
|
1315
|
+
elif preserve_selection and previous_array in available:
|
|
1316
|
+
target_array = previous_array
|
|
1317
|
+
else:
|
|
1318
|
+
target_array = None
|
|
1319
|
+
if target_array is not None:
|
|
1320
|
+
# Keep the user's current settings across a switch - not just which
|
|
1321
|
+
# array is selected, but also its Min/Max color range on a cycle
|
|
1322
|
+
# switch: don't let _on_array_changed's _reset_clim_range() (fired
|
|
1252
1323
|
# by setCurrentText() below re-selecting the same array name in
|
|
1253
1324
|
# the freshly repopulated combo) clobber a manually-entered range
|
|
1254
1325
|
# with this cycle's own data range.
|
|
1255
1326
|
self.array_combo.blockSignals(True)
|
|
1256
|
-
self.array_combo.setCurrentText(
|
|
1327
|
+
self.array_combo.setCurrentText(target_array)
|
|
1257
1328
|
self.array_combo.blockSignals(False)
|
|
1329
|
+
if target_array == self._preferred_array and target_array != previous_array:
|
|
1330
|
+
# back to the user's choice after a fallback: its own scale settings
|
|
1331
|
+
self._current_cmap = self._preferred_cmap
|
|
1332
|
+
self.log_scale_cb.blockSignals(True)
|
|
1333
|
+
self.log_scale_cb.setChecked(self._preferred_log)
|
|
1334
|
+
self.log_scale_cb.blockSignals(False)
|
|
1335
|
+
self._update_range_controls()
|
|
1336
|
+
if not keep_range or target_array != previous_array:
|
|
1337
|
+
self._reset_clim_range()
|
|
1338
|
+
elif self.log_scale_cb.isChecked():
|
|
1339
|
+
self._reset_clim_max()
|
|
1258
1340
|
self._update_vector_checkbox_state()
|
|
1259
1341
|
return
|
|
1260
1342
|
|
|
@@ -1263,6 +1345,7 @@ class FieldViewerWindow(QDialog):
|
|
|
1263
1345
|
self.log_scale_cb.blockSignals(True)
|
|
1264
1346
|
self.log_scale_cb.setChecked(default_log_scale)
|
|
1265
1347
|
self.log_scale_cb.blockSignals(False)
|
|
1348
|
+
self._update_range_controls()
|
|
1266
1349
|
if default_array is not None:
|
|
1267
1350
|
self.array_combo.setCurrentText(default_array)
|
|
1268
1351
|
# Explicit calls, not just relying on currentTextChanged above: that
|
|
@@ -1464,6 +1547,13 @@ class FieldViewerWindow(QDialog):
|
|
|
1464
1547
|
|
|
1465
1548
|
# ---------- Field/array picker ----------
|
|
1466
1549
|
|
|
1550
|
+
def _remember_field_choice(self, _value=None):
|
|
1551
|
+
"""Remember the field type (array, log scale, colormap) the user picked,
|
|
1552
|
+
see __init__'s self._preferred_array."""
|
|
1553
|
+
self._preferred_array = self.array_combo.currentText() or None
|
|
1554
|
+
self._preferred_log = self.log_scale_cb.isChecked()
|
|
1555
|
+
self._preferred_cmap = self._current_cmap
|
|
1556
|
+
|
|
1467
1557
|
def _on_array_changed(self, _text=None):
|
|
1468
1558
|
self._reset_clim_range()
|
|
1469
1559
|
self._update_vector_checkbox_state()
|
|
@@ -1514,19 +1604,56 @@ class FieldViewerWindow(QDialog):
|
|
|
1514
1604
|
self.clim_min_edit.setText(f"{magnitudes.min():.6g}")
|
|
1515
1605
|
self.clim_max_edit.setText(f"{magnitudes.max():.6g}")
|
|
1516
1606
|
|
|
1607
|
+
def _reset_clim_max(self):
|
|
1608
|
+
"""Set only Max to the selected array's data maximum - a log scale's
|
|
1609
|
+
lower limit follows from Max and the dB range, see _get_clim()."""
|
|
1610
|
+
array_name = self.array_combo.currentText()
|
|
1611
|
+
if self._full_mesh is None or not array_name or array_name not in self._full_mesh.point_data:
|
|
1612
|
+
return
|
|
1613
|
+
magnitudes = _array_magnitudes(self._full_mesh[array_name])
|
|
1614
|
+
if magnitudes.size:
|
|
1615
|
+
self.clim_max_edit.setText(f"{magnitudes.max():.6g}")
|
|
1616
|
+
|
|
1517
1617
|
def _on_clim_reset_clicked(self):
|
|
1518
1618
|
self._reset_clim_range()
|
|
1519
1619
|
self._schedule_redraw()
|
|
1520
1620
|
|
|
1621
|
+
def _update_range_controls(self):
|
|
1622
|
+
"""Log scale: show the dB "Range" dropdown instead of the Min field."""
|
|
1623
|
+
log = self.log_scale_cb.isChecked()
|
|
1624
|
+
self.clim_min_row.setVisible(not log)
|
|
1625
|
+
self.log_range_row.setVisible(log)
|
|
1626
|
+
|
|
1627
|
+
def _on_log_scale_toggled(self, _checked=None):
|
|
1628
|
+
self._update_range_controls()
|
|
1629
|
+
self._schedule_redraw()
|
|
1630
|
+
|
|
1631
|
+
def _set_log_range_db(self, db_range):
|
|
1632
|
+
"""Select db_range in the dB dropdown, adding it if it isn't one of the
|
|
1633
|
+
standard choices (e.g. from the CLI's --log-range-db)."""
|
|
1634
|
+
index = self.log_range_combo.findData(db_range)
|
|
1635
|
+
if index < 0:
|
|
1636
|
+
self.log_range_combo.addItem(f"-{db_range:g} dB", db_range)
|
|
1637
|
+
index = self.log_range_combo.count() - 1
|
|
1638
|
+
self.log_range_combo.setCurrentIndex(index)
|
|
1639
|
+
|
|
1521
1640
|
def _get_clim(self):
|
|
1522
1641
|
"""(min, max) parsed from the Min/Max fields, or None to let PyVista
|
|
1523
1642
|
auto-scale from the currently displayed mesh (matches the behavior before
|
|
1524
1643
|
this control existed). Invalid/empty text, or min >= max, both fall back
|
|
1525
1644
|
to None rather than raising or blocking the redraw - VTK's own clim
|
|
1526
|
-
requires min < max, and a typo here shouldn't break the view.
|
|
1645
|
+
requires min < max, and a typo here shouldn't break the view.
|
|
1646
|
+
|
|
1647
|
+
Log scale: min is the dB "Range" below Max instead of the Min field."""
|
|
1527
1648
|
try:
|
|
1528
|
-
clim_min = float(self.clim_min_edit.text())
|
|
1529
1649
|
clim_max = float(self.clim_max_edit.text())
|
|
1650
|
+
if self.log_scale_cb.isChecked():
|
|
1651
|
+
if clim_max <= 0:
|
|
1652
|
+
return None
|
|
1653
|
+
clim_min = _db_floor(self.array_combo.currentText(), clim_max,
|
|
1654
|
+
self.log_range_combo.currentData())
|
|
1655
|
+
else:
|
|
1656
|
+
clim_min = float(self.clim_min_edit.text())
|
|
1530
1657
|
except ValueError:
|
|
1531
1658
|
return None
|
|
1532
1659
|
if clim_min >= clim_max:
|
|
@@ -1901,7 +2028,9 @@ def main():
|
|
|
1901
2028
|
"thermal only). Ignored if --array is also given.")
|
|
1902
2029
|
parser.add_argument("--array", help="exact point-data array name to color by "
|
|
1903
2030
|
"(e.g. E_imag, U_e, B_imag) - overrides --field")
|
|
1904
|
-
parser.add_argument("--log-scale", action="store_true",
|
|
2031
|
+
parser.add_argument("--log-scale", action="store_true",
|
|
2032
|
+
help=f"use a log color scale, down to {_LOG_RANGE_DB_DEFAULT} dB below the "
|
|
2033
|
+
"maximum unless --log-range-db is given")
|
|
1905
2034
|
parser.add_argument("--log-range-db", type=float,
|
|
1906
2035
|
help="set the color range's minimum this many dB below the "
|
|
1907
2036
|
"data's own maximum (implies --log-scale) instead of using "
|
|
@@ -2008,12 +2137,10 @@ def main():
|
|
|
2008
2137
|
window.log_scale_cb.setChecked(True)
|
|
2009
2138
|
if args.log_range_db is not None:
|
|
2010
2139
|
array_name = window.array_combo.currentText()
|
|
2011
|
-
|
|
2012
|
-
if result is None:
|
|
2140
|
+
if _db_range_to_clim(window._full_mesh, array_name, args.log_range_db) is None:
|
|
2013
2141
|
die(f"--log-range-db: array {array_name!r} has no positive values to scale from")
|
|
2014
|
-
|
|
2015
|
-
window.
|
|
2016
|
-
window.clim_max_edit.setText(f"{data_max:.6g}")
|
|
2142
|
+
window._set_log_range_db(args.log_range_db)
|
|
2143
|
+
window._reset_clim_max()
|
|
2017
2144
|
window._redraw()
|
|
2018
2145
|
|
|
2019
2146
|
if args.opacity is not None:
|
|
@@ -3210,7 +3210,7 @@ class PreferencesDialog(QDialog):
|
|
|
3210
3210
|
self.simplify_excluded_layers_edit = add_row(
|
|
3211
3211
|
simplify_form, "Layers excluded from simplification", "simplify_excluded_layers", "")
|
|
3212
3212
|
self.simplify_excluded_layers_edit.setPlaceholderText("e.g. 10,11 - blank = none")
|
|
3213
|
-
self.simplify_merge_per_layer_checkbox = QCheckBox("Merge polygons per layer
|
|
3213
|
+
self.simplify_merge_per_layer_checkbox = QCheckBox("Merge polygons per layer")
|
|
3214
3214
|
self.simplify_merge_per_layer_checkbox.setChecked(
|
|
3215
3215
|
get_preference_bool(self.app_name, "simplify_merge_per_layer", True))
|
|
3216
3216
|
simplify_form.addWidget(self.simplify_merge_per_layer_checkbox)
|
|
@@ -1225,7 +1225,7 @@ class PreferencesDialog(QDialog):
|
|
|
1225
1225
|
self.simplify_excluded_layers_edit = add_row(
|
|
1226
1226
|
simplify_form, "Layers excluded from simplification", "simplify_excluded_layers", "")
|
|
1227
1227
|
self.simplify_excluded_layers_edit.setPlaceholderText("e.g. 10,11 - blank = none")
|
|
1228
|
-
self.simplify_merge_per_layer_checkbox = QCheckBox("Merge polygons per layer
|
|
1228
|
+
self.simplify_merge_per_layer_checkbox = QCheckBox("Merge polygons per layer")
|
|
1229
1229
|
self.simplify_merge_per_layer_checkbox.setChecked(
|
|
1230
1230
|
get_preference_bool(self.app_name, "simplify_merge_per_layer", True))
|
|
1231
1231
|
simplify_form.addWidget(self.simplify_merge_per_layer_checkbox)
|
|
@@ -474,6 +474,40 @@ def resolve_value_text(value_text, known_constants):
|
|
|
474
474
|
return eval_simple_python_expression(node, known_constants)
|
|
475
475
|
|
|
476
476
|
|
|
477
|
+
def normalize_purpose_list(value, default=(0,)):
|
|
478
|
+
# Normalize a GDSII purpose (datatype) value to the flat list of ints that
|
|
479
|
+
# gds_reader.read_gds() expects as purposelist ("purpose in purposelist").
|
|
480
|
+
# Accepts an int, comma-separated text ("0, 35, 4", also "[0]" or "(0, 35)"),
|
|
481
|
+
# or a list/tuple. Nested lists/tuples are flattened, so values stored by
|
|
482
|
+
# older versions as [(0, 35, 4)] (or [[0, 35, 4]] after a JSON round trip)
|
|
483
|
+
# load back correctly instead of matching no datatype at all. Empty or
|
|
484
|
+
# unparseable input returns the default.
|
|
485
|
+
if isinstance(value, str):
|
|
486
|
+
text = value.strip()
|
|
487
|
+
if not text:
|
|
488
|
+
return list(default)
|
|
489
|
+
try:
|
|
490
|
+
value = ast.literal_eval('[' + text + ']')
|
|
491
|
+
except (SyntaxError, ValueError):
|
|
492
|
+
return list(default)
|
|
493
|
+
|
|
494
|
+
result = []
|
|
495
|
+
pending = [value]
|
|
496
|
+
while pending:
|
|
497
|
+
item = pending.pop(0)
|
|
498
|
+
if isinstance(item, (list, tuple)):
|
|
499
|
+
pending[0:0] = list(item)
|
|
500
|
+
elif isinstance(item, bool):
|
|
501
|
+
return list(default)
|
|
502
|
+
elif isinstance(item, int):
|
|
503
|
+
result.append(item)
|
|
504
|
+
elif isinstance(item, float) and item.is_integer():
|
|
505
|
+
result.append(int(item))
|
|
506
|
+
else:
|
|
507
|
+
return list(default)
|
|
508
|
+
return result if result else list(default)
|
|
509
|
+
|
|
510
|
+
|
|
477
511
|
def parse_assignments(file_path):
|
|
478
512
|
# parse lines from a Python model code for variable assigments
|
|
479
513
|
parameters = {}
|
|
@@ -1090,10 +1124,9 @@ class FileInputTab(QWidget):
|
|
|
1090
1124
|
self.preprocess_gds_checkbox.setChecked(bool(get_saved_value(saved_values, "preprocess_gds", True)))
|
|
1091
1125
|
|
|
1092
1126
|
purpose_default = get_preference(self.MainWindow.APP_NAME, "purpose", "0")
|
|
1093
|
-
int_list = saved_values.get("purpose", purpose_default)
|
|
1094
|
-
|
|
1095
|
-
self.purpose_edit.setText(
|
|
1096
|
-
# self.purpose_edit.setText(','.join(map(str, int_list)))
|
|
1127
|
+
int_list = normalize_purpose_list(saved_values.get("purpose", purpose_default),
|
|
1128
|
+
default=normalize_purpose_list(purpose_default))
|
|
1129
|
+
self.purpose_edit.setText(', '.join(map(str, int_list)))
|
|
1097
1130
|
|
|
1098
1131
|
# read_XML(self.XML_file_edit.text()) # safe if invalid filename
|
|
1099
1132
|
|
|
@@ -1121,13 +1154,10 @@ class FileInputTab(QWidget):
|
|
|
1121
1154
|
# a gds2palace that doesn't support this setting for this application
|
|
1122
1155
|
saved_values.pop("fill_factor_correction", None)
|
|
1123
1156
|
|
|
1124
|
-
text
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
else:
|
|
1129
|
-
purpose_default = get_preference(self.MainWindow.APP_NAME, "purpose", "0")
|
|
1130
|
-
saved_values["purpose"] = ast.literal_eval('[' + str(purpose_default) + ']')
|
|
1157
|
+
# save as flat list of ints, empty text falls back to the preference default
|
|
1158
|
+
purpose_default = get_preference(self.MainWindow.APP_NAME, "purpose", "0")
|
|
1159
|
+
saved_values["purpose"] = normalize_purpose_list(self.purpose_edit.text(),
|
|
1160
|
+
default=normalize_purpose_list(purpose_default))
|
|
1131
1161
|
|
|
1132
1162
|
# also trigger the load function of CreateModelTab, because that uses gds file info
|
|
1133
1163
|
self.MainWindow.create_model_tab.load_values()
|
|
@@ -4210,7 +4240,7 @@ class MainWindowBase(QMainWindow):
|
|
|
4210
4240
|
saved_values.clear()
|
|
4211
4241
|
# set values that are not included in import
|
|
4212
4242
|
saved_values["unit"] = 1e-6
|
|
4213
|
-
saved_values["purpose"] = 0
|
|
4243
|
+
saved_values["purpose"] = [0]
|
|
4214
4244
|
|
|
4215
4245
|
# check what directory the Python code is in, we might use that to prefix gdsfile and XML file
|
|
4216
4246
|
modelcode_path = os.path.dirname(file_path)
|
|
@@ -4236,6 +4266,15 @@ class MainWindowBase(QMainWindow):
|
|
|
4236
4266
|
if not isinstance(values, (list, tuple)):
|
|
4237
4267
|
values = [values]
|
|
4238
4268
|
saved_values[varname] = [f / 1e9 for f in values]
|
|
4269
|
+
elif varname == "purpose":
|
|
4270
|
+
# scalar or list in the script, always a flat list here
|
|
4271
|
+
# (stripping "[]" and resolving would give a tuple for
|
|
4272
|
+
# [0, 35, 4], which then never matched any datatype)
|
|
4273
|
+
try:
|
|
4274
|
+
value = resolve_value_text(import_value, known_constants)
|
|
4275
|
+
except (SyntaxError, ValueError, TypeError, ZeroDivisionError):
|
|
4276
|
+
value = import_value
|
|
4277
|
+
saved_values[varname] = normalize_purpose_list(value)
|
|
4239
4278
|
elif varname in ("variable_overrides", "refined_cellsize_override"):
|
|
4240
4279
|
saved_values[varname] = resolve_value_text(import_value, known_constants)
|
|
4241
4280
|
elif varname in ["gds_filename", "XML_filename", "GdsFile", "SubstrateFile"]:
|
|
@@ -4543,11 +4582,7 @@ class MainWindowBase(QMainWindow):
|
|
|
4543
4582
|
if not os.path.isfile(gdsfile) or self.metals_list is None:
|
|
4544
4583
|
return set()
|
|
4545
4584
|
cellname = cellname_from_display(self.file_tab.cellname_box.currentText())
|
|
4546
|
-
|
|
4547
|
-
try:
|
|
4548
|
-
purposelist = ast.literal_eval('[' + purpose_text + ']') if purpose_text else [0]
|
|
4549
|
-
except Exception:
|
|
4550
|
-
purposelist = [0]
|
|
4585
|
+
purposelist = normalize_purpose_list(self.file_tab.purpose_edit.text())
|
|
4551
4586
|
|
|
4552
4587
|
if _derived_layer_range_is_safe(self.metals_list, layer_min, layer_max):
|
|
4553
4588
|
try:
|
|
@@ -19,16 +19,17 @@
|
|
|
19
19
|
"""
|
|
20
20
|
simplify_gds.py
|
|
21
21
|
|
|
22
|
-
Tools > Simplify GDS... : removes floating (unconnected) metal fill
|
|
23
|
-
|
|
24
|
-
to a new GDS file.
|
|
25
|
-
package, already an editable install in this
|
|
26
|
-
- no subprocess/CLI involved.
|
|
27
|
-
|
|
28
|
-
No technology-specific layer numbers are hardcoded here: the
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
other setupEM feature that needs
|
|
22
|
+
Tools > Simplify GDS... : removes floating (unconnected) metal fill, fills
|
|
23
|
+
in small cutouts and/or merges via arrays on the currently loaded GDS file,
|
|
24
|
+
writing the result to a new GDS file. All operations come from
|
|
25
|
+
gds_prepare_for_EM (a sibling package, already an editable install in this
|
|
26
|
+
venv) and are called in-process - no subprocess/CLI involved.
|
|
27
|
+
|
|
28
|
+
No technology-specific layer numbers are hardcoded here: the metal layers to
|
|
29
|
+
process, the via layers and the metal layers above/below each via are derived
|
|
30
|
+
entirely from the XML stackup already parsed into MainWindow.metals_list
|
|
31
|
+
(gds2palace's stackup_reader), same as every other setupEM feature that needs
|
|
32
|
+
to know "which GDS layers are metal".
|
|
32
33
|
"""
|
|
33
34
|
|
|
34
35
|
import os, io, contextlib, traceback, tempfile
|
|
@@ -130,7 +131,7 @@ def _flatten_cell(lib, cellname):
|
|
|
130
131
|
def run_simplify(gds_path, metal_layers, output_path, cellname="",
|
|
131
132
|
do_cutouts=False, max_hole_area=None,
|
|
132
133
|
do_floating=False, fill_minsize=1.0, fill_maxsize=None, fill_mincount=20,
|
|
133
|
-
do_merge=False):
|
|
134
|
+
do_merge=False, via_layers=None, via_merge_size=0):
|
|
134
135
|
"""Load gds_path, optionally fill small cutouts, remove floating metal
|
|
135
136
|
fill, and/or merge (boolean OR) polygons per layer as a final pass, on
|
|
136
137
|
the given metal_layers, write the result to output_path. Mirrors the
|
|
@@ -139,12 +140,21 @@ def run_simplify(gds_path, metal_layers, output_path, cellname="",
|
|
|
139
140
|
(needs a flattened, per-layer-merged cell first so that touching fill
|
|
140
141
|
tiles aren't mistaken for isolated ones - see
|
|
141
142
|
find_isolated_same_size_polygons_by_layer()'s own docstring), per-layer
|
|
142
|
-
merge
|
|
143
|
+
merge, then via array merging.
|
|
144
|
+
|
|
145
|
+
via_layers: list of (via layer, metal layers above, metal layers below),
|
|
146
|
+
GDS layer numbers. Vias closer than via_merge_size (microns) are merged
|
|
147
|
+
if they connect the same metal shapes above and below, see
|
|
148
|
+
gds_prepare_for_EM.merge_via_array_by_metal_overlap(). Vias without metal
|
|
149
|
+
above or below are kept unchanged. None or via_merge_size <= 0: no via
|
|
150
|
+
merging.
|
|
143
151
|
"""
|
|
144
152
|
from gds_prepare_for_EM import (
|
|
145
153
|
remove_cutout_keep_hierarchy,
|
|
146
154
|
merge_polygons_by_layer,
|
|
147
155
|
find_isolated_same_size_polygons_by_layer,
|
|
156
|
+
merge_polygons,
|
|
157
|
+
merge_via_array_by_metal_overlap,
|
|
148
158
|
)
|
|
149
159
|
from gds_geometry_utils import validate_and_repair_polygons
|
|
150
160
|
|
|
@@ -187,6 +197,31 @@ def run_simplify(gds_path, metal_layers, output_path, cellname="",
|
|
|
187
197
|
lib, top_cell = _flatten_cell(lib, cellname)
|
|
188
198
|
lib = merge_polygons_by_layer(top_cell, layers_list=metal_layers)
|
|
189
199
|
|
|
200
|
+
if via_layers and via_merge_size > 0:
|
|
201
|
+
lib, top_cell = _flatten_cell(lib, cellname)
|
|
202
|
+
lpp = top_cell.get_polygons(by_spec=True)
|
|
203
|
+
print(f'Via array merging (distance {via_merge_size} um):')
|
|
204
|
+
|
|
205
|
+
def metal_polygons(layers):
|
|
206
|
+
polygons = [p for (layer, purpose), polys in lpp.items() if layer in layers for p in polys]
|
|
207
|
+
# touching metal pieces are one metal shape for via grouping
|
|
208
|
+
return merge_polygons(polygons) if polygons else []
|
|
209
|
+
|
|
210
|
+
for via_layer, above_layers, below_layers in via_layers:
|
|
211
|
+
purposes = [purpose for (layer, purpose) in lpp if layer == via_layer]
|
|
212
|
+
if not purposes:
|
|
213
|
+
continue
|
|
214
|
+
above, below = metal_polygons(above_layers), metal_polygons(below_layers)
|
|
215
|
+
for purpose in purposes:
|
|
216
|
+
vias = lpp[(via_layer, purpose)]
|
|
217
|
+
merged = merge_via_array_by_metal_overlap(
|
|
218
|
+
vias, above, below, via_merge_size, keep_unmatched=True)
|
|
219
|
+
top_cell.remove_polygons(
|
|
220
|
+
lambda pts, layer, datatype: layer == via_layer and datatype == purpose)
|
|
221
|
+
for pts in merged:
|
|
222
|
+
top_cell.add(gdspy.Polygon(pts, layer=via_layer, datatype=purpose))
|
|
223
|
+
print(f' layer {via_layer}:{purpose}: {len(vias)} -> {len(merged)} polygons')
|
|
224
|
+
|
|
190
225
|
# Final polygon validity check/repair - always runs, regardless of which
|
|
191
226
|
# operations above were selected. Catches any polygon still geometrically
|
|
192
227
|
# invalid (self-intersecting) no matter the reason - e.g. a cutout
|
|
@@ -235,6 +270,14 @@ class SimplifyGdsDialog(QDialog):
|
|
|
235
270
|
int(m.layernum) for m in metals_list.getallplanarmetals()
|
|
236
271
|
if not (port_layer_min <= int(m.layernum) <= port_layer_max)
|
|
237
272
|
) if metals_list is not None else []
|
|
273
|
+
# via layers with the metal layers above/below them, for via array merging
|
|
274
|
+
self._via_layers = [
|
|
275
|
+
(int(m.layernum),
|
|
276
|
+
[int(n.layernum) for n in m.above if n.is_metal],
|
|
277
|
+
[int(n.layernum) for n in m.below if n.is_metal])
|
|
278
|
+
for m in metals_list.metals
|
|
279
|
+
if m.is_via and not (port_layer_min <= int(m.layernum) <= port_layer_max)
|
|
280
|
+
] if metals_list is not None else []
|
|
238
281
|
|
|
239
282
|
layout = QVBoxLayout(self)
|
|
240
283
|
|
|
@@ -326,13 +369,40 @@ class SimplifyGdsDialog(QDialog):
|
|
|
326
369
|
|
|
327
370
|
layout.addWidget(cutout_group)
|
|
328
371
|
|
|
372
|
+
# ---- Merge via arrays ----
|
|
373
|
+
via_group = QGroupBox("Merge via arrays")
|
|
374
|
+
via_group.setCheckable(True)
|
|
375
|
+
via_group.setChecked(False)
|
|
376
|
+
via_group.setToolTip(
|
|
377
|
+
"Vias closer than this distance are merged into one shape, but only if they connect "
|
|
378
|
+
"the same metal shapes above and below, so merging never shorts different metal "
|
|
379
|
+
"shapes. Via layers and the metal layers above/below them come from the XML stackup."
|
|
380
|
+
)
|
|
381
|
+
via_layout = QVBoxLayout(via_group)
|
|
382
|
+
self.via_group = via_group
|
|
383
|
+
|
|
384
|
+
via_size_row = QHBoxLayout()
|
|
385
|
+
via_size_row.addWidget(row_label("Distance (µm)"))
|
|
386
|
+
self.via_merge_size_edit = QLineEdit(str(get_preference(app_name, "merge_polygon_size", "0.5")))
|
|
387
|
+
self.via_merge_size_edit.setStyleSheet(_EDIT_STYLE)
|
|
388
|
+
via_size_row.addWidget(self.via_merge_size_edit)
|
|
389
|
+
via_size_row.addStretch(1)
|
|
390
|
+
via_layout.addLayout(via_size_row)
|
|
391
|
+
|
|
392
|
+
via_note = QLabel("Merged vias in the output file are treated as solid metal: fill factor "
|
|
393
|
+
"correction needs the original vias, which are then no longer in the file. "
|
|
394
|
+
"For accurate via resistance, merge vias in the model instead (via array "
|
|
395
|
+
"merge distance > 0 with fill factor correction).")
|
|
396
|
+
via_note.setWordWrap(True)
|
|
397
|
+
via_layout.addWidget(via_note)
|
|
398
|
+
|
|
329
399
|
# ---- Merge per layer ----
|
|
330
400
|
# remembers the user's own choice while the checkbox is forced/greyed
|
|
331
401
|
# out by "Remove floating (unconnected) metal" (see
|
|
332
402
|
# _on_floating_group_toggled() below), so it's restored rather than
|
|
333
403
|
# lost once that group is unchecked again
|
|
334
404
|
self._merge_user_choice = get_preference_bool(app_name, "simplify_merge_per_layer", True)
|
|
335
|
-
self.merge_per_layer_checkbox = QCheckBox("Merge polygons per layer
|
|
405
|
+
self.merge_per_layer_checkbox = QCheckBox("Merge polygons per layer")
|
|
336
406
|
self.merge_per_layer_checkbox.setChecked(self._merge_user_choice)
|
|
337
407
|
self.merge_per_layer_checkbox.setToolTip(
|
|
338
408
|
"Boolean-OR touching/overlapping polygons on the same layer into the minimal "
|
|
@@ -347,6 +417,9 @@ class SimplifyGdsDialog(QDialog):
|
|
|
347
417
|
floating_group.toggled.connect(self._on_floating_group_toggled)
|
|
348
418
|
self._on_floating_group_toggled(floating_group.isChecked())
|
|
349
419
|
|
|
420
|
+
# after "Merge polygons per layer": via merging runs last, see run_simplify()
|
|
421
|
+
layout.addWidget(via_group)
|
|
422
|
+
|
|
350
423
|
# ---- Log area ----
|
|
351
424
|
self.log_area = QPlainTextEdit()
|
|
352
425
|
self.log_area.setReadOnly(True)
|
|
@@ -437,7 +510,8 @@ class SimplifyGdsDialog(QDialog):
|
|
|
437
510
|
do_floating = self.floating_group.isChecked()
|
|
438
511
|
do_cutouts = self.cutout_group.isChecked()
|
|
439
512
|
do_merge = self.merge_per_layer_checkbox.isChecked()
|
|
440
|
-
|
|
513
|
+
do_via_merge = self.via_group.isChecked()
|
|
514
|
+
if not do_floating and not do_cutouts and not do_merge and not do_via_merge:
|
|
441
515
|
QMessageBox.warning(self, "Error", "Enable at least one operation")
|
|
442
516
|
return
|
|
443
517
|
|
|
@@ -455,6 +529,11 @@ class SimplifyGdsDialog(QDialog):
|
|
|
455
529
|
fill_mincount = int(mincount_text) if mincount_text else 20
|
|
456
530
|
max_hole_area = _parse_optional_float(self.max_hole_area_edit.text(), "Max cutout area")
|
|
457
531
|
excluded_layers = _parse_layer_list(self.excluded_layers_edit.text())
|
|
532
|
+
via_merge_size = 0
|
|
533
|
+
if do_via_merge:
|
|
534
|
+
via_merge_size = _parse_optional_float(self.via_merge_size_edit.text(), "Via merge distance")
|
|
535
|
+
if via_merge_size is None or via_merge_size <= 0:
|
|
536
|
+
raise ValueError("'Via merge distance' must be a number > 0")
|
|
458
537
|
except ValueError as e:
|
|
459
538
|
QMessageBox.warning(self, "Error", str(e))
|
|
460
539
|
return
|
|
@@ -489,7 +568,9 @@ class SimplifyGdsDialog(QDialog):
|
|
|
489
568
|
do_cutouts=do_cutouts, max_hole_area=max_hole_area,
|
|
490
569
|
do_floating=do_floating, fill_minsize=fill_minsize,
|
|
491
570
|
fill_maxsize=fill_maxsize, fill_mincount=fill_mincount,
|
|
492
|
-
do_merge=do_merge
|
|
571
|
+
do_merge=do_merge,
|
|
572
|
+
via_layers=[v for v in self._via_layers if v[0] not in excluded_layers],
|
|
573
|
+
via_merge_size=via_merge_size)
|
|
493
574
|
except (Exception, SystemExit):
|
|
494
575
|
printed = captured_stdout.getvalue().strip()
|
|
495
576
|
details = (printed + "\n\n" + traceback.format_exc()) if printed else traceback.format_exc()
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: setupEM
|
|
3
|
+
Version: 0.11.1
|
|
4
|
+
Summary: Python tool for configuration of gds2palace workflow with GUI.
|
|
5
|
+
Author-email: Volker Muehlhaus <volker@muehlhaus.com>
|
|
6
|
+
License-Expression: GPL-3.0-or-later
|
|
7
|
+
Project-URL: Homepage, https://github.com/VolkerMuehlhaus/setupEM
|
|
8
|
+
Project-URL: Repository, https://github.com/VolkerMuehlhaus/setupEM
|
|
9
|
+
Project-URL: Changelog, https://github.com/VolkerMuehlhaus/setupEM/blob/main/doc/CHANGES.md
|
|
10
|
+
Requires-Python: >=3.9
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: gds2palace>=0.8.0
|
|
14
|
+
Requires-Dist: gds_prepare_for_EM>=1.3.0
|
|
15
|
+
Requires-Dist: PySide6
|
|
16
|
+
Requires-Dist: shiboken6
|
|
17
|
+
Requires-Dist: scipy
|
|
18
|
+
Requires-Dist: requests
|
|
19
|
+
Requires-Dist: scikit-rf
|
|
20
|
+
Requires-Dist: matplotlib
|
|
21
|
+
Requires-Dist: numpy
|
|
22
|
+
Requires-Dist: gdspy
|
|
23
|
+
Requires-Dist: meshio
|
|
24
|
+
Requires-Dist: pyvista
|
|
25
|
+
Requires-Dist: pyvistaqt
|
|
26
|
+
Dynamic: license-file
|
setupem-0.10.2/PKG-INFO
DELETED
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: setupEM
|
|
3
|
-
Version: 0.10.2
|
|
4
|
-
Summary: Python tool for configuration of gds2palace workflow with GUI.
|
|
5
|
-
Author-email: Volker Muehlhaus <volker@muehlhaus.com>
|
|
6
|
-
License-Expression: GPL-3.0-or-later
|
|
7
|
-
Project-URL: Homepage, https://github.com/VolkerMuehlhaus/setupEM
|
|
8
|
-
Project-URL: Repository, https://github.com/VolkerMuehlhaus/setupEM
|
|
9
|
-
Project-URL: Changelog, https://github.com/VolkerMuehlhaus/setupEM/blob/main/doc/CHANGES.md
|
|
10
|
-
Requires-Python: >=3.9
|
|
11
|
-
Description-Content-Type: text/markdown
|
|
12
|
-
License-File: LICENSE
|
|
13
|
-
Requires-Dist: gds2palace>=0.8.0
|
|
14
|
-
Requires-Dist: gds_prepare_for_EM>=1.2.0
|
|
15
|
-
Requires-Dist: PySide6
|
|
16
|
-
Requires-Dist: shiboken6
|
|
17
|
-
Requires-Dist: scipy
|
|
18
|
-
Requires-Dist: requests
|
|
19
|
-
Requires-Dist: scikit-rf
|
|
20
|
-
Requires-Dist: matplotlib
|
|
21
|
-
Requires-Dist: numpy
|
|
22
|
-
Requires-Dist: gdspy
|
|
23
|
-
Requires-Dist: meshio
|
|
24
|
-
Requires-Dist: pyvista
|
|
25
|
-
Requires-Dist: pyvistaqt
|
|
26
|
-
Dynamic: license-file
|
|
27
|
-
|
|
28
|
-
# Python GUI for gds2palace
|
|
29
|
-
|
|
30
|
-

|
|
31
|
-
|
|
32
|
-
## Install
|
|
33
|
-
|
|
34
|
-
pip install setupEM
|
|
35
|
-
|
|
36
|
-
**Full documentation, installation guide, and usage walkthrough:**
|
|
37
|
-
https://github.com/VolkerMuehlhaus/setupEM
|
|
38
|
-
|
|
39
|
-
## Recent changes
|
|
40
|
-
|
|
41
|
-
# What's New - September 30, 2026
|
|
42
|
-
|
|
43
|
-
The **Adaptive mesh refinement (AMR)** group on the Mesh and Boundaries tab is now called **Solver & Adaptive Mesh Refinement (AMR)** and has a new Palace linear solver setting, shown with **Show advanced configuration** = **Yes**.
|
|
44
|
-
|
|
45
|
-
**Complex coarse solve** (default **Yes** for every model, set in **Preferences > Palace**) lets Palace's direct coarse solver factorize the full complex system instead of only its real part. With **No** (Palace's own default), some models don't converge at some frequencies and get wrong S-parameters there: a D-band line failed at 170 GHz, and an inductor failed at 2.4 GHz with 1.1 nH instead of 2.5 nH, while it converged at 0.1 and 10 GHz. With **Yes** both converged in 1-31 iterations and ran 3.5-6x faster. Models that already converge give identical results in about the same or less time. The cost is peak RAM, depending on the model 1.0-1.6x at order 2 and 1.5-1.9x at order 1; the AMR RAM estimate includes this.
|
|
46
|
-
|
|
47
|
-
**Maximum solver iterations** (400) and **Solver tolerance** (1e-6) can now be changed too, in **Preferences > Palace**. These settings require gds2palace v0.8.0 or later and are hidden with an older version. setupEM module installation now requires gds2palace v0.8.0 or later, which also restores the dielectric loss tangent in Palace models that had been deleted by mistake.
|
|
48
|
-
|
|
49
|
-
**Conformal AMR (experimental)** is a new advanced setting in the **Solver & Adaptive Mesh Refinement (AMR)** group (default **No**, set in **Preferences > Palace**). With AMR iterations > 0, it switches Palace from its default nonconformal (hanging-node) mesh refinement to conformal refinement (gds2palace `settings['adaptive_mesh_conformal']`). In the test cases so far it converged in fewer AMR iterations (D-band balun: 2 iterations instead of 4, in half the time), but the mesh cell count grows faster per iteration.
|
|
50
|
-
|
|
51
|
-
setupEM now warns when Palace's linear solver does **not converge**: Palace carries on and writes S-parameters for that frequency anyway, but they are unreliable. Each case gets a ⚠ warning in the Log panel with its frequency, and all of them are listed again at the end of the run.
|
|
52
|
-
|
|
53
|
-
# What's New - September 29, 2026
|
|
54
|
-
|
|
55
|
-
The built-in **3D field viewer** now shows frequencies instead of bare numbers: the **Cycle** picker of a multi-frequency Palace field dump lists e.g. "6 GHz (cycle 1)", and Palace's extra error-indicator dump is listed as "geometry" right away. For Elmer (EM), each result file in the **Result File** picker shows its frequency, e.g. "fields_t0002.vtu - 7.5 GHz". The cycle number is kept in the label, so it still matches `field_viewer.py --cycle <N>`.
|
|
56
|
-
|
|
57
|
-
# What's New - September 26, 2026
|
|
58
|
-
|
|
59
|
-
**Result Viewer**: new frequency **Marker** that reads out all curves at the same frequency, shown on the plots and in a table below. Set it by clicking a plot, typing a frequency or using the Left/Right arrow keys; right-click a plot to jump to the (next) min/max.
|
|
60
|
-
|
|
61
|
-
The **AMR maximum DOF** setting now shows an estimate of the RAM that Palace will need at that mesh size, both on the Mesh tab and in Preferences > Palace. The estimate is based on existing Palace results (about 12 GB per million DOF, up to 15 GB). On the Mesh tab, it turns into a warning when the worst case exceeds the **"Stop Palace if memory exceeds"** limit.
|
|
62
|
-
|
|
63
|
-
**Stackup Preview**: three or more layers at the same height (e.g. resistor sheets on top of Activ) are now drawn side by side instead of on top of each other, via labels sit near the upper end of the via, and sheet resistance labels show the correct unit (e.g. RHIGH: Rs=1360 Ω, was shown as 1360000 mΩ).
|
|
64
|
-
|
|
65
|
-
The bundled stackup files now include the latest parameterized gds2palace stackups for EM and thermal simulation: **SG13G2_FEM_200um.xml**, **SG13G2_FEM_200um_passi3D.xml** (conformal passivation around TopMetal2) and **SG13CMOS5L_200um.xml**. Chip and air height can be changed with the **Override stackup Variables** table. The previous SG13G2_100um.xml, SG13G2_200um.xml and SG13G2_nosub.xml are still included, so existing models keep working.
|
|
66
|
-
|
|
67
|
-
# What's New - September 25, 2026
|
|
68
|
-
|
|
69
|
-
Added a **Correction for via array cross section** option on the Input Files tab, below via array merging, in setupEM (Palace and Elmer) and setupThermal. Merging fills the gaps between vias with via material, which overestimates the conductivity of a via array; when enabled, the conductivity of each merged via polygon (heat conductivity for thermal models) is multiplied by its fill factor (original via area / merged polygon area). This option requires gds2palace v0.7.0 or later and is hidden with an older version, which would silently ignore it.
|
|
70
|
-
|
|
71
|
-
# What's New - September 22-24, 2026
|
|
72
|
-
|
|
73
|
-
Added a **Conductor meshing** option (Palace only) on the Mesh and Boundaries tab: **Surface impedance** (default, as before) or **Solve inside (volume mesh)**, which models conductors as solid bulk-conductivity volumes instead - more accurate at low frequency, at the cost of more RAM/simulation time and reduced accuracy at higher frequencies. This option requires gds2palace v0.6.0 or later, earlier version silently ignore this setting.
|
|
74
|
-
|
|
75
|
-
Better display in **Stackup Preview**'s for metals/vias that push outside their base dieelectric.
|
|
76
|
-
|
|
77
|
-
Palace's real memory usage is now measured directly from the running solver process(es) instead of parsed from its own log output, which could be inaccurate - this also makes the **"Stop Palace if memory exceeds"** kill switch more reliable.
|
|
78
|
-
|
|
79
|
-
The built-in **3D field viewer** can now switch between the results of a multi-frequency Palace field-dump ("fdump") run: a new **Cycle** picker appears whenever the loaded result file has more than one solved frequency, and `field_viewer.py --cycle <N>` selects one from the command line. Previously the viewer could only ever show one fixed cycle of such a file, with no way to see the others.
|
|
80
|
-
|
|
81
|
-
Added **File > New**, which resets setupEM/setupThermal to the same blank state as a freshly started application - useful for starting a new model without restarting the app.
|
|
82
|
-
|
|
83
|
-
Full history: [CHANGES.md](https://github.com/VolkerMuehlhaus/setupEM/blob/main/doc/CHANGES.md)
|
|
84
|
-
|
|
85
|
-
---
|
|
86
|
-
|
|
87
|
-
**Note:** the `setupEM` PyPI package itself requires:
|
|
88
|
-
|
|
89
|
-
- gds2palace>=0.7.0
|
|
90
|
-
- gds_prepare_for_EM>=1.2.0
|
|
91
|
-
- PySide6
|
|
92
|
-
- shiboken6
|
|
93
|
-
- scipy
|
|
94
|
-
- requests
|
|
95
|
-
- scikit-rf
|
|
96
|
-
- matplotlib
|
|
97
|
-
- numpy
|
|
98
|
-
- gdspy
|
|
99
|
-
- meshio
|
|
100
|
-
- pyvista
|
|
101
|
-
- pyvistaqt
|
|
102
|
-
|
|
103
|
-
(Other Python modules mentioned above are only needed to run standalone helper scripts in this repository, not to use the installed package.)
|
setupem-0.10.2/README_pypi.md
DELETED
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
# Python GUI for gds2palace
|
|
2
|
-
|
|
3
|
-

|
|
4
|
-
|
|
5
|
-
## Install
|
|
6
|
-
|
|
7
|
-
pip install setupEM
|
|
8
|
-
|
|
9
|
-
**Full documentation, installation guide, and usage walkthrough:**
|
|
10
|
-
https://github.com/VolkerMuehlhaus/setupEM
|
|
11
|
-
|
|
12
|
-
## Recent changes
|
|
13
|
-
|
|
14
|
-
# What's New - September 30, 2026
|
|
15
|
-
|
|
16
|
-
The **Adaptive mesh refinement (AMR)** group on the Mesh and Boundaries tab is now called **Solver & Adaptive Mesh Refinement (AMR)** and has a new Palace linear solver setting, shown with **Show advanced configuration** = **Yes**.
|
|
17
|
-
|
|
18
|
-
**Complex coarse solve** (default **Yes** for every model, set in **Preferences > Palace**) lets Palace's direct coarse solver factorize the full complex system instead of only its real part. With **No** (Palace's own default), some models don't converge at some frequencies and get wrong S-parameters there: a D-band line failed at 170 GHz, and an inductor failed at 2.4 GHz with 1.1 nH instead of 2.5 nH, while it converged at 0.1 and 10 GHz. With **Yes** both converged in 1-31 iterations and ran 3.5-6x faster. Models that already converge give identical results in about the same or less time. The cost is peak RAM, depending on the model 1.0-1.6x at order 2 and 1.5-1.9x at order 1; the AMR RAM estimate includes this.
|
|
19
|
-
|
|
20
|
-
**Maximum solver iterations** (400) and **Solver tolerance** (1e-6) can now be changed too, in **Preferences > Palace**. These settings require gds2palace v0.8.0 or later and are hidden with an older version. setupEM module installation now requires gds2palace v0.8.0 or later, which also restores the dielectric loss tangent in Palace models that had been deleted by mistake.
|
|
21
|
-
|
|
22
|
-
**Conformal AMR (experimental)** is a new advanced setting in the **Solver & Adaptive Mesh Refinement (AMR)** group (default **No**, set in **Preferences > Palace**). With AMR iterations > 0, it switches Palace from its default nonconformal (hanging-node) mesh refinement to conformal refinement (gds2palace `settings['adaptive_mesh_conformal']`). In the test cases so far it converged in fewer AMR iterations (D-band balun: 2 iterations instead of 4, in half the time), but the mesh cell count grows faster per iteration.
|
|
23
|
-
|
|
24
|
-
setupEM now warns when Palace's linear solver does **not converge**: Palace carries on and writes S-parameters for that frequency anyway, but they are unreliable. Each case gets a ⚠ warning in the Log panel with its frequency, and all of them are listed again at the end of the run.
|
|
25
|
-
|
|
26
|
-
# What's New - September 29, 2026
|
|
27
|
-
|
|
28
|
-
The built-in **3D field viewer** now shows frequencies instead of bare numbers: the **Cycle** picker of a multi-frequency Palace field dump lists e.g. "6 GHz (cycle 1)", and Palace's extra error-indicator dump is listed as "geometry" right away. For Elmer (EM), each result file in the **Result File** picker shows its frequency, e.g. "fields_t0002.vtu - 7.5 GHz". The cycle number is kept in the label, so it still matches `field_viewer.py --cycle <N>`.
|
|
29
|
-
|
|
30
|
-
# What's New - September 26, 2026
|
|
31
|
-
|
|
32
|
-
**Result Viewer**: new frequency **Marker** that reads out all curves at the same frequency, shown on the plots and in a table below. Set it by clicking a plot, typing a frequency or using the Left/Right arrow keys; right-click a plot to jump to the (next) min/max.
|
|
33
|
-
|
|
34
|
-
The **AMR maximum DOF** setting now shows an estimate of the RAM that Palace will need at that mesh size, both on the Mesh tab and in Preferences > Palace. The estimate is based on existing Palace results (about 12 GB per million DOF, up to 15 GB). On the Mesh tab, it turns into a warning when the worst case exceeds the **"Stop Palace if memory exceeds"** limit.
|
|
35
|
-
|
|
36
|
-
**Stackup Preview**: three or more layers at the same height (e.g. resistor sheets on top of Activ) are now drawn side by side instead of on top of each other, via labels sit near the upper end of the via, and sheet resistance labels show the correct unit (e.g. RHIGH: Rs=1360 Ω, was shown as 1360000 mΩ).
|
|
37
|
-
|
|
38
|
-
The bundled stackup files now include the latest parameterized gds2palace stackups for EM and thermal simulation: **SG13G2_FEM_200um.xml**, **SG13G2_FEM_200um_passi3D.xml** (conformal passivation around TopMetal2) and **SG13CMOS5L_200um.xml**. Chip and air height can be changed with the **Override stackup Variables** table. The previous SG13G2_100um.xml, SG13G2_200um.xml and SG13G2_nosub.xml are still included, so existing models keep working.
|
|
39
|
-
|
|
40
|
-
# What's New - September 25, 2026
|
|
41
|
-
|
|
42
|
-
Added a **Correction for via array cross section** option on the Input Files tab, below via array merging, in setupEM (Palace and Elmer) and setupThermal. Merging fills the gaps between vias with via material, which overestimates the conductivity of a via array; when enabled, the conductivity of each merged via polygon (heat conductivity for thermal models) is multiplied by its fill factor (original via area / merged polygon area). This option requires gds2palace v0.7.0 or later and is hidden with an older version, which would silently ignore it.
|
|
43
|
-
|
|
44
|
-
# What's New - September 22-24, 2026
|
|
45
|
-
|
|
46
|
-
Added a **Conductor meshing** option (Palace only) on the Mesh and Boundaries tab: **Surface impedance** (default, as before) or **Solve inside (volume mesh)**, which models conductors as solid bulk-conductivity volumes instead - more accurate at low frequency, at the cost of more RAM/simulation time and reduced accuracy at higher frequencies. This option requires gds2palace v0.6.0 or later, earlier version silently ignore this setting.
|
|
47
|
-
|
|
48
|
-
Better display in **Stackup Preview**'s for metals/vias that push outside their base dieelectric.
|
|
49
|
-
|
|
50
|
-
Palace's real memory usage is now measured directly from the running solver process(es) instead of parsed from its own log output, which could be inaccurate - this also makes the **"Stop Palace if memory exceeds"** kill switch more reliable.
|
|
51
|
-
|
|
52
|
-
The built-in **3D field viewer** can now switch between the results of a multi-frequency Palace field-dump ("fdump") run: a new **Cycle** picker appears whenever the loaded result file has more than one solved frequency, and `field_viewer.py --cycle <N>` selects one from the command line. Previously the viewer could only ever show one fixed cycle of such a file, with no way to see the others.
|
|
53
|
-
|
|
54
|
-
Added **File > New**, which resets setupEM/setupThermal to the same blank state as a freshly started application - useful for starting a new model without restarting the app.
|
|
55
|
-
|
|
56
|
-
Full history: [CHANGES.md](https://github.com/VolkerMuehlhaus/setupEM/blob/main/doc/CHANGES.md)
|
|
57
|
-
|
|
58
|
-
---
|
|
59
|
-
|
|
60
|
-
**Note:** the `setupEM` PyPI package itself requires:
|
|
61
|
-
|
|
62
|
-
- gds2palace>=0.7.0
|
|
63
|
-
- gds_prepare_for_EM>=1.2.0
|
|
64
|
-
- PySide6
|
|
65
|
-
- shiboken6
|
|
66
|
-
- scipy
|
|
67
|
-
- requests
|
|
68
|
-
- scikit-rf
|
|
69
|
-
- matplotlib
|
|
70
|
-
- numpy
|
|
71
|
-
- gdspy
|
|
72
|
-
- meshio
|
|
73
|
-
- pyvista
|
|
74
|
-
- pyvistaqt
|
|
75
|
-
|
|
76
|
-
(Other Python modules mentioned above are only needed to run standalone helper scripts in this repository, not to use the installed package.)
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: setupEM
|
|
3
|
-
Version: 0.10.2
|
|
4
|
-
Summary: Python tool for configuration of gds2palace workflow with GUI.
|
|
5
|
-
Author-email: Volker Muehlhaus <volker@muehlhaus.com>
|
|
6
|
-
License-Expression: GPL-3.0-or-later
|
|
7
|
-
Project-URL: Homepage, https://github.com/VolkerMuehlhaus/setupEM
|
|
8
|
-
Project-URL: Repository, https://github.com/VolkerMuehlhaus/setupEM
|
|
9
|
-
Project-URL: Changelog, https://github.com/VolkerMuehlhaus/setupEM/blob/main/doc/CHANGES.md
|
|
10
|
-
Requires-Python: >=3.9
|
|
11
|
-
Description-Content-Type: text/markdown
|
|
12
|
-
License-File: LICENSE
|
|
13
|
-
Requires-Dist: gds2palace>=0.8.0
|
|
14
|
-
Requires-Dist: gds_prepare_for_EM>=1.2.0
|
|
15
|
-
Requires-Dist: PySide6
|
|
16
|
-
Requires-Dist: shiboken6
|
|
17
|
-
Requires-Dist: scipy
|
|
18
|
-
Requires-Dist: requests
|
|
19
|
-
Requires-Dist: scikit-rf
|
|
20
|
-
Requires-Dist: matplotlib
|
|
21
|
-
Requires-Dist: numpy
|
|
22
|
-
Requires-Dist: gdspy
|
|
23
|
-
Requires-Dist: meshio
|
|
24
|
-
Requires-Dist: pyvista
|
|
25
|
-
Requires-Dist: pyvistaqt
|
|
26
|
-
Dynamic: license-file
|
|
27
|
-
|
|
28
|
-
# Python GUI for gds2palace
|
|
29
|
-
|
|
30
|
-

|
|
31
|
-
|
|
32
|
-
## Install
|
|
33
|
-
|
|
34
|
-
pip install setupEM
|
|
35
|
-
|
|
36
|
-
**Full documentation, installation guide, and usage walkthrough:**
|
|
37
|
-
https://github.com/VolkerMuehlhaus/setupEM
|
|
38
|
-
|
|
39
|
-
## Recent changes
|
|
40
|
-
|
|
41
|
-
# What's New - September 30, 2026
|
|
42
|
-
|
|
43
|
-
The **Adaptive mesh refinement (AMR)** group on the Mesh and Boundaries tab is now called **Solver & Adaptive Mesh Refinement (AMR)** and has a new Palace linear solver setting, shown with **Show advanced configuration** = **Yes**.
|
|
44
|
-
|
|
45
|
-
**Complex coarse solve** (default **Yes** for every model, set in **Preferences > Palace**) lets Palace's direct coarse solver factorize the full complex system instead of only its real part. With **No** (Palace's own default), some models don't converge at some frequencies and get wrong S-parameters there: a D-band line failed at 170 GHz, and an inductor failed at 2.4 GHz with 1.1 nH instead of 2.5 nH, while it converged at 0.1 and 10 GHz. With **Yes** both converged in 1-31 iterations and ran 3.5-6x faster. Models that already converge give identical results in about the same or less time. The cost is peak RAM, depending on the model 1.0-1.6x at order 2 and 1.5-1.9x at order 1; the AMR RAM estimate includes this.
|
|
46
|
-
|
|
47
|
-
**Maximum solver iterations** (400) and **Solver tolerance** (1e-6) can now be changed too, in **Preferences > Palace**. These settings require gds2palace v0.8.0 or later and are hidden with an older version. setupEM module installation now requires gds2palace v0.8.0 or later, which also restores the dielectric loss tangent in Palace models that had been deleted by mistake.
|
|
48
|
-
|
|
49
|
-
**Conformal AMR (experimental)** is a new advanced setting in the **Solver & Adaptive Mesh Refinement (AMR)** group (default **No**, set in **Preferences > Palace**). With AMR iterations > 0, it switches Palace from its default nonconformal (hanging-node) mesh refinement to conformal refinement (gds2palace `settings['adaptive_mesh_conformal']`). In the test cases so far it converged in fewer AMR iterations (D-band balun: 2 iterations instead of 4, in half the time), but the mesh cell count grows faster per iteration.
|
|
50
|
-
|
|
51
|
-
setupEM now warns when Palace's linear solver does **not converge**: Palace carries on and writes S-parameters for that frequency anyway, but they are unreliable. Each case gets a ⚠ warning in the Log panel with its frequency, and all of them are listed again at the end of the run.
|
|
52
|
-
|
|
53
|
-
# What's New - September 29, 2026
|
|
54
|
-
|
|
55
|
-
The built-in **3D field viewer** now shows frequencies instead of bare numbers: the **Cycle** picker of a multi-frequency Palace field dump lists e.g. "6 GHz (cycle 1)", and Palace's extra error-indicator dump is listed as "geometry" right away. For Elmer (EM), each result file in the **Result File** picker shows its frequency, e.g. "fields_t0002.vtu - 7.5 GHz". The cycle number is kept in the label, so it still matches `field_viewer.py --cycle <N>`.
|
|
56
|
-
|
|
57
|
-
# What's New - September 26, 2026
|
|
58
|
-
|
|
59
|
-
**Result Viewer**: new frequency **Marker** that reads out all curves at the same frequency, shown on the plots and in a table below. Set it by clicking a plot, typing a frequency or using the Left/Right arrow keys; right-click a plot to jump to the (next) min/max.
|
|
60
|
-
|
|
61
|
-
The **AMR maximum DOF** setting now shows an estimate of the RAM that Palace will need at that mesh size, both on the Mesh tab and in Preferences > Palace. The estimate is based on existing Palace results (about 12 GB per million DOF, up to 15 GB). On the Mesh tab, it turns into a warning when the worst case exceeds the **"Stop Palace if memory exceeds"** limit.
|
|
62
|
-
|
|
63
|
-
**Stackup Preview**: three or more layers at the same height (e.g. resistor sheets on top of Activ) are now drawn side by side instead of on top of each other, via labels sit near the upper end of the via, and sheet resistance labels show the correct unit (e.g. RHIGH: Rs=1360 Ω, was shown as 1360000 mΩ).
|
|
64
|
-
|
|
65
|
-
The bundled stackup files now include the latest parameterized gds2palace stackups for EM and thermal simulation: **SG13G2_FEM_200um.xml**, **SG13G2_FEM_200um_passi3D.xml** (conformal passivation around TopMetal2) and **SG13CMOS5L_200um.xml**. Chip and air height can be changed with the **Override stackup Variables** table. The previous SG13G2_100um.xml, SG13G2_200um.xml and SG13G2_nosub.xml are still included, so existing models keep working.
|
|
66
|
-
|
|
67
|
-
# What's New - September 25, 2026
|
|
68
|
-
|
|
69
|
-
Added a **Correction for via array cross section** option on the Input Files tab, below via array merging, in setupEM (Palace and Elmer) and setupThermal. Merging fills the gaps between vias with via material, which overestimates the conductivity of a via array; when enabled, the conductivity of each merged via polygon (heat conductivity for thermal models) is multiplied by its fill factor (original via area / merged polygon area). This option requires gds2palace v0.7.0 or later and is hidden with an older version, which would silently ignore it.
|
|
70
|
-
|
|
71
|
-
# What's New - September 22-24, 2026
|
|
72
|
-
|
|
73
|
-
Added a **Conductor meshing** option (Palace only) on the Mesh and Boundaries tab: **Surface impedance** (default, as before) or **Solve inside (volume mesh)**, which models conductors as solid bulk-conductivity volumes instead - more accurate at low frequency, at the cost of more RAM/simulation time and reduced accuracy at higher frequencies. This option requires gds2palace v0.6.0 or later, earlier version silently ignore this setting.
|
|
74
|
-
|
|
75
|
-
Better display in **Stackup Preview**'s for metals/vias that push outside their base dieelectric.
|
|
76
|
-
|
|
77
|
-
Palace's real memory usage is now measured directly from the running solver process(es) instead of parsed from its own log output, which could be inaccurate - this also makes the **"Stop Palace if memory exceeds"** kill switch more reliable.
|
|
78
|
-
|
|
79
|
-
The built-in **3D field viewer** can now switch between the results of a multi-frequency Palace field-dump ("fdump") run: a new **Cycle** picker appears whenever the loaded result file has more than one solved frequency, and `field_viewer.py --cycle <N>` selects one from the command line. Previously the viewer could only ever show one fixed cycle of such a file, with no way to see the others.
|
|
80
|
-
|
|
81
|
-
Added **File > New**, which resets setupEM/setupThermal to the same blank state as a freshly started application - useful for starting a new model without restarting the app.
|
|
82
|
-
|
|
83
|
-
Full history: [CHANGES.md](https://github.com/VolkerMuehlhaus/setupEM/blob/main/doc/CHANGES.md)
|
|
84
|
-
|
|
85
|
-
---
|
|
86
|
-
|
|
87
|
-
**Note:** the `setupEM` PyPI package itself requires:
|
|
88
|
-
|
|
89
|
-
- gds2palace>=0.7.0
|
|
90
|
-
- gds_prepare_for_EM>=1.2.0
|
|
91
|
-
- PySide6
|
|
92
|
-
- shiboken6
|
|
93
|
-
- scipy
|
|
94
|
-
- requests
|
|
95
|
-
- scikit-rf
|
|
96
|
-
- matplotlib
|
|
97
|
-
- numpy
|
|
98
|
-
- gdspy
|
|
99
|
-
- meshio
|
|
100
|
-
- pyvista
|
|
101
|
-
- pyvistaqt
|
|
102
|
-
|
|
103
|
-
(Other Python modules mentioned above are only needed to run standalone helper scripts in this repository, not to use the installed package.)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|