setupEM 0.3.16__tar.gz → 0.4.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.3.16/src/setupEM.egg-info → setupem-0.4.0}/PKG-INFO +26 -1
  2. {setupem-0.3.16 → setupem-0.4.0}/README.md +25 -0
  3. {setupem-0.3.16 → setupem-0.4.0}/README_pypi.md +25 -0
  4. {setupem-0.3.16 → setupem-0.4.0}/pyproject.toml +1 -0
  5. {setupem-0.3.16 → setupem-0.4.0}/src/setupEM/__init__.py +1 -1
  6. setupem-0.4.0/src/setupEM/result_viewer.py +659 -0
  7. {setupem-0.3.16 → setupem-0.4.0}/src/setupEM/setupEM.py +30 -0
  8. {setupem-0.3.16 → setupem-0.4.0/src/setupEM.egg-info}/PKG-INFO +26 -1
  9. {setupem-0.3.16 → setupem-0.4.0}/src/setupEM.egg-info/SOURCES.txt +1 -0
  10. {setupem-0.3.16 → setupem-0.4.0}/src/setupEM.egg-info/entry_points.txt +1 -0
  11. {setupem-0.3.16 → setupem-0.4.0}/LICENSE +0 -0
  12. {setupem-0.3.16 → setupem-0.4.0}/setup.cfg +0 -0
  13. {setupem-0.3.16 → setupem-0.4.0}/src/setupEM/__main__.py +0 -0
  14. {setupem-0.3.16 → setupem-0.4.0}/src/setupEM/data/SG13G2_100um.xml +0 -0
  15. {setupem-0.3.16 → setupem-0.4.0}/src/setupEM/data/SG13G2_200um.xml +0 -0
  16. {setupem-0.3.16 → setupem-0.4.0}/src/setupEM/data/SG13G2_200um_with_parameters.xml +0 -0
  17. {setupem-0.3.16 → setupem-0.4.0}/src/setupEM/data/SG13G2_nosub.xml +0 -0
  18. {setupem-0.3.16 → setupem-0.4.0}/src/setupEM/momentum_import.py +0 -0
  19. {setupem-0.3.16 → setupem-0.4.0}/src/setupEM/palace_results.py +0 -0
  20. {setupem-0.3.16 → setupem-0.4.0}/src/setupEM/setupThermal.py +0 -0
  21. {setupem-0.3.16 → setupem-0.4.0}/src/setupEM/setup_common.py +0 -0
  22. {setupem-0.3.16 → setupem-0.4.0}/src/setupEM/stackupEditor.py +0 -0
  23. {setupem-0.3.16 → setupem-0.4.0}/src/setupEM/stackup_writer.py +0 -0
  24. {setupem-0.3.16 → setupem-0.4.0}/src/setupEM.egg-info/dependency_links.txt +0 -0
  25. {setupem-0.3.16 → setupem-0.4.0}/src/setupEM.egg-info/requires.txt +0 -0
  26. {setupem-0.3.16 → setupem-0.4.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.16
3
+ Version: 0.4.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,6 +21,7 @@ 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
24
25
  - **setupThermal**, a companion app for Elmer thermal simulation, alongside setupEM
25
26
  - A graphical **Stackup XML Editor** (Tools > Edit Stackup XML...), including Variables, Reference-relative positioning, Derived Layers, and Thermal Tables
26
27
  - Input Files tab can now **override stackup Variables** (e.g. `total_thickness`) directly, without hand-editing the XML or the generated model
@@ -205,6 +206,30 @@ To start simulation on Windows from the WSL terminal, type
205
206
 
206
207
  To **create Touchstone SnP output** from simulation results, please have a look at the scripts directory. Script "combine_snp" runs Python code "combine_extend_snp.py", which scans your directories (working directory and below) and converts simulation results to Touchstone file format. Supported input file format: Palace and Elmer S-parameter data.
207
208
 
209
+ ## Result Viewer
210
+
211
+ Once you have Touchstone SnP results, click **View Results...** on the Create Model tab to open the built-in **Result Viewer** - no need to run the standalone `plot_snp.py` script by hand.
212
+
213
+ <img src="https://raw.githubusercontent.com/VolkerMuehlhaus/setupEM/main/doc/png/resultviewer_button.png" alt="view results button" width="700">
214
+
215
+ The Result Viewer recursively scans the Target Directory for `.sNp` Touchstone files and lists them in a tree, grouped by the folder each file came from. Check individual files to overlay them, or check/uncheck a whole run's group entry to select or deselect every file below it at once.
216
+ The **Include _dc files** / **Include _deembedded files** checkboxes filter out DC-extrapolated and de-embedded variants created by `combine_extend_snp.py`, so you can start with just the raw result and bring in the others only when needed.
217
+
218
+ <img src="https://raw.githubusercontent.com/VolkerMuehlhaus/setupEM/main/doc/png/resultviewer1.png" alt="result viewer" width="750">
219
+
220
+ Pick which S-parameters to plot from the S-Parameters grid.
221
+ Checking files across several runs, or a whole run group, overlays all of them at once:
222
+
223
+ <img src="https://raw.githubusercontent.com/VolkerMuehlhaus/setupEM/main/doc/png/resultviewer2.png" alt="result viewer multi-file overlay" width="750">
224
+
225
+ For reflection parameters (S11, S22, ...), the Display panel can switch to a **Smith chart** or a **zoomed Smith chart** instead of dB+phase.
226
+
227
+ <img src="https://raw.githubusercontent.com/VolkerMuehlhaus/setupEM/main/doc/png/resultviewer3.png" alt="result viewer smith chart" width="750">
228
+
229
+ The matplotlib toolbar above the plot (pan/zoom/save as PNG) works as usual. A file with only a single simulated frequency point is marked with a dot instead of a line, since there is nothing to draw a line between.
230
+
231
+ 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
+
208
233
  ## Code
209
234
  Behind the scenes, the setupEM user interface created Python model code for gds2palace, and you can check the resulting code on the "Code" tab.
210
235
 
@@ -2,6 +2,7 @@
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
5
6
  - **setupThermal**, a companion app for Elmer thermal simulation, alongside setupEM
6
7
  - A graphical **Stackup XML Editor** (Tools > Edit Stackup XML...), including Variables, Reference-relative positioning, Derived Layers, and Thermal Tables
7
8
  - Input Files tab can now **override stackup Variables** (e.g. `total_thickness`) directly, without hand-editing the XML or the generated model
@@ -186,6 +187,30 @@ To start simulation on Windows from the WSL terminal, type
186
187
 
187
188
  To **create Touchstone SnP output** from simulation results, please have a look at the scripts directory. Script "combine_snp" runs Python code "combine_extend_snp.py", which scans your directories (working directory and below) and converts simulation results to Touchstone file format. Supported input file format: Palace and Elmer S-parameter data.
188
189
 
190
+ ## Result Viewer
191
+
192
+ Once you have Touchstone SnP results, click **View Results...** on the Create Model tab to open the built-in **Result Viewer** - no need to run the standalone `plot_snp.py` script by hand.
193
+
194
+ <img src="./doc/png/resultviewer_button.png" alt="view results button" width="700">
195
+
196
+ The Result Viewer recursively scans the Target Directory for `.sNp` Touchstone files and lists them in a tree, grouped by the folder each file came from. Check individual files to overlay them, or check/uncheck a whole run's group entry to select or deselect every file below it at once.
197
+ The **Include _dc files** / **Include _deembedded files** checkboxes filter out DC-extrapolated and de-embedded variants created by `combine_extend_snp.py`, so you can start with just the raw result and bring in the others only when needed.
198
+
199
+ <img src="./doc/png/resultviewer1.png" alt="result viewer" width="750">
200
+
201
+ Pick which S-parameters to plot from the S-Parameters grid.
202
+ Checking files across several runs, or a whole run group, overlays all of them at once:
203
+
204
+ <img src="./doc/png/resultviewer2.png" alt="result viewer multi-file overlay" width="750">
205
+
206
+ For reflection parameters (S11, S22, ...), the Display panel can switch to a **Smith chart** or a **zoomed Smith chart** instead of dB+phase.
207
+
208
+ <img src="./doc/png/resultviewer3.png" alt="result viewer smith chart" width="750">
209
+
210
+ The matplotlib toolbar above the plot (pan/zoom/save as PNG) works as usual. A file with only a single simulated frequency point is marked with a dot instead of a line, since there is nothing to draw a line between.
211
+
212
+ 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
+
189
214
  ## Code
190
215
  Behind the scenes, the setupEM user interface created Python model code for gds2palace, and you can check the resulting code on the "Code" tab.
191
216
 
@@ -2,6 +2,7 @@
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
5
6
  - **setupThermal**, a companion app for Elmer thermal simulation, alongside setupEM
6
7
  - A graphical **Stackup XML Editor** (Tools > Edit Stackup XML...), including Variables, Reference-relative positioning, Derived Layers, and Thermal Tables
7
8
  - Input Files tab can now **override stackup Variables** (e.g. `total_thickness`) directly, without hand-editing the XML or the generated model
@@ -186,6 +187,30 @@ To start simulation on Windows from the WSL terminal, type
186
187
 
187
188
  To **create Touchstone SnP output** from simulation results, please have a look at the scripts directory. Script "combine_snp" runs Python code "combine_extend_snp.py", which scans your directories (working directory and below) and converts simulation results to Touchstone file format. Supported input file format: Palace and Elmer S-parameter data.
188
189
 
190
+ ## Result Viewer
191
+
192
+ Once you have Touchstone SnP results, click **View Results...** on the Create Model tab to open the built-in **Result Viewer** - no need to run the standalone `plot_snp.py` script by hand.
193
+
194
+ <img src="https://raw.githubusercontent.com/VolkerMuehlhaus/setupEM/main/doc/png/resultviewer_button.png" alt="view results button" width="700">
195
+
196
+ The Result Viewer recursively scans the Target Directory for `.sNp` Touchstone files and lists them in a tree, grouped by the folder each file came from. Check individual files to overlay them, or check/uncheck a whole run's group entry to select or deselect every file below it at once.
197
+ The **Include _dc files** / **Include _deembedded files** checkboxes filter out DC-extrapolated and de-embedded variants created by `combine_extend_snp.py`, so you can start with just the raw result and bring in the others only when needed.
198
+
199
+ <img src="https://raw.githubusercontent.com/VolkerMuehlhaus/setupEM/main/doc/png/resultviewer1.png" alt="result viewer" width="750">
200
+
201
+ Pick which S-parameters to plot from the S-Parameters grid.
202
+ Checking files across several runs, or a whole run group, overlays all of them at once:
203
+
204
+ <img src="https://raw.githubusercontent.com/VolkerMuehlhaus/setupEM/main/doc/png/resultviewer2.png" alt="result viewer multi-file overlay" width="750">
205
+
206
+ For reflection parameters (S11, S22, ...), the Display panel can switch to a **Smith chart** or a **zoomed Smith chart** instead of dB+phase.
207
+
208
+ <img src="https://raw.githubusercontent.com/VolkerMuehlhaus/setupEM/main/doc/png/resultviewer3.png" alt="result viewer smith chart" width="750">
209
+
210
+ The matplotlib toolbar above the plot (pan/zoom/save as PNG) works as usual. A file with only a single simulated frequency point is marked with a dot instead of a line, since there is nothing to draw a line between.
211
+
212
+ 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
+
189
214
  ## Code
190
215
  Behind the scenes, the setupEM user interface created Python model code for gds2palace, and you can check the resulting code on the "Code" tab.
191
216
 
@@ -27,6 +27,7 @@ requires-python = ">=3.9"
27
27
  setupEM = "setupEM.setupEM:main"
28
28
  setupThermal = "setupEM.setupThermal:main"
29
29
  stackupEditor = "setupEM.stackupEditor:main"
30
+ resultViewer = "setupEM.result_viewer:main"
30
31
 
31
32
  [tool.setuptools.dynamic]
32
33
  version = {attr = "setupEM.__version__"}
@@ -24,4 +24,4 @@ A Python tool for EM setup using gds2palace.
24
24
  from .setupEM import main
25
25
 
26
26
  __all__ = ["main"]
27
- __version__ = "0.3.16"
27
+ __version__ = "0.4.0"
@@ -0,0 +1,659 @@
1
+ ########################################################################
2
+ #
3
+ # Copyright 2025-2026 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
+ """
20
+ result_viewer.py
21
+
22
+ GUI S-parameter result viewer: a PySide6 equivalent of the standalone
23
+ plot_snp.py script, embedded in one window instead of popping up separate
24
+ matplotlib windows per plot type. Lists Touchstone (.sNp) files found
25
+ recursively under a target directory, lets the user check any number of
26
+ them to overlay, pick which S-parameters to plot from a dynamically sized
27
+ grid, and choose whether reflection (Snn) parameters show phase, a Smith
28
+ chart, or a zoomed Smith chart in place of the rectangular phase plot.
29
+
30
+ Normally opened from setupEM's Create Model tab (View Results button, see
31
+ MainWindow.open_result_viewer() in setupEM.py) - but also runnable
32
+ standalone, either directly (`python result_viewer.py [target_dir]`) or via
33
+ the `resultViewer` console script installed with this package (see main()
34
+ below and pyproject.toml).
35
+ """
36
+
37
+ import argparse
38
+ import math
39
+ import os
40
+ import re
41
+ import sys
42
+
43
+ import numpy as np
44
+ import skrf as rf
45
+
46
+ from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg, NavigationToolbar2QT
47
+ from matplotlib.figure import Figure
48
+ from matplotlib.lines import Line2D
49
+ from matplotlib.patches import Circle
50
+ from matplotlib.ticker import MultipleLocator
51
+
52
+ from PySide6.QtWidgets import (
53
+ QApplication, QDialog, QVBoxLayout, QHBoxLayout, QGridLayout,
54
+ QGroupBox, QLabel, QTreeWidget, QTreeWidgetItem, QPushButton,
55
+ QRadioButton, QButtonGroup, QCheckBox, QSizePolicy, QStyleFactory,
56
+ )
57
+ from PySide6.QtCore import Qt
58
+
59
+
60
+ # ------------------------------------------------------------------
61
+ # Plot data helpers (ported from plot_snp.py)
62
+ # ------------------------------------------------------------------
63
+
64
+ COLORS = ['r', 'b', 'm', 'c', 'g', 'y', 'k', 'w']
65
+ LINESTYLES = ['solid', 'dashed', 'dashdot', 'dotted', 'solid', 'dashed', 'dashdot', 'dotted']
66
+
67
+ # marker size for the fat dot marking a network with a single frequency point
68
+ # (no line can be drawn between points, so it would otherwise be invisible)
69
+ SINGLE_POINT_MARKERSIZE = 7
70
+
71
+ # reflection coefficient magnitude shown in the zoomed Smith chart
72
+ ZOOM_GAMMA = 0.5
73
+ GRID_COLOR = 'lightgrey'
74
+ GRID_LW = 0.8
75
+ # constant-resistance/-reactance grid values for the zoomed Smith chart,
76
+ # denser than skrf's default labeled grid ([0.2, 0.5, 1, 2, 5])
77
+ ZOOM_GRID_VALUES = [0.2, 0.5, 1.0, 1.5, 2.0, 3.0]
78
+
79
+ TOUCHSTONE_RE = re.compile(r'\.s(\d+)p$', re.IGNORECASE)
80
+
81
+
82
+ def dB(value):
83
+ return 20.0 * np.log10(np.abs(value))
84
+
85
+
86
+ def phase_deg(value):
87
+ return np.angle(value, deg=True)
88
+
89
+
90
+ def Sxx(network, m, n):
91
+ return network.s[:, m-1, n-1]
92
+
93
+
94
+ def draw_zoomed_smith_grid(ax, gamma):
95
+ # draw a denser Smith chart grid for the zoomed view, with labels placed where
96
+ # each grid circle crosses the real (for r) or imaginary (for x) axis, since
97
+ # skrf's own label placement is designed for the full chart and would fall
98
+ # outside the zoomed axis limits
99
+ ax.axhline(0, color='grey', lw=0.5)
100
+
101
+ for r in ZOOM_GRID_VALUES:
102
+ center = (r/(1+r), 0)
103
+ radius = 1/(1+r)
104
+ ax.add_patch(Circle(center, radius, ec=GRID_COLOR, fc='none', lw=GRID_LW))
105
+ label_pos = center[0] - radius
106
+ if abs(label_pos) < gamma:
107
+ ax.annotate(f"{r:g}", xy=(label_pos, 0), xytext=(label_pos, 0.01),
108
+ fontsize=8, color='dimgrey', ha='center', va='bottom')
109
+
110
+ for sign in (1, -1):
111
+ for x in ZOOM_GRID_VALUES:
112
+ xv = sign * x
113
+ center = (1, 1/xv)
114
+ radius = abs(1/xv)
115
+ ax.add_patch(Circle(center, radius, ec=GRID_COLOR, fc='none', lw=GRID_LW))
116
+ if radius >= 1:
117
+ # crossing point with the imaginary axis nearest the origin
118
+ y0 = 1/xv - math.copysign(math.sqrt(radius**2 - 1), 1/xv)
119
+ if abs(y0) < gamma:
120
+ ax.annotate(f"{xv:g}j", xy=(0, y0), xytext=(0.01, y0),
121
+ fontsize=8, color='dimgrey', ha='left', va='center')
122
+
123
+ ax.plot(gamma*np.array([-1.1, 1.1]), gamma*np.array([-1.1, 1.1]), 'w.', markersize=0)
124
+
125
+
126
+ def draw_rectangular(ax, m, n, plotted, mode):
127
+ """Draw dB magnitude (mode="db") or phase (mode="phase") of Smn, one line per
128
+ (network, color, linestyle, label) tuple in plotted, into the given ax. No
129
+ per-axis legend - every subplot shows the same file set, so redraw_plot()
130
+ draws one shared legend for the whole figure instead."""
131
+ func = dB if mode == "db" else phase_deg
132
+ label_prefix = 'dB' if mode == "db" else 'phase'
133
+ for network, color, linestyle, label in plotted:
134
+ data = func(Sxx(network, m, n))
135
+ freq = network.frequency.f / 1e9
136
+ if len(freq) == 1:
137
+ # a single frequency point has no line to draw between points and
138
+ # would otherwise be invisible - mark it with a fat dot instead
139
+ ax.plot(freq, data, color=color, linestyle=linestyle, label=label,
140
+ marker='o', markersize=SINGLE_POINT_MARKERSIZE)
141
+ else:
142
+ ax.plot(freq, data, color=color, linestyle=linestyle, label=label)
143
+ ax.set_xlabel("Frequency (GHz)")
144
+ ax.set_ylabel(f"{label_prefix} S{m}{n}")
145
+ ax.set_xmargin(0)
146
+ if mode == "phase":
147
+ # phase_deg() is np.angle(..., deg=True), always wrapped to (-180, 180] -
148
+ # fix the axis to that full range with a clean 45-degree grid instead of
149
+ # leaving it to matplotlib's autoscale, which picks an arbitrary spacing
150
+ # (e.g. 50/100) that doesn't divide the natural -180..180 range evenly
151
+ ax.set_ylim(-180, 180)
152
+ ax.yaxis.set_major_locator(MultipleLocator(45))
153
+ ax.grid()
154
+
155
+
156
+ def draw_smith(ax, m, n, plotted, zoomed):
157
+ """Draw a Smith chart (or zoomed Smith chart) of reflection parameter Smm/Snn,
158
+ one trace per (network, color, linestyle, label) tuple in plotted, into ax."""
159
+ if zoomed:
160
+ draw_zoomed_smith_grid(ax, ZOOM_GAMMA)
161
+ for network, color, linestyle, label in plotted:
162
+ data = Sxx(network, m, n)
163
+ if len(data) == 1:
164
+ # a single frequency point has no line to draw between points
165
+ # and would otherwise be invisible - mark it with a fat dot
166
+ ax.plot(data.real, data.imag, color=color, linestyle=linestyle,
167
+ label=label, marker='o', markersize=SINGLE_POINT_MARKERSIZE)
168
+ else:
169
+ ax.plot(data.real, data.imag, color=color, linestyle=linestyle, label=label)
170
+ ax.set_xlim(-ZOOM_GAMMA, ZOOM_GAMMA)
171
+ ax.set_ylim(-ZOOM_GAMMA, ZOOM_GAMMA)
172
+ ax.set_xticks([])
173
+ ax.set_yticks([])
174
+ else:
175
+ for network, color, linestyle, label in plotted:
176
+ marker_kwargs = {'marker': 'o', 'markersize': SINGLE_POINT_MARKERSIZE} if Sxx(network, m, n).size == 1 else {}
177
+ network.plot_s_smith(m-1, n-1, ax=ax, show_legend=False, draw_labels=True,
178
+ color=color, linestyle=linestyle, label=label, **marker_kwargs)
179
+ # skrf draws the grid circles with hardcoded colors (black for r=0/1, x=+-1);
180
+ # recolor them to match the zoomed chart's uniform light grid style
181
+ for patch in ax.patches:
182
+ patch.set_edgecolor(GRID_COLOR)
183
+ patch.set_linewidth(GRID_LW)
184
+
185
+ ax.set_title(f"S{m}{n}")
186
+ ax.set_aspect('equal')
187
+ # no per-axis legend here - every Smith column shows the same file set, so
188
+ # redraw_plot() draws one shared legend for the whole figure instead (a
189
+ # per-axis legend placed outside each small subplot got clipped by the
190
+ # figure edge or the next subplot once more than one column was shown)
191
+
192
+
193
+ def find_touchstone_files(target_dir):
194
+ """Recursively find every Touchstone (.sNp, any port count) file at or below
195
+ target_dir. Returns a sorted list of absolute paths, or [] for any bad input
196
+ (empty/missing directory) - never raises."""
197
+ if not target_dir or not os.path.isdir(target_dir):
198
+ return []
199
+ matches = []
200
+ for root, dirnames, files in os.walk(target_dir):
201
+ dirnames[:] = [d for d in dirnames if not d.startswith('.')]
202
+ for fn in files:
203
+ if TOUCHSTONE_RE.search(fn):
204
+ matches.append(os.path.join(root, fn))
205
+ return sorted(matches)
206
+
207
+
208
+ # ------------------------------------------------------------------
209
+ # Result Viewer window
210
+ # ------------------------------------------------------------------
211
+
212
+ class ResultViewerWindow(QDialog):
213
+ """Own top-level window (no Qt parent, WA_DeleteOnClose - same lifecycle as
214
+ StackupEditorWindow in stackupEditor.py) that lists Touchstone files under
215
+ MainWindow.saved_values['sim_path'] and plots the ones checked."""
216
+
217
+ def __init__(self, MainWindow):
218
+ super().__init__()
219
+ self.setAttribute(Qt.WA_DeleteOnClose)
220
+ self.MainWindow = MainWindow
221
+
222
+ self._target_dir = ''
223
+ self._master_files = [] # sorted absolute paths, last scan
224
+ self._checked_paths = set() # subset of _master_files currently checked
225
+ self._checked_params = {(1, 1)} # set of (m, n) S-parameters to plot
226
+ self._network_cache = {} # path -> (mtime, skrf.Network | None)
227
+ self._last_n = None # common port count as of last parameter-grid rebuild
228
+ self.smith_mode = "phase" # "phase" | "smith" | "zoom"
229
+ self._updating_checks = False # re-entrancy guard for group<->leaf checkbox propagation
230
+
231
+ self._build_ui()
232
+ self._rescan_files()
233
+
234
+ # ---------- UI construction ----------
235
+
236
+ def _build_ui(self):
237
+ self.setWindowTitle("Result Viewer")
238
+ self.resize(1200, 800)
239
+ main_layout = QVBoxLayout(self)
240
+
241
+ controls_layout = QHBoxLayout()
242
+
243
+ files_group = QGroupBox("Files")
244
+ files_layout = QVBoxLayout()
245
+ filter_layout = QHBoxLayout()
246
+ self.include_dc_cb = QCheckBox("Include _dc files")
247
+ self.include_dc_cb.setChecked(False) # start showing only the raw result file
248
+ self.include_dc_cb.toggled.connect(self._rescan_files)
249
+ filter_layout.addWidget(self.include_dc_cb)
250
+ self.include_deembedded_cb = QCheckBox("Include _deembedded files")
251
+ self.include_deembedded_cb.setChecked(False) # start showing only the raw result file
252
+ self.include_deembedded_cb.toggled.connect(self._rescan_files)
253
+ filter_layout.addWidget(self.include_deembedded_cb)
254
+ filter_layout.addStretch()
255
+ files_layout.addLayout(filter_layout)
256
+ self.file_list = QTreeWidget()
257
+ self.file_list.setHeaderHidden(True)
258
+ self.file_list.itemChanged.connect(self._on_file_item_changed)
259
+ files_layout.addWidget(self.file_list)
260
+ files_group.setLayout(files_layout)
261
+ controls_layout.addWidget(files_group, 2)
262
+
263
+ param_group = QGroupBox("S-Parameters")
264
+ self.param_grid_layout = QGridLayout()
265
+ param_group.setLayout(self.param_grid_layout)
266
+ controls_layout.addWidget(param_group, 1)
267
+
268
+ display_group = QGroupBox("Display")
269
+ display_layout = QVBoxLayout()
270
+ self.phase_radio = QRadioButton("dB + Phase")
271
+ self.smith_radio = QRadioButton("Smith chart")
272
+ self.zoom_radio = QRadioButton("Smith chart (zoomed)")
273
+ self.phase_radio.setChecked(True)
274
+ self.display_button_group = QButtonGroup(self)
275
+ for rb in (self.phase_radio, self.smith_radio, self.zoom_radio):
276
+ self.display_button_group.addButton(rb)
277
+ display_layout.addWidget(rb)
278
+ rb.toggled.connect(self._on_mode_changed)
279
+ self.refresh_btn = QPushButton("Refresh")
280
+ self.refresh_btn.clicked.connect(self._rescan_files)
281
+ display_layout.addWidget(self.refresh_btn)
282
+ self.warning_label = QLabel("")
283
+ self.warning_label.setWordWrap(True)
284
+ self.warning_label.setStyleSheet("color: #b00000;")
285
+ display_layout.addWidget(self.warning_label)
286
+ display_group.setLayout(display_layout)
287
+ controls_layout.addWidget(display_group, 1)
288
+
289
+ main_layout.addLayout(controls_layout)
290
+
291
+ # constrained layout (not tight_layout()) recomputes margins on every draw,
292
+ # including window resizes - tight_layout() only computes them once at the
293
+ # call site and goes stale (clipped axis labels) as the Qt widget is resized
294
+ self.figure = Figure(figsize=(10, 6), layout='constrained')
295
+ self.canvas = FigureCanvasQTAgg(self.figure)
296
+ self.canvas.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
297
+ self.nav_toolbar = NavigationToolbar2QT(self.canvas, self)
298
+ main_layout.addWidget(self.nav_toolbar)
299
+ main_layout.addWidget(self.canvas, 1)
300
+
301
+ # ---------- Qt event hooks ----------
302
+
303
+ def showEvent(self, event):
304
+ super().showEvent(event)
305
+ self._rescan_files()
306
+
307
+ # ---------- File list ----------
308
+
309
+ def _relpath_for_path(self, path):
310
+ """Full relative-to-target-dir path, untruncated - used for the file list,
311
+ which has room to show it in full (or scroll) rather than shortening it."""
312
+ rel = os.path.relpath(path, self._target_dir) if self._target_dir else path
313
+ return rel.replace('\\', '/')
314
+
315
+ def _legend_label_for_path(self, path):
316
+ """Shortened label for plot legends, which have much less room than the
317
+ file list."""
318
+ rel = self._relpath_for_path(path)
319
+ if len(rel) <= 17:
320
+ return rel
321
+ return rel[:10] + '..' + rel[-20:]
322
+
323
+ def _filtered_files(self, files):
324
+ include_dc = self.include_dc_cb.isChecked()
325
+ include_deembedded = self.include_deembedded_cb.isChecked()
326
+ result = []
327
+ for path in files:
328
+ name = os.path.basename(path)
329
+ if not include_dc and '_dc' in name:
330
+ continue
331
+ if not include_deembedded and '_deembedded' in name:
332
+ continue
333
+ result.append(path)
334
+ return result
335
+
336
+ def _rescan_files(self):
337
+ saved_values = self.MainWindow.saved_values
338
+ target_dir = saved_values.get('sim_path', '') if isinstance(saved_values, dict) else ''
339
+ self._target_dir = target_dir.replace('\\', '/') if target_dir else ''
340
+
341
+ self.file_list.blockSignals(True)
342
+ self.file_list.clear()
343
+
344
+ if not target_dir:
345
+ self._master_files = []
346
+ item = QTreeWidgetItem(["No Target Directory set (see Create Model tab)."])
347
+ item.setFlags(Qt.NoItemFlags)
348
+ self.file_list.addTopLevelItem(item)
349
+ elif not os.path.isdir(target_dir):
350
+ self._master_files = []
351
+ item = QTreeWidgetItem([f"Target Directory does not exist: {target_dir}"])
352
+ item.setFlags(Qt.NoItemFlags)
353
+ self.file_list.addTopLevelItem(item)
354
+ else:
355
+ all_files = find_touchstone_files(target_dir)
356
+ self._master_files = self._filtered_files(all_files)
357
+ if not self._master_files:
358
+ if all_files:
359
+ message = "No files match the current _dc/_deembedded filters " \
360
+ f"under {target_dir}"
361
+ else:
362
+ message = f"No Touchstone (.sNp) files found under {target_dir}"
363
+ item = QTreeWidgetItem([message])
364
+ item.setFlags(Qt.NoItemFlags)
365
+ self.file_list.addTopLevelItem(item)
366
+ else:
367
+ # drop checked paths that no longer exist; auto-check the newest
368
+ # file if nothing is checked (e.g. first open), so the window
369
+ # isn't blank
370
+ self._checked_paths &= set(self._master_files)
371
+ if not self._checked_paths:
372
+ newest = max(self._master_files, key=os.path.getmtime)
373
+ self._checked_paths = {newest}
374
+
375
+ # group by each file's immediate parent directory (relative to
376
+ # target_dir), not a Palace/Elmer-specific convention like
377
+ # <model>_data, so this stays correct for either output layout.
378
+ # Files sitting directly in target_dir (parent == "") get no
379
+ # wrapper group node - they're added straight to the tree.
380
+ groups = {}
381
+ for path in self._master_files:
382
+ parent = os.path.dirname(self._relpath_for_path(path))
383
+ groups.setdefault(parent, []).append(path)
384
+
385
+ for path in groups.pop("", []):
386
+ self.file_list.addTopLevelItem(self._make_file_item(path))
387
+ for parent in sorted(groups):
388
+ group_item = QTreeWidgetItem([parent])
389
+ # checkable so the whole group can be checked/unchecked at once
390
+ # (propagated to/from its children in _on_file_item_changed);
391
+ # not given Qt.ItemIsAutoTristate - propagation is done manually
392
+ # below so exactly one _on_control_changed()/redraw happens per
393
+ # user action, not one per child
394
+ group_item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsUserCheckable)
395
+ self.file_list.addTopLevelItem(group_item)
396
+ for path in groups[parent]:
397
+ group_item.addChild(self._make_file_item(path))
398
+ self._refresh_group_checkstate(group_item)
399
+
400
+ self.file_list.expandAll()
401
+
402
+ self.file_list.blockSignals(False)
403
+ self._on_control_changed()
404
+
405
+ def _make_file_item(self, path):
406
+ item = QTreeWidgetItem([os.path.basename(path)])
407
+ item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
408
+ item.setData(0, Qt.UserRole, path)
409
+ item.setCheckState(0, Qt.Checked if path in self._checked_paths else Qt.Unchecked)
410
+ return item
411
+
412
+ def _on_file_item_changed(self, item, column=0):
413
+ if self._updating_checks:
414
+ return # this change is itself a propagation side-effect below - ignore it
415
+ path = item.data(0, Qt.UserRole)
416
+ # placeholder rows are never checkable, so itemChanged never fires for them;
417
+ # anything checkable with no path data here is a group header
418
+ is_group = path is None
419
+
420
+ self._updating_checks = True
421
+ try:
422
+ if is_group:
423
+ state = item.checkState(0)
424
+ for i in range(item.childCount()):
425
+ child = item.child(i)
426
+ child.setCheckState(0, state)
427
+ child_path = child.data(0, Qt.UserRole)
428
+ if state == Qt.Checked:
429
+ self._checked_paths.add(child_path)
430
+ else:
431
+ self._checked_paths.discard(child_path)
432
+ else:
433
+ if item.checkState(0) == Qt.Checked:
434
+ self._checked_paths.add(path)
435
+ else:
436
+ self._checked_paths.discard(path)
437
+ parent = item.parent()
438
+ if parent is not None:
439
+ self._refresh_group_checkstate(parent)
440
+ finally:
441
+ self._updating_checks = False
442
+
443
+ self._on_control_changed()
444
+
445
+ def _refresh_group_checkstate(self, group_item):
446
+ """Set a group header's own checkbox to reflect its children: checked if
447
+ all children are checked, unchecked if none are, partially-checked if mixed."""
448
+ states = [group_item.child(i).checkState(0) for i in range(group_item.childCount())]
449
+ if all(s == Qt.Checked for s in states):
450
+ group_item.setCheckState(0, Qt.Checked)
451
+ elif all(s == Qt.Unchecked for s in states):
452
+ group_item.setCheckState(0, Qt.Unchecked)
453
+ else:
454
+ group_item.setCheckState(0, Qt.PartiallyChecked)
455
+
456
+ # ---------- Network loading ----------
457
+
458
+ def _load_network_cached(self, path):
459
+ try:
460
+ mtime = os.path.getmtime(path)
461
+ except OSError:
462
+ return None
463
+ cached = self._network_cache.get(path)
464
+ if cached is not None and cached[0] == mtime:
465
+ return cached[1]
466
+ try:
467
+ network = rf.Network(path)
468
+ except Exception:
469
+ network = None
470
+ self._network_cache[path] = (mtime, network)
471
+ return network
472
+
473
+ def _get_checked_plotted(self):
474
+ """Return [(network, color, linestyle, label), ...] for currently checked,
475
+ successfully-loaded files, and update the warning label for any that failed
476
+ to load. Color/linestyle are keyed to position among the checked, loaded
477
+ files (master-list order), so the first curve is always COLORS[0]/
478
+ LINESTYLES[0] (red, solid) - matches plot_snp.py's own convention. Colors
479
+ can shift for other files as check state changes; that trade-off is
480
+ accepted so "the first curve" always means red/solid."""
481
+ checked_in_order = [path for path in self._master_files if path in self._checked_paths]
482
+ networks = []
483
+ warnings = []
484
+ for path in checked_in_order:
485
+ network = self._load_network_cached(path)
486
+ label = self._legend_label_for_path(path)
487
+ if network is None:
488
+ warnings.append(label)
489
+ else:
490
+ networks.append((network, label))
491
+ self.warning_label.setText("Failed to load: " + ", ".join(warnings) if warnings else "")
492
+ return [
493
+ (network, COLORS[i % len(COLORS)], LINESTYLES[i % len(LINESTYLES)], label)
494
+ for i, (network, label) in enumerate(networks)
495
+ ]
496
+
497
+ def _current_common_nports(self):
498
+ plotted = self._get_checked_plotted()
499
+ if not plotted:
500
+ return 0
501
+ return min(network.nports for network, _, _, _ in plotted)
502
+
503
+ # ---------- S-parameter picker ----------
504
+
505
+ def _rebuild_parameter_grid(self, n):
506
+ while self.param_grid_layout.count():
507
+ child = self.param_grid_layout.takeAt(0)
508
+ widget = child.widget()
509
+ if widget is not None:
510
+ widget.deleteLater()
511
+
512
+ self._checked_params = {(m, k) for (m, k) in self._checked_params if m <= n and k <= n}
513
+ if n >= 1 and not self._checked_params:
514
+ self._checked_params = {(1, 1)} # never let the plot go silently empty
515
+
516
+ if n == 0:
517
+ self.param_grid_layout.addWidget(QLabel("Check a file to choose S-parameters"), 0, 0)
518
+ else:
519
+ for m in range(1, n + 1):
520
+ for k in range(1, n + 1):
521
+ btn = QPushButton(f"S{m}{k}")
522
+ btn.setCheckable(True)
523
+ btn.setChecked((m, k) in self._checked_params)
524
+ btn.setFixedWidth(50)
525
+ btn.toggled.connect(lambda checked, m=m, k=k: self._on_param_toggled(m, k, checked))
526
+ self.param_grid_layout.addWidget(btn, m - 1, k - 1)
527
+
528
+ self.redraw_plot()
529
+
530
+ def _on_param_toggled(self, m, n, checked):
531
+ if checked:
532
+ self._checked_params.add((m, n))
533
+ else:
534
+ self._checked_params.discard((m, n))
535
+ self.redraw_plot()
536
+
537
+ # ---------- Display mode ----------
538
+
539
+ def _on_mode_changed(self, checked):
540
+ if not checked:
541
+ return # QButtonGroup fires toggled(False) for the button losing the selection too
542
+ if self.smith_radio.isChecked():
543
+ self.smith_mode = "smith"
544
+ elif self.zoom_radio.isChecked():
545
+ self.smith_mode = "zoom"
546
+ else:
547
+ self.smith_mode = "phase"
548
+ self.redraw_plot()
549
+
550
+ # ---------- Redraw ----------
551
+
552
+ def _on_control_changed(self):
553
+ n = self._current_common_nports()
554
+ if n != self._last_n:
555
+ self._last_n = n
556
+ self._rebuild_parameter_grid(n) # rebuilds grid and redraws
557
+ else:
558
+ self.redraw_plot()
559
+
560
+ def redraw_plot(self):
561
+ self.figure.clear()
562
+ plotted = self._get_checked_plotted() # also sets warning_label for load failures
563
+ params = sorted(self._checked_params)
564
+
565
+ if not plotted or not params:
566
+ ax = self.figure.add_subplot(111)
567
+ ax.axis('off')
568
+ ax.text(0.5, 0.5, "Check a file and at least one S-parameter to plot",
569
+ ha='center', va='center', transform=ax.transAxes)
570
+ self.canvas.draw_idle()
571
+ return
572
+
573
+ if self.smith_mode in ("smith", "zoom"):
574
+ # Smith/zoomed-Smith replaces dB+phase entirely - only reflection (Snn)
575
+ # parameters have a Smith representation, so non-reflection selections
576
+ # are left out of this view (noted in the warning label) rather than
577
+ # shown as an empty/meaningless chart
578
+ reflection_params = [(m, n) for (m, n) in params if m == n]
579
+ excluded = [(m, n) for (m, n) in params if m != n]
580
+ if excluded:
581
+ note = "Not shown in Smith view (not reflection): " + \
582
+ ", ".join(f"S{m}{n}" for m, n in excluded)
583
+ current = self.warning_label.text()
584
+ self.warning_label.setText((current + " " if current else "") + note)
585
+
586
+ if not reflection_params:
587
+ ax = self.figure.add_subplot(111)
588
+ ax.axis('off')
589
+ ax.text(0.5, 0.5, "No reflection (Snn) parameter selected for Smith view",
590
+ ha='center', va='center', transform=ax.transAxes)
591
+ self.canvas.draw_idle()
592
+ return
593
+
594
+ axes = self.figure.subplots(1, len(reflection_params), squeeze=False)
595
+ for a, (m, n) in enumerate(reflection_params):
596
+ draw_smith(axes[0][a], m, n, plotted, zoomed=(self.smith_mode == "zoom"))
597
+ else:
598
+ axes = self.figure.subplots(2, len(params), squeeze=False)
599
+ for a, (m, n) in enumerate(params):
600
+ draw_rectangular(axes[0][a], m, n, plotted, mode="db")
601
+ draw_rectangular(axes[1][a], m, n, plotted, mode="phase")
602
+
603
+ self.figure.suptitle("S Parameters")
604
+ self._draw_shared_legend(plotted)
605
+ self.canvas.draw_idle()
606
+
607
+ def _draw_shared_legend(self, plotted):
608
+ """One legend below the whole figure instead of one per subplot - every
609
+ subplot shows the same file set, and a per-axis legend got clipped by the
610
+ figure edge or a neighboring subplot once more than one column was shown.
611
+ Proxy handles (not pulled from an axis) so this doesn't depend on which
612
+ internal artists a particular draw helper (e.g. skrf's plot_s_smith)
613
+ created."""
614
+ handles = [Line2D([0], [0], color=color, linestyle=linestyle)
615
+ for _, color, linestyle, _ in plotted]
616
+ labels = [label for _, _, _, label in plotted]
617
+ # "outside lower center" (not "lower center") is what makes the constrained
618
+ # layout engine reserve room for this legend below the axes on every draw,
619
+ # instead of the legend floating over/under-clipped by the figure edge
620
+ self.figure.legend(handles, labels, loc='outside lower center',
621
+ ncol=min(len(plotted), 4), fontsize=8)
622
+
623
+
624
+ # ------------------------------------------------------------------
625
+ # Standalone launch (python result_viewer.py [target_dir], or the
626
+ # resultViewer console script - see pyproject.toml)
627
+ # ------------------------------------------------------------------
628
+
629
+ class _StandaloneMainWindow:
630
+ """Minimal stand-in for the real setupEM MainWindow, used only when this module
631
+ is run on its own rather than opened from within the full app (Create Model
632
+ tab's View Results button). Provides just the saved_values dict
633
+ ResultViewerWindow needs from its MainWindow argument."""
634
+ APP_NAME = "Result Viewer"
635
+
636
+ def __init__(self, target_dir):
637
+ self.saved_values = {'sim_path': target_dir}
638
+
639
+
640
+ def main():
641
+ app = QApplication(sys.argv)
642
+ if sys.platform.startswith("win"):
643
+ # matches setupEM.py's/setupThermal.py's main() - without this, Qt's default
644
+ # style on Windows looks visibly different (fonts/widget chrome) from the full app
645
+ app.setStyle(QStyleFactory.create("Windows"))
646
+
647
+ parser = argparse.ArgumentParser(description="Standalone S-parameter result viewer")
648
+ parser.add_argument("target_dir", nargs="?", default=os.getcwd(),
649
+ help="directory to search recursively for Touchstone (.sNp) "
650
+ "files (default: current directory)")
651
+ args = parser.parse_args()
652
+
653
+ window = ResultViewerWindow(_StandaloneMainWindow(args.target_dir))
654
+ window.show()
655
+ sys.exit(app.exec())
656
+
657
+
658
+ if __name__ == "__main__":
659
+ main()
@@ -1126,6 +1126,12 @@ class CreateModelTab(CreateModelTabBase):
1126
1126
  # simulation run apart from a mesh-creation run (self.process is reused for both).
1127
1127
  self._process_purpose = None
1128
1128
 
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.
1131
+ self.view_results_btn = QPushButton("📈 View Results...")
1132
+ self.view_results_btn.clicked.connect(self.MainWindow.open_result_viewer)
1133
+ self.file_layout.addWidget(self.view_results_btn)
1134
+
1129
1135
  def _append_results_summary(self):
1130
1136
  # Palace-only: parse palace.json / error-indicators.csv and append a results summary
1131
1137
  # to the log. Called from on_finished() after a real simulation run.
@@ -1541,6 +1547,9 @@ class MainWindow(MainWindowBase):
1541
1547
  self.dielectrics_list = None
1542
1548
  self.metals_list = None
1543
1549
 
1550
+ # S-parameter Result Viewer window, lazily created - see open_result_viewer()
1551
+ self.result_viewer_window = None
1552
+
1544
1553
  # Do not auto-load default values at this early startup stage,
1545
1554
  # instead this is done from File menu
1546
1555
  # self.user_inputs_file = DEFAULT_SETTINGS_FILE
@@ -1596,6 +1605,27 @@ class MainWindow(MainWindowBase):
1596
1605
  self.mesh_tab.on_meshorder_changed(self.mesh_tab.mesh_order_box.currentText())
1597
1606
 
1598
1607
 
1608
+ def open_result_viewer(self):
1609
+ # local import: matplotlib/skrf are only needed once the viewer is actually
1610
+ # opened, so this keeps them off setupEM's startup path. __package__ is
1611
+ # None/"" when this module was loaded outside the setupEM package (e.g.
1612
+ # setupEM.py run directly), so relative import fails - same dual-mode
1613
+ # pattern used throughout this file/setup_common.py for sibling imports.
1614
+ if __package__ in (None, ""):
1615
+ from result_viewer import ResultViewerWindow
1616
+ else:
1617
+ from .result_viewer import ResultViewerWindow
1618
+
1619
+ if self.result_viewer_window is not None:
1620
+ self.result_viewer_window.raise_()
1621
+ self.result_viewer_window.activateWindow()
1622
+ return
1623
+
1624
+ self.result_viewer_window = ResultViewerWindow(self)
1625
+ self.result_viewer_window.destroyed.connect(lambda: setattr(self, "result_viewer_window", None))
1626
+ self.result_viewer_window.show()
1627
+
1628
+
1599
1629
  def show_version(self):
1600
1630
  setupEM_version = importlib.metadata.version("setupEM")
1601
1631
  gds2palace_version = importlib.metadata.version("gds2palace")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: setupEM
3
- Version: 0.3.16
3
+ Version: 0.4.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,6 +21,7 @@ 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
24
25
  - **setupThermal**, a companion app for Elmer thermal simulation, alongside setupEM
25
26
  - A graphical **Stackup XML Editor** (Tools > Edit Stackup XML...), including Variables, Reference-relative positioning, Derived Layers, and Thermal Tables
26
27
  - Input Files tab can now **override stackup Variables** (e.g. `total_thickness`) directly, without hand-editing the XML or the generated model
@@ -205,6 +206,30 @@ To start simulation on Windows from the WSL terminal, type
205
206
 
206
207
  To **create Touchstone SnP output** from simulation results, please have a look at the scripts directory. Script "combine_snp" runs Python code "combine_extend_snp.py", which scans your directories (working directory and below) and converts simulation results to Touchstone file format. Supported input file format: Palace and Elmer S-parameter data.
207
208
 
209
+ ## Result Viewer
210
+
211
+ Once you have Touchstone SnP results, click **View Results...** on the Create Model tab to open the built-in **Result Viewer** - no need to run the standalone `plot_snp.py` script by hand.
212
+
213
+ <img src="https://raw.githubusercontent.com/VolkerMuehlhaus/setupEM/main/doc/png/resultviewer_button.png" alt="view results button" width="700">
214
+
215
+ The Result Viewer recursively scans the Target Directory for `.sNp` Touchstone files and lists them in a tree, grouped by the folder each file came from. Check individual files to overlay them, or check/uncheck a whole run's group entry to select or deselect every file below it at once.
216
+ The **Include _dc files** / **Include _deembedded files** checkboxes filter out DC-extrapolated and de-embedded variants created by `combine_extend_snp.py`, so you can start with just the raw result and bring in the others only when needed.
217
+
218
+ <img src="https://raw.githubusercontent.com/VolkerMuehlhaus/setupEM/main/doc/png/resultviewer1.png" alt="result viewer" width="750">
219
+
220
+ Pick which S-parameters to plot from the S-Parameters grid.
221
+ Checking files across several runs, or a whole run group, overlays all of them at once:
222
+
223
+ <img src="https://raw.githubusercontent.com/VolkerMuehlhaus/setupEM/main/doc/png/resultviewer2.png" alt="result viewer multi-file overlay" width="750">
224
+
225
+ For reflection parameters (S11, S22, ...), the Display panel can switch to a **Smith chart** or a **zoomed Smith chart** instead of dB+phase.
226
+
227
+ <img src="https://raw.githubusercontent.com/VolkerMuehlhaus/setupEM/main/doc/png/resultviewer3.png" alt="result viewer smith chart" width="750">
228
+
229
+ The matplotlib toolbar above the plot (pan/zoom/save as PNG) works as usual. A file with only a single simulated frequency point is marked with a dot instead of a line, since there is nothing to draw a line between.
230
+
231
+ 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
+
208
233
  ## Code
209
234
  Behind the scenes, the setupEM user interface created Python model code for gds2palace, and you can check the resulting code on the "Code" tab.
210
235
 
@@ -6,6 +6,7 @@ src/setupEM/__init__.py
6
6
  src/setupEM/__main__.py
7
7
  src/setupEM/momentum_import.py
8
8
  src/setupEM/palace_results.py
9
+ src/setupEM/result_viewer.py
9
10
  src/setupEM/setupEM.py
10
11
  src/setupEM/setupThermal.py
11
12
  src/setupEM/setup_common.py
@@ -1,4 +1,5 @@
1
1
  [console_scripts]
2
+ resultViewer = setupEM.result_viewer:main
2
3
  setupEM = setupEM.setupEM:main
3
4
  setupThermal = setupEM.setupThermal:main
4
5
  stackupEditor = setupEM.stackupEditor:main
File without changes
File without changes