setupEM 0.3.14__tar.gz → 0.3.16__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 (25) hide show
  1. {setupem-0.3.14/src/setupEM.egg-info → setupem-0.3.16}/PKG-INFO +4 -4
  2. {setupem-0.3.14 → setupem-0.3.16}/README.md +1 -1
  3. {setupem-0.3.14 → setupem-0.3.16}/README_pypi.md +2 -2
  4. {setupem-0.3.14 → setupem-0.3.16}/pyproject.toml +1 -1
  5. {setupem-0.3.14 → setupem-0.3.16}/src/setupEM/__init__.py +1 -1
  6. setupem-0.3.16/src/setupEM/palace_results.py +199 -0
  7. {setupem-0.3.14 → setupem-0.3.16}/src/setupEM/setupEM.py +97 -24
  8. {setupem-0.3.14 → setupem-0.3.16}/src/setupEM/setup_common.py +28 -4
  9. {setupem-0.3.14 → setupem-0.3.16/src/setupEM.egg-info}/PKG-INFO +4 -4
  10. {setupem-0.3.14 → setupem-0.3.16}/src/setupEM.egg-info/SOURCES.txt +1 -0
  11. {setupem-0.3.14 → setupem-0.3.16}/src/setupEM.egg-info/requires.txt +1 -1
  12. {setupem-0.3.14 → setupem-0.3.16}/LICENSE +0 -0
  13. {setupem-0.3.14 → setupem-0.3.16}/setup.cfg +0 -0
  14. {setupem-0.3.14 → setupem-0.3.16}/src/setupEM/__main__.py +0 -0
  15. {setupem-0.3.14 → setupem-0.3.16}/src/setupEM/data/SG13G2_100um.xml +0 -0
  16. {setupem-0.3.14 → setupem-0.3.16}/src/setupEM/data/SG13G2_200um.xml +0 -0
  17. {setupem-0.3.14 → setupem-0.3.16}/src/setupEM/data/SG13G2_200um_with_parameters.xml +0 -0
  18. {setupem-0.3.14 → setupem-0.3.16}/src/setupEM/data/SG13G2_nosub.xml +0 -0
  19. {setupem-0.3.14 → setupem-0.3.16}/src/setupEM/momentum_import.py +0 -0
  20. {setupem-0.3.14 → setupem-0.3.16}/src/setupEM/setupThermal.py +0 -0
  21. {setupem-0.3.14 → setupem-0.3.16}/src/setupEM/stackupEditor.py +0 -0
  22. {setupem-0.3.14 → setupem-0.3.16}/src/setupEM/stackup_writer.py +0 -0
  23. {setupem-0.3.14 → setupem-0.3.16}/src/setupEM.egg-info/dependency_links.txt +0 -0
  24. {setupem-0.3.14 → setupem-0.3.16}/src/setupEM.egg-info/entry_points.txt +0 -0
  25. {setupem-0.3.14 → setupem-0.3.16}/src/setupEM.egg-info/top_level.txt +0 -0
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: setupEM
3
- Version: 0.3.14
3
+ Version: 0.3.16
4
4
  Summary: Python tool for configuration of gds2palace workflow with GUI.
5
5
  Author-email: Volker Muehlhaus <volker@muehlhaus.com>
6
6
  License-Expression: GPL-3.0-or-later
7
7
  Requires-Python: >=3.9
8
8
  Description-Content-Type: text/markdown
9
9
  License-File: LICENSE
10
- Requires-Dist: gds2palace>=0.3.6
10
+ Requires-Dist: gds2palace>=0.4.0
11
11
  Requires-Dist: PySide6
12
12
  Requires-Dist: scipy
13
13
  Requires-Dist: requests
@@ -126,7 +126,7 @@ On this tab, you configure input files:
126
126
 
127
127
  The fields for GDSII and XML file support drag & drop or you can use the Browse... buttons.
128
128
 
129
- Some pre-processing of the layout is also defined here: You can specify a distance (in micron) which is used for **via array merging**, to speed up simulation by replacing many individual vias with one large via box. If your layout includes **polygons with holes**, you need to set the "Preprocess GDSII file" checkbox, otherwise you will get error messages during meshing.
129
+ Some pre-processing of the layout is also defined here: You can specify a distance (in micron) which is used for **via array merging**, to speed up simulation by replacing many individual vias with one large via box.
130
130
 
131
131
  <img src="https://raw.githubusercontent.com/VolkerMuehlhaus/setupEM/main/doc/png/inputfiles1.png" alt="input files" width="700">
132
132
 
@@ -272,7 +272,7 @@ This project is licensed under the GNU General Public License v3.0 or later (GPL
272
272
 
273
273
  **Note:** the `setupEM` PyPI package itself requires:
274
274
 
275
- - gds2palace>=0.3.6
275
+ - gds2palace>=0.4.0
276
276
  - PySide6
277
277
  - scipy
278
278
  - requests
@@ -107,7 +107,7 @@ On this tab, you configure input files:
107
107
 
108
108
  The fields for GDSII and XML file support drag & drop or you can use the Browse... buttons.
109
109
 
110
- Some pre-processing of the layout is also defined here: You can specify a distance (in micron) which is used for **via array merging**, to speed up simulation by replacing many individual vias with one large via box. If your layout includes **polygons with holes**, you need to set the "Preprocess GDSII file" checkbox, otherwise you will get error messages during meshing.
110
+ Some pre-processing of the layout is also defined here: You can specify a distance (in micron) which is used for **via array merging**, to speed up simulation by replacing many individual vias with one large via box.
111
111
 
112
112
  <img src="./doc/png/inputfiles1.png" alt="input files" width="700">
113
113
 
@@ -107,7 +107,7 @@ On this tab, you configure input files:
107
107
 
108
108
  The fields for GDSII and XML file support drag & drop or you can use the Browse... buttons.
109
109
 
110
- Some pre-processing of the layout is also defined here: You can specify a distance (in micron) which is used for **via array merging**, to speed up simulation by replacing many individual vias with one large via box. If your layout includes **polygons with holes**, you need to set the "Preprocess GDSII file" checkbox, otherwise you will get error messages during meshing.
110
+ Some pre-processing of the layout is also defined here: You can specify a distance (in micron) which is used for **via array merging**, to speed up simulation by replacing many individual vias with one large via box.
111
111
 
112
112
  <img src="https://raw.githubusercontent.com/VolkerMuehlhaus/setupEM/main/doc/png/inputfiles1.png" alt="input files" width="700">
113
113
 
@@ -253,7 +253,7 @@ This project is licensed under the GNU General Public License v3.0 or later (GPL
253
253
 
254
254
  **Note:** the `setupEM` PyPI package itself requires:
255
255
 
256
- - gds2palace>=0.3.6
256
+ - gds2palace>=0.4.0
257
257
  - PySide6
258
258
  - scipy
259
259
  - requests
@@ -12,7 +12,7 @@ authors = [
12
12
  { name = "Volker Muehlhaus", email = "volker@muehlhaus.com" }
13
13
  ]
14
14
  dependencies = [
15
- "gds2palace>=0.3.6",
15
+ "gds2palace>=0.4.0",
16
16
  "PySide6",
17
17
  "scipy",
18
18
  "requests",
@@ -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.3.14"
27
+ __version__ = "0.3.16"
@@ -0,0 +1,199 @@
1
+ ########################################################################
2
+ #
3
+ # Copyright 2025 Volker Muehlhaus and IHP PDK Authors
4
+ #
5
+ # Licensed under the GNU General Public License, Version 3.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # https://www.gnu.org/licenses/gpl-3.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+ ########################################################################
18
+
19
+ """Parse AWS Palace solver output (palace.json / error-indicators.csv) into a
20
+ human-readable summary for the setupEM "Create Model" log. No Qt dependency,
21
+ so this can be exercised standalone against real Palace output directories.
22
+ """
23
+
24
+ import os
25
+ import json
26
+ import csv
27
+ import re
28
+
29
+ _ITERATION_RE = re.compile(r'^iteration(\d+)$')
30
+
31
+
32
+ def _find_output_dir(run_path, model_basename):
33
+ # config.json's Problem.Output is a path relative to config.json's own directory
34
+ config_path = os.path.join(run_path, 'config.json')
35
+ output_rel = None
36
+ if os.path.isfile(config_path):
37
+ try:
38
+ with open(config_path, 'r', encoding='utf-8') as f:
39
+ config = json.load(f)
40
+ output_rel = config.get('Problem', {}).get('Output')
41
+ except (OSError, json.JSONDecodeError):
42
+ output_rel = None
43
+ if not output_rel:
44
+ output_rel = 'output/' + model_basename
45
+ return os.path.normpath(os.path.join(run_path, output_rel))
46
+
47
+
48
+ def _read_palace_json(dir_path):
49
+ path = os.path.join(dir_path, 'palace.json')
50
+ if not os.path.isfile(path):
51
+ return None
52
+ try:
53
+ with open(path, 'r', encoding='utf-8') as f:
54
+ data = json.load(f)
55
+ except (OSError, json.JSONDecodeError):
56
+ return None
57
+ return {
58
+ 'duration_s': data.get('ElapsedTime', {}).get('Durations', {}).get('Total'),
59
+ 'peak_ram_mb': data.get('PeakMemoryMegabytes', {}).get('Total'),
60
+ 'dof': data.get('Problem', {}).get('DegreesOfFreedom'),
61
+ 'mesh_elements': data.get('Problem', {}).get('MeshElements'),
62
+ }
63
+
64
+
65
+ def _read_error_indicators(dir_path):
66
+ path = os.path.join(dir_path, 'error-indicators.csv')
67
+ if not os.path.isfile(path):
68
+ return None
69
+ try:
70
+ with open(path, 'r', encoding='utf-8', newline='') as f:
71
+ rows = [row for row in csv.reader(f, skipinitialspace=True) if row]
72
+ except OSError:
73
+ return None
74
+ if len(rows) < 2:
75
+ return None
76
+ headers = [h.strip() for h in rows[0]]
77
+ values = [v.strip() for v in rows[1]]
78
+ fields = dict(zip(headers, values))
79
+ try:
80
+ return {
81
+ 'norm': float(fields['Norm']),
82
+ 'minimum': float(fields['Minimum']),
83
+ 'maximum': float(fields['Maximum']),
84
+ 'mean': float(fields['Mean']),
85
+ }
86
+ except (KeyError, ValueError):
87
+ return None
88
+
89
+
90
+ def _list_iteration_dirs(output_dir):
91
+ if not os.path.isdir(output_dir):
92
+ return []
93
+ found = []
94
+ for name in os.listdir(output_dir):
95
+ match = _ITERATION_RE.match(name)
96
+ full_path = os.path.join(output_dir, name)
97
+ if match and os.path.isdir(full_path):
98
+ found.append((int(match.group(1)), full_path))
99
+ found.sort(key=lambda item: item[0])
100
+ return [full_path for _, full_path in found]
101
+
102
+
103
+ def _format_duration(seconds):
104
+ if seconds is None:
105
+ return 'n/a'
106
+ if seconds < 60:
107
+ return f"{seconds:.1f} s"
108
+ minutes, secs = divmod(int(round(seconds)), 60)
109
+ hours, minutes = divmod(minutes, 60)
110
+ if hours:
111
+ return f"{hours}h {minutes}m {secs}s"
112
+ return f"{minutes}m {secs}s"
113
+
114
+
115
+ def _format_ram(mb):
116
+ if mb is None:
117
+ return 'n/a'
118
+ if mb >= 1024:
119
+ return f"{mb / 1024:.2f} GB"
120
+ return f"{mb:.1f} MB"
121
+
122
+
123
+ def _format_int(n):
124
+ return 'n/a' if n is None else f"{n:,}"
125
+
126
+
127
+ def _format_sci(x):
128
+ return 'n/a' if x is None else f"{x:.3e}"
129
+
130
+
131
+ def build_results_summary(run_path, model_basename):
132
+ """Return a formatted multi-line summary of Palace results found under run_path,
133
+ or an explanatory message if nothing is there yet.
134
+ """
135
+ output_dir = _find_output_dir(run_path, model_basename)
136
+ if not os.path.isdir(output_dir):
137
+ return (f"No Palace results found yet (expected output directory: {output_dir}) "
138
+ "-- has the simulation finished?")
139
+
140
+ iteration_dirs = _list_iteration_dirs(output_dir)
141
+
142
+ if not iteration_dirs:
143
+ summary = _read_palace_json(output_dir)
144
+ errors = _read_error_indicators(output_dir)
145
+ if summary is None:
146
+ return (f"No palace.json found yet in {output_dir} "
147
+ "-- has the simulation finished?")
148
+ lines = [
149
+ f"=== Simulation results: {model_basename} ===",
150
+ f"Degrees of freedom : {_format_int(summary['dof'])}",
151
+ f"Mesh elements : {_format_int(summary['mesh_elements'])}",
152
+ f"Simulation time : {_format_duration(summary['duration_s'])}",
153
+ f"Peak RAM : {_format_ram(summary['peak_ram_mb'])}",
154
+ ]
155
+ if errors:
156
+ lines.append(
157
+ f"Error indicator : Norm={_format_sci(errors['norm'])} "
158
+ f"Max={_format_sci(errors['maximum'])} Mean={_format_sci(errors['mean'])}"
159
+ )
160
+ lines.append("=" * 40)
161
+ return "\n".join(lines)
162
+
163
+ # Adaptive mesh refinement: one row per iteration subfolder, plus the root as "Final"
164
+ rows = [(os.path.basename(it_dir), _read_palace_json(it_dir), _read_error_indicators(it_dir))
165
+ for it_dir in iteration_dirs]
166
+ rows.append(("Final", _read_palace_json(output_dir), _read_error_indicators(output_dir)))
167
+
168
+ headers = ["Iteration", "DOF", "Mesh elems", "Error Norm", "Error Max", "Error Mean", "Time", "Peak RAM"]
169
+ table_rows = []
170
+ for label, summary, errors in rows:
171
+ summary = summary or {}
172
+ errors = errors or {}
173
+ table_rows.append([
174
+ label,
175
+ _format_int(summary.get('dof')),
176
+ _format_int(summary.get('mesh_elements')),
177
+ _format_sci(errors.get('norm')),
178
+ _format_sci(errors.get('maximum')),
179
+ _format_sci(errors.get('mean')),
180
+ _format_duration(summary.get('duration_s')),
181
+ _format_ram(summary.get('peak_ram_mb')),
182
+ ])
183
+
184
+ widths = [max(len(headers[i]), *(len(r[i]) for r in table_rows)) for i in range(len(headers))]
185
+
186
+ def fmt_row(cells):
187
+ return " | ".join(
188
+ cell.ljust(widths[i]) if i == 0 else cell.rjust(widths[i])
189
+ for i, cell in enumerate(cells)
190
+ )
191
+
192
+ lines = [f"=== Simulation results: {model_basename} (adaptive mesh refinement) ==="]
193
+ header_line = fmt_row(headers)
194
+ lines.append(header_line)
195
+ lines.append("-+-".join("-" * w for w in widths))
196
+ for row in table_rows:
197
+ lines.append(fmt_row(row))
198
+ lines.append("=" * len(header_line))
199
+ return "\n".join(lines)
@@ -54,6 +54,7 @@ if __package__ in (None, ""):
54
54
  VectorWidget, PopUpWindow, CreateModelTabBase, MainWindowBase,
55
55
  epsilon_to_color, default_stackup_dielectric_label, default_stackup_metal_label,
56
56
  )
57
+ from palace_results import build_results_summary
57
58
  else:
58
59
  from .setup_common import (
59
60
  EDIT_STYLE_OPTIONAL, EDIT_STYLE_REQUIRED, COMBO_STYLE_REQUIRED, COMBO_STYLE_OPTIONAL,
@@ -61,6 +62,7 @@ else:
61
62
  VectorWidget, PopUpWindow, CreateModelTabBase, MainWindowBase,
62
63
  epsilon_to_color, default_stackup_dielectric_label, default_stackup_metal_label,
63
64
  )
65
+ from .palace_results import build_results_summary
64
66
 
65
67
 
66
68
  '''
@@ -179,29 +181,43 @@ class FrequenciesTab(QWidget):
179
181
 
180
182
 
181
183
  def save_values(self):
182
- try:
183
- fstart = float(self.start_edit.text())
184
- except Exception:
185
- # the only case when this field can be empty is when fpoint or fdump are defined
186
- if self.start_edit.text()=="" and (self.fpoint_edit.text() != "" or self.fdump_edit.text() != ""):
187
- return True
184
+ # fstart/fstop may be left empty only if fpoint or fdump supplies at least
185
+ # one frequency instead (gds2palace treats fstart/fstop as fully optional)
186
+ discrete_freqs_given = (self.fpoint_edit.text() != "" or self.fdump_edit.text() != "")
187
+
188
+ fstart = None
189
+ if self.start_edit.text() == "":
190
+ if discrete_freqs_given:
191
+ saved_values.pop("fstart", None) # don't leave stale sweep data behind
188
192
  else:
189
193
  QMessageBox.warning(self, "Error", "Not a valid value for fstart")
190
194
  self.start_edit.setText("0")
191
195
  return False
192
- saved_values ["fstart"] = float(fstart)
196
+ else:
197
+ try:
198
+ fstart = float(self.start_edit.text())
199
+ except Exception:
200
+ QMessageBox.warning(self, "Error", "Not a valid value for fstart")
201
+ self.start_edit.setText("0")
202
+ return False
203
+ saved_values ["fstart"] = fstart
193
204
 
194
- try:
195
- fstop = float(self.stop_edit.text())
196
- except Exception:
197
- # the only case when this field can be empty is when fpoint or fdump are defined
198
- if self.stop_edit.text()=="" and (self.fpoint_edit.text() != "" or self.fdump_edit.text() != ""):
199
- return True
205
+ fstop = None
206
+ if self.stop_edit.text() == "":
207
+ if discrete_freqs_given:
208
+ saved_values.pop("fstop", None) # don't leave stale sweep data behind
200
209
  else:
201
210
  QMessageBox.warning(self, "Error", "Not a valid value for fstop")
202
211
  self.stop_edit.setText("50")
203
212
  return False
204
- saved_values ["fstop"] = float(fstop)
213
+ else:
214
+ try:
215
+ fstop = float(self.stop_edit.text())
216
+ except Exception:
217
+ QMessageBox.warning(self, "Error", "Not a valid value for fstop")
218
+ self.stop_edit.setText("50")
219
+ return False
220
+ saved_values ["fstop"] = fstop
205
221
 
206
222
  if self.step_edit.text() != "":
207
223
  try:
@@ -229,10 +245,13 @@ class FrequenciesTab(QWidget):
229
245
  saved_values.pop("fdump",None)
230
246
 
231
247
  # if fstart == fstop == fdump or fstart == fstop == fstep, then remove fstart, fstop
232
- if saved_values ["fstart"] == saved_values ["fstop"]:
248
+ if fstart is not None and fstop is not None and fstart == fstop:
233
249
  discrete_list1 = saved_values.get("fpoint", [])
234
250
  discrete_list2 = saved_values.get("fdump", [])
235
- if saved_values ["fstart"] in discrete_list1 or saved_values ["fstart"] in discrete_list2:
251
+ if fstart in discrete_list1 or fstart in discrete_list2:
252
+ saved_values.pop("fstart", None)
253
+ saved_values.pop("fstop", None)
254
+ saved_values.pop("fstep", None)
236
255
  self.start_edit.setText("")
237
256
  self.stop_edit.setText("")
238
257
  self.step_edit.setText("")
@@ -241,8 +260,13 @@ class FrequenciesTab(QWidget):
241
260
  return True # Tab change only possible when returning True
242
261
 
243
262
  def load_values(self):
244
- self.start_edit.setText(str(saved_values.get("fstart","0")))
245
- self.stop_edit.setText(str(saved_values.get("fstop","50")))
263
+ # if fstart/fstop were intentionally omitted in favor of fpoint/fdump, keep the
264
+ # fields blank on reload instead of repopulating the "0"/"50" sweep defaults
265
+ discrete_freqs_given = ("fpoint" in saved_values) or ("fdump" in saved_values)
266
+ fstart_default = "" if discrete_freqs_given else "0"
267
+ fstop_default = "" if discrete_freqs_given else "50"
268
+ self.start_edit.setText(str(saved_values.get("fstart",fstart_default)))
269
+ self.stop_edit.setText(str(saved_values.get("fstop",fstop_default)))
246
270
  self.step_edit.setText(str(saved_values.get("fstep","")))
247
271
 
248
272
  float_list = saved_values.get("fpoint","")
@@ -629,7 +653,7 @@ class MeshTab(QWidget):
629
653
  self.main_layout.setAlignment(Qt.AlignTop)
630
654
 
631
655
  label_width = 250
632
- edit_width = 150
656
+ edit_width = 170
633
657
 
634
658
 
635
659
  # ---------- MESH GROUP ----------
@@ -685,7 +709,7 @@ class MeshTab(QWidget):
685
709
  self.mesh_order_box = QComboBox()
686
710
  self.mesh_order_box.setFixedWidth(edit_width)
687
711
  self.mesh_order_box.setStyleSheet(COMBO_STYLE_OPTIONAL)
688
- self.mesh_order_box.addItems(["faster, less accurate","most accurate"])
712
+ self.mesh_order_box.addItems(["faster, less accurate (N=1)","recommended (N=2)", "slower, most accurate (N=3)"])
689
713
  self.meshorder_layout.addWidget(self.mesh_order_box)
690
714
  self.mesh_order_box.setCurrentIndex(0)
691
715
  self.meshorder_layout.addStretch()
@@ -902,6 +926,22 @@ class MeshTab(QWidget):
902
926
  except:
903
927
  pass
904
928
 
929
+ # order=3 (ultra accurate) is Palace-only: Elmer has no cubic-order solver
930
+ # templates in util_elmer.write_case_and_solver_files(), so it would silently
931
+ # fall back to first-order there. Disable that option under Elmer mode, and
932
+ # fall back to the default order if it was selected when switching into Elmer.
933
+ try:
934
+ ultra_accurate_index = 2
935
+ item = self.mesh_order_box.model().item(ultra_accurate_index)
936
+ if self.MainWindow.ElmerMode:
937
+ item.setEnabled(False)
938
+ if self.mesh_order_box.currentIndex() == ultra_accurate_index:
939
+ self.mesh_order_box.setCurrentIndex(1)
940
+ else:
941
+ item.setEnabled(True)
942
+ except:
943
+ pass
944
+
905
945
 
906
946
  def save_values(self):
907
947
  try:
@@ -1079,6 +1119,26 @@ class CreateModelTab(CreateModelTabBase):
1079
1119
  are specific to this app.
1080
1120
  """
1081
1121
 
1122
+ def __init__(self, MainWindow):
1123
+ super().__init__(MainWindow)
1124
+
1125
+ # Tracks which action self.process is currently running, so on_finished() can tell a
1126
+ # simulation run apart from a mesh-creation run (self.process is reused for both).
1127
+ self._process_purpose = None
1128
+
1129
+ def _append_results_summary(self):
1130
+ # Palace-only: parse palace.json / error-indicators.csv and append a results summary
1131
+ # to the log. Called from on_finished() after a real simulation run.
1132
+ run_path = saved_values['sim_path'] + "/palace_model/" + saved_values['model_basename'] + "_data"
1133
+ summary = build_results_summary(run_path, saved_values['model_basename'])
1134
+ self.log_area.appendPlainText("\n" + summary + "\n")
1135
+
1136
+ def on_finished(self, exit_code, exit_status):
1137
+ super().on_finished(exit_code, exit_status)
1138
+ # Auto-append the results summary after a real simulation run (not after mesh creation)
1139
+ if self._process_purpose == "run_simulation" and self.MainWindow.PalaceMode:
1140
+ self._append_results_summary()
1141
+
1082
1142
  def create_model(self):
1083
1143
  # Request all tabs to save values again,
1084
1144
  # which can do some update to saved_values
@@ -1105,6 +1165,7 @@ class CreateModelTab(CreateModelTabBase):
1105
1165
 
1106
1166
  # Run Python interpreter on that file
1107
1167
  python_exe = sys.executable # Use the same Python interpreter
1168
+ self._process_purpose = "create_mesh"
1108
1169
  self.process.start(python_exe, [pymodel_filename])
1109
1170
 
1110
1171
 
@@ -1118,6 +1179,7 @@ class CreateModelTab(CreateModelTabBase):
1118
1179
 
1119
1180
  # clear log
1120
1181
  self.log_area.clear()
1182
+ self._process_purpose = "run_simulation"
1121
1183
 
1122
1184
  if self.MainWindow.PalaceMode:
1123
1185
  self.log_area.appendPlainText("Trying to start Palace using script ./run_sim now")
@@ -1143,10 +1205,21 @@ class CreateModelTab(CreateModelTabBase):
1143
1205
 
1144
1206
 
1145
1207
  wsl_run_path = windows_to_wsl_path(run_path)
1146
- # tell user what to do, we can open WSL in the right place but not start simulation
1147
- self.log_area.appendPlainText("Running on Windows with WSL, you need to type ./run_sim in the terminal yourself!")
1148
- self.log_area.appendPlainText("Note that this works for LOCAL drives only, we can't open WSL on network drive.")
1149
- self.process.start("cmd.exe", ["/c", "start", "wt.exe", "wsl", "--cd", wsl_run_path])
1208
+ self.log_area.appendPlainText("Running on Windows with WSL: starting ./run_sim ...")
1209
+ self.log_area.appendPlainText("Note that this works for LOCAL drives only, we can't open WSL on network drive.\n")
1210
+ # Run wsl.exe directly as self.process (no terminal emulator in between), so Palace's
1211
+ # stdout/stderr stream into this log via the existing on_stdout/on_stderr handlers, and
1212
+ # on_finished fires with the real exit code when ./run_sim actually completes -- instead
1213
+ # of opening a detached terminal window, whose own quoting/tokenization rules (cmd's
1214
+ # "start", and wt.exe's own use of ";" as a pane/command separator) make embedding a
1215
+ # multi-step shell command fragile, and whose completion can't be tracked anyway.
1216
+ # bash -lc: login shell so ~/.profile (where PATH additions for run_palace/combine_snp
1217
+ # usually live, per gds2palace's scripts/README.md) gets sourced, same as a manually
1218
+ # typed ./run_sim in a fresh WSL login shell would.
1219
+ self.process.start("wsl.exe", [
1220
+ "--cd", wsl_run_path,
1221
+ "--", "bash", "-lc", "./run_sim"
1222
+ ])
1150
1223
  else:
1151
1224
  # Linux
1152
1225
  self.log_area.appendPlainText('Setting work directory ' + run_path)
@@ -32,7 +32,7 @@ mesh fields, the Python model code generator bodies) is intentionally left
32
32
  in setupEM.py / setupThermal.py, not here.
33
33
  """
34
34
 
35
- import sys, os, json, pathlib, ast, webbrowser
35
+ import sys, os, json, pathlib, ast, webbrowser, io, contextlib
36
36
  import xml.etree.ElementTree as ET
37
37
  import numpy as np
38
38
  import requests
@@ -497,7 +497,7 @@ class FileInputTab(QWidget):
497
497
  self.MainWindow.saved_values["SubstrateFile"] = filename.replace('\\', '/')
498
498
  self.update_XML_description(filename)
499
499
  self.update_variable_overrides_grid(filename)
500
- self.MainWindow.read_XML() # safe if invalid filename
500
+ self.MainWindow.read_XML() # shows an error dialog and keeps prior data on invalid/unparseable XML
501
501
  # file is read when leaving the files tab
502
502
 
503
503
  def update_XML_description(self, filename):
@@ -1246,6 +1246,16 @@ class CreateModelTabBase(QWidget):
1246
1246
  self.log_group.setLayout(self.log_layout)
1247
1247
  self.log_area = QPlainTextEdit()
1248
1248
  self.log_area.setReadOnly(True)
1249
+ log_font = QFont()
1250
+ # Consolas/Cascadia Mono: Windows. Ubuntu Mono: default on Ubuntu (this app's primary
1251
+ # Linux target, see the Ubuntu 24.04 notice below) and narrower than DejaVu Sans Mono.
1252
+ # Liberation Mono/DejaVu Sans Mono: broader Linux fallbacks. "monospace": generic
1253
+ # fontconfig alias, guaranteed to resolve to an installed monospace font on Linux.
1254
+ log_font.setFamilies(["Consolas", "Cascadia Mono", "Ubuntu Mono", "Liberation Mono", "DejaVu Sans Mono", "monospace"])
1255
+ log_font.setStyleHint(QFont.Monospace)
1256
+ log_font.setFixedPitch(True)
1257
+ log_font.setPointSize(9)
1258
+ self.log_area.setFont(log_font)
1249
1259
  self.log_layout.addWidget(self.log_area)
1250
1260
 
1251
1261
  self.main_layout.addWidget(self.file_group)
@@ -1824,8 +1834,22 @@ class MainWindowBase(QMainWindow):
1824
1834
  def read_XML(self):
1825
1835
  filename = self.saved_values["SubstrateFile"]
1826
1836
  if pathlib.Path(filename).exists():
1827
- self.materials_list, self.dielectrics_list, self.metals_list = stackup_reader.read_substrate(
1828
- filename, variable_overrides=self.saved_values.get("variable_overrides"))
1837
+ captured_stdout = io.StringIO()
1838
+ try:
1839
+ with contextlib.redirect_stdout(captured_stdout):
1840
+ materials_list, dielectrics_list, metals_list = stackup_reader.read_substrate(
1841
+ filename, variable_overrides=self.saved_values.get("variable_overrides"))
1842
+ except (Exception, SystemExit) as e:
1843
+ # SystemExit is caught deliberately (not just Exception): the reader
1844
+ # reports hard validation failures (circular/ambiguous Reference,
1845
+ # Offset+Reference conflict, ...) via print(...); exit(1) instead of
1846
+ # raising - see the same pattern in stackupEditor.py's _refresh_preview().
1847
+ # Capture stdout so that printed ERROR text (otherwise invisible in a
1848
+ # GUI with no attached console) can be shown to the user.
1849
+ details = captured_stdout.getvalue().strip() or str(e)
1850
+ QMessageBox.critical(self, "Error", f"Could not load stackup {filename}:\n\n{details}")
1851
+ return # keep last-known-good materials_list/dielectrics_list/metals_list
1852
+ self.materials_list, self.dielectrics_list, self.metals_list = materials_list, dielectrics_list, metals_list
1829
1853
  self.update_target_layer_choices(self.metals_list)
1830
1854
  self.file_tab.update_XML_description(filename)
1831
1855
  self.file_tab.update_variable_overrides_grid(filename)
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: setupEM
3
- Version: 0.3.14
3
+ Version: 0.3.16
4
4
  Summary: Python tool for configuration of gds2palace workflow with GUI.
5
5
  Author-email: Volker Muehlhaus <volker@muehlhaus.com>
6
6
  License-Expression: GPL-3.0-or-later
7
7
  Requires-Python: >=3.9
8
8
  Description-Content-Type: text/markdown
9
9
  License-File: LICENSE
10
- Requires-Dist: gds2palace>=0.3.6
10
+ Requires-Dist: gds2palace>=0.4.0
11
11
  Requires-Dist: PySide6
12
12
  Requires-Dist: scipy
13
13
  Requires-Dist: requests
@@ -126,7 +126,7 @@ On this tab, you configure input files:
126
126
 
127
127
  The fields for GDSII and XML file support drag & drop or you can use the Browse... buttons.
128
128
 
129
- Some pre-processing of the layout is also defined here: You can specify a distance (in micron) which is used for **via array merging**, to speed up simulation by replacing many individual vias with one large via box. If your layout includes **polygons with holes**, you need to set the "Preprocess GDSII file" checkbox, otherwise you will get error messages during meshing.
129
+ Some pre-processing of the layout is also defined here: You can specify a distance (in micron) which is used for **via array merging**, to speed up simulation by replacing many individual vias with one large via box.
130
130
 
131
131
  <img src="https://raw.githubusercontent.com/VolkerMuehlhaus/setupEM/main/doc/png/inputfiles1.png" alt="input files" width="700">
132
132
 
@@ -272,7 +272,7 @@ This project is licensed under the GNU General Public License v3.0 or later (GPL
272
272
 
273
273
  **Note:** the `setupEM` PyPI package itself requires:
274
274
 
275
- - gds2palace>=0.3.6
275
+ - gds2palace>=0.4.0
276
276
  - PySide6
277
277
  - scipy
278
278
  - requests
@@ -5,6 +5,7 @@ pyproject.toml
5
5
  src/setupEM/__init__.py
6
6
  src/setupEM/__main__.py
7
7
  src/setupEM/momentum_import.py
8
+ src/setupEM/palace_results.py
8
9
  src/setupEM/setupEM.py
9
10
  src/setupEM/setupThermal.py
10
11
  src/setupEM/setup_common.py
@@ -1,4 +1,4 @@
1
- gds2palace>=0.3.6
1
+ gds2palace>=0.4.0
2
2
  PySide6
3
3
  scipy
4
4
  requests
File without changes
File without changes