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.
Files changed (35) hide show
  1. setupem-0.11.1/PKG-INFO +26 -0
  2. {setupem-0.10.2 → setupem-0.11.1}/README.md +1 -1
  3. {setupem-0.10.2 → setupem-0.11.1}/pyproject.toml +1 -1
  4. {setupem-0.10.2 → setupem-0.11.1}/src/setupEM/__init__.py +1 -1
  5. {setupem-0.10.2 → setupem-0.11.1}/src/setupEM/field_viewer.py +162 -35
  6. {setupem-0.10.2 → setupem-0.11.1}/src/setupEM/setupEM.py +1 -1
  7. {setupem-0.10.2 → setupem-0.11.1}/src/setupEM/setupThermal.py +1 -1
  8. {setupem-0.10.2 → setupem-0.11.1}/src/setupEM/setup_common.py +52 -17
  9. {setupem-0.10.2 → setupem-0.11.1}/src/setupEM/simplify_gds.py +96 -15
  10. setupem-0.11.1/src/setupEM.egg-info/PKG-INFO +26 -0
  11. {setupem-0.10.2 → setupem-0.11.1}/src/setupEM.egg-info/SOURCES.txt +0 -1
  12. {setupem-0.10.2 → setupem-0.11.1}/src/setupEM.egg-info/requires.txt +1 -1
  13. setupem-0.10.2/PKG-INFO +0 -103
  14. setupem-0.10.2/README_pypi.md +0 -76
  15. setupem-0.10.2/src/setupEM.egg-info/PKG-INFO +0 -103
  16. {setupem-0.10.2 → setupem-0.11.1}/LICENSE +0 -0
  17. {setupem-0.10.2 → setupem-0.11.1}/setup.cfg +0 -0
  18. {setupem-0.10.2 → setupem-0.11.1}/src/setupEM/__main__.py +0 -0
  19. {setupem-0.10.2 → setupem-0.11.1}/src/setupEM/data/SG13CMOS5L_200um.xml +0 -0
  20. {setupem-0.10.2 → setupem-0.11.1}/src/setupEM/data/SG13G2_100um.xml +0 -0
  21. {setupem-0.10.2 → setupem-0.11.1}/src/setupEM/data/SG13G2_200um.xml +0 -0
  22. {setupem-0.10.2 → setupem-0.11.1}/src/setupEM/data/SG13G2_FEM_200um.xml +0 -0
  23. {setupem-0.10.2 → setupem-0.11.1}/src/setupEM/data/SG13G2_FEM_200um_passi3D.xml +0 -0
  24. {setupem-0.10.2 → setupem-0.11.1}/src/setupEM/data/SG13G2_nosub.xml +0 -0
  25. {setupem-0.10.2 → setupem-0.11.1}/src/setupEM/gds_hierarchy_scan.py +0 -0
  26. {setupem-0.10.2 → setupem-0.11.1}/src/setupEM/layout_preview.py +0 -0
  27. {setupem-0.10.2 → setupem-0.11.1}/src/setupEM/momentum_import.py +0 -0
  28. {setupem-0.10.2 → setupem-0.11.1}/src/setupEM/palace_results.py +0 -0
  29. {setupem-0.10.2 → setupem-0.11.1}/src/setupEM/result_viewer.py +0 -0
  30. {setupem-0.10.2 → setupem-0.11.1}/src/setupEM/stackupEditor.py +0 -0
  31. {setupem-0.10.2 → setupem-0.11.1}/src/setupEM/stackup_writer.py +0 -0
  32. {setupem-0.10.2 → setupem-0.11.1}/src/setupEM/thermal_results.py +0 -0
  33. {setupem-0.10.2 → setupem-0.11.1}/src/setupEM.egg-info/dependency_links.txt +0 -0
  34. {setupem-0.10.2 → setupem-0.11.1}/src/setupEM.egg-info/entry_points.txt +0 -0
  35. {setupem-0.10.2 → setupem-0.11.1}/src/setupEM.egg-info/top_level.txt +0 -0
@@ -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
 
@@ -13,7 +13,7 @@ authors = [
13
13
  ]
14
14
  dependencies = [
15
15
  "gds2palace>=0.8.0",
16
- "gds_prepare_for_EM>=1.2.0",
16
+ "gds_prepare_for_EM>=1.3.0",
17
17
  "PySide6",
18
18
  "shiboken6",
19
19
  "scipy",
@@ -24,4 +24,4 @@ A Python tool for EM setup using gds2palace.
24
24
  from .setupEM import main
25
25
 
26
26
  __all__ = ["main"]
27
- __version__ = "0.10.2"
27
+ __version__ = "0.11.1"
@@ -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
- db_per_decade = 20.0 if _is_field_amplitude_array(array_name) else 10.0
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._on_redraw_needed)
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
- min_layout = QHBoxLayout()
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.addLayout(min_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
- self._load_mesh()
1142
- self._on_axis_changed() # resets the clip slider for the new mesh's bounds, redraws
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 (only passed by _on_cycle_changed()) keeps
1176
- the current array/color-by selection instead of recomputing a
1177
- default - array names are the same across cycles of one file, only
1178
- the underlying values/ranges differ per cycle. Every other caller
1179
- (initial load, _switch_to_file()) leaves this False, unchanged from
1180
- before this parameter existed.
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
- if preserve_selection and previous_array in available:
1249
- # Keep the user's current settings across a cycle-only switch -
1250
- # not just which array is selected, but also its Min/Max color
1251
- # range: don't let _on_array_changed's _reset_clim_range() (fired
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(previous_array)
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", help="use a log color scale")
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
- result = _db_range_to_clim(window._full_mesh, array_name, args.log_range_db)
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
- floor, data_max = result
2015
- window.clim_min_edit.setText(f"{floor:.6g}")
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 (final step)")
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 (final step)")
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
- purpose_string = str(int_list).replace('[', '').replace(']', '')
1095
- self.purpose_edit.setText(purpose_string)
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 = self.purpose_edit.text()
1125
- if text != "":
1126
- # save as list of comma separated values
1127
- saved_values["purpose"] = ast.literal_eval('[' + text + ']')
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
- purpose_text = self.file_tab.purpose_edit.text().strip()
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 and/or
23
- fills in small cutouts on the currently loaded GDS file, writing the result
24
- to a new GDS file. Both operations come from gds_prepare_for_EM (a sibling
25
- package, already an editable install in this venv) and are called in-process
26
- - no subprocess/CLI involved.
27
-
28
- No technology-specific layer numbers are hardcoded here: the list of metal
29
- layers to process is derived entirely from the XML stackup already parsed
30
- into MainWindow.metals_list (gds2palace's stackup_reader), same as every
31
- other setupEM feature that needs to know "which GDS layers are metal".
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 last.
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 (final step)")
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
- if not do_floating and not do_cutouts and not do_merge:
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
@@ -1,6 +1,5 @@
1
1
  LICENSE
2
2
  README.md
3
- README_pypi.md
4
3
  pyproject.toml
5
4
  src/setupEM/__init__.py
6
5
  src/setupEM/__main__.py
@@ -1,5 +1,5 @@
1
1
  gds2palace>=0.8.0
2
- gds_prepare_for_EM>=1.2.0
2
+ gds_prepare_for_EM>=1.3.0
3
3
  PySide6
4
4
  shiboken6
5
5
  scipy
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
- ![Intro](https://raw.githubusercontent.com/VolkerMuehlhaus/setupEM/main/doc/png/setupEM_banner.png)
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.)
@@ -1,76 +0,0 @@
1
- # Python GUI for gds2palace
2
-
3
- ![Intro](https://raw.githubusercontent.com/VolkerMuehlhaus/setupEM/main/doc/png/setupEM_banner.png)
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
- ![Intro](https://raw.githubusercontent.com/VolkerMuehlhaus/setupEM/main/doc/png/setupEM_banner.png)
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