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.
- {setupem-0.3.14/src/setupEM.egg-info → setupem-0.3.16}/PKG-INFO +4 -4
- {setupem-0.3.14 → setupem-0.3.16}/README.md +1 -1
- {setupem-0.3.14 → setupem-0.3.16}/README_pypi.md +2 -2
- {setupem-0.3.14 → setupem-0.3.16}/pyproject.toml +1 -1
- {setupem-0.3.14 → setupem-0.3.16}/src/setupEM/__init__.py +1 -1
- setupem-0.3.16/src/setupEM/palace_results.py +199 -0
- {setupem-0.3.14 → setupem-0.3.16}/src/setupEM/setupEM.py +97 -24
- {setupem-0.3.14 → setupem-0.3.16}/src/setupEM/setup_common.py +28 -4
- {setupem-0.3.14 → setupem-0.3.16/src/setupEM.egg-info}/PKG-INFO +4 -4
- {setupem-0.3.14 → setupem-0.3.16}/src/setupEM.egg-info/SOURCES.txt +1 -0
- {setupem-0.3.14 → setupem-0.3.16}/src/setupEM.egg-info/requires.txt +1 -1
- {setupem-0.3.14 → setupem-0.3.16}/LICENSE +0 -0
- {setupem-0.3.14 → setupem-0.3.16}/setup.cfg +0 -0
- {setupem-0.3.14 → setupem-0.3.16}/src/setupEM/__main__.py +0 -0
- {setupem-0.3.14 → setupem-0.3.16}/src/setupEM/data/SG13G2_100um.xml +0 -0
- {setupem-0.3.14 → setupem-0.3.16}/src/setupEM/data/SG13G2_200um.xml +0 -0
- {setupem-0.3.14 → setupem-0.3.16}/src/setupEM/data/SG13G2_200um_with_parameters.xml +0 -0
- {setupem-0.3.14 → setupem-0.3.16}/src/setupEM/data/SG13G2_nosub.xml +0 -0
- {setupem-0.3.14 → setupem-0.3.16}/src/setupEM/momentum_import.py +0 -0
- {setupem-0.3.14 → setupem-0.3.16}/src/setupEM/setupThermal.py +0 -0
- {setupem-0.3.14 → setupem-0.3.16}/src/setupEM/stackupEditor.py +0 -0
- {setupem-0.3.14 → setupem-0.3.16}/src/setupEM/stackup_writer.py +0 -0
- {setupem-0.3.14 → setupem-0.3.16}/src/setupEM.egg-info/dependency_links.txt +0 -0
- {setupem-0.3.14 → setupem-0.3.16}/src/setupEM.egg-info/entry_points.txt +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
256
|
+
- gds2palace>=0.4.0
|
|
257
257
|
- PySide6
|
|
258
258
|
- scipy
|
|
259
259
|
- requests
|
|
@@ -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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
245
|
-
|
|
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 =
|
|
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
|
-
|
|
1147
|
-
self.log_area.appendPlainText("
|
|
1148
|
-
|
|
1149
|
-
|
|
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() #
|
|
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
|
-
|
|
1828
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
275
|
+
- gds2palace>=0.4.0
|
|
276
276
|
- PySide6
|
|
277
277
|
- scipy
|
|
278
278
|
- requests
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|