setupEM 0.5.0__tar.gz → 0.5.1__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.5.0/src/setupEM.egg-info → setupem-0.5.1}/PKG-INFO +1 -1
  2. {setupem-0.5.0 → setupem-0.5.1}/src/setupEM/__init__.py +1 -1
  3. {setupem-0.5.0 → setupem-0.5.1}/src/setupEM/palace_results.py +136 -10
  4. {setupem-0.5.0 → setupem-0.5.1}/src/setupEM/setup_common.py +36 -0
  5. {setupem-0.5.0 → setupem-0.5.1/src/setupEM.egg-info}/PKG-INFO +1 -1
  6. {setupem-0.5.0 → setupem-0.5.1}/LICENSE +0 -0
  7. {setupem-0.5.0 → setupem-0.5.1}/README.md +0 -0
  8. {setupem-0.5.0 → setupem-0.5.1}/README_pypi.md +0 -0
  9. {setupem-0.5.0 → setupem-0.5.1}/pyproject.toml +0 -0
  10. {setupem-0.5.0 → setupem-0.5.1}/setup.cfg +0 -0
  11. {setupem-0.5.0 → setupem-0.5.1}/src/setupEM/__main__.py +0 -0
  12. {setupem-0.5.0 → setupem-0.5.1}/src/setupEM/data/SG13G2_100um.xml +0 -0
  13. {setupem-0.5.0 → setupem-0.5.1}/src/setupEM/data/SG13G2_200um.xml +0 -0
  14. {setupem-0.5.0 → setupem-0.5.1}/src/setupEM/data/SG13G2_200um_with_parameters.xml +0 -0
  15. {setupem-0.5.0 → setupem-0.5.1}/src/setupEM/data/SG13G2_nosub.xml +0 -0
  16. {setupem-0.5.0 → setupem-0.5.1}/src/setupEM/momentum_import.py +0 -0
  17. {setupem-0.5.0 → setupem-0.5.1}/src/setupEM/result_viewer.py +0 -0
  18. {setupem-0.5.0 → setupem-0.5.1}/src/setupEM/setupEM.py +0 -0
  19. {setupem-0.5.0 → setupem-0.5.1}/src/setupEM/setupThermal.py +0 -0
  20. {setupem-0.5.0 → setupem-0.5.1}/src/setupEM/stackupEditor.py +0 -0
  21. {setupem-0.5.0 → setupem-0.5.1}/src/setupEM/stackup_writer.py +0 -0
  22. {setupem-0.5.0 → setupem-0.5.1}/src/setupEM.egg-info/SOURCES.txt +0 -0
  23. {setupem-0.5.0 → setupem-0.5.1}/src/setupEM.egg-info/dependency_links.txt +0 -0
  24. {setupem-0.5.0 → setupem-0.5.1}/src/setupEM.egg-info/entry_points.txt +0 -0
  25. {setupem-0.5.0 → setupem-0.5.1}/src/setupEM.egg-info/requires.txt +0 -0
  26. {setupem-0.5.0 → setupem-0.5.1}/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.5.0
3
+ Version: 0.5.1
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
@@ -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.5.0"
27
+ __version__ = "0.5.1"
@@ -25,6 +25,8 @@ import os
25
25
  import json
26
26
  import csv
27
27
  import re
28
+ import math
29
+ import cmath
28
30
 
29
31
  _ITERATION_RE = re.compile(r'^iteration(\d+)$')
30
32
 
@@ -79,14 +81,121 @@ def _read_error_indicators(dir_path):
79
81
  try:
80
82
  return {
81
83
  'norm': float(fields['Norm']),
82
- 'minimum': float(fields['Minimum']),
83
84
  'maximum': float(fields['Maximum']),
84
- 'mean': float(fields['Mean']),
85
85
  }
86
86
  except (KeyError, ValueError):
87
87
  return None
88
88
 
89
89
 
90
+ def _parse_palace_port_s_csv(path):
91
+ """Parse a Palace port-S.csv file into (freq_list, S_dB_list, S_arg_list,
92
+ num_ports), where S_dB_list/S_arg_list are one dict-per-frequency mapping
93
+ 'i j' port-pair strings to their |S| (dB) / phase (deg) string values.
94
+ """
95
+ freq = []
96
+ S_dB = []
97
+ S_arg = []
98
+ params = []
99
+ num_ports = 0
100
+
101
+ with open(path, 'r', encoding='utf-8') as f:
102
+ for line in f:
103
+ aline = line.rstrip()
104
+ aline = aline.replace(",", "")
105
+ aline = aline.replace("(dB)", "")
106
+ aline = aline.replace("(deg.)", "")
107
+
108
+ if 'Hz)' in aline:
109
+ # header line: items are like |S[1][1]| arg(S[1][1])
110
+ items = aline.split()
111
+ for item in items:
112
+ if '|' in item:
113
+ Sxx = item.replace('|S', '').replace('|', '')
114
+ Sxx = Sxx.replace('][', ' ').replace('[', '').replace(']', '')
115
+ params.append(Sxx)
116
+ a, b = (int(x) for x in Sxx.split())
117
+ num_ports = max(num_ports, a, b)
118
+ continue
119
+
120
+ items = aline.split()
121
+ if not items:
122
+ continue
123
+ dB = {}
124
+ arg = {}
125
+ for param in params:
126
+ dB_index = 2 * params.index(param) + 1
127
+ arg_index = dB_index + 1
128
+ dB[param] = items[dB_index]
129
+ arg[param] = items[arg_index]
130
+ freq.append(items[0])
131
+ S_dB.append(dB)
132
+ S_arg.append(arg)
133
+
134
+ return freq, S_dB, S_arg, num_ports
135
+
136
+
137
+ def _read_port_s_data(dir_path):
138
+ """Parse dir_path/port-S.csv (if present), or None if the file is
139
+ missing, unfinished, or malformed (e.g. a still-running pass).
140
+ """
141
+ path = os.path.join(dir_path, 'port-S.csv')
142
+ if not os.path.isfile(path):
143
+ return None
144
+ try:
145
+ freq, S_dB, S_arg, num_ports = _parse_palace_port_s_csv(path)
146
+ except (OSError, ValueError, IndexError):
147
+ return None
148
+ if not freq:
149
+ return None
150
+ return freq, S_dB, S_arg, num_ports
151
+
152
+
153
+ def _max_delta_s(prev_data, curr_data):
154
+ """Max |S_curr - S_prev| across all ports and frequencies common to both
155
+ passes, mirroring HFSS's per-pass Max Delta S. Returns None if either
156
+ pass has no port-S data, or the two use different port counts.
157
+ """
158
+ if prev_data is None or curr_data is None:
159
+ return None
160
+ freq_p, dB_p, arg_p, ports_p = prev_data
161
+ freq_c, dB_c, arg_c, ports_c = curr_data
162
+ if ports_p != ports_c:
163
+ return None
164
+
165
+ max_delta = 0.0
166
+ found_any = False
167
+ for idx in range(min(len(freq_p), len(freq_c))):
168
+ for key in set(dB_p[idx]) & set(dB_c[idx]):
169
+ try:
170
+ Sp = cmath.rect(10 ** (float(dB_p[idx][key]) / 20.0), math.radians(float(arg_p[idx][key])))
171
+ Sc = cmath.rect(10 ** (float(dB_c[idx][key]) / 20.0), math.radians(float(arg_c[idx][key])))
172
+ except ValueError:
173
+ continue
174
+ found_any = True
175
+ max_delta = max(max_delta, abs(Sc - Sp))
176
+ return max_delta if found_any else None
177
+
178
+
179
+ def _collect_amr_rows(output_dir, iteration_dirs):
180
+ """One row per AMR iteration subfolder, plus the root output_dir as
181
+ 'Final': (label, palace_json_dict_or_None, error_indicators_dict_or_None,
182
+ max_delta_s_vs_previous_row_or_None).
183
+ """
184
+ dirs_in_order = list(iteration_dirs) + [output_dir]
185
+ labels = [os.path.basename(d) for d in iteration_dirs] + ["Final"]
186
+
187
+ rows = []
188
+ prev_port_s = None
189
+ for label, d in zip(labels, dirs_in_order):
190
+ summary = _read_palace_json(d)
191
+ errors = _read_error_indicators(d)
192
+ port_s = _read_port_s_data(d)
193
+ delta_s = _max_delta_s(prev_port_s, port_s)
194
+ rows.append((label, summary, errors, delta_s))
195
+ prev_port_s = port_s
196
+ return rows
197
+
198
+
90
199
  def _list_iteration_dirs(output_dir):
91
200
  if not os.path.isdir(output_dir):
92
201
  return []
@@ -128,6 +237,22 @@ def _format_sci(x):
128
237
  return 'n/a' if x is None else f"{x:.3e}"
129
238
 
130
239
 
240
+ def _format_delta(x):
241
+ return 'n/a' if x is None else f"{x:.4f}"
242
+
243
+
244
+ def _save_convergence_summary(run_path, summary_text):
245
+ """Save the AMR summary table alongside the run's config.json /
246
+ port_information.json, so it's available on disk without re-running the
247
+ simulation or digging through the setupEM log.
248
+ """
249
+ try:
250
+ with open(os.path.join(run_path, "mesh_convergence_summary.txt"), "w", encoding="utf-8") as f:
251
+ f.write(summary_text + "\n")
252
+ except OSError:
253
+ pass
254
+
255
+
131
256
  def build_results_summary(run_path, model_basename):
132
257
  """Return a formatted multi-line summary of Palace results found under run_path,
133
258
  or an explanatory message if nothing is there yet.
@@ -155,19 +280,18 @@ def build_results_summary(run_path, model_basename):
155
280
  if errors:
156
281
  lines.append(
157
282
  f"Error indicator : Norm={_format_sci(errors['norm'])} "
158
- f"Max={_format_sci(errors['maximum'])} Mean={_format_sci(errors['mean'])}"
283
+ f"Max={_format_sci(errors['maximum'])}"
159
284
  )
160
285
  lines.append("=" * 40)
161
286
  return "\n".join(lines)
162
287
 
163
288
  # Adaptive mesh refinement: one row per iteration subfolder, plus the root as "Final"
164
- rows = [(os.path.basename(it_dir), _read_palace_json(it_dir), _read_error_indicators(it_dir))
165
- for it_dir in iteration_dirs]
166
- rows.append(("Final", _read_palace_json(output_dir), _read_error_indicators(output_dir)))
289
+ rows = _collect_amr_rows(output_dir, iteration_dirs)
167
290
 
168
- headers = ["Iteration", "DOF", "Mesh elems", "Error Norm", "Error Max", "Error Mean", "Time", "Peak RAM"]
291
+ headers = ["Iteration", "DOF", "Mesh elems", "Error Norm", "Error Max",
292
+ "Max dS", "Time", "Peak RAM"]
169
293
  table_rows = []
170
- for label, summary, errors in rows:
294
+ for label, summary, errors, delta_s in rows:
171
295
  summary = summary or {}
172
296
  errors = errors or {}
173
297
  table_rows.append([
@@ -176,7 +300,7 @@ def build_results_summary(run_path, model_basename):
176
300
  _format_int(summary.get('mesh_elements')),
177
301
  _format_sci(errors.get('norm')),
178
302
  _format_sci(errors.get('maximum')),
179
- _format_sci(errors.get('mean')),
303
+ _format_delta(delta_s),
180
304
  _format_duration(summary.get('duration_s')),
181
305
  _format_ram(summary.get('peak_ram_mb')),
182
306
  ])
@@ -196,4 +320,6 @@ def build_results_summary(run_path, model_basename):
196
320
  for row in table_rows:
197
321
  lines.append(fmt_row(row))
198
322
  lines.append("=" * len(header_line))
199
- return "\n".join(lines)
323
+ summary_text = "\n".join(lines)
324
+ _save_convergence_summary(run_path, summary_text)
325
+ return summary_text
@@ -1411,6 +1411,42 @@ class MainWindowBase(QMainWindow):
1411
1411
 
1412
1412
  TAB_HEADER_COLORS = ["#FFCDD2", "#C8E6C9", "#BBDEFB", "#FFF9C4", "#D1C4E9"]
1413
1413
 
1414
+ def __init__(self):
1415
+ super().__init__()
1416
+ # let the whole window accept a dropped *.simcfg / *.tsimcfg file,
1417
+ # not just the individual file-path fields (see FileDropLineEdit)
1418
+ self.setAcceptDrops(True)
1419
+
1420
+ # ---------- Drag & drop native config file onto the window ----------
1421
+ def _config_file_from_drop(self, event):
1422
+ if not event.mimeData().hasUrls():
1423
+ return None
1424
+ for url in event.mimeData().urls():
1425
+ path = url.toLocalFile()
1426
+ if path and pathlib.Path(path).suffix.upper() == "." + self.CONFIG_SUFFIX.upper():
1427
+ return path
1428
+ return None
1429
+
1430
+ def dragEnterEvent(self, event):
1431
+ if self._config_file_from_drop(event):
1432
+ event.acceptProposedAction()
1433
+ else:
1434
+ event.ignore()
1435
+
1436
+ def dragMoveEvent(self, event):
1437
+ if self._config_file_from_drop(event):
1438
+ event.acceptProposedAction()
1439
+ else:
1440
+ event.ignore()
1441
+
1442
+ def dropEvent(self, event):
1443
+ file_path = self._config_file_from_drop(event)
1444
+ if file_path:
1445
+ event.acceptProposedAction()
1446
+ self.load_configuration_from_file(file_path)
1447
+ else:
1448
+ event.ignore()
1449
+
1414
1450
  # ---------- Menu Bar ----------
1415
1451
  def create_menu_bar(self):
1416
1452
  menu_bar = self.menuBar()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: setupEM
3
- Version: 0.5.0
3
+ Version: 0.5.1
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
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes