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.
- {setupem-0.3.16/src/setupEM.egg-info → setupem-0.4.0}/PKG-INFO +26 -1
- {setupem-0.3.16 → setupem-0.4.0}/README.md +25 -0
- {setupem-0.3.16 → setupem-0.4.0}/README_pypi.md +25 -0
- {setupem-0.3.16 → setupem-0.4.0}/pyproject.toml +1 -0
- {setupem-0.3.16 → setupem-0.4.0}/src/setupEM/__init__.py +1 -1
- setupem-0.4.0/src/setupEM/result_viewer.py +659 -0
- {setupem-0.3.16 → setupem-0.4.0}/src/setupEM/setupEM.py +30 -0
- {setupem-0.3.16 → setupem-0.4.0/src/setupEM.egg-info}/PKG-INFO +26 -1
- {setupem-0.3.16 → setupem-0.4.0}/src/setupEM.egg-info/SOURCES.txt +1 -0
- {setupem-0.3.16 → setupem-0.4.0}/src/setupEM.egg-info/entry_points.txt +1 -0
- {setupem-0.3.16 → setupem-0.4.0}/LICENSE +0 -0
- {setupem-0.3.16 → setupem-0.4.0}/setup.cfg +0 -0
- {setupem-0.3.16 → setupem-0.4.0}/src/setupEM/__main__.py +0 -0
- {setupem-0.3.16 → setupem-0.4.0}/src/setupEM/data/SG13G2_100um.xml +0 -0
- {setupem-0.3.16 → setupem-0.4.0}/src/setupEM/data/SG13G2_200um.xml +0 -0
- {setupem-0.3.16 → setupem-0.4.0}/src/setupEM/data/SG13G2_200um_with_parameters.xml +0 -0
- {setupem-0.3.16 → setupem-0.4.0}/src/setupEM/data/SG13G2_nosub.xml +0 -0
- {setupem-0.3.16 → setupem-0.4.0}/src/setupEM/momentum_import.py +0 -0
- {setupem-0.3.16 → setupem-0.4.0}/src/setupEM/palace_results.py +0 -0
- {setupem-0.3.16 → setupem-0.4.0}/src/setupEM/setupThermal.py +0 -0
- {setupem-0.3.16 → setupem-0.4.0}/src/setupEM/setup_common.py +0 -0
- {setupem-0.3.16 → setupem-0.4.0}/src/setupEM/stackupEditor.py +0 -0
- {setupem-0.3.16 → setupem-0.4.0}/src/setupEM/stackup_writer.py +0 -0
- {setupem-0.3.16 → setupem-0.4.0}/src/setupEM.egg-info/dependency_links.txt +0 -0
- {setupem-0.3.16 → setupem-0.4.0}/src/setupEM.egg-info/requires.txt +0 -0
- {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
|
+
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__"}
|
|
@@ -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
|
+
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
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|