setupEM 0.5.0__tar.gz → 0.6.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.5.0/src/setupEM.egg-info → setupem-0.6.0}/PKG-INFO +1 -1
- {setupem-0.5.0 → setupem-0.6.0}/src/setupEM/__init__.py +1 -1
- {setupem-0.5.0 → setupem-0.6.0}/src/setupEM/palace_results.py +136 -10
- {setupem-0.5.0 → setupem-0.6.0}/src/setupEM/result_viewer.py +30 -5
- {setupem-0.5.0 → setupem-0.6.0}/src/setupEM/setupEM.py +203 -37
- {setupem-0.5.0 → setupem-0.6.0}/src/setupEM/setupThermal.py +4 -4
- {setupem-0.5.0 → setupem-0.6.0}/src/setupEM/setup_common.py +55 -4
- {setupem-0.5.0 → setupem-0.6.0/src/setupEM.egg-info}/PKG-INFO +1 -1
- {setupem-0.5.0 → setupem-0.6.0}/LICENSE +0 -0
- {setupem-0.5.0 → setupem-0.6.0}/README.md +0 -0
- {setupem-0.5.0 → setupem-0.6.0}/README_pypi.md +0 -0
- {setupem-0.5.0 → setupem-0.6.0}/pyproject.toml +0 -0
- {setupem-0.5.0 → setupem-0.6.0}/setup.cfg +0 -0
- {setupem-0.5.0 → setupem-0.6.0}/src/setupEM/__main__.py +0 -0
- {setupem-0.5.0 → setupem-0.6.0}/src/setupEM/data/SG13G2_100um.xml +0 -0
- {setupem-0.5.0 → setupem-0.6.0}/src/setupEM/data/SG13G2_200um.xml +0 -0
- {setupem-0.5.0 → setupem-0.6.0}/src/setupEM/data/SG13G2_200um_with_parameters.xml +0 -0
- {setupem-0.5.0 → setupem-0.6.0}/src/setupEM/data/SG13G2_nosub.xml +0 -0
- {setupem-0.5.0 → setupem-0.6.0}/src/setupEM/momentum_import.py +0 -0
- {setupem-0.5.0 → setupem-0.6.0}/src/setupEM/stackupEditor.py +0 -0
- {setupem-0.5.0 → setupem-0.6.0}/src/setupEM/stackup_writer.py +0 -0
- {setupem-0.5.0 → setupem-0.6.0}/src/setupEM.egg-info/SOURCES.txt +0 -0
- {setupem-0.5.0 → setupem-0.6.0}/src/setupEM.egg-info/dependency_links.txt +0 -0
- {setupem-0.5.0 → setupem-0.6.0}/src/setupEM.egg-info/entry_points.txt +0 -0
- {setupem-0.5.0 → setupem-0.6.0}/src/setupEM.egg-info/requires.txt +0 -0
- {setupem-0.5.0 → setupem-0.6.0}/src/setupEM.egg-info/top_level.txt +0 -0
|
@@ -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'])}
|
|
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 =
|
|
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",
|
|
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
|
-
|
|
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
|
-
|
|
323
|
+
summary_text = "\n".join(lines)
|
|
324
|
+
_save_convergence_summary(run_path, summary_text)
|
|
325
|
+
return summary_text
|
|
@@ -205,6 +205,32 @@ def find_touchstone_files(target_dir):
|
|
|
205
205
|
return sorted(matches)
|
|
206
206
|
|
|
207
207
|
|
|
208
|
+
# AWS Palace adaptive mesh refinement writes one result snapshot per
|
|
209
|
+
# iteration<N>/ subfolder alongside the final, fully-refined result in the
|
|
210
|
+
# parent directory itself (see _ITERATION_RE in palace_results.py, reverse
|
|
211
|
+
# engineered from real Palace output) - same convention, applied per path
|
|
212
|
+
# component so it matches regardless of how deep target_dir's own scan went.
|
|
213
|
+
_AMR_ITERATION_DIR_RE = re.compile(r'^iteration\d+$')
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def is_amr_iteration_snapshot(path):
|
|
217
|
+
"""True if path sits inside an AMR "iterationN" output folder, i.e. it's
|
|
218
|
+
a per-iteration snapshot rather than the final result."""
|
|
219
|
+
parts = os.path.normpath(path).split(os.sep)
|
|
220
|
+
return any(_AMR_ITERATION_DIR_RE.match(part) for part in parts)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def pick_final_result_file(paths):
|
|
224
|
+
"""Given a list of touchstone file paths, prefer the final result (any
|
|
225
|
+
path with no "iterationN" component) over AMR per-iteration snapshots;
|
|
226
|
+
break ties (or fall back, if every path is a snapshot) by newest mtime.
|
|
227
|
+
Returns None for an empty list."""
|
|
228
|
+
if not paths:
|
|
229
|
+
return None
|
|
230
|
+
final_candidates = [p for p in paths if not is_amr_iteration_snapshot(p)] or paths
|
|
231
|
+
return max(final_candidates, key=os.path.getmtime)
|
|
232
|
+
|
|
233
|
+
|
|
208
234
|
# ------------------------------------------------------------------
|
|
209
235
|
# Result Viewer window
|
|
210
236
|
# ------------------------------------------------------------------
|
|
@@ -364,13 +390,12 @@ class ResultViewerWindow(QDialog):
|
|
|
364
390
|
item.setFlags(Qt.NoItemFlags)
|
|
365
391
|
self.file_list.addTopLevelItem(item)
|
|
366
392
|
else:
|
|
367
|
-
# drop checked paths that no longer exist; auto-check the
|
|
368
|
-
#
|
|
369
|
-
# isn't blank
|
|
393
|
+
# drop checked paths that no longer exist; auto-check the final
|
|
394
|
+
# result (preferring it over any AMR per-iteration snapshot) if
|
|
395
|
+
# nothing is checked (e.g. first open), so the window isn't blank
|
|
370
396
|
self._checked_paths &= set(self._master_files)
|
|
371
397
|
if not self._checked_paths:
|
|
372
|
-
|
|
373
|
-
self._checked_paths = {newest}
|
|
398
|
+
self._checked_paths = {pick_final_result_file(self._master_files)}
|
|
374
399
|
|
|
375
400
|
# group by each file's immediate parent directory (relative to
|
|
376
401
|
# target_dir), not a Palace/Elmer-specific convention like
|
|
@@ -644,6 +644,130 @@ class PortsTab(QWidget):
|
|
|
644
644
|
|
|
645
645
|
|
|
646
646
|
|
|
647
|
+
class RefinedCellsizeOverrideDialog(QDialog):
|
|
648
|
+
"""Popup editor for settings['refined_cellsize_override'], opened from
|
|
649
|
+
MeshTab's "Advanced..." button. Lets the user set a different mesh
|
|
650
|
+
refinement cell size for specific metal/sheet layers, instead of the
|
|
651
|
+
single global "Mesh refinement at metal edges" value. The Layer column
|
|
652
|
+
is restricted to metals_list.getallplanarmetals() (conductor/sheet
|
|
653
|
+
layers) - vias and dielectrics never produce boundary curves in
|
|
654
|
+
gds2palace's meshing code, so a name outside that list would silently
|
|
655
|
+
have no effect if it were allowed to be typed in.
|
|
656
|
+
"""
|
|
657
|
+
|
|
658
|
+
def __init__(self, parent, layer_choices, current_overrides):
|
|
659
|
+
super().__init__(parent)
|
|
660
|
+
self.setAttribute(Qt.WA_DeleteOnClose)
|
|
661
|
+
self.setWindowTitle("Mesh Refinement Overrides")
|
|
662
|
+
self.resize(420, 400)
|
|
663
|
+
self.setModal(True)
|
|
664
|
+
|
|
665
|
+
self._layer_choices = layer_choices
|
|
666
|
+
self._result = []
|
|
667
|
+
|
|
668
|
+
layout = QVBoxLayout()
|
|
669
|
+
|
|
670
|
+
info = QLabel("Override the mesh refinement cell size for specific layers\n"
|
|
671
|
+
"(all other layers keep using the default set above).")
|
|
672
|
+
layout.addWidget(info)
|
|
673
|
+
|
|
674
|
+
self.table = QTableWidget()
|
|
675
|
+
self.table.setColumnCount(3)
|
|
676
|
+
self.table.setHorizontalHeaderLabels(["Layer", "Cell size (µm)", ""])
|
|
677
|
+
self.table.verticalHeader().setVisible(False)
|
|
678
|
+
self.table.setSelectionMode(QAbstractItemView.SelectionMode.NoSelection)
|
|
679
|
+
header = self.table.horizontalHeader()
|
|
680
|
+
header.setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
|
|
681
|
+
header.setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents)
|
|
682
|
+
header.setSectionResizeMode(2, QHeaderView.ResizeMode.ResizeToContents)
|
|
683
|
+
layout.addWidget(self.table)
|
|
684
|
+
|
|
685
|
+
for name, value in current_overrides:
|
|
686
|
+
self._add_row(name, value)
|
|
687
|
+
|
|
688
|
+
add_row_layout = QHBoxLayout()
|
|
689
|
+
add_btn = QPushButton("+ Add Row")
|
|
690
|
+
add_btn.clicked.connect(lambda: self._add_row())
|
|
691
|
+
add_row_layout.addWidget(add_btn)
|
|
692
|
+
add_row_layout.addStretch()
|
|
693
|
+
layout.addLayout(add_row_layout)
|
|
694
|
+
|
|
695
|
+
button_layout = QHBoxLayout()
|
|
696
|
+
button_layout.addStretch()
|
|
697
|
+
ok_btn = QPushButton("OK")
|
|
698
|
+
ok_btn.clicked.connect(self._on_ok)
|
|
699
|
+
button_layout.addWidget(ok_btn)
|
|
700
|
+
cancel_btn = QPushButton("Cancel")
|
|
701
|
+
cancel_btn.clicked.connect(self.reject)
|
|
702
|
+
button_layout.addWidget(cancel_btn)
|
|
703
|
+
layout.addLayout(button_layout)
|
|
704
|
+
|
|
705
|
+
self.setLayout(layout)
|
|
706
|
+
|
|
707
|
+
def _add_row(self, name="", value=""):
|
|
708
|
+
row = self.table.rowCount()
|
|
709
|
+
self.table.insertRow(row)
|
|
710
|
+
|
|
711
|
+
combo = QComboBox()
|
|
712
|
+
combo.setStyleSheet(COMBO_STYLE_OPTIONAL)
|
|
713
|
+
combo.addItem("")
|
|
714
|
+
combo.addItems(self._layer_choices)
|
|
715
|
+
if name and name not in self._layer_choices:
|
|
716
|
+
# tolerate a saved override for a layer no longer in the current
|
|
717
|
+
# stackup, same tolerant style as the Cellname combo box
|
|
718
|
+
combo.addItem(name)
|
|
719
|
+
if name:
|
|
720
|
+
combo.setCurrentText(name)
|
|
721
|
+
self.table.setCellWidget(row, 0, combo)
|
|
722
|
+
|
|
723
|
+
value_text = "" if value == "" else str(value)
|
|
724
|
+
self.table.setItem(row, 1, QTableWidgetItem(value_text))
|
|
725
|
+
|
|
726
|
+
remove_btn = QPushButton("✕")
|
|
727
|
+
remove_btn.setFixedWidth(28)
|
|
728
|
+
remove_btn.clicked.connect(lambda: self._remove_row_containing(remove_btn))
|
|
729
|
+
self.table.setCellWidget(row, 2, remove_btn)
|
|
730
|
+
|
|
731
|
+
def _remove_row_containing(self, button):
|
|
732
|
+
# look up the row by identity rather than a captured index, since
|
|
733
|
+
# row indices shift whenever an earlier row is removed
|
|
734
|
+
for row in range(self.table.rowCount()):
|
|
735
|
+
if self.table.cellWidget(row, 2) is button:
|
|
736
|
+
self.table.removeRow(row)
|
|
737
|
+
return
|
|
738
|
+
|
|
739
|
+
def _on_ok(self):
|
|
740
|
+
overrides = []
|
|
741
|
+
seen_layers = set()
|
|
742
|
+
for row in range(self.table.rowCount()):
|
|
743
|
+
combo = self.table.cellWidget(row, 0)
|
|
744
|
+
layer = combo.currentText().strip() if combo else ""
|
|
745
|
+
if not layer:
|
|
746
|
+
continue # blank row - not yet configured, skip silently
|
|
747
|
+
|
|
748
|
+
value_item = self.table.item(row, 1)
|
|
749
|
+
value_text = value_item.text().strip() if value_item else ""
|
|
750
|
+
try:
|
|
751
|
+
value = float(value_text)
|
|
752
|
+
if value <= 0:
|
|
753
|
+
raise ValueError
|
|
754
|
+
except ValueError:
|
|
755
|
+
QMessageBox.warning(self, "Error", f"Not a valid cell size for layer '{layer}'")
|
|
756
|
+
return
|
|
757
|
+
|
|
758
|
+
if layer in seen_layers:
|
|
759
|
+
QMessageBox.warning(self, "Error", f"Layer '{layer}' is selected more than once")
|
|
760
|
+
return
|
|
761
|
+
seen_layers.add(layer)
|
|
762
|
+
overrides.append([layer, value])
|
|
763
|
+
|
|
764
|
+
self._result = overrides
|
|
765
|
+
self.accept()
|
|
766
|
+
|
|
767
|
+
def get_overrides(self):
|
|
768
|
+
return self._result
|
|
769
|
+
|
|
770
|
+
|
|
647
771
|
class MeshTab(QWidget):
|
|
648
772
|
def __init__(self, MainWindow):
|
|
649
773
|
super().__init__()
|
|
@@ -662,42 +786,44 @@ class MeshTab(QWidget):
|
|
|
662
786
|
self.mesh_layout = QVBoxLayout()
|
|
663
787
|
|
|
664
788
|
self.refinement_layout = QHBoxLayout()
|
|
665
|
-
self.label2 = QLabel("Mesh refinement at metal edges")
|
|
789
|
+
self.label2 = QLabel("Mesh refinement at metal edges (µm)")
|
|
666
790
|
self.label2.setFixedWidth(label_width)
|
|
667
791
|
self.refinement_layout.addWidget(self.label2)
|
|
668
792
|
self.refinement_edit = QLineEdit("5")
|
|
669
793
|
self.refinement_edit.setFixedWidth(edit_width)
|
|
670
794
|
self.refinement_edit.setStyleSheet(EDIT_STYLE_REQUIRED)
|
|
671
795
|
self.refinement_layout.addWidget(self.refinement_edit)
|
|
672
|
-
self.
|
|
673
|
-
self.
|
|
796
|
+
self.refined_override_btn = QPushButton("Advanced...")
|
|
797
|
+
self.refined_override_btn.clicked.connect(self.open_refined_cellsize_override_dialog)
|
|
798
|
+
self.refinement_layout.addWidget(self.refined_override_btn)
|
|
674
799
|
self.refinement_layout.addStretch()
|
|
675
800
|
self.mesh_layout.addLayout(self.refinement_layout)
|
|
676
801
|
|
|
802
|
+
# in-memory copy of settings['refined_cellsize_override'] (list of
|
|
803
|
+
# [layername, value] pairs), edited via the "Advanced..." dialog;
|
|
804
|
+
# harvested into saved_values by save_values() like every other field
|
|
805
|
+
self._refined_cellsize_override = []
|
|
806
|
+
|
|
677
807
|
self.cells_lambda_layout = QHBoxLayout()
|
|
678
|
-
self.label4 = QLabel("Mesh cells per wavelength")
|
|
808
|
+
self.label4 = QLabel("Mesh cells per wavelength (min 10)")
|
|
679
809
|
self.label4.setFixedWidth(label_width)
|
|
680
810
|
self.cells_lambda_layout.addWidget(self.label4)
|
|
681
811
|
self.cells_lambda_edit = QLineEdit("10")
|
|
682
812
|
self.cells_lambda_edit.setFixedWidth(edit_width)
|
|
683
813
|
self.cells_lambda_edit.setStyleSheet(EDIT_STYLE_OPTIONAL)
|
|
684
814
|
self.cells_lambda_layout.addWidget(self.cells_lambda_edit)
|
|
685
|
-
self.label5 = QLabel(" (min 10)")
|
|
686
|
-
self.cells_lambda_layout.addWidget(self.label5)
|
|
687
815
|
self.cells_lambda_layout.addStretch()
|
|
688
816
|
self.mesh_layout.addLayout(self.cells_lambda_layout)
|
|
689
817
|
|
|
690
818
|
|
|
691
819
|
self.cells_maxsize_layout = QHBoxLayout()
|
|
692
|
-
self.label6 = QLabel("Mesh cell maximum size absolute")
|
|
820
|
+
self.label6 = QLabel("Mesh cell maximum size absolute (µm)")
|
|
693
821
|
self.label6.setFixedWidth(label_width)
|
|
694
822
|
self.cells_maxsize_layout.addWidget(self.label6)
|
|
695
823
|
self.cells_maxsize_edit = QLineEdit("100")
|
|
696
824
|
self.cells_maxsize_edit.setFixedWidth(edit_width)
|
|
697
825
|
self.cells_maxsize_edit.setStyleSheet(EDIT_STYLE_OPTIONAL)
|
|
698
826
|
self.cells_maxsize_layout.addWidget(self.cells_maxsize_edit)
|
|
699
|
-
self.label5 = QLabel(" µm ")
|
|
700
|
-
self.cells_maxsize_layout.addWidget(self.label5)
|
|
701
827
|
self.cells_maxsize_layout.addStretch()
|
|
702
828
|
self.mesh_layout.addLayout(self.cells_maxsize_layout)
|
|
703
829
|
|
|
@@ -916,6 +1042,22 @@ class MeshTab(QWidget):
|
|
|
916
1042
|
self.setLayout(self.main_layout)
|
|
917
1043
|
|
|
918
1044
|
|
|
1045
|
+
def _update_refined_override_button_label(self):
|
|
1046
|
+
n = len(self._refined_cellsize_override)
|
|
1047
|
+
self.refined_override_btn.setText(f"Advanced... ({n})" if n > 0 else "Advanced...")
|
|
1048
|
+
|
|
1049
|
+
def open_refined_cellsize_override_dialog(self):
|
|
1050
|
+
metals_list = self.MainWindow.metals_list
|
|
1051
|
+
if metals_list is None:
|
|
1052
|
+
QMessageBox.warning(self, "Error", "Load a GDSII file and XML stackup first")
|
|
1053
|
+
return
|
|
1054
|
+
|
|
1055
|
+
layer_choices = [metal.name for metal in metals_list.getallplanarmetals()]
|
|
1056
|
+
dialog = RefinedCellsizeOverrideDialog(self, layer_choices, self._refined_cellsize_override)
|
|
1057
|
+
if dialog.exec() == QDialog.Accepted:
|
|
1058
|
+
self._refined_cellsize_override = dialog.get_overrides()
|
|
1059
|
+
self._update_refined_override_button_label()
|
|
1060
|
+
|
|
919
1061
|
def on_meshorder_changed(self, value):
|
|
920
1062
|
# callback when mesh order changed, so that we can show/hide edit fields
|
|
921
1063
|
try:
|
|
@@ -952,6 +1094,7 @@ class MeshTab(QWidget):
|
|
|
952
1094
|
self.refinement_edit.setText("5")
|
|
953
1095
|
return False
|
|
954
1096
|
saved_values ["refined_cellsize"] = float(value)
|
|
1097
|
+
saved_values ["refined_cellsize_override"] = self._refined_cellsize_override
|
|
955
1098
|
|
|
956
1099
|
saved_values ["order"] = self.mesh_order_box.currentIndex()+1
|
|
957
1100
|
|
|
@@ -1053,6 +1196,8 @@ class MeshTab(QWidget):
|
|
|
1053
1196
|
|
|
1054
1197
|
def load_values(self):
|
|
1055
1198
|
self.refinement_edit.setText(str(saved_values.get("refined_cellsize","5")))
|
|
1199
|
+
self._refined_cellsize_override = list(saved_values.get("refined_cellsize_override", []))
|
|
1200
|
+
self._update_refined_override_button_label()
|
|
1056
1201
|
self.cells_lambda_edit.setText(str(saved_values.get("cells_per_wavelength","10")))
|
|
1057
1202
|
self.cells_maxsize_edit.setText(str(saved_values.get("meshsize_max","100")))
|
|
1058
1203
|
self.AMR_iterations_edit.setText(str(saved_values.get("adaptive_mesh_iterations","0")))
|
|
@@ -1080,17 +1225,23 @@ class MeshTab(QWidget):
|
|
|
1080
1225
|
self.airaround_edit.setText(saved_values.get("margin","200"))
|
|
1081
1226
|
self.airaround_box.setCurrentIndex(0)
|
|
1082
1227
|
else:
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1228
|
+
# native JSON round-trip stores this as a real list of floats;
|
|
1229
|
+
# .py import stores it as a comma-separated string instead
|
|
1230
|
+
if isinstance(air, list):
|
|
1231
|
+
air_as_list = [str(v) for v in air]
|
|
1232
|
+
elif "," in str(air):
|
|
1233
|
+
air_as_list = [v.strip() for v in str(air).split(',')]
|
|
1234
|
+
else:
|
|
1235
|
+
air_as_list = None
|
|
1236
|
+
|
|
1237
|
+
if air_as_list and len(air_as_list) == 6:
|
|
1238
|
+
self.airaround_box.setCurrentIndex(1)
|
|
1239
|
+
self.airxmin_edit.setText(air_as_list[0])
|
|
1240
|
+
self.airxmax_edit.setText(air_as_list[1])
|
|
1241
|
+
self.airymin_edit.setText(air_as_list[2])
|
|
1242
|
+
self.airymax_edit.setText(air_as_list[3])
|
|
1243
|
+
self.airzmin_edit.setText(air_as_list[4])
|
|
1244
|
+
self.airzmax_edit.setText(air_as_list[5])
|
|
1094
1245
|
else:
|
|
1095
1246
|
# we have air_around defined as a single value
|
|
1096
1247
|
self.airaround_box.setCurrentIndex(0)
|
|
@@ -1171,9 +1322,9 @@ class CreateModelTab(CreateModelTabBase):
|
|
|
1171
1322
|
# local import: only needed once snp2le is actually launched, same lazy-import
|
|
1172
1323
|
# style used by open_result_viewer() for its own heavy imports.
|
|
1173
1324
|
if __package__ in (None, ""):
|
|
1174
|
-
from result_viewer import find_touchstone_files
|
|
1325
|
+
from result_viewer import find_touchstone_files, is_amr_iteration_snapshot
|
|
1175
1326
|
else:
|
|
1176
|
-
from .result_viewer import find_touchstone_files
|
|
1327
|
+
from .result_viewer import find_touchstone_files, is_amr_iteration_snapshot
|
|
1177
1328
|
|
|
1178
1329
|
if self.MainWindow.PalaceMode:
|
|
1179
1330
|
run_path = saved_values['sim_path'] + "/palace_model/" + saved_values['model_basename'] + "_data"
|
|
@@ -1189,23 +1340,28 @@ class CreateModelTab(CreateModelTabBase):
|
|
|
1189
1340
|
QMessageBox.warning(self, "Model Fit",
|
|
1190
1341
|
f"No raw S-parameter result file found in {run_path}.\nRun a simulation first.")
|
|
1191
1342
|
return
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1343
|
+
|
|
1344
|
+
# adaptive mesh refinement leaves one snapshot touchstone file per
|
|
1345
|
+
# iteration<N>/ subfolder alongside the final, fully-refined result
|
|
1346
|
+
# directly in run_path - prefer that final result over the snapshots.
|
|
1347
|
+
final_candidates = [p for p in raw_files if not is_amr_iteration_snapshot(p)] or raw_files
|
|
1348
|
+
if len(final_candidates) > 1:
|
|
1349
|
+
self.log_area.appendPlainText("⚠️ Multiple raw S-parameter result files found, using the newest one:")
|
|
1350
|
+
for path in final_candidates:
|
|
1195
1351
|
self.log_area.appendPlainText(" " + path)
|
|
1196
|
-
raw_file =
|
|
1352
|
+
raw_file = max(final_candidates, key=os.path.getmtime)
|
|
1197
1353
|
|
|
1198
|
-
#
|
|
1199
|
-
#
|
|
1200
|
-
#
|
|
1201
|
-
#
|
|
1354
|
+
# naming a .sNp file on the command line opens snp2le's GUI on it directly
|
|
1355
|
+
# (see https://github.com/iic-jku/snp2le/blob/main/doc/architecture.md);
|
|
1356
|
+
# still start it in that file's directory, so any relative paths it uses
|
|
1357
|
+
# (e.g. for exporting a fit) resolve there instead of wherever setupEM
|
|
1358
|
+
# happened to be launched from.
|
|
1202
1359
|
raw_dir = os.path.dirname(raw_file)
|
|
1203
|
-
self.log_area.appendPlainText("Starting snp2le ...")
|
|
1360
|
+
self.log_area.appendPlainText(f"Starting snp2le on {raw_file} ...")
|
|
1204
1361
|
self.log_area.appendPlainText(SNP2LE_URL)
|
|
1205
|
-
self.log_area.appendPlainText(f"In the snp2le window, load: {raw_file}")
|
|
1206
1362
|
self._process_purpose = "model_fit"
|
|
1207
1363
|
self.process.setWorkingDirectory(raw_dir)
|
|
1208
|
-
self.process.start(sys.executable, ["-m", "snp2le"])
|
|
1364
|
+
self.process.start(sys.executable, ["-m", "snp2le", raw_file])
|
|
1209
1365
|
|
|
1210
1366
|
def _append_results_summary(self):
|
|
1211
1367
|
# Palace-only: parse palace.json / error-indicators.csv and append a results summary
|
|
@@ -1516,7 +1672,7 @@ class ModelEditorTab(QWidget):
|
|
|
1516
1672
|
|
|
1517
1673
|
if forExport:
|
|
1518
1674
|
# these commands are only used within this GUI application to control gmsh
|
|
1519
|
-
ignore_list.
|
|
1675
|
+
ignore_list.extend(['preview_only','no_preview'])
|
|
1520
1676
|
|
|
1521
1677
|
for key in saved_values.keys():
|
|
1522
1678
|
if not key in special_keylist:
|
|
@@ -1802,20 +1958,30 @@ class MainWindow(MainWindowBase):
|
|
|
1802
1958
|
def apply_native_config_data(self, data):
|
|
1803
1959
|
# update ports, they are separate from the other internal data
|
|
1804
1960
|
self.ports_tab.update_port_from_import(data.get("ports"))
|
|
1961
|
+
# restore simulator mode (not part of saved_values, see native_config_extra_struct)
|
|
1962
|
+
if data.get("elmer_mode", False):
|
|
1963
|
+
self.setElmerMode()
|
|
1964
|
+
else:
|
|
1965
|
+
self.setPalaceMode()
|
|
1805
1966
|
|
|
1806
1967
|
def apply_python_import_data(self, file_path):
|
|
1807
1968
|
# read port assignments in workflow syntax for gds2palace Python code
|
|
1808
1969
|
ports = parse_python_ports_definitions(file_path)
|
|
1809
1970
|
self.ports_tab.update_port_from_import(ports)
|
|
1810
1971
|
|
|
1811
|
-
#
|
|
1812
|
-
|
|
1972
|
+
# Elmer/Palace mode is expressed only by which simulation_setup.create_*()
|
|
1973
|
+
# call the script makes, never as a literal settings['elmer'] assignment
|
|
1974
|
+
# (parse_assignments() skips lines whose value contains "settings"), so
|
|
1975
|
+
# detect it directly from the source text instead.
|
|
1976
|
+
with open(file_path) as f:
|
|
1977
|
+
text = f.read()
|
|
1978
|
+
if "create_elmer(" in text:
|
|
1813
1979
|
self.setElmerMode()
|
|
1814
1980
|
else:
|
|
1815
1981
|
self.setPalaceMode()
|
|
1816
1982
|
|
|
1817
1983
|
def native_config_extra_struct(self):
|
|
1818
|
-
return {"ports": simulation_ports_to_struct(simulation_ports)}
|
|
1984
|
+
return {"ports": simulation_ports_to_struct(simulation_ports), "elmer_mode": self.ElmerMode}
|
|
1819
1985
|
|
|
1820
1986
|
def update_target_layer_choices(self, metals_list):
|
|
1821
1987
|
self.ports_tab.update_layers(metals_list)
|
|
@@ -486,7 +486,7 @@ class MeshTab(QWidget):
|
|
|
486
486
|
self.label6 = QLabel("Mesh cell maximum size")
|
|
487
487
|
self.label6.setFixedWidth(label_width)
|
|
488
488
|
self.cells_maxsize_layout.addWidget(self.label6)
|
|
489
|
-
self.cells_maxsize_edit = QLineEdit("
|
|
489
|
+
self.cells_maxsize_edit = QLineEdit("100")
|
|
490
490
|
self.cells_maxsize_edit.setFixedWidth(edit_width)
|
|
491
491
|
self.cells_maxsize_edit.setStyleSheet(EDIT_STYLE_OPTIONAL)
|
|
492
492
|
self.cells_maxsize_layout.addWidget(self.cells_maxsize_edit)
|
|
@@ -507,7 +507,7 @@ class MeshTab(QWidget):
|
|
|
507
507
|
self.label7 = QLabel("Dielectric stackup: oversize by")
|
|
508
508
|
self.label7.setFixedWidth(label_width)
|
|
509
509
|
self.margins_layout.addWidget(self.label7)
|
|
510
|
-
self.margins_edit = QLineEdit("
|
|
510
|
+
self.margins_edit = QLineEdit("100")
|
|
511
511
|
self.margins_edit.setFixedWidth(edit_width)
|
|
512
512
|
self.margins_edit.setStyleSheet(EDIT_STYLE_REQUIRED)
|
|
513
513
|
self.margins_layout.addWidget(self.margins_edit)
|
|
@@ -545,7 +545,7 @@ class MeshTab(QWidget):
|
|
|
545
545
|
value = float(self.margins_edit.text())
|
|
546
546
|
except Exception:
|
|
547
547
|
QMessageBox.warning(self, "Error", "Not a valid value for dielectric oversize margin")
|
|
548
|
-
self.margins_edit.setText("
|
|
548
|
+
self.margins_edit.setText("100")
|
|
549
549
|
return False
|
|
550
550
|
saved_values ["margin"] = float(value)
|
|
551
551
|
|
|
@@ -737,7 +737,7 @@ class ModelEditorTab(QWidget):
|
|
|
737
737
|
|
|
738
738
|
if forExport:
|
|
739
739
|
# these commands are only used within this GUI application to control gmsh
|
|
740
|
-
ignore_list.
|
|
740
|
+
ignore_list.extend(['preview_only','no_preview'])
|
|
741
741
|
|
|
742
742
|
for key in saved_values.keys():
|
|
743
743
|
if not key in special_keylist:
|
|
@@ -324,7 +324,7 @@ class FileInputTab(QWidget):
|
|
|
324
324
|
self.viamerge_layout = QHBoxLayout()
|
|
325
325
|
self.viamerge_label1 = QLabel("Merge via arrays with spacing ")
|
|
326
326
|
self.viamerge_label1.setFixedWidth(left_label_width)
|
|
327
|
-
self.viamerge_edit = QLineEdit("0")
|
|
327
|
+
self.viamerge_edit = QLineEdit("0.5")
|
|
328
328
|
self.viamerge_edit.setStyleSheet(EDIT_STYLE_OPTIONAL)
|
|
329
329
|
self.viamerge_edit.setFixedWidth(70)
|
|
330
330
|
self.viamerge_label2 = QLabel(" micron or more, value 0 disables via array merging")
|
|
@@ -643,7 +643,7 @@ class FileInputTab(QWidget):
|
|
|
643
643
|
merge_polygon_size = float(self.viamerge_edit.text())
|
|
644
644
|
except Exception:
|
|
645
645
|
QMessageBox.warning(self, "Error", f"Not a valid value for via array merging")
|
|
646
|
-
self.viamerge_edit.setText("0")
|
|
646
|
+
self.viamerge_edit.setText("0.5")
|
|
647
647
|
return False
|
|
648
648
|
saved_values["merge_polygon_size"] = float(merge_polygon_size)
|
|
649
649
|
|
|
@@ -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()
|
|
@@ -1633,6 +1669,8 @@ class MainWindowBase(QMainWindow):
|
|
|
1633
1669
|
"XML_filename": "SubstrateFile",
|
|
1634
1670
|
"GdsFile": "GdsFile",
|
|
1635
1671
|
"purpose": "purpose",
|
|
1672
|
+
"cellname": "cellname",
|
|
1673
|
+
"variable_overrides": "variable_overrides",
|
|
1636
1674
|
"SubstrateFile": "SubstrateFile",
|
|
1637
1675
|
"merge_polygon_size": "merge_polygon_size",
|
|
1638
1676
|
"preprocess_gds": "preprocess_gds",
|
|
@@ -1645,12 +1683,12 @@ class MainWindowBase(QMainWindow):
|
|
|
1645
1683
|
"fpoint": "fpoint",
|
|
1646
1684
|
"fdump": "fdump",
|
|
1647
1685
|
"refined_cellsize": "refined_cellsize",
|
|
1686
|
+
"refined_cellsize_override": "refined_cellsize_override",
|
|
1648
1687
|
"cells_per_wavelength": "cells_per_wavelength",
|
|
1649
1688
|
"meshsize_max": "meshsize_max",
|
|
1650
1689
|
"adaptive_mesh_iterations": "adaptive_mesh_iterations",
|
|
1651
1690
|
"order": "order",
|
|
1652
1691
|
"iterative": "iterative",
|
|
1653
|
-
"elmer": "elmer",
|
|
1654
1692
|
"ELMER_MPI_THREADS": "ELMER_MPI_THREADS"
|
|
1655
1693
|
}
|
|
1656
1694
|
|
|
@@ -1670,7 +1708,7 @@ class MainWindowBase(QMainWindow):
|
|
|
1670
1708
|
if import_key not in import_value: # skip the section where key might appear in different context
|
|
1671
1709
|
# get the internal name for this variable
|
|
1672
1710
|
varname = import_mapping.get(import_key, '')
|
|
1673
|
-
if varname
|
|
1711
|
+
if varname in ("fpoint", "fdump", "variable_overrides", "refined_cellsize_override"):
|
|
1674
1712
|
saved_values[varname] = ast.literal_eval(import_value)
|
|
1675
1713
|
elif varname in ["gds_filename", "XML_filename", "GdsFile", "SubstrateFile"]:
|
|
1676
1714
|
# check if we have full path for files in imported Python script,
|
|
@@ -1691,6 +1729,19 @@ class MainWindowBase(QMainWindow):
|
|
|
1691
1729
|
else:
|
|
1692
1730
|
saved_values[varname] = raw
|
|
1693
1731
|
|
|
1732
|
+
# ask whether future "Create Model" output should overwrite this same
|
|
1733
|
+
# file, or start a fresh model (today's GDS-derived default)
|
|
1734
|
+
reuse = QMessageBox.question(
|
|
1735
|
+
self, "Import Model",
|
|
1736
|
+
f"Use '{os.path.basename(file_path)}' as the output file for this model too?\n\n"
|
|
1737
|
+
"Yes: Create Model / Start Simulation will overwrite this file.\n"
|
|
1738
|
+
"No: pick a model name and target directory on the Create Model(s) tab.",
|
|
1739
|
+
QMessageBox.Yes | QMessageBox.No, QMessageBox.No
|
|
1740
|
+
) == QMessageBox.Yes
|
|
1741
|
+
if reuse:
|
|
1742
|
+
saved_values['sim_path'] = os.path.dirname(file_path).replace('\\', '/')
|
|
1743
|
+
saved_values['model_basename'] = pathlib.Path(file_path).stem
|
|
1744
|
+
|
|
1694
1745
|
# read port/thermal assignments in workflow syntax for gds2palace Python code, and
|
|
1695
1746
|
# apply any app-specific post-import state (e.g. setupEM's simulator mode)
|
|
1696
1747
|
self.apply_python_import_data(file_path)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|