setupEM 0.4.0__tar.gz → 0.5.0__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.4.0/src/setupEM.egg-info → setupem-0.5.0}/PKG-INFO +27 -3
- {setupem-0.4.0 → setupem-0.5.0}/README.md +26 -2
- {setupem-0.4.0 → setupem-0.5.0}/README_pypi.md +26 -2
- {setupem-0.4.0 → setupem-0.5.0}/src/setupEM/__init__.py +1 -1
- {setupem-0.4.0 → setupem-0.5.0}/src/setupEM/palace_results.py +199 -199
- {setupem-0.4.0 → setupem-0.5.0}/src/setupEM/setupEM.py +161 -8
- {setupem-0.4.0 → setupem-0.5.0}/src/setupEM/setup_common.py +30 -21
- {setupem-0.4.0 → setupem-0.5.0/src/setupEM.egg-info}/PKG-INFO +27 -3
- {setupem-0.4.0 → setupem-0.5.0}/LICENSE +0 -0
- {setupem-0.4.0 → setupem-0.5.0}/pyproject.toml +0 -0
- {setupem-0.4.0 → setupem-0.5.0}/setup.cfg +0 -0
- {setupem-0.4.0 → setupem-0.5.0}/src/setupEM/__main__.py +0 -0
- {setupem-0.4.0 → setupem-0.5.0}/src/setupEM/data/SG13G2_100um.xml +0 -0
- {setupem-0.4.0 → setupem-0.5.0}/src/setupEM/data/SG13G2_200um.xml +0 -0
- {setupem-0.4.0 → setupem-0.5.0}/src/setupEM/data/SG13G2_200um_with_parameters.xml +0 -0
- {setupem-0.4.0 → setupem-0.5.0}/src/setupEM/data/SG13G2_nosub.xml +0 -0
- {setupem-0.4.0 → setupem-0.5.0}/src/setupEM/momentum_import.py +0 -0
- {setupem-0.4.0 → setupem-0.5.0}/src/setupEM/result_viewer.py +0 -0
- {setupem-0.4.0 → setupem-0.5.0}/src/setupEM/setupThermal.py +0 -0
- {setupem-0.4.0 → setupem-0.5.0}/src/setupEM/stackupEditor.py +0 -0
- {setupem-0.4.0 → setupem-0.5.0}/src/setupEM/stackup_writer.py +0 -0
- {setupem-0.4.0 → setupem-0.5.0}/src/setupEM.egg-info/SOURCES.txt +0 -0
- {setupem-0.4.0 → setupem-0.5.0}/src/setupEM.egg-info/dependency_links.txt +0 -0
- {setupem-0.4.0 → setupem-0.5.0}/src/setupEM.egg-info/entry_points.txt +0 -0
- {setupem-0.4.0 → setupem-0.5.0}/src/setupEM.egg-info/requires.txt +0 -0
- {setupem-0.4.0 → setupem-0.5.0}/src/setupEM.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: setupEM
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
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
|
|
@@ -21,10 +21,14 @@ Dynamic: license-file
|
|
|
21
21
|
|
|
22
22
|
## What's New
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
setupEM includes two built-in tools for working with simulation results directly, without external scripts:
|
|
25
|
+
|
|
26
|
+
- **Result Viewer** (Create Model tab > View Results...) plots Touchstone S-parameter results - dB/phase, Smith chart, zoomed Smith chart - without leaving setupEM. See section "[Result Viewer](#result-viewer)".
|
|
27
|
+
- **Model Fit** (Create Model tab > Model Fit...) launches [snp2le](https://github.com/iic-jku/snp2le), an external open-source tool that extracts a lumped-element SPICE/Spectre netlist from S-parameter results - offering to install it via pip if it isn't already present. See section "[Model Fit](#model-fit)".
|
|
28
|
+
|
|
26
29
|
- A graphical **Stackup XML Editor** (Tools > Edit Stackup XML...), including Variables, Reference-relative positioning, Derived Layers, and Thermal Tables
|
|
27
30
|
- Input Files tab can now **override stackup Variables** (e.g. `total_thickness`) directly, without hand-editing the XML or the generated model
|
|
31
|
+
- **setupThermal**, a companion app for Elmer thermal simulation, alongside setupEM
|
|
28
32
|
|
|
29
33
|
See [CHANGES.md](https://raw.githubusercontent.com/VolkerMuehlhaus/setupEM/main/doc/CHANGES.md) for details.
|
|
30
34
|
|
|
@@ -195,6 +199,8 @@ The buttons are used top down: You can first preview the resulting model geometr
|
|
|
195
199
|
|
|
196
200
|
To start simulation, use the "Run palace" button. If you are on Linux, this will start Palace using script "run_sim". If you are on Windows, this will start the Linux Subsystem for Windows (WSL) and open a command prompt in the simulation directory.
|
|
197
201
|
|
|
202
|
+
If the output directory already has results from a previous run, you are asked whether to delete or keep them before starting - default is to delete, so a rerun with different settings doesn't leave stale results mixed in with the new ones.
|
|
203
|
+
|
|
198
204
|
<img src="https://raw.githubusercontent.com/VolkerMuehlhaus/setupEM/main/doc/png/createmodel3.png" alt="create" width="700">
|
|
199
205
|
|
|
200
206
|
To start simulation on Linux, it is required that you have configured a script "run_sim" as described in the [gds2palace documentation](https://github.com/VolkerMuehlhaus/gds2palace_ihp_sg13g2/blob/main/doc/gds2palace_workflow_userguide.pdf). You can find a template [here](https://github.com/VolkerMuehlhaus/gds2palace_ihp_sg13g2/tree/main/scripts) in the gds2palace repository.
|
|
@@ -230,6 +236,24 @@ The matplotlib toolbar above the plot (pan/zoom/save as PNG) works as usual. A f
|
|
|
230
236
|
|
|
231
237
|
Result Viewer can also be run standalone, without the full setupEM GUI, either directly (`python result_viewer.py [target_dir]`) or via the `resultViewer` console script installed with the package.
|
|
232
238
|
|
|
239
|
+
## Model Fit
|
|
240
|
+
|
|
241
|
+
Click **Model Fit...** on the Create Model tab (next to **View Results...**) to extract a lumped-element netlist from the current run's S-parameter result, using [snp2le](https://github.com/iic-jku/snp2le) - an external, open-source tool, not part of setupEM.
|
|
242
|
+
|
|
243
|
+
<img src="https://raw.githubusercontent.com/VolkerMuehlhaus/setupEM/main/doc/png/resultviewer_button.png" alt="model fit button" width="700">
|
|
244
|
+
|
|
245
|
+
If snp2le isn't installed, setupEM offers to install it for you via pip:
|
|
246
|
+
|
|
247
|
+
<img src="https://raw.githubusercontent.com/VolkerMuehlhaus/setupEM/main/doc/png/modelfit1.png" alt="snp2le not installed" width="350">
|
|
248
|
+
|
|
249
|
+
Choosing **Install** runs `pip install snp2le` in the Log panel and, once it succeeds, continues automatically - no need to click Model Fit a second time. Choosing **Cancel** logs the manual install command and the project link instead.
|
|
250
|
+
|
|
251
|
+
Once snp2le is available, Model Fit locates the raw (not `_dc`, not `_deembedded`) Touchstone result file for the current run and launches the snp2le GUI in that file's directory. snp2le's GUI has no command-line option to preload a file, so the exact path is printed to the Log panel - load it via snp2le's own file picker:
|
|
252
|
+
|
|
253
|
+
<img src="https://raw.githubusercontent.com/VolkerMuehlhaus/setupEM/main/doc/png/modelfit2.png" alt="snp2le starting" width="700">
|
|
254
|
+
|
|
255
|
+
If no raw result file exists yet (no simulation has been run), Model Fit shows a warning instead of starting snp2le - run a simulation first.
|
|
256
|
+
|
|
233
257
|
## Code
|
|
234
258
|
Behind the scenes, the setupEM user interface created Python model code for gds2palace, and you can check the resulting code on the "Code" tab.
|
|
235
259
|
|
|
@@ -2,10 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
## What's New
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
setupEM includes two built-in tools for working with simulation results directly, without external scripts:
|
|
6
|
+
|
|
7
|
+
- **Result Viewer** (Create Model tab > View Results...) plots Touchstone S-parameter results - dB/phase, Smith chart, zoomed Smith chart - without leaving setupEM. See section "[Result Viewer](#result-viewer)".
|
|
8
|
+
- **Model Fit** (Create Model tab > Model Fit...) launches [snp2le](https://github.com/iic-jku/snp2le), an external open-source tool that extracts a lumped-element SPICE/Spectre netlist from S-parameter results - offering to install it via pip if it isn't already present. See section "[Model Fit](#model-fit)".
|
|
9
|
+
|
|
7
10
|
- A graphical **Stackup XML Editor** (Tools > Edit Stackup XML...), including Variables, Reference-relative positioning, Derived Layers, and Thermal Tables
|
|
8
11
|
- Input Files tab can now **override stackup Variables** (e.g. `total_thickness`) directly, without hand-editing the XML or the generated model
|
|
12
|
+
- **setupThermal**, a companion app for Elmer thermal simulation, alongside setupEM
|
|
9
13
|
|
|
10
14
|
See [CHANGES.md](doc/CHANGES.md) for details.
|
|
11
15
|
|
|
@@ -176,6 +180,8 @@ The buttons are used top down: You can first preview the resulting model geometr
|
|
|
176
180
|
|
|
177
181
|
To start simulation, use the "Run palace" button. If you are on Linux, this will start Palace using script "run_sim". If you are on Windows, this will start the Linux Subsystem for Windows (WSL) and open a command prompt in the simulation directory.
|
|
178
182
|
|
|
183
|
+
If the output directory already has results from a previous run, you are asked whether to delete or keep them before starting - default is to delete, so a rerun with different settings doesn't leave stale results mixed in with the new ones.
|
|
184
|
+
|
|
179
185
|
<img src="./doc/png/createmodel3.png" alt="create" width="700">
|
|
180
186
|
|
|
181
187
|
To start simulation on Linux, it is required that you have configured a script "run_sim" as described in the [gds2palace documentation](https://github.com/VolkerMuehlhaus/gds2palace_ihp_sg13g2/blob/main/doc/gds2palace_workflow_userguide.pdf). You can find a template [here](https://github.com/VolkerMuehlhaus/gds2palace_ihp_sg13g2/tree/main/scripts) in the gds2palace repository.
|
|
@@ -211,6 +217,24 @@ The matplotlib toolbar above the plot (pan/zoom/save as PNG) works as usual. A f
|
|
|
211
217
|
|
|
212
218
|
Result Viewer can also be run standalone, without the full setupEM GUI, either directly (`python result_viewer.py [target_dir]`) or via the `resultViewer` console script installed with the package.
|
|
213
219
|
|
|
220
|
+
## Model Fit
|
|
221
|
+
|
|
222
|
+
Click **Model Fit...** on the Create Model tab (next to **View Results...**) to extract a lumped-element netlist from the current run's S-parameter result, using [snp2le](https://github.com/iic-jku/snp2le) - an external, open-source tool, not part of setupEM.
|
|
223
|
+
|
|
224
|
+
<img src="./doc/png/resultviewer_button.png" alt="model fit button" width="700">
|
|
225
|
+
|
|
226
|
+
If snp2le isn't installed, setupEM offers to install it for you via pip:
|
|
227
|
+
|
|
228
|
+
<img src="./doc/png/modelfit1.png" alt="snp2le not installed" width="350">
|
|
229
|
+
|
|
230
|
+
Choosing **Install** runs `pip install snp2le` in the Log panel and, once it succeeds, continues automatically - no need to click Model Fit a second time. Choosing **Cancel** logs the manual install command and the project link instead.
|
|
231
|
+
|
|
232
|
+
Once snp2le is available, Model Fit locates the raw (not `_dc`, not `_deembedded`) Touchstone result file for the current run and launches the snp2le GUI in that file's directory. snp2le's GUI has no command-line option to preload a file, so the exact path is printed to the Log panel - load it via snp2le's own file picker:
|
|
233
|
+
|
|
234
|
+
<img src="./doc/png/modelfit2.png" alt="snp2le starting" width="700">
|
|
235
|
+
|
|
236
|
+
If no raw result file exists yet (no simulation has been run), Model Fit shows a warning instead of starting snp2le - run a simulation first.
|
|
237
|
+
|
|
214
238
|
## Code
|
|
215
239
|
Behind the scenes, the setupEM user interface created Python model code for gds2palace, and you can check the resulting code on the "Code" tab.
|
|
216
240
|
|
|
@@ -2,10 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
## What's New
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
setupEM includes two built-in tools for working with simulation results directly, without external scripts:
|
|
6
|
+
|
|
7
|
+
- **Result Viewer** (Create Model tab > View Results...) plots Touchstone S-parameter results - dB/phase, Smith chart, zoomed Smith chart - without leaving setupEM. See section "[Result Viewer](#result-viewer)".
|
|
8
|
+
- **Model Fit** (Create Model tab > Model Fit...) launches [snp2le](https://github.com/iic-jku/snp2le), an external open-source tool that extracts a lumped-element SPICE/Spectre netlist from S-parameter results - offering to install it via pip if it isn't already present. See section "[Model Fit](#model-fit)".
|
|
9
|
+
|
|
7
10
|
- A graphical **Stackup XML Editor** (Tools > Edit Stackup XML...), including Variables, Reference-relative positioning, Derived Layers, and Thermal Tables
|
|
8
11
|
- Input Files tab can now **override stackup Variables** (e.g. `total_thickness`) directly, without hand-editing the XML or the generated model
|
|
12
|
+
- **setupThermal**, a companion app for Elmer thermal simulation, alongside setupEM
|
|
9
13
|
|
|
10
14
|
See [CHANGES.md](https://raw.githubusercontent.com/VolkerMuehlhaus/setupEM/main/doc/CHANGES.md) for details.
|
|
11
15
|
|
|
@@ -176,6 +180,8 @@ The buttons are used top down: You can first preview the resulting model geometr
|
|
|
176
180
|
|
|
177
181
|
To start simulation, use the "Run palace" button. If you are on Linux, this will start Palace using script "run_sim". If you are on Windows, this will start the Linux Subsystem for Windows (WSL) and open a command prompt in the simulation directory.
|
|
178
182
|
|
|
183
|
+
If the output directory already has results from a previous run, you are asked whether to delete or keep them before starting - default is to delete, so a rerun with different settings doesn't leave stale results mixed in with the new ones.
|
|
184
|
+
|
|
179
185
|
<img src="https://raw.githubusercontent.com/VolkerMuehlhaus/setupEM/main/doc/png/createmodel3.png" alt="create" width="700">
|
|
180
186
|
|
|
181
187
|
To start simulation on Linux, it is required that you have configured a script "run_sim" as described in the [gds2palace documentation](https://github.com/VolkerMuehlhaus/gds2palace_ihp_sg13g2/blob/main/doc/gds2palace_workflow_userguide.pdf). You can find a template [here](https://github.com/VolkerMuehlhaus/gds2palace_ihp_sg13g2/tree/main/scripts) in the gds2palace repository.
|
|
@@ -211,6 +217,24 @@ The matplotlib toolbar above the plot (pan/zoom/save as PNG) works as usual. A f
|
|
|
211
217
|
|
|
212
218
|
Result Viewer can also be run standalone, without the full setupEM GUI, either directly (`python result_viewer.py [target_dir]`) or via the `resultViewer` console script installed with the package.
|
|
213
219
|
|
|
220
|
+
## Model Fit
|
|
221
|
+
|
|
222
|
+
Click **Model Fit...** on the Create Model tab (next to **View Results...**) to extract a lumped-element netlist from the current run's S-parameter result, using [snp2le](https://github.com/iic-jku/snp2le) - an external, open-source tool, not part of setupEM.
|
|
223
|
+
|
|
224
|
+
<img src="https://raw.githubusercontent.com/VolkerMuehlhaus/setupEM/main/doc/png/resultviewer_button.png" alt="model fit button" width="700">
|
|
225
|
+
|
|
226
|
+
If snp2le isn't installed, setupEM offers to install it for you via pip:
|
|
227
|
+
|
|
228
|
+
<img src="https://raw.githubusercontent.com/VolkerMuehlhaus/setupEM/main/doc/png/modelfit1.png" alt="snp2le not installed" width="350">
|
|
229
|
+
|
|
230
|
+
Choosing **Install** runs `pip install snp2le` in the Log panel and, once it succeeds, continues automatically - no need to click Model Fit a second time. Choosing **Cancel** logs the manual install command and the project link instead.
|
|
231
|
+
|
|
232
|
+
Once snp2le is available, Model Fit locates the raw (not `_dc`, not `_deembedded`) Touchstone result file for the current run and launches the snp2le GUI in that file's directory. snp2le's GUI has no command-line option to preload a file, so the exact path is printed to the Log panel - load it via snp2le's own file picker:
|
|
233
|
+
|
|
234
|
+
<img src="https://raw.githubusercontent.com/VolkerMuehlhaus/setupEM/main/doc/png/modelfit2.png" alt="snp2le starting" width="700">
|
|
235
|
+
|
|
236
|
+
If no raw result file exists yet (no simulation has been run), Model Fit shows a warning instead of starting snp2le - run a simulation first.
|
|
237
|
+
|
|
214
238
|
## Code
|
|
215
239
|
Behind the scenes, the setupEM user interface created Python model code for gds2palace, and you can check the resulting code on the "Code" tab.
|
|
216
240
|
|
|
@@ -1,199 +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
|
|
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 =
|
|
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)
|
|
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)
|
|
@@ -17,9 +17,10 @@
|
|
|
17
17
|
########################################################################
|
|
18
18
|
|
|
19
19
|
|
|
20
|
-
import sys, json, os, pathlib, ast, webbrowser, argparse
|
|
20
|
+
import sys, json, os, pathlib, ast, webbrowser, argparse, shutil, re
|
|
21
21
|
import numpy as np
|
|
22
22
|
import importlib.metadata
|
|
23
|
+
import importlib.util
|
|
23
24
|
import requests
|
|
24
25
|
import gdspy
|
|
25
26
|
from scipy.interpolate import interp1d
|
|
@@ -54,7 +55,7 @@ if __package__ in (None, ""):
|
|
|
54
55
|
VectorWidget, PopUpWindow, CreateModelTabBase, MainWindowBase,
|
|
55
56
|
epsilon_to_color, default_stackup_dielectric_label, default_stackup_metal_label,
|
|
56
57
|
)
|
|
57
|
-
from palace_results import build_results_summary
|
|
58
|
+
from palace_results import build_results_summary, find_output_dir
|
|
58
59
|
else:
|
|
59
60
|
from .setup_common import (
|
|
60
61
|
EDIT_STYLE_OPTIONAL, EDIT_STYLE_REQUIRED, COMBO_STYLE_REQUIRED, COMBO_STYLE_OPTIONAL,
|
|
@@ -62,7 +63,7 @@ else:
|
|
|
62
63
|
VectorWidget, PopUpWindow, CreateModelTabBase, MainWindowBase,
|
|
63
64
|
epsilon_to_color, default_stackup_dielectric_label, default_stackup_metal_label,
|
|
64
65
|
)
|
|
65
|
-
from .palace_results import build_results_summary
|
|
66
|
+
from .palace_results import build_results_summary, find_output_dir
|
|
66
67
|
|
|
67
68
|
|
|
68
69
|
'''
|
|
@@ -1126,11 +1127,85 @@ class CreateModelTab(CreateModelTabBase):
|
|
|
1126
1127
|
# simulation run apart from a mesh-creation run (self.process is reused for both).
|
|
1127
1128
|
self._process_purpose = None
|
|
1128
1129
|
|
|
1129
|
-
# S-parameter result viewer: appended here (not in the shared
|
|
1130
|
-
# since setupThermal has no S-parameters and must not
|
|
1130
|
+
# S-parameter result viewer + model fit: appended here (not in the shared
|
|
1131
|
+
# CreateModelTabBase) since setupThermal has no S-parameters and must not
|
|
1132
|
+
# show these buttons. Added as a row of the base class's buttons_grid
|
|
1133
|
+
# (not a separate layout) so this row's two-thirds/one-third split lines
|
|
1134
|
+
# up exactly with Preview/Create Mesh/Start Simulation above, in the same
|
|
1135
|
+
# Actions group.
|
|
1136
|
+
row = self.buttons_grid.rowCount()
|
|
1131
1137
|
self.view_results_btn = QPushButton("📈 View Results...")
|
|
1132
1138
|
self.view_results_btn.clicked.connect(self.MainWindow.open_result_viewer)
|
|
1133
|
-
self.
|
|
1139
|
+
self.buttons_grid.addWidget(self.view_results_btn, row, 0)
|
|
1140
|
+
self.model_fit_btn = QPushButton("🧩 Model Fit...")
|
|
1141
|
+
self.model_fit_btn.clicked.connect(self.open_model_fit)
|
|
1142
|
+
self.buttons_grid.addWidget(self.model_fit_btn, row, 1)
|
|
1143
|
+
|
|
1144
|
+
def open_model_fit(self):
|
|
1145
|
+
SNP2LE_URL = "https://github.com/iic-jku/snp2le"
|
|
1146
|
+
|
|
1147
|
+
# invalidate_caches(): after a just-completed "pip install snp2le" run (see
|
|
1148
|
+
# on_finished() below), the import system's directory-listing cache for
|
|
1149
|
+
# site-packages can still be stale in this same process, so a find_spec()
|
|
1150
|
+
# right after install could still report "not found" without this.
|
|
1151
|
+
importlib.invalidate_caches()
|
|
1152
|
+
if importlib.util.find_spec("snp2le") is None:
|
|
1153
|
+
box = QMessageBox(self)
|
|
1154
|
+
box.setIcon(QMessageBox.Question)
|
|
1155
|
+
box.setWindowTitle("Model Fit")
|
|
1156
|
+
box.setText("snp2le is not installed. Install it now with pip?")
|
|
1157
|
+
install_btn = box.addButton("Install", QMessageBox.AcceptRole)
|
|
1158
|
+
box.addButton("Cancel", QMessageBox.RejectRole)
|
|
1159
|
+
box.setDefaultButton(install_btn)
|
|
1160
|
+
box.exec()
|
|
1161
|
+
|
|
1162
|
+
if box.clickedButton() is install_btn:
|
|
1163
|
+
self.log_area.appendPlainText("Installing snp2le with pip ...")
|
|
1164
|
+
self._process_purpose = "install_snp2le"
|
|
1165
|
+
self.process.start(sys.executable, ["-m", "pip", "install", "snp2le"])
|
|
1166
|
+
else:
|
|
1167
|
+
self.log_area.appendPlainText("⚠️ snp2le is not installed. Install it with: pip install snp2le")
|
|
1168
|
+
self.log_area.appendPlainText(SNP2LE_URL)
|
|
1169
|
+
return
|
|
1170
|
+
|
|
1171
|
+
# local import: only needed once snp2le is actually launched, same lazy-import
|
|
1172
|
+
# style used by open_result_viewer() for its own heavy imports.
|
|
1173
|
+
if __package__ in (None, ""):
|
|
1174
|
+
from result_viewer import find_touchstone_files
|
|
1175
|
+
else:
|
|
1176
|
+
from .result_viewer import find_touchstone_files
|
|
1177
|
+
|
|
1178
|
+
if self.MainWindow.PalaceMode:
|
|
1179
|
+
run_path = saved_values['sim_path'] + "/palace_model/" + saved_values['model_basename'] + "_data"
|
|
1180
|
+
else:
|
|
1181
|
+
run_path = saved_values['sim_path'] + "/elmer_model/" + saved_values['model_basename'] + "_data"
|
|
1182
|
+
|
|
1183
|
+
raw_files = [
|
|
1184
|
+
path for path in find_touchstone_files(run_path)
|
|
1185
|
+
if '_dc' not in os.path.basename(path) and '_deembedded' not in os.path.basename(path)
|
|
1186
|
+
]
|
|
1187
|
+
|
|
1188
|
+
if not raw_files:
|
|
1189
|
+
QMessageBox.warning(self, "Model Fit",
|
|
1190
|
+
f"No raw S-parameter result file found in {run_path}.\nRun a simulation first.")
|
|
1191
|
+
return
|
|
1192
|
+
if len(raw_files) > 1:
|
|
1193
|
+
self.log_area.appendPlainText("⚠️ Multiple raw S-parameter result files found, using the first one:")
|
|
1194
|
+
for path in raw_files:
|
|
1195
|
+
self.log_area.appendPlainText(" " + path)
|
|
1196
|
+
raw_file = raw_files[0]
|
|
1197
|
+
|
|
1198
|
+
# snp2le's GUI mode takes no filename argument (only its "-b" batch mode does) -
|
|
1199
|
+
# the file has to be loaded by hand via the GUI's own file picker, so point the
|
|
1200
|
+
# user at it explicitly and start snp2le in that file's directory, so its file
|
|
1201
|
+
# picker opens there instead of wherever setupEM happened to be launched from.
|
|
1202
|
+
raw_dir = os.path.dirname(raw_file)
|
|
1203
|
+
self.log_area.appendPlainText("Starting snp2le ...")
|
|
1204
|
+
self.log_area.appendPlainText(SNP2LE_URL)
|
|
1205
|
+
self.log_area.appendPlainText(f"In the snp2le window, load: {raw_file}")
|
|
1206
|
+
self._process_purpose = "model_fit"
|
|
1207
|
+
self.process.setWorkingDirectory(raw_dir)
|
|
1208
|
+
self.process.start(sys.executable, ["-m", "snp2le"])
|
|
1134
1209
|
|
|
1135
1210
|
def _append_results_summary(self):
|
|
1136
1211
|
# Palace-only: parse palace.json / error-indicators.csv and append a results summary
|
|
@@ -1144,6 +1219,18 @@ class CreateModelTab(CreateModelTabBase):
|
|
|
1144
1219
|
# Auto-append the results summary after a real simulation run (not after mesh creation)
|
|
1145
1220
|
if self._process_purpose == "run_simulation" and self.MainWindow.PalaceMode:
|
|
1146
1221
|
self._append_results_summary()
|
|
1222
|
+
elif self._process_purpose == "install_snp2le":
|
|
1223
|
+
importlib.invalidate_caches()
|
|
1224
|
+
if exit_code == 0 and importlib.util.find_spec("snp2le") is not None:
|
|
1225
|
+
self.log_area.appendPlainText("snp2le installed successfully.\n")
|
|
1226
|
+
# re-enter open_model_fit(): this time find_spec() succeeds, so it
|
|
1227
|
+
# goes straight to finding the raw result file and launching snp2le.
|
|
1228
|
+
self.open_model_fit()
|
|
1229
|
+
else:
|
|
1230
|
+
self.log_area.appendPlainText(
|
|
1231
|
+
f"⚠️ snp2le installation failed (exit code {exit_code}). "
|
|
1232
|
+
f"Try manually: pip install snp2le\n"
|
|
1233
|
+
)
|
|
1147
1234
|
|
|
1148
1235
|
def create_model(self):
|
|
1149
1236
|
# Request all tabs to save values again,
|
|
@@ -1180,9 +1267,64 @@ class CreateModelTab(CreateModelTabBase):
|
|
|
1180
1267
|
|
|
1181
1268
|
|
|
1182
1269
|
|
|
1270
|
+
def _confirm_clear_previous_results(self, run_path):
|
|
1271
|
+
"""If run_path already holds solver output from a previous run, ask the user
|
|
1272
|
+
(default: delete) whether to remove it before starting a new simulation. Only
|
|
1273
|
+
ever touches solver-OUTPUT data - never the mesh/config/run script that "Create
|
|
1274
|
+
Mesh" just wrote, since those are still needed to start this run.
|
|
1275
|
+
|
|
1276
|
+
Palace writes all of its results into one dedicated subdirectory (named in
|
|
1277
|
+
config.json's Problem.Output, resolved via find_output_dir()), so that whole
|
|
1278
|
+
subtree is the deletion target. Elmer has no such subdirectory - its solver
|
|
1279
|
+
writes scalar_results.*/fields*.vtu*/*.sNp files directly into run_path
|
|
1280
|
+
alongside the mesh/config - so those are matched and removed individually.
|
|
1281
|
+
"""
|
|
1282
|
+
if self.MainWindow.PalaceMode:
|
|
1283
|
+
output_dir = find_output_dir(run_path, saved_values['model_basename'])
|
|
1284
|
+
if not os.path.isdir(output_dir) or not os.listdir(output_dir):
|
|
1285
|
+
return
|
|
1286
|
+
targets = [output_dir]
|
|
1287
|
+
else:
|
|
1288
|
+
targets = []
|
|
1289
|
+
if os.path.isdir(run_path):
|
|
1290
|
+
for fn in os.listdir(run_path):
|
|
1291
|
+
full_path = os.path.join(run_path, fn)
|
|
1292
|
+
if not os.path.isfile(full_path):
|
|
1293
|
+
continue
|
|
1294
|
+
if fn in ("scalar_results.dat", "scalar_results.names") or \
|
|
1295
|
+
fn.startswith("fields") or re.search(r'\.s\d+p$', fn, re.IGNORECASE):
|
|
1296
|
+
targets.append(full_path)
|
|
1297
|
+
if not targets:
|
|
1298
|
+
return
|
|
1299
|
+
|
|
1300
|
+
box = QMessageBox(self)
|
|
1301
|
+
box.setIcon(QMessageBox.Question)
|
|
1302
|
+
box.setWindowTitle("Previous simulation results found")
|
|
1303
|
+
box.setText(
|
|
1304
|
+
f"This run's output directory already contains results from a previous "
|
|
1305
|
+
f"simulation:\n\n{run_path}\n\nDelete the existing results before starting?"
|
|
1306
|
+
)
|
|
1307
|
+
delete_btn = box.addButton("Delete", QMessageBox.AcceptRole)
|
|
1308
|
+
box.addButton("Keep", QMessageBox.RejectRole)
|
|
1309
|
+
box.setDefaultButton(delete_btn)
|
|
1310
|
+
box.exec()
|
|
1311
|
+
|
|
1312
|
+
if box.clickedButton() is delete_btn:
|
|
1313
|
+
for target in targets:
|
|
1314
|
+
if os.path.isdir(target):
|
|
1315
|
+
shutil.rmtree(target)
|
|
1316
|
+
else:
|
|
1317
|
+
os.remove(target)
|
|
1318
|
+
|
|
1183
1319
|
def run_model(self):
|
|
1184
1320
|
# Run model that we created before
|
|
1185
1321
|
|
|
1322
|
+
if self.MainWindow.PalaceMode:
|
|
1323
|
+
run_path = saved_values['sim_path'] + "/palace_model/" + saved_values['model_basename'] + "_data"
|
|
1324
|
+
else:
|
|
1325
|
+
run_path = saved_values['sim_path'] + "/elmer_model/" + saved_values['model_basename'] + "_data"
|
|
1326
|
+
self._confirm_clear_previous_results(run_path)
|
|
1327
|
+
|
|
1186
1328
|
# clear log
|
|
1187
1329
|
self.log_area.clear()
|
|
1188
1330
|
self._process_purpose = "run_simulation"
|
|
@@ -1629,14 +1771,25 @@ class MainWindow(MainWindowBase):
|
|
|
1629
1771
|
def show_version(self):
|
|
1630
1772
|
setupEM_version = importlib.metadata.version("setupEM")
|
|
1631
1773
|
gds2palace_version = importlib.metadata.version("gds2palace")
|
|
1632
|
-
|
|
1774
|
+
# snp2le (used by Model Fit) is an optional dependency, not installed by default
|
|
1775
|
+
try:
|
|
1776
|
+
snp2le_version = importlib.metadata.version("snp2le")
|
|
1777
|
+
except importlib.metadata.PackageNotFoundError:
|
|
1778
|
+
snp2le_version = "not installed"
|
|
1779
|
+
version_info = f"Installed:\nsetupEM {setupEM_version}\ngds2palace {gds2palace_version}\nsnp2le {snp2le_version}"
|
|
1633
1780
|
|
|
1634
1781
|
# get latest available version information
|
|
1635
1782
|
latest_setupEM = self.get_latest_version("setupEM")
|
|
1636
1783
|
latest_gds2palace = self.get_latest_version("gds2palace")
|
|
1637
1784
|
latest_info = f"Latest version:\nsetupEM {latest_setupEM}\ngds2palace : {latest_gds2palace}"
|
|
1785
|
+
upgrade_info = "\n\nYou can update using\n pip install gds2palace --upgrade\n pip install setupEM --upgrade"
|
|
1786
|
+
if snp2le_version != "not installed":
|
|
1787
|
+
# only worth checking/offering an upgrade for a package the user actually has
|
|
1788
|
+
latest_snp2le = self.get_latest_version("snp2le")
|
|
1789
|
+
latest_info += f"\nsnp2le {latest_snp2le}"
|
|
1790
|
+
upgrade_info += "\n pip install snp2le --upgrade"
|
|
1791
|
+
upgrade_info += '\nafter exiting this program\n'
|
|
1638
1792
|
version_info = version_info + '\n\n' + latest_info
|
|
1639
|
-
upgrade_info = "\n\nYou can update using\n pip install gds2palace --upgrade\n pip install setupEM --upgrade\nafter exiting this program"
|
|
1640
1793
|
|
|
1641
1794
|
QMessageBox.information(self,"Version information",version_info + upgrade_info)
|
|
1642
1795
|
|
|
@@ -39,7 +39,7 @@ import requests
|
|
|
39
39
|
import gdspy
|
|
40
40
|
from scipy.interpolate import interp1d
|
|
41
41
|
from PySide6.QtWidgets import (
|
|
42
|
-
QMainWindow, QWidget, QHBoxLayout, QVBoxLayout,
|
|
42
|
+
QMainWindow, QWidget, QHBoxLayout, QVBoxLayout, QGridLayout,
|
|
43
43
|
QLabel, QLineEdit, QComboBox,
|
|
44
44
|
QPushButton, QFileDialog, QMessageBox, QGroupBox,
|
|
45
45
|
QCheckBox, QPlainTextEdit, QDialog, QSizePolicy, QFrame,
|
|
@@ -1208,42 +1208,49 @@ class CreateModelTabBase(QWidget):
|
|
|
1208
1208
|
self.label1.setFixedWidth(120)
|
|
1209
1209
|
self.modelname_layout.addWidget(self.label1)
|
|
1210
1210
|
self.modelname_edit = QLineEdit("")
|
|
1211
|
-
self.modelname_edit.setFixedWidth(250)
|
|
1212
1211
|
self.modelname_edit.setStyleSheet(EDIT_STYLE_OPTIONAL)
|
|
1213
1212
|
# install event filter, so we capture when edit looses focus
|
|
1214
1213
|
self.modelname_edit.editingFinished.connect(self.on_modelname_edit_done)
|
|
1215
|
-
|
|
1216
1214
|
self.modelname_layout.addWidget(self.modelname_edit)
|
|
1217
|
-
self.modelname_layout.addStretch()
|
|
1218
1215
|
self.file_layout.addLayout(self.modelname_layout)
|
|
1219
1216
|
|
|
1217
|
+
self.file_group.setLayout(self.file_layout)
|
|
1218
|
+
|
|
1219
|
+
# Actions group - kept visually separate (its own framed group) from the
|
|
1220
|
+
# input fields above. Preview/Create Mesh/Start Simulation/Terminate (plus,
|
|
1221
|
+
# in setupEM's subclass, View Results/Model Fit) all share this grid so
|
|
1222
|
+
# their right edges line up at exactly the same two-thirds/one-third split
|
|
1223
|
+
# - a QGridLayout keeps columns aligned across rows; independent
|
|
1224
|
+
# QHBoxLayouts can't guarantee that once some rows have two widgets (e.g.
|
|
1225
|
+
# Start Simulation/Terminate) and others have one (Preview/Create Mesh).
|
|
1226
|
+
self.actions_group = QGroupBox("Actions")
|
|
1227
|
+
self.actions_layout = QVBoxLayout()
|
|
1228
|
+
self.buttons_grid = QGridLayout()
|
|
1229
|
+
self.buttons_grid.setColumnStretch(0, 2) # primary column: two thirds
|
|
1230
|
+
self.buttons_grid.setColumnStretch(1, 1) # secondary column: one third
|
|
1231
|
+
|
|
1220
1232
|
self.preview_model_btn = QPushButton("⚙️ Preview model geometry in gmsh")
|
|
1221
1233
|
self.preview_model_btn.clicked.connect(self.preview_model)
|
|
1222
|
-
self.
|
|
1234
|
+
self.buttons_grid.addWidget(self.preview_model_btn, 0, 0)
|
|
1223
1235
|
|
|
1224
1236
|
self.create_model_btn = QPushButton("⚙️ Create mesh and simulation settings file")
|
|
1225
1237
|
self.create_model_btn.clicked.connect(self.create_mesh)
|
|
1226
|
-
self.
|
|
1238
|
+
self.buttons_grid.addWidget(self.create_model_btn, 1, 0)
|
|
1227
1239
|
|
|
1228
|
-
self.run_layout = QHBoxLayout()
|
|
1229
1240
|
self.create_run_btn = QPushButton("▶️ Start Simulation")
|
|
1230
1241
|
self.create_run_btn.clicked.connect(self.run_model)
|
|
1231
|
-
self.
|
|
1242
|
+
self.buttons_grid.addWidget(self.create_run_btn, 2, 0)
|
|
1232
1243
|
self.kill_btn = QPushButton("🛑 Terminate ")
|
|
1233
1244
|
self.kill_btn.clicked.connect(self.terminate_run)
|
|
1234
|
-
self.
|
|
1235
|
-
self.run_layout.setStretch(0, 5) # index 0 = Run
|
|
1236
|
-
self.run_layout.setStretch(1, 1) # index 1 = Terminate
|
|
1245
|
+
self.buttons_grid.addWidget(self.kill_btn, 2, 1)
|
|
1237
1246
|
|
|
1238
|
-
self.
|
|
1247
|
+
self.actions_layout.addLayout(self.buttons_grid)
|
|
1239
1248
|
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
#
|
|
1243
|
-
|
|
1244
|
-
self.
|
|
1245
|
-
self.log_layout = QVBoxLayout()
|
|
1246
|
-
self.log_group.setLayout(self.log_layout)
|
|
1249
|
+
# Log file: kept inside the Actions frame (not its own group box) since it is
|
|
1250
|
+
# the direct output of the actions above (Preview/Create Mesh/Start Simulation),
|
|
1251
|
+
# not an independent input section.
|
|
1252
|
+
self.actions_layout.addSpacing(10)
|
|
1253
|
+
self.actions_layout.addWidget(QLabel("Log file:"))
|
|
1247
1254
|
self.log_area = QPlainTextEdit()
|
|
1248
1255
|
self.log_area.setReadOnly(True)
|
|
1249
1256
|
log_font = QFont()
|
|
@@ -1256,11 +1263,13 @@ class CreateModelTabBase(QWidget):
|
|
|
1256
1263
|
log_font.setFixedPitch(True)
|
|
1257
1264
|
log_font.setPointSize(9)
|
|
1258
1265
|
self.log_area.setFont(log_font)
|
|
1259
|
-
self.
|
|
1266
|
+
self.actions_layout.addWidget(self.log_area)
|
|
1267
|
+
|
|
1268
|
+
self.actions_group.setLayout(self.actions_layout)
|
|
1260
1269
|
|
|
1261
1270
|
self.main_layout.addWidget(self.file_group)
|
|
1262
1271
|
self.main_layout.addSpacing(20)
|
|
1263
|
-
self.main_layout.addWidget(self.
|
|
1272
|
+
self.main_layout.addWidget(self.actions_group)
|
|
1264
1273
|
self.setLayout(self.main_layout)
|
|
1265
1274
|
|
|
1266
1275
|
# --- QProcess setup ---
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: setupEM
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
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
|
|
@@ -21,10 +21,14 @@ Dynamic: license-file
|
|
|
21
21
|
|
|
22
22
|
## What's New
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
setupEM includes two built-in tools for working with simulation results directly, without external scripts:
|
|
25
|
+
|
|
26
|
+
- **Result Viewer** (Create Model tab > View Results...) plots Touchstone S-parameter results - dB/phase, Smith chart, zoomed Smith chart - without leaving setupEM. See section "[Result Viewer](#result-viewer)".
|
|
27
|
+
- **Model Fit** (Create Model tab > Model Fit...) launches [snp2le](https://github.com/iic-jku/snp2le), an external open-source tool that extracts a lumped-element SPICE/Spectre netlist from S-parameter results - offering to install it via pip if it isn't already present. See section "[Model Fit](#model-fit)".
|
|
28
|
+
|
|
26
29
|
- A graphical **Stackup XML Editor** (Tools > Edit Stackup XML...), including Variables, Reference-relative positioning, Derived Layers, and Thermal Tables
|
|
27
30
|
- Input Files tab can now **override stackup Variables** (e.g. `total_thickness`) directly, without hand-editing the XML or the generated model
|
|
31
|
+
- **setupThermal**, a companion app for Elmer thermal simulation, alongside setupEM
|
|
28
32
|
|
|
29
33
|
See [CHANGES.md](https://raw.githubusercontent.com/VolkerMuehlhaus/setupEM/main/doc/CHANGES.md) for details.
|
|
30
34
|
|
|
@@ -195,6 +199,8 @@ The buttons are used top down: You can first preview the resulting model geometr
|
|
|
195
199
|
|
|
196
200
|
To start simulation, use the "Run palace" button. If you are on Linux, this will start Palace using script "run_sim". If you are on Windows, this will start the Linux Subsystem for Windows (WSL) and open a command prompt in the simulation directory.
|
|
197
201
|
|
|
202
|
+
If the output directory already has results from a previous run, you are asked whether to delete or keep them before starting - default is to delete, so a rerun with different settings doesn't leave stale results mixed in with the new ones.
|
|
203
|
+
|
|
198
204
|
<img src="https://raw.githubusercontent.com/VolkerMuehlhaus/setupEM/main/doc/png/createmodel3.png" alt="create" width="700">
|
|
199
205
|
|
|
200
206
|
To start simulation on Linux, it is required that you have configured a script "run_sim" as described in the [gds2palace documentation](https://github.com/VolkerMuehlhaus/gds2palace_ihp_sg13g2/blob/main/doc/gds2palace_workflow_userguide.pdf). You can find a template [here](https://github.com/VolkerMuehlhaus/gds2palace_ihp_sg13g2/tree/main/scripts) in the gds2palace repository.
|
|
@@ -230,6 +236,24 @@ The matplotlib toolbar above the plot (pan/zoom/save as PNG) works as usual. A f
|
|
|
230
236
|
|
|
231
237
|
Result Viewer can also be run standalone, without the full setupEM GUI, either directly (`python result_viewer.py [target_dir]`) or via the `resultViewer` console script installed with the package.
|
|
232
238
|
|
|
239
|
+
## Model Fit
|
|
240
|
+
|
|
241
|
+
Click **Model Fit...** on the Create Model tab (next to **View Results...**) to extract a lumped-element netlist from the current run's S-parameter result, using [snp2le](https://github.com/iic-jku/snp2le) - an external, open-source tool, not part of setupEM.
|
|
242
|
+
|
|
243
|
+
<img src="https://raw.githubusercontent.com/VolkerMuehlhaus/setupEM/main/doc/png/resultviewer_button.png" alt="model fit button" width="700">
|
|
244
|
+
|
|
245
|
+
If snp2le isn't installed, setupEM offers to install it for you via pip:
|
|
246
|
+
|
|
247
|
+
<img src="https://raw.githubusercontent.com/VolkerMuehlhaus/setupEM/main/doc/png/modelfit1.png" alt="snp2le not installed" width="350">
|
|
248
|
+
|
|
249
|
+
Choosing **Install** runs `pip install snp2le` in the Log panel and, once it succeeds, continues automatically - no need to click Model Fit a second time. Choosing **Cancel** logs the manual install command and the project link instead.
|
|
250
|
+
|
|
251
|
+
Once snp2le is available, Model Fit locates the raw (not `_dc`, not `_deembedded`) Touchstone result file for the current run and launches the snp2le GUI in that file's directory. snp2le's GUI has no command-line option to preload a file, so the exact path is printed to the Log panel - load it via snp2le's own file picker:
|
|
252
|
+
|
|
253
|
+
<img src="https://raw.githubusercontent.com/VolkerMuehlhaus/setupEM/main/doc/png/modelfit2.png" alt="snp2le starting" width="700">
|
|
254
|
+
|
|
255
|
+
If no raw result file exists yet (no simulation has been run), Model Fit shows a warning instead of starting snp2le - run a simulation first.
|
|
256
|
+
|
|
233
257
|
## Code
|
|
234
258
|
Behind the scenes, the setupEM user interface created Python model code for gds2palace, and you can check the resulting code on the "Code" tab.
|
|
235
259
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|