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.
Files changed (26) hide show
  1. {setupem-0.4.0/src/setupEM.egg-info → setupem-0.5.0}/PKG-INFO +27 -3
  2. {setupem-0.4.0 → setupem-0.5.0}/README.md +26 -2
  3. {setupem-0.4.0 → setupem-0.5.0}/README_pypi.md +26 -2
  4. {setupem-0.4.0 → setupem-0.5.0}/src/setupEM/__init__.py +1 -1
  5. {setupem-0.4.0 → setupem-0.5.0}/src/setupEM/palace_results.py +199 -199
  6. {setupem-0.4.0 → setupem-0.5.0}/src/setupEM/setupEM.py +161 -8
  7. {setupem-0.4.0 → setupem-0.5.0}/src/setupEM/setup_common.py +30 -21
  8. {setupem-0.4.0 → setupem-0.5.0/src/setupEM.egg-info}/PKG-INFO +27 -3
  9. {setupem-0.4.0 → setupem-0.5.0}/LICENSE +0 -0
  10. {setupem-0.4.0 → setupem-0.5.0}/pyproject.toml +0 -0
  11. {setupem-0.4.0 → setupem-0.5.0}/setup.cfg +0 -0
  12. {setupem-0.4.0 → setupem-0.5.0}/src/setupEM/__main__.py +0 -0
  13. {setupem-0.4.0 → setupem-0.5.0}/src/setupEM/data/SG13G2_100um.xml +0 -0
  14. {setupem-0.4.0 → setupem-0.5.0}/src/setupEM/data/SG13G2_200um.xml +0 -0
  15. {setupem-0.4.0 → setupem-0.5.0}/src/setupEM/data/SG13G2_200um_with_parameters.xml +0 -0
  16. {setupem-0.4.0 → setupem-0.5.0}/src/setupEM/data/SG13G2_nosub.xml +0 -0
  17. {setupem-0.4.0 → setupem-0.5.0}/src/setupEM/momentum_import.py +0 -0
  18. {setupem-0.4.0 → setupem-0.5.0}/src/setupEM/result_viewer.py +0 -0
  19. {setupem-0.4.0 → setupem-0.5.0}/src/setupEM/setupThermal.py +0 -0
  20. {setupem-0.4.0 → setupem-0.5.0}/src/setupEM/stackupEditor.py +0 -0
  21. {setupem-0.4.0 → setupem-0.5.0}/src/setupEM/stackup_writer.py +0 -0
  22. {setupem-0.4.0 → setupem-0.5.0}/src/setupEM.egg-info/SOURCES.txt +0 -0
  23. {setupem-0.4.0 → setupem-0.5.0}/src/setupEM.egg-info/dependency_links.txt +0 -0
  24. {setupem-0.4.0 → setupem-0.5.0}/src/setupEM.egg-info/entry_points.txt +0 -0
  25. {setupem-0.4.0 → setupem-0.5.0}/src/setupEM.egg-info/requires.txt +0 -0
  26. {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.4.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
- - A built-in **Result Viewer** (Create Model tab > View Results...) to browse and plot Touchstone S-parameter results without leaving setupEM
25
- - **setupThermal**, a companion app for Elmer thermal simulation, alongside setupEM
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
- - A built-in **Result Viewer** (Create Model tab > View Results...) to browse and plot Touchstone S-parameter results without leaving setupEM
6
- - **setupThermal**, a companion app for Elmer thermal simulation, alongside setupEM
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
- - A built-in **Result Viewer** (Create Model tab > View Results...) to browse and plot Touchstone S-parameter results without leaving setupEM
6
- - **setupThermal**, a companion app for Elmer thermal simulation, alongside setupEM
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
 
@@ -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.4.0"
27
+ __version__ = "0.5.0"
@@ -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 _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)
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 CreateModelTabBase)
1130
- # since setupThermal has no S-parameters and must not show this button.
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.file_layout.addWidget(self.view_results_btn)
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
- version_info = f"Installed:\nsetupEM {setupEM_version}\ngds2palace {gds2palace_version}"
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.file_layout.addWidget(self.preview_model_btn)
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.file_layout.addWidget(self.create_model_btn)
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.run_layout.addWidget(self.create_run_btn)
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.run_layout.addWidget(self.kill_btn)
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.file_layout.addLayout(self.run_layout)
1247
+ self.actions_layout.addLayout(self.buttons_grid)
1239
1248
 
1240
- self.file_group.setLayout(self.file_layout)
1241
-
1242
- # Log group
1243
-
1244
- self.log_group = QGroupBox("Log file")
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.log_layout.addWidget(self.log_area)
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.log_group)
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.4.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
- - A built-in **Result Viewer** (Create Model tab > View Results...) to browse and plot Touchstone S-parameter results without leaving setupEM
25
- - **setupThermal**, a companion app for Elmer thermal simulation, alongside setupEM
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