setupEM 0.15.0__tar.gz → 0.16.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.15.0/src/setupEM.egg-info → setupem-0.16.0}/PKG-INFO +21 -7
- {setupem-0.15.0 → setupem-0.16.0}/README_pypi.md +19 -5
- {setupem-0.15.0 → setupem-0.16.0}/pyproject.toml +1 -1
- {setupem-0.15.0 → setupem-0.16.0}/src/setupEM/__init__.py +1 -1
- setupem-0.16.0/src/setupEM/config_update.py +179 -0
- {setupem-0.15.0 → setupem-0.16.0}/src/setupEM/script_model.py +1 -1
- {setupem-0.15.0 → setupem-0.16.0}/src/setupEM/setupEM.py +37 -4
- {setupem-0.15.0 → setupem-0.16.0}/src/setupEM/setup_common.py +168 -6
- {setupem-0.15.0 → setupem-0.16.0/src/setupEM.egg-info}/PKG-INFO +21 -7
- {setupem-0.15.0 → setupem-0.16.0}/src/setupEM.egg-info/SOURCES.txt +3 -0
- {setupem-0.15.0 → setupem-0.16.0}/src/setupEM.egg-info/requires.txt +1 -1
- setupem-0.16.0/tests/test_config_update.py +83 -0
- setupem-0.16.0/tests/test_config_update_gui.py +169 -0
- {setupem-0.15.0 → setupem-0.16.0}/LICENSE +0 -0
- {setupem-0.15.0 → setupem-0.16.0}/README.md +0 -0
- {setupem-0.15.0 → setupem-0.16.0}/setup.cfg +0 -0
- {setupem-0.15.0 → setupem-0.16.0}/src/setupEM/__main__.py +0 -0
- {setupem-0.15.0 → setupem-0.16.0}/src/setupEM/data/SG13CMOS5L_200um.xml +0 -0
- {setupem-0.15.0 → setupem-0.16.0}/src/setupEM/data/SG13G2_100um.xml +0 -0
- {setupem-0.15.0 → setupem-0.16.0}/src/setupEM/data/SG13G2_200um.xml +0 -0
- {setupem-0.15.0 → setupem-0.16.0}/src/setupEM/data/SG13G2_FEM_200um.xml +0 -0
- {setupem-0.15.0 → setupem-0.16.0}/src/setupEM/data/SG13G2_FEM_200um_passi3D.xml +0 -0
- {setupem-0.15.0 → setupem-0.16.0}/src/setupEM/data/SG13G2_nosub.xml +0 -0
- {setupem-0.15.0 → setupem-0.16.0}/src/setupEM/field_viewer.py +0 -0
- {setupem-0.15.0 → setupem-0.16.0}/src/setupEM/gds_hierarchy_scan.py +0 -0
- {setupem-0.15.0 → setupem-0.16.0}/src/setupEM/layout_preview.py +0 -0
- {setupem-0.15.0 → setupem-0.16.0}/src/setupEM/momentum_import.py +0 -0
- {setupem-0.15.0 → setupem-0.16.0}/src/setupEM/palace_results.py +0 -0
- {setupem-0.15.0 → setupem-0.16.0}/src/setupEM/result_viewer.py +0 -0
- {setupem-0.15.0 → setupem-0.16.0}/src/setupEM/run_with_overrides.py +0 -0
- {setupem-0.15.0 → setupem-0.16.0}/src/setupEM/script_probe.py +0 -0
- {setupem-0.15.0 → setupem-0.16.0}/src/setupEM/setupThermal.py +0 -0
- {setupem-0.15.0 → setupem-0.16.0}/src/setupEM/simplify_gds.py +0 -0
- {setupem-0.15.0 → setupem-0.16.0}/src/setupEM/stackupEditor.py +0 -0
- {setupem-0.15.0 → setupem-0.16.0}/src/setupEM/stackup_writer.py +0 -0
- {setupem-0.15.0 → setupem-0.16.0}/src/setupEM/thermal_results.py +0 -0
- {setupem-0.15.0 → setupem-0.16.0}/src/setupEM.egg-info/dependency_links.txt +0 -0
- {setupem-0.15.0 → setupem-0.16.0}/src/setupEM.egg-info/entry_points.txt +0 -0
- {setupem-0.15.0 → setupem-0.16.0}/src/setupEM.egg-info/top_level.txt +0 -0
- {setupem-0.15.0 → setupem-0.16.0}/tests/test_run_with_overrides.py +0 -0
- {setupem-0.15.0 → setupem-0.16.0}/tests/test_script_model.py +0 -0
- {setupem-0.15.0 → setupem-0.16.0}/tests/test_script_probe.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: setupEM
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.16.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
|
|
@@ -10,7 +10,7 @@ Project-URL: Changelog, https://github.com/VolkerMuehlhaus/setupEM/blob/main/doc
|
|
|
10
10
|
Requires-Python: >=3.9
|
|
11
11
|
Description-Content-Type: text/markdown
|
|
12
12
|
License-File: LICENSE
|
|
13
|
-
Requires-Dist: gds2palace>=0.9.
|
|
13
|
+
Requires-Dist: gds2palace>=0.9.3
|
|
14
14
|
Requires-Dist: gds_prepare_for_EM>=1.6.0
|
|
15
15
|
Requires-Dist: PySide6
|
|
16
16
|
Requires-Dist: shiboken6
|
|
@@ -40,6 +40,24 @@ https://github.com/VolkerMuehlhaus/setupEM
|
|
|
40
40
|
|
|
41
41
|
## Recent changes
|
|
42
42
|
|
|
43
|
+
# What's New - October 31, 2026 (setupEM 0.16.0)
|
|
44
|
+
|
|
45
|
+
**After a new export from the layout tool** (setupEM): a layout tool such as the KLayout plug-in klayout_em_export writes the GDSII file and a `.simcfg`, and writes both again when the layout changes. The settings you made in setupEM since are only in your model script, because Create Model writes the script, not the `.simcfg`. setupEM now takes from such a `.simcfg` only the ports, GDSII file, stackup, cell and purpose, and keeps everything else as in the script:
|
|
46
|
+
- Opening a `.simcfg` when a model script for the same GDSII file exists next to it asks whether to **Continue with the script** (edit it in place, ports and layout settings from the `.simcfg`) or **Start from the config**.
|
|
47
|
+
- Loading a `.simcfg` while editing a script in place lists the ports and layout settings that differ and asks whether to **Update the script from it** or **Open it as a new model**. Before, loading it ended editing in place and replaced all settings.
|
|
48
|
+
- While editing a script in place, the bar below the menu points out a newer `.simcfg` of the same GDSII file with other ports or layout settings, with **Update from it**.
|
|
49
|
+
|
|
50
|
+
**Load Config, Import from \*.py model and Edit \*.py model in place** start in the folder of the model you are working on (the config or script loaded last, else the GDSII file). Before, they started in the folder setupEM was launched from, e.g. KLayout's when the KLayout plug-in opened setupEM.
|
|
51
|
+
|
|
52
|
+
setupEM 0.16.0 needs gds2palace 0.9.3 or later: a layout with several shapes on one port layer (e.g. a transistor terminal on several fingers, from the KLayout plug-in) makes one port with several elements there; older gds2palace versions used only the last shape.
|
|
53
|
+
|
|
54
|
+
**Fixes:**
|
|
55
|
+
- `scripts/combine_extend_snp.py` is the same as gds2palace's again. It had missed three fixes made there in September: the port impedance lookup for AMR iterations, the missing `.sNp` extension for a model name that contains a dot, and Touchstone output after an interrupted Palace run.
|
|
56
|
+
- On Linux with GNOME (Wayland), the mouse cursor no longer keeps spinning for about 10 seconds after setupEM starts. setupEM briefly opened three invisible windows while building the Input Files tab, and GNOME waited for them to finish starting.
|
|
57
|
+
- Via ports with direction `-Z` keep their from and to layers. Before, loading a `.simcfg` dropped them, and Create Model wrote such a port as an in-plane port.
|
|
58
|
+
- An incomplete port (direction and layers that don't fit, e.g. from a `.simcfg` written by another tool) gives a warning in the Ports tab. Before, gds2palace exited and setupEM closed. A lowercase `z` counts as a via port too.
|
|
59
|
+
- `combine_extend_snp` (Touchstone output after a Palace run) handles ports with several elements (several shapes on one port layer, gds2palace 0.9.3): one Z0 per port in the option line, and the elements' port inductances combined in parallel for de-embedding. Before, the last element overwrote the others.
|
|
60
|
+
|
|
43
61
|
# What's New - October 29, 2026
|
|
44
62
|
|
|
45
63
|
**File > Edit \*.py model in place** (new, setupEM and setupThermal): edits a gds2palace model script directly, e.g. one from the gds2palace MCP server, an example or your own code. Create Model and **File > Save** write only the values you change; the script's loops, comments and custom code stay. Settings the script computes itself are greyed out. A script that builds several models (a parameter sweep) lists them on the Create Model tab, and Start Simulation runs the ticked ones. After importing a script that setupEM didn't write, a bar below the menu offers **Edit in place instead**.
|
|
@@ -56,17 +74,13 @@ Import and edit in place now read a stackup given as `utilities.get_stackup_path
|
|
|
56
74
|
|
|
57
75
|
Fix: with **Merge via arrays** (and no fill removal or merging per layer), the simplified file still contained the original subcells as extra top-level cells. Without a cell name, gds2palace reads the first top-level cell, which could be one of them, e.g. a pad cell instead of the design. These left-over cells are now removed.
|
|
58
76
|
|
|
59
|
-
# What's New - October 19, 2026
|
|
60
|
-
|
|
61
|
-
**Stackup Editor, ADS Momentum (*.ltd) import:** metals with an EXPAND thickness operation now make the dielectric next to them thicker, as in ADS, instead of reaching into the dielectric below. Before, a thick EXPAND DOWN metal (e.g. MET1/MET2 in a GaAs stackup) overlapped the layers underneath it, and a via could end up with zero height. Files where long lines are wrapped or END_... markers follow the last entry on the same line now import too; before, they failed with "No usable dielectric slabs".
|
|
62
|
-
|
|
63
77
|
Full history: [CHANGES.md](https://github.com/VolkerMuehlhaus/setupEM/blob/main/doc/CHANGES.md)
|
|
64
78
|
|
|
65
79
|
---
|
|
66
80
|
|
|
67
81
|
**Note:** the `setupEM` PyPI package itself requires:
|
|
68
82
|
|
|
69
|
-
- gds2palace>=0.9.
|
|
83
|
+
- gds2palace>=0.9.3
|
|
70
84
|
- gds_prepare_for_EM>=1.6.0
|
|
71
85
|
- PySide6
|
|
72
86
|
- shiboken6
|
|
@@ -11,6 +11,24 @@ https://github.com/VolkerMuehlhaus/setupEM
|
|
|
11
11
|
|
|
12
12
|
## Recent changes
|
|
13
13
|
|
|
14
|
+
# What's New - October 31, 2026 (setupEM 0.16.0)
|
|
15
|
+
|
|
16
|
+
**After a new export from the layout tool** (setupEM): a layout tool such as the KLayout plug-in klayout_em_export writes the GDSII file and a `.simcfg`, and writes both again when the layout changes. The settings you made in setupEM since are only in your model script, because Create Model writes the script, not the `.simcfg`. setupEM now takes from such a `.simcfg` only the ports, GDSII file, stackup, cell and purpose, and keeps everything else as in the script:
|
|
17
|
+
- Opening a `.simcfg` when a model script for the same GDSII file exists next to it asks whether to **Continue with the script** (edit it in place, ports and layout settings from the `.simcfg`) or **Start from the config**.
|
|
18
|
+
- Loading a `.simcfg` while editing a script in place lists the ports and layout settings that differ and asks whether to **Update the script from it** or **Open it as a new model**. Before, loading it ended editing in place and replaced all settings.
|
|
19
|
+
- While editing a script in place, the bar below the menu points out a newer `.simcfg` of the same GDSII file with other ports or layout settings, with **Update from it**.
|
|
20
|
+
|
|
21
|
+
**Load Config, Import from \*.py model and Edit \*.py model in place** start in the folder of the model you are working on (the config or script loaded last, else the GDSII file). Before, they started in the folder setupEM was launched from, e.g. KLayout's when the KLayout plug-in opened setupEM.
|
|
22
|
+
|
|
23
|
+
setupEM 0.16.0 needs gds2palace 0.9.3 or later: a layout with several shapes on one port layer (e.g. a transistor terminal on several fingers, from the KLayout plug-in) makes one port with several elements there; older gds2palace versions used only the last shape.
|
|
24
|
+
|
|
25
|
+
**Fixes:**
|
|
26
|
+
- `scripts/combine_extend_snp.py` is the same as gds2palace's again. It had missed three fixes made there in September: the port impedance lookup for AMR iterations, the missing `.sNp` extension for a model name that contains a dot, and Touchstone output after an interrupted Palace run.
|
|
27
|
+
- On Linux with GNOME (Wayland), the mouse cursor no longer keeps spinning for about 10 seconds after setupEM starts. setupEM briefly opened three invisible windows while building the Input Files tab, and GNOME waited for them to finish starting.
|
|
28
|
+
- Via ports with direction `-Z` keep their from and to layers. Before, loading a `.simcfg` dropped them, and Create Model wrote such a port as an in-plane port.
|
|
29
|
+
- An incomplete port (direction and layers that don't fit, e.g. from a `.simcfg` written by another tool) gives a warning in the Ports tab. Before, gds2palace exited and setupEM closed. A lowercase `z` counts as a via port too.
|
|
30
|
+
- `combine_extend_snp` (Touchstone output after a Palace run) handles ports with several elements (several shapes on one port layer, gds2palace 0.9.3): one Z0 per port in the option line, and the elements' port inductances combined in parallel for de-embedding. Before, the last element overwrote the others.
|
|
31
|
+
|
|
14
32
|
# What's New - October 29, 2026
|
|
15
33
|
|
|
16
34
|
**File > Edit \*.py model in place** (new, setupEM and setupThermal): edits a gds2palace model script directly, e.g. one from the gds2palace MCP server, an example or your own code. Create Model and **File > Save** write only the values you change; the script's loops, comments and custom code stay. Settings the script computes itself are greyed out. A script that builds several models (a parameter sweep) lists them on the Create Model tab, and Start Simulation runs the ticked ones. After importing a script that setupEM didn't write, a bar below the menu offers **Edit in place instead**.
|
|
@@ -27,17 +45,13 @@ Import and edit in place now read a stackup given as `utilities.get_stackup_path
|
|
|
27
45
|
|
|
28
46
|
Fix: with **Merge via arrays** (and no fill removal or merging per layer), the simplified file still contained the original subcells as extra top-level cells. Without a cell name, gds2palace reads the first top-level cell, which could be one of them, e.g. a pad cell instead of the design. These left-over cells are now removed.
|
|
29
47
|
|
|
30
|
-
# What's New - October 19, 2026
|
|
31
|
-
|
|
32
|
-
**Stackup Editor, ADS Momentum (*.ltd) import:** metals with an EXPAND thickness operation now make the dielectric next to them thicker, as in ADS, instead of reaching into the dielectric below. Before, a thick EXPAND DOWN metal (e.g. MET1/MET2 in a GaAs stackup) overlapped the layers underneath it, and a via could end up with zero height. Files where long lines are wrapped or END_... markers follow the last entry on the same line now import too; before, they failed with "No usable dielectric slabs".
|
|
33
|
-
|
|
34
48
|
Full history: [CHANGES.md](https://github.com/VolkerMuehlhaus/setupEM/blob/main/doc/CHANGES.md)
|
|
35
49
|
|
|
36
50
|
---
|
|
37
51
|
|
|
38
52
|
**Note:** the `setupEM` PyPI package itself requires:
|
|
39
53
|
|
|
40
|
-
- gds2palace>=0.9.
|
|
54
|
+
- gds2palace>=0.9.3
|
|
41
55
|
- gds_prepare_for_EM>=1.6.0
|
|
42
56
|
- PySide6
|
|
43
57
|
- shiboken6
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
########################################################################
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2025 Volker Muehlhaus and IHP PDK Authors
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the GNU General Public License, Version 3.0 (the "License");
|
|
6
|
+
# you may not use this file except in compliance with the License.
|
|
7
|
+
# You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# https://www.gnu.org/licenses/gpl-3.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
# See the License for the specific language governing permissions and
|
|
15
|
+
# limitations under the License.
|
|
16
|
+
#
|
|
17
|
+
########################################################################
|
|
18
|
+
|
|
19
|
+
"""Update a script edited in place from a settings file (*.simcfg) of the same layout.
|
|
20
|
+
|
|
21
|
+
A layout tool such as the KLayout plug-in klayout_em_export writes the GDS file and a *.simcfg next to it. When the
|
|
22
|
+
layout changes, it exports again: same GDS file name, new ports (numbered 1..N without gaps, so they can shift).
|
|
23
|
+
A model script made before, maybe edited by hand, still has the old ports. setupEM's edit-in-place mode can update
|
|
24
|
+
it: the settings that belong to the layout (LAYOUT_KEYS and the ports) come from the *.simcfg, everything else
|
|
25
|
+
(frequencies, mesh, the script's own code) stays.
|
|
26
|
+
|
|
27
|
+
No Qt in here, so the module can be tested on its own.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
import ast
|
|
31
|
+
import glob
|
|
32
|
+
import json
|
|
33
|
+
import os
|
|
34
|
+
|
|
35
|
+
__all__ = ["LAYOUT_KEYS", "port_differences", "layout_differences", "newer_configs", "model_scripts"]
|
|
36
|
+
|
|
37
|
+
# saved_values that belong to the layout export, besides the ports
|
|
38
|
+
LAYOUT_KEYS = ("GdsFile", "SubstrateFile", "cellname", "purpose")
|
|
39
|
+
LAYOUT_LABELS = {"GdsFile": "GDSII file", "SubstrateFile": "stackup", "cellname": "cell", "purpose": "purpose"}
|
|
40
|
+
|
|
41
|
+
_PORT_KEYS = ("source_layernum", "direction", "from_layername", "to_layername", "target_layername")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _port_signature(port):
|
|
45
|
+
"""(source layer, direction, from, to, target) of a port dict; Z0 and excitation are not part of it."""
|
|
46
|
+
values = []
|
|
47
|
+
for key in _PORT_KEYS:
|
|
48
|
+
value = port.get(key)
|
|
49
|
+
if key == "source_layernum" and value not in (None, ""):
|
|
50
|
+
value = int(value)
|
|
51
|
+
elif key == "direction" and isinstance(value, str):
|
|
52
|
+
value = value.upper()
|
|
53
|
+
values.append(value or None)
|
|
54
|
+
layer, direction, frm, to, target = values
|
|
55
|
+
if direction and "Z" in direction:
|
|
56
|
+
target = None
|
|
57
|
+
else:
|
|
58
|
+
frm = to = None
|
|
59
|
+
return layer, direction, frm, to, target
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _describe(signature):
|
|
63
|
+
layer, direction, frm, to, target = signature
|
|
64
|
+
if direction and "Z" in direction:
|
|
65
|
+
return f"via {frm} -> {to} ({direction}) on layer {layer}"
|
|
66
|
+
return f"in-plane {direction} on {target}, layer {layer}"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def port_differences(old_ports, new_ports):
|
|
70
|
+
"""Readable differences from old_ports to new_ports (lists of port dicts), [] if they match. Compared per port
|
|
71
|
+
number: layer, direction, from/to/target layer; not Z0 or excitation."""
|
|
72
|
+
old = {int(p["portnumber"]): _port_signature(p) for p in old_ports if p.get("portnumber") not in (None, "")}
|
|
73
|
+
new = {int(p["portnumber"]): _port_signature(p) for p in new_ports if p.get("portnumber") not in (None, "")}
|
|
74
|
+
out = []
|
|
75
|
+
for number in sorted(set(old) | set(new)):
|
|
76
|
+
if number not in new:
|
|
77
|
+
out.append(f"port {number}: removed")
|
|
78
|
+
elif number not in old:
|
|
79
|
+
out.append(f"port {number}: new, {_describe(new[number])}")
|
|
80
|
+
elif old[number] != new[number]:
|
|
81
|
+
out.append(f"port {number}: was {_describe(old[number])}, now {_describe(new[number])}")
|
|
82
|
+
return out
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _same_path(a, b, base_dir=""):
|
|
86
|
+
if not a or not b:
|
|
87
|
+
return a == b
|
|
88
|
+
norm = lambda p: os.path.normcase(os.path.abspath(os.path.join(base_dir, p)))
|
|
89
|
+
return norm(a) == norm(b)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def layout_differences(old_values, new_values, new_base_dir=""):
|
|
93
|
+
"""Readable differences of the LAYOUT_KEYS from old_values to new_values (saved_values dicts); file paths in
|
|
94
|
+
new_values relative to new_base_dir."""
|
|
95
|
+
out = []
|
|
96
|
+
for key in LAYOUT_KEYS:
|
|
97
|
+
if key not in new_values:
|
|
98
|
+
continue
|
|
99
|
+
old, new = old_values.get(key), new_values.get(key)
|
|
100
|
+
if key in ("GdsFile", "SubstrateFile"):
|
|
101
|
+
if not _same_path(old, new, new_base_dir):
|
|
102
|
+
out.append(f"{LAYOUT_LABELS[key]}: {os.path.basename(old or '-')} -> {os.path.basename(new or '-')}")
|
|
103
|
+
elif key == "purpose":
|
|
104
|
+
if [int(p) for p in old or []] != [int(p) for p in new or []]:
|
|
105
|
+
out.append(f"{LAYOUT_LABELS[key]}: {old} -> {new}")
|
|
106
|
+
elif old != new:
|
|
107
|
+
out.append(f"{LAYOUT_LABELS[key]}: {old} -> {new}")
|
|
108
|
+
return out
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def newer_configs(script_path, gds_path, folders, suffix=".simcfg", application="setupEM"):
|
|
112
|
+
"""[(path, data)] of the settings files in folders that belong to gds_path (their GdsFile is that file) and are
|
|
113
|
+
newer than script_path, newest first."""
|
|
114
|
+
try:
|
|
115
|
+
script_time = os.path.getmtime(script_path)
|
|
116
|
+
except OSError:
|
|
117
|
+
return []
|
|
118
|
+
seen, out = set(), []
|
|
119
|
+
for folder in folders:
|
|
120
|
+
if not folder or not os.path.isdir(folder):
|
|
121
|
+
continue
|
|
122
|
+
for path in glob.glob(os.path.join(folder, "*" + suffix)):
|
|
123
|
+
key = os.path.normcase(os.path.abspath(path))
|
|
124
|
+
if key in seen:
|
|
125
|
+
continue
|
|
126
|
+
seen.add(key)
|
|
127
|
+
try:
|
|
128
|
+
if os.path.getmtime(path) <= script_time:
|
|
129
|
+
continue
|
|
130
|
+
with open(path, encoding="utf-8") as f:
|
|
131
|
+
data = json.load(f)
|
|
132
|
+
except (OSError, ValueError):
|
|
133
|
+
continue
|
|
134
|
+
if not isinstance(data, dict) or data.get("application") != application:
|
|
135
|
+
continue
|
|
136
|
+
gds = (data.get("saved_values") or {}).get("GdsFile")
|
|
137
|
+
if gds and _same_path(gds, gds_path, os.path.dirname(path)):
|
|
138
|
+
out.append((path, data))
|
|
139
|
+
out.sort(key=lambda item: os.path.getmtime(item[0]), reverse=True)
|
|
140
|
+
return out
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _reads_gds(script_path, gds_path):
|
|
144
|
+
"""True if one of the script's string literals, taken relative to the script's folder, is gds_path (the script
|
|
145
|
+
is read with ast, never run)."""
|
|
146
|
+
try:
|
|
147
|
+
with open(script_path, encoding="utf-8", errors="replace") as f:
|
|
148
|
+
text = f.read()
|
|
149
|
+
except OSError:
|
|
150
|
+
return False
|
|
151
|
+
if os.path.basename(gds_path) not in text: # cheap test before parsing
|
|
152
|
+
return False
|
|
153
|
+
try:
|
|
154
|
+
tree = ast.parse(text)
|
|
155
|
+
except (SyntaxError, ValueError):
|
|
156
|
+
return False
|
|
157
|
+
folder = os.path.dirname(os.path.abspath(script_path))
|
|
158
|
+
return any(isinstance(n, ast.Constant) and isinstance(n.value, str) and n.value.lower().endswith(".gds")
|
|
159
|
+
and _same_path(n.value, gds_path, folder) for n in ast.walk(tree))
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def model_scripts(gds_path, folders):
|
|
163
|
+
"""Model scripts (*.py) in folders that read gds_path, newest first. Their age says nothing against a settings
|
|
164
|
+
file of the same layout: a layout tool writes that file again on every export, also after the script was made,
|
|
165
|
+
while the settings made in setupEM since are only in the script (Create Model writes the script, not the
|
|
166
|
+
settings file)."""
|
|
167
|
+
seen, out = set(), []
|
|
168
|
+
for folder in folders:
|
|
169
|
+
if not folder or not os.path.isdir(folder):
|
|
170
|
+
continue
|
|
171
|
+
for path in glob.glob(os.path.join(folder, "*.py")):
|
|
172
|
+
key = os.path.normcase(os.path.abspath(path))
|
|
173
|
+
if key in seen:
|
|
174
|
+
continue
|
|
175
|
+
seen.add(key)
|
|
176
|
+
if _reads_gds(path, gds_path):
|
|
177
|
+
out.append(path)
|
|
178
|
+
out.sort(key=os.path.getmtime, reverse=True)
|
|
179
|
+
return out
|
|
@@ -830,7 +830,7 @@ def _port_values(port):
|
|
|
830
830
|
"port_Z0": _number(port.get("port_Z0", 50)),
|
|
831
831
|
"source_layernum": _number(int(port["source_layernum"])),
|
|
832
832
|
}
|
|
833
|
-
if str(port.get("direction", "")).upper()
|
|
833
|
+
if "Z" in str(port.get("direction", "")).upper():
|
|
834
834
|
values["from_layername"] = repr(port.get("from_layername", ""))
|
|
835
835
|
values["to_layername"] = repr(port.get("to_layername", ""))
|
|
836
836
|
else:
|
|
@@ -61,10 +61,11 @@ if __package__ in (None, ""):
|
|
|
61
61
|
eval_simple_python_expression, collect_module_level_constants,
|
|
62
62
|
find_paraview_exe, FILL_FACTOR_CORRECTION_SOLVERS, PALACE_LINEAR_SOLVER_SETTINGS,
|
|
63
63
|
GDS2PALACE_CONDUCTOR_MODELS, MODEL_KIND_EM,
|
|
64
|
-
protected_script_message,
|
|
64
|
+
protected_script_message, resolve_missing_file_paths,
|
|
65
65
|
)
|
|
66
66
|
from palace_results import build_results_summary, find_output_dir, find_paraview_files
|
|
67
67
|
from script_model import ScriptModel
|
|
68
|
+
from config_update import LAYOUT_KEYS, layout_differences, port_differences
|
|
68
69
|
else:
|
|
69
70
|
from .setup_common import (
|
|
70
71
|
EDIT_STYLE_OPTIONAL, EDIT_STYLE_REQUIRED, COMBO_STYLE_REQUIRED, COMBO_STYLE_OPTIONAL,
|
|
@@ -77,10 +78,11 @@ else:
|
|
|
77
78
|
eval_simple_python_expression, collect_module_level_constants,
|
|
78
79
|
find_paraview_exe, FILL_FACTOR_CORRECTION_SOLVERS, PALACE_LINEAR_SOLVER_SETTINGS,
|
|
79
80
|
GDS2PALACE_CONDUCTOR_MODELS, MODEL_KIND_EM,
|
|
80
|
-
protected_script_message,
|
|
81
|
+
protected_script_message, resolve_missing_file_paths,
|
|
81
82
|
)
|
|
82
83
|
from .palace_results import build_results_summary, find_output_dir, find_paraview_files
|
|
83
84
|
from .script_model import ScriptModel
|
|
85
|
+
from .config_update import LAYOUT_KEYS, layout_differences, port_differences
|
|
84
86
|
|
|
85
87
|
|
|
86
88
|
'''
|
|
@@ -778,8 +780,18 @@ class PortsTab(QWidget):
|
|
|
778
780
|
except Exception:
|
|
779
781
|
QMessageBox.warning(self, "Error", "Invalid input for port " + str(portnumber))
|
|
780
782
|
return False
|
|
783
|
+
# gds2palace's simulation_port() exits the whole program on a direction that does not
|
|
784
|
+
# match its layers, so check here and warn instead (e.g. an incomplete port from a .simcfg)
|
|
785
|
+
via = "Z" in direction.upper()
|
|
786
|
+
if direction.strip().upper() not in ("X", "Y", "Z", "-X", "-Y", "-Z") or \
|
|
787
|
+
not ((from_name and to_name) if via else target_name):
|
|
788
|
+
QMessageBox.warning(self, "Error",
|
|
789
|
+
f"Port {portnumber} is incomplete: direction X, Y, -X or -Y needs a "
|
|
790
|
+
"target layer (in-plane port), Z or -Z needs a from and a to layer "
|
|
791
|
+
"(via port).")
|
|
792
|
+
return False
|
|
781
793
|
# create port
|
|
782
|
-
if
|
|
794
|
+
if via:
|
|
783
795
|
# via port
|
|
784
796
|
simulation_ports.add_port(simulation_setup.simulation_port(portnumber=portnumber,
|
|
785
797
|
voltage=voltage,
|
|
@@ -843,7 +855,7 @@ class PortsTab(QWidget):
|
|
|
843
855
|
self.portslist.setItem(portnum-1, 2, QTableWidgetItem(str(port.get("source_layernum",""))))
|
|
844
856
|
|
|
845
857
|
direction = str(port.get("direction","")).upper()
|
|
846
|
-
if
|
|
858
|
+
if "Z" in direction:
|
|
847
859
|
self.portslist.setItem(portnum-1, 4, QTableWidgetItem(str(port.get("from_layername",""))))
|
|
848
860
|
self.portslist.setItem(portnum-1, 5, QTableWidgetItem(str(port.get("to_layername",""))))
|
|
849
861
|
self.portslist.setItem(portnum-1, 3, QTableWidgetItem(""))
|
|
@@ -3813,6 +3825,27 @@ class MainWindow(MainWindowBase):
|
|
|
3813
3825
|
def preserve_create_call(self):
|
|
3814
3826
|
return "create_elmer" if self.ElmerMode else "create_palace"
|
|
3815
3827
|
|
|
3828
|
+
# ---------- Update a script edited in place from a *.simcfg (see MainWindowBase) ----------
|
|
3829
|
+
def config_update_supported(self):
|
|
3830
|
+
return True
|
|
3831
|
+
|
|
3832
|
+
def config_update_differences(self, data, base_dir):
|
|
3833
|
+
return (port_differences(self.preserve_objects(), data.get("ports", []))
|
|
3834
|
+
+ layout_differences(self.saved_values, data.get("saved_values") or {}, base_dir))
|
|
3835
|
+
|
|
3836
|
+
def apply_config_update(self, data, base_dir):
|
|
3837
|
+
# ports, GDS file, stackup, cell and purpose from the settings file; everything else stays
|
|
3838
|
+
saved = data.get("saved_values") or {}
|
|
3839
|
+
layout = {key: saved[key] for key in LAYOUT_KEYS if key in saved}
|
|
3840
|
+
for key in ("GdsFile", "SubstrateFile"):
|
|
3841
|
+
if layout.get(key):
|
|
3842
|
+
layout[key] = os.path.join(base_dir, layout[key]).replace("\\", "/")
|
|
3843
|
+
resolve_missing_file_paths(layout, base_dir)
|
|
3844
|
+
self.saved_values.update(layout)
|
|
3845
|
+
self.ports_tab.update_port_from_import(data.get("ports", []))
|
|
3846
|
+
self.load_all_tabs()
|
|
3847
|
+
self.save_all_tabs()
|
|
3848
|
+
|
|
3816
3849
|
def preserve_extra(self, baseline_values, current_values):
|
|
3817
3850
|
# fdump_enabled is Elmer's GUI-only field dump checkbox; in Elmer mode it
|
|
3818
3851
|
# decides settings['fdump'] (see create_model_text())
|
|
@@ -63,10 +63,12 @@ if __package__ in (None, ""):
|
|
|
63
63
|
import gds_hierarchy_scan
|
|
64
64
|
from script_model import eval_simple_python_expression, ScriptModel, patch_script, LEGACY_SETTINGS
|
|
65
65
|
from script_probe import probe_script
|
|
66
|
+
from config_update import model_scripts, newer_configs
|
|
66
67
|
else:
|
|
67
68
|
from . import gds_hierarchy_scan
|
|
68
69
|
from .script_model import eval_simple_python_expression, ScriptModel, patch_script, LEGACY_SETTINGS
|
|
69
70
|
from .script_probe import probe_script
|
|
71
|
+
from .config_update import model_scripts, newer_configs
|
|
70
72
|
|
|
71
73
|
# ------------------------------------------------------------------
|
|
72
74
|
# gds2palace feature-compatibility detection: an older gds2palace (e.g. a stale
|
|
@@ -725,12 +727,16 @@ class FileInputTab(QWidget):
|
|
|
725
727
|
|
|
726
728
|
# settings['fill_factor_correction'], shown only for solvers where the installed
|
|
727
729
|
# gds2palace supports it, see show_fill_factor_correction()
|
|
730
|
+
# Created with this tab as parent: show_fill_factor_correction() below calls setVisible() before the
|
|
731
|
+
# layout is installed, and a parentless widget made visible becomes a top-level window for a moment.
|
|
732
|
+
# On Wayland each such window requests an activation token that is never used, and GNOME shows a
|
|
733
|
+
# spinning cursor until it times out (about 10 s after setupEM starts).
|
|
728
734
|
self.fill_factor_layout = QHBoxLayout()
|
|
729
|
-
self.fill_factor_label1 = QLabel("Correction for via array cross section is ")
|
|
735
|
+
self.fill_factor_label1 = QLabel("Correction for via array cross section is ", self)
|
|
730
736
|
# minimum instead of fixed width: lines up with the rows above, but grows rather
|
|
731
737
|
# than truncating if the text is wider at a larger font or DPI setting
|
|
732
738
|
self.fill_factor_label1.setMinimumWidth(left_label_width)
|
|
733
|
-
self.fill_factor_box = QComboBox()
|
|
739
|
+
self.fill_factor_box = QComboBox(self)
|
|
734
740
|
self.fill_factor_box.setStyleSheet(COMBO_STYLE_OPTIONAL)
|
|
735
741
|
self.fill_factor_box.addItems(["enabled", "disabled"])
|
|
736
742
|
self.fill_factor_box.setCurrentText("disabled")
|
|
@@ -740,7 +746,7 @@ class FileInputTab(QWidget):
|
|
|
740
746
|
"each merged via polygon (heat conductivity for thermal models) is multiplied by\n"
|
|
741
747
|
"its fill factor (original via area / merged polygon area). Only has an effect\n"
|
|
742
748
|
"when via array merging is enabled.")
|
|
743
|
-
self.fill_factor_label2 = QLabel(" (conductivity × via fill factor)")
|
|
749
|
+
self.fill_factor_label2 = QLabel(" (conductivity × via fill factor)", self)
|
|
744
750
|
self.fill_factor_layout.addWidget(self.fill_factor_label1)
|
|
745
751
|
self.fill_factor_layout.addWidget(self.fill_factor_box)
|
|
746
752
|
self.fill_factor_layout.addWidget(self.fill_factor_label2)
|
|
@@ -4480,6 +4486,8 @@ class MainWindowBase(QMainWindow):
|
|
|
4480
4486
|
elif self.script_model is not None:
|
|
4481
4487
|
# keep the unsaved-changes * in the title current
|
|
4482
4488
|
self.script_has_unsaved_changes()
|
|
4489
|
+
# a newer settings file of the same layout (e.g. after an export from KLayout): offer the update
|
|
4490
|
+
self.refresh_config_notice()
|
|
4483
4491
|
|
|
4484
4492
|
# Save model code only when model tab active
|
|
4485
4493
|
self.export_model_action.setEnabled(index == modeleditor_index)
|
|
@@ -4550,8 +4558,20 @@ class MainWindowBase(QMainWindow):
|
|
|
4550
4558
|
return {}
|
|
4551
4559
|
return {}
|
|
4552
4560
|
|
|
4561
|
+
def _open_dialog_folder(self):
|
|
4562
|
+
"""Where Load Config / Import / Edit in place start: the folder of the script edited in place or of the
|
|
4563
|
+
settings file loaded last, else of the GDSII file; '' (the process' working folder) if there is none.
|
|
4564
|
+
Started from a layout tool, that working folder is the tool's, not the one of its export."""
|
|
4565
|
+
candidates = [getattr(getattr(self, "script_model", None), "path", None),
|
|
4566
|
+
getattr(self, "user_inputs_file", None), self.saved_values.get("GdsFile")]
|
|
4567
|
+
for path in candidates:
|
|
4568
|
+
if path and os.path.isdir(os.path.dirname(os.path.abspath(path))):
|
|
4569
|
+
return os.path.dirname(os.path.abspath(path))
|
|
4570
|
+
return ""
|
|
4571
|
+
|
|
4553
4572
|
def load_configuration_dialog(self):
|
|
4554
|
-
file_path, _ = QFileDialog.getOpenFileName(self, "Select Config File",
|
|
4573
|
+
file_path, _ = QFileDialog.getOpenFileName(self, "Select Config File", self._open_dialog_folder(),
|
|
4574
|
+
filter=f"*.{self.CONFIG_SUFFIX};;Python model code *.py")
|
|
4555
4575
|
# we can load JSON or Python models, decide which suffix we have
|
|
4556
4576
|
if file_path:
|
|
4557
4577
|
self.open_file(file_path)
|
|
@@ -4596,6 +4616,27 @@ class MainWindowBase(QMainWindow):
|
|
|
4596
4616
|
data.get("application", "") in MODEL_KIND_APPS.values():
|
|
4597
4617
|
self._warn_wrong_app_file(file_path, data["application"])
|
|
4598
4618
|
return
|
|
4619
|
+
if data.get("application", "") == self.APP_NAME and self.config_update_supported():
|
|
4620
|
+
if self.script_model is not None:
|
|
4621
|
+
# a script is edited in place: update it from this config, or open the config instead
|
|
4622
|
+
choice = self._ask_update_script_from_config(file_path, data)
|
|
4623
|
+
if choice == "cancel":
|
|
4624
|
+
return
|
|
4625
|
+
if choice == "update":
|
|
4626
|
+
self._update_script_from_config(file_path, data)
|
|
4627
|
+
return
|
|
4628
|
+
else:
|
|
4629
|
+
# a model script for the same layout has the settings made in setupEM since
|
|
4630
|
+
script = self._model_script_for_config(file_path, data)
|
|
4631
|
+
choice = self._ask_continue_with_script(file_path, script) if script else "config"
|
|
4632
|
+
if choice == "cancel":
|
|
4633
|
+
return
|
|
4634
|
+
if choice == "script":
|
|
4635
|
+
self.load_configuration_from_file(script, in_place=True, ask_unsaved=False)
|
|
4636
|
+
if self.script_model is not None:
|
|
4637
|
+
self.hide_info_bar()
|
|
4638
|
+
self._update_script_from_config(file_path, data)
|
|
4639
|
+
return
|
|
4599
4640
|
leave_current_script()
|
|
4600
4641
|
self.user_inputs_file = file_path
|
|
4601
4642
|
if data.get("application", "") == self.APP_NAME:
|
|
@@ -4894,6 +4935,8 @@ class MainWindowBase(QMainWindow):
|
|
|
4894
4935
|
if path_messages or not_editable:
|
|
4895
4936
|
status += " - see the log on the Create Model tab"
|
|
4896
4937
|
self.statusBar().showMessage(status, 15000)
|
|
4938
|
+
if in_place:
|
|
4939
|
+
self.refresh_config_notice()
|
|
4897
4940
|
|
|
4898
4941
|
else:
|
|
4899
4942
|
QMessageBox.information(self, "Error", f"Could not load file {file_path}")
|
|
@@ -4980,6 +5023,123 @@ class MainWindowBase(QMainWindow):
|
|
|
4980
5023
|
"objects": copy.deepcopy(self.preserve_objects()),
|
|
4981
5024
|
"create_call": self.preserve_create_call()}
|
|
4982
5025
|
|
|
5026
|
+
# ---------- Update a script edited in place from a settings file of the same layout ----------
|
|
5027
|
+
# A layout tool (e.g. the KLayout plug-in klayout_em_export) writes the GDS file and a settings file next to
|
|
5028
|
+
# it, and writes both again when the layout changes. The model script holds everything set later (Create Model
|
|
5029
|
+
# writes the script, not the settings file). So the settings that belong to the layout (ports, GDS file,
|
|
5030
|
+
# stackup, cell, purpose) come from the settings file, everything else stays as in the script. See
|
|
5031
|
+
# config_update.py.
|
|
5032
|
+
|
|
5033
|
+
def config_update_supported(self):
|
|
5034
|
+
"""Hook: True if this app can update a script edited in place from its settings file."""
|
|
5035
|
+
return False
|
|
5036
|
+
|
|
5037
|
+
def config_update_differences(self, data, base_dir):
|
|
5038
|
+
"""Hook: readable differences between the current settings and the layout part of data (a loaded
|
|
5039
|
+
settings file from base_dir), [] if none. The caller saves the tabs first if needed."""
|
|
5040
|
+
return []
|
|
5041
|
+
|
|
5042
|
+
def apply_config_update(self, data, base_dir):
|
|
5043
|
+
"""Hook: take the layout part of data (ports, GDS file, stackup, cell, purpose) into the tabs."""
|
|
5044
|
+
|
|
5045
|
+
def _update_script_from_config(self, file_path, data):
|
|
5046
|
+
"""Take the layout part of a settings file into the script edited in place (written on Create Model or
|
|
5047
|
+
Save, like every change in edit-in-place mode)."""
|
|
5048
|
+
self.apply_config_update(data, os.path.dirname(os.path.abspath(file_path)))
|
|
5049
|
+
dirty = self.script_has_unsaved_changes()
|
|
5050
|
+
if self.tabs_widget.currentWidget() is self.modeleditor_tab:
|
|
5051
|
+
self.show_preserve_preview(self.modeleditor_tab.model_edit)
|
|
5052
|
+
self.show_info_bar(f"Ports and layout settings taken from {os.path.basename(file_path)}. "
|
|
5053
|
+
+ ("Create Model or File > Save writes them into the script." if dirty
|
|
5054
|
+
else "The script already had them."))
|
|
5055
|
+
|
|
5056
|
+
def _ask_update_script_from_config(self, file_path, data):
|
|
5057
|
+
"""Loading a settings file while a script is edited in place: "update", "new" or "cancel"."""
|
|
5058
|
+
self.save_all_tabs()
|
|
5059
|
+
differences = self.config_update_differences(data, os.path.dirname(os.path.abspath(file_path)))
|
|
5060
|
+
name, script = os.path.basename(file_path), os.path.basename(self.script_model.path)
|
|
5061
|
+
if differences:
|
|
5062
|
+
found = (f"{name} has other ports or layout settings than the script:\n "
|
|
5063
|
+
+ "\n ".join(differences[:8]) + ("\n ..." if len(differences) > 8 else ""))
|
|
5064
|
+
else:
|
|
5065
|
+
found = f"{name} has the same ports and layout settings as the script."
|
|
5066
|
+
box = QMessageBox(self)
|
|
5067
|
+
box.setWindowTitle("Load Config")
|
|
5068
|
+
box.setIcon(QMessageBox.Question)
|
|
5069
|
+
box.setText(f"You are editing {script} in place.\n\n{found}\n\n"
|
|
5070
|
+
"Update the script from it: ports, GDSII file, stackup, cell and purpose come from the "
|
|
5071
|
+
"config, everything else (frequencies, mesh, your own code) stays.\n\n"
|
|
5072
|
+
"Open it as a new model: leave the script, the config's settings replace all.")
|
|
5073
|
+
update = box.addButton("Update the script from it", QMessageBox.AcceptRole)
|
|
5074
|
+
new = box.addButton("Open it as a new model", QMessageBox.ActionRole)
|
|
5075
|
+
cancel = box.addButton(QMessageBox.Cancel)
|
|
5076
|
+
box.setDefaultButton(update)
|
|
5077
|
+
box.setEscapeButton(cancel)
|
|
5078
|
+
box.exec()
|
|
5079
|
+
clicked = box.clickedButton()
|
|
5080
|
+
return "update" if clicked is update else "new" if clicked is new else "cancel"
|
|
5081
|
+
|
|
5082
|
+
def _model_script_for_config(self, file_path, data):
|
|
5083
|
+
"""The newest model script for the settings file's layout that can be edited in place, else None. It holds
|
|
5084
|
+
the settings made in setupEM after the settings file was last saved (Create Model writes the script); the
|
|
5085
|
+
settings file itself is written again by the layout tool on every export, so it is usually the newer
|
|
5086
|
+
file and its age says nothing."""
|
|
5087
|
+
saved = data.get("saved_values") or {}
|
|
5088
|
+
gds = saved.get("GdsFile")
|
|
5089
|
+
if not gds:
|
|
5090
|
+
return None
|
|
5091
|
+
base = os.path.dirname(os.path.abspath(file_path))
|
|
5092
|
+
gds = os.path.join(base, gds)
|
|
5093
|
+
folders = [base, os.path.dirname(os.path.abspath(gds))]
|
|
5094
|
+
if saved.get("sim_path"):
|
|
5095
|
+
folders.append(os.path.join(base, saved["sim_path"]))
|
|
5096
|
+
for script in model_scripts(gds, folders):
|
|
5097
|
+
if not self._in_place_problem(script):
|
|
5098
|
+
return script
|
|
5099
|
+
return None
|
|
5100
|
+
|
|
5101
|
+
def _ask_continue_with_script(self, file_path, script):
|
|
5102
|
+
"""A model script exists for the settings file's layout: "script", "config" or "cancel"."""
|
|
5103
|
+
box = QMessageBox(self)
|
|
5104
|
+
box.setWindowTitle("Load Config")
|
|
5105
|
+
box.setIcon(QMessageBox.Question)
|
|
5106
|
+
stamp = time.localtime(os.path.getmtime(script))
|
|
5107
|
+
when = time.strftime("%H:%M" if stamp[:3] == time.localtime()[:3] else "%Y-%m-%d %H:%M", stamp)
|
|
5108
|
+
box.setText(f"{os.path.basename(script)} (changed {when}) is the model script for this layout. It has "
|
|
5109
|
+
"the settings you last used for it (frequencies, mesh, ...), also those not saved in "
|
|
5110
|
+
f"{os.path.basename(file_path)}.\n\n"
|
|
5111
|
+
"Continue with the script: edit it in place, its settings and your own code stay, ports "
|
|
5112
|
+
"and layout settings come from the config.\n\n"
|
|
5113
|
+
"Start from the config: the config's settings for a new model.")
|
|
5114
|
+
script_button = box.addButton("Continue with the script", QMessageBox.AcceptRole)
|
|
5115
|
+
config_button = box.addButton("Start from the config", QMessageBox.ActionRole)
|
|
5116
|
+
cancel = box.addButton(QMessageBox.Cancel)
|
|
5117
|
+
box.setDefaultButton(script_button)
|
|
5118
|
+
box.setEscapeButton(cancel)
|
|
5119
|
+
box.exec()
|
|
5120
|
+
clicked = box.clickedButton()
|
|
5121
|
+
return "script" if clicked is script_button else "config" if clicked is config_button else "cancel"
|
|
5122
|
+
|
|
5123
|
+
def refresh_config_notice(self):
|
|
5124
|
+
"""Edit-in-place mode: point out a settings file of the same layout that is newer than the script and has
|
|
5125
|
+
other ports or layout settings, with an Update button (info bar; only when it shows nothing else)."""
|
|
5126
|
+
if self.script_model is None or not self.config_update_supported() or not self.info_bar.isHidden():
|
|
5127
|
+
return
|
|
5128
|
+
gds = self.saved_values.get("GdsFile")
|
|
5129
|
+
script = self.script_model.path
|
|
5130
|
+
if not gds or not script:
|
|
5131
|
+
return
|
|
5132
|
+
folders = [os.path.dirname(os.path.abspath(script)), os.path.dirname(os.path.abspath(gds))]
|
|
5133
|
+
for path, data in newer_configs(script, gds, folders, "." + self.CONFIG_SUFFIX, self.APP_NAME):
|
|
5134
|
+
differences = self.config_update_differences(data, os.path.dirname(os.path.abspath(path)))
|
|
5135
|
+
if differences:
|
|
5136
|
+
stamp = time.localtime(os.path.getmtime(path))
|
|
5137
|
+
when = time.strftime("%H:%M" if stamp[:3] == time.localtime()[:3] else "%Y-%m-%d %H:%M", stamp)
|
|
5138
|
+
self.show_info_bar(f"{os.path.basename(path)} (saved {when}) has other ports or layout settings "
|
|
5139
|
+
f"than this script ({len(differences)} difference(s)).", "Update from it",
|
|
5140
|
+
lambda p=path, d=data: self._update_script_from_config(p, d))
|
|
5141
|
+
return
|
|
5142
|
+
|
|
4983
5143
|
def _in_place_problem(self, file_path):
|
|
4984
5144
|
"""Why a script can't be opened for editing in place, or "" if it can."""
|
|
4985
5145
|
try:
|
|
@@ -5008,7 +5168,8 @@ class MainWindowBase(QMainWindow):
|
|
|
5008
5168
|
|
|
5009
5169
|
def import_py_model(self):
|
|
5010
5170
|
"""File > Import from *.py model: its settings for a new, generated model."""
|
|
5011
|
-
file_path, _ = QFileDialog.getOpenFileName(self, "Import from *.py Model",
|
|
5171
|
+
file_path, _ = QFileDialog.getOpenFileName(self, "Import from *.py Model", self._open_dialog_folder(),
|
|
5172
|
+
filter="*.py model code")
|
|
5012
5173
|
if file_path:
|
|
5013
5174
|
self.load_configuration_from_file(file_path)
|
|
5014
5175
|
|
|
@@ -5016,7 +5177,8 @@ class MainWindowBase(QMainWindow):
|
|
|
5016
5177
|
"""File > Edit *.py model in place: Create Model / Save write only the
|
|
5017
5178
|
changed values back into the script, its own code stays."""
|
|
5018
5179
|
if not file_path:
|
|
5019
|
-
file_path, _ = QFileDialog.getOpenFileName(self, "Edit *.py Model in Place",
|
|
5180
|
+
file_path, _ = QFileDialog.getOpenFileName(self, "Edit *.py Model in Place", self._open_dialog_folder(),
|
|
5181
|
+
filter="*.py model code")
|
|
5020
5182
|
if not file_path:
|
|
5021
5183
|
return
|
|
5022
5184
|
problem = self._in_place_problem(file_path)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: setupEM
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.16.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
|
|
@@ -10,7 +10,7 @@ Project-URL: Changelog, https://github.com/VolkerMuehlhaus/setupEM/blob/main/doc
|
|
|
10
10
|
Requires-Python: >=3.9
|
|
11
11
|
Description-Content-Type: text/markdown
|
|
12
12
|
License-File: LICENSE
|
|
13
|
-
Requires-Dist: gds2palace>=0.9.
|
|
13
|
+
Requires-Dist: gds2palace>=0.9.3
|
|
14
14
|
Requires-Dist: gds_prepare_for_EM>=1.6.0
|
|
15
15
|
Requires-Dist: PySide6
|
|
16
16
|
Requires-Dist: shiboken6
|
|
@@ -40,6 +40,24 @@ https://github.com/VolkerMuehlhaus/setupEM
|
|
|
40
40
|
|
|
41
41
|
## Recent changes
|
|
42
42
|
|
|
43
|
+
# What's New - October 31, 2026 (setupEM 0.16.0)
|
|
44
|
+
|
|
45
|
+
**After a new export from the layout tool** (setupEM): a layout tool such as the KLayout plug-in klayout_em_export writes the GDSII file and a `.simcfg`, and writes both again when the layout changes. The settings you made in setupEM since are only in your model script, because Create Model writes the script, not the `.simcfg`. setupEM now takes from such a `.simcfg` only the ports, GDSII file, stackup, cell and purpose, and keeps everything else as in the script:
|
|
46
|
+
- Opening a `.simcfg` when a model script for the same GDSII file exists next to it asks whether to **Continue with the script** (edit it in place, ports and layout settings from the `.simcfg`) or **Start from the config**.
|
|
47
|
+
- Loading a `.simcfg` while editing a script in place lists the ports and layout settings that differ and asks whether to **Update the script from it** or **Open it as a new model**. Before, loading it ended editing in place and replaced all settings.
|
|
48
|
+
- While editing a script in place, the bar below the menu points out a newer `.simcfg` of the same GDSII file with other ports or layout settings, with **Update from it**.
|
|
49
|
+
|
|
50
|
+
**Load Config, Import from \*.py model and Edit \*.py model in place** start in the folder of the model you are working on (the config or script loaded last, else the GDSII file). Before, they started in the folder setupEM was launched from, e.g. KLayout's when the KLayout plug-in opened setupEM.
|
|
51
|
+
|
|
52
|
+
setupEM 0.16.0 needs gds2palace 0.9.3 or later: a layout with several shapes on one port layer (e.g. a transistor terminal on several fingers, from the KLayout plug-in) makes one port with several elements there; older gds2palace versions used only the last shape.
|
|
53
|
+
|
|
54
|
+
**Fixes:**
|
|
55
|
+
- `scripts/combine_extend_snp.py` is the same as gds2palace's again. It had missed three fixes made there in September: the port impedance lookup for AMR iterations, the missing `.sNp` extension for a model name that contains a dot, and Touchstone output after an interrupted Palace run.
|
|
56
|
+
- On Linux with GNOME (Wayland), the mouse cursor no longer keeps spinning for about 10 seconds after setupEM starts. setupEM briefly opened three invisible windows while building the Input Files tab, and GNOME waited for them to finish starting.
|
|
57
|
+
- Via ports with direction `-Z` keep their from and to layers. Before, loading a `.simcfg` dropped them, and Create Model wrote such a port as an in-plane port.
|
|
58
|
+
- An incomplete port (direction and layers that don't fit, e.g. from a `.simcfg` written by another tool) gives a warning in the Ports tab. Before, gds2palace exited and setupEM closed. A lowercase `z` counts as a via port too.
|
|
59
|
+
- `combine_extend_snp` (Touchstone output after a Palace run) handles ports with several elements (several shapes on one port layer, gds2palace 0.9.3): one Z0 per port in the option line, and the elements' port inductances combined in parallel for de-embedding. Before, the last element overwrote the others.
|
|
60
|
+
|
|
43
61
|
# What's New - October 29, 2026
|
|
44
62
|
|
|
45
63
|
**File > Edit \*.py model in place** (new, setupEM and setupThermal): edits a gds2palace model script directly, e.g. one from the gds2palace MCP server, an example or your own code. Create Model and **File > Save** write only the values you change; the script's loops, comments and custom code stay. Settings the script computes itself are greyed out. A script that builds several models (a parameter sweep) lists them on the Create Model tab, and Start Simulation runs the ticked ones. After importing a script that setupEM didn't write, a bar below the menu offers **Edit in place instead**.
|
|
@@ -56,17 +74,13 @@ Import and edit in place now read a stackup given as `utilities.get_stackup_path
|
|
|
56
74
|
|
|
57
75
|
Fix: with **Merge via arrays** (and no fill removal or merging per layer), the simplified file still contained the original subcells as extra top-level cells. Without a cell name, gds2palace reads the first top-level cell, which could be one of them, e.g. a pad cell instead of the design. These left-over cells are now removed.
|
|
58
76
|
|
|
59
|
-
# What's New - October 19, 2026
|
|
60
|
-
|
|
61
|
-
**Stackup Editor, ADS Momentum (*.ltd) import:** metals with an EXPAND thickness operation now make the dielectric next to them thicker, as in ADS, instead of reaching into the dielectric below. Before, a thick EXPAND DOWN metal (e.g. MET1/MET2 in a GaAs stackup) overlapped the layers underneath it, and a via could end up with zero height. Files where long lines are wrapped or END_... markers follow the last entry on the same line now import too; before, they failed with "No usable dielectric slabs".
|
|
62
|
-
|
|
63
77
|
Full history: [CHANGES.md](https://github.com/VolkerMuehlhaus/setupEM/blob/main/doc/CHANGES.md)
|
|
64
78
|
|
|
65
79
|
---
|
|
66
80
|
|
|
67
81
|
**Note:** the `setupEM` PyPI package itself requires:
|
|
68
82
|
|
|
69
|
-
- gds2palace>=0.9.
|
|
83
|
+
- gds2palace>=0.9.3
|
|
70
84
|
- gds_prepare_for_EM>=1.6.0
|
|
71
85
|
- PySide6
|
|
72
86
|
- shiboken6
|
|
@@ -4,6 +4,7 @@ README_pypi.md
|
|
|
4
4
|
pyproject.toml
|
|
5
5
|
src/setupEM/__init__.py
|
|
6
6
|
src/setupEM/__main__.py
|
|
7
|
+
src/setupEM/config_update.py
|
|
7
8
|
src/setupEM/field_viewer.py
|
|
8
9
|
src/setupEM/gds_hierarchy_scan.py
|
|
9
10
|
src/setupEM/layout_preview.py
|
|
@@ -32,6 +33,8 @@ src/setupEM/data/SG13G2_200um.xml
|
|
|
32
33
|
src/setupEM/data/SG13G2_FEM_200um.xml
|
|
33
34
|
src/setupEM/data/SG13G2_FEM_200um_passi3D.xml
|
|
34
35
|
src/setupEM/data/SG13G2_nosub.xml
|
|
36
|
+
tests/test_config_update.py
|
|
37
|
+
tests/test_config_update_gui.py
|
|
35
38
|
tests/test_run_with_overrides.py
|
|
36
39
|
tests/test_script_model.py
|
|
37
40
|
tests/test_script_probe.py
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Tests for updating a script edited in place from a *.simcfg of the same layout (src/setupEM/config_update.py).
|
|
2
|
+
#
|
|
3
|
+
# Run from the repo root: python -m pytest tests
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
import time
|
|
9
|
+
|
|
10
|
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
11
|
+
sys.path.insert(0, os.path.join(HERE, "..", "src"))
|
|
12
|
+
|
|
13
|
+
from setupEM.config_update import (layout_differences, model_scripts, newer_configs, # noqa: E402
|
|
14
|
+
port_differences)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def via(n, layer, frm="Metal1", to="TopMetal2", direction="Z", z0=50.0, voltage=1.0):
|
|
18
|
+
return {"portnumber": n, "source_layernum": layer, "from_layername": frm, "to_layername": to,
|
|
19
|
+
"target_layername": None, "direction": direction, "port_Z0": z0, "voltage": voltage}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_same_ports_no_differences():
|
|
23
|
+
ports = [via(1, 201), via(2, 202)]
|
|
24
|
+
assert port_differences(ports, [dict(p) for p in ports]) == []
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_z0_and_excitation_are_not_compared():
|
|
28
|
+
assert port_differences([via(1, 201)], [via(1, 201, z0=25.0, voltage=0.0)]) == []
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_renumbered_new_and_removed_ports():
|
|
32
|
+
old = [via(1, 201), via(2, 202), via(3, 203)]
|
|
33
|
+
new = [via(1, 201), via(2, 203), via(3, 204, "Metal2", "Metal5", "-Z"), via(4, 205)]
|
|
34
|
+
diff = port_differences(old, new)
|
|
35
|
+
assert diff[0] == "port 2: was via Metal1 -> TopMetal2 (Z) on layer 202, now via Metal1 -> TopMetal2 (Z) on layer 203"
|
|
36
|
+
assert diff[1].startswith("port 3: was") and "(-Z)" in diff[1]
|
|
37
|
+
assert diff[2].startswith("port 4: new")
|
|
38
|
+
assert port_differences(new, old)[-1] == "port 4: removed"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_direction_case_and_in_plane_ports():
|
|
42
|
+
in_plane = {"portnumber": 1, "source_layernum": "201", "direction": "x", "target_layername": "Metal5",
|
|
43
|
+
"from_layername": "", "to_layername": ""}
|
|
44
|
+
assert port_differences([in_plane], [dict(in_plane, direction="X", source_layernum=201)]) == []
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_layout_differences(tmp_path):
|
|
48
|
+
gds = tmp_path / "chip_em.gds"
|
|
49
|
+
old = {"GdsFile": str(gds), "SubstrateFile": "/x/SG13G2.xml", "cellname": "TOP", "purpose": [0]}
|
|
50
|
+
assert layout_differences(old, {"GdsFile": "chip_em.gds", "purpose": ["0"]}, str(tmp_path)) == []
|
|
51
|
+
diff = layout_differences(old, {"GdsFile": "other_em.gds", "cellname": "CORE"}, str(tmp_path))
|
|
52
|
+
assert diff == ["GDSII file: chip_em.gds -> other_em.gds", "cell: TOP -> CORE"]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _touch(path, text, age):
|
|
56
|
+
path.write_text(text)
|
|
57
|
+
stamp = time.time() - age
|
|
58
|
+
os.utime(path, (stamp, stamp))
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_newer_configs_and_scripts(tmp_path):
|
|
62
|
+
gds = tmp_path / "chip_em.gds"
|
|
63
|
+
gds.write_bytes(b"")
|
|
64
|
+
config = {"application": "setupEM", "saved_values": {"GdsFile": "chip_em.gds"}, "ports": [via(1, 201)]}
|
|
65
|
+
script_text = f"settings = {{}}\nsettings['GdsFile'] = {str(gds)!r}\n"
|
|
66
|
+
_touch(tmp_path / "chip_em.py", script_text, 100)
|
|
67
|
+
_touch(tmp_path / "chip_em.simcfg", json.dumps(config), 10)
|
|
68
|
+
_touch(tmp_path / "other.simcfg", json.dumps(dict(config, saved_values={"GdsFile": "other.gds"})), 5)
|
|
69
|
+
_touch(tmp_path / "thermal.simcfg", json.dumps(dict(config, application="setupThermal")), 5)
|
|
70
|
+
|
|
71
|
+
found = newer_configs(str(tmp_path / "chip_em.py"), str(gds), [str(tmp_path), str(tmp_path)])
|
|
72
|
+
assert [os.path.basename(p) for p, _ in found] == ["chip_em.simcfg"]
|
|
73
|
+
|
|
74
|
+
# model scripts for the GDS, whatever their age against the config (the layout tool writes the config again on
|
|
75
|
+
# every export), newest first
|
|
76
|
+
_touch(tmp_path / "relative.py", "settings = {}\nsettings['GdsFile'] = 'chip_em.gds'\n", 2)
|
|
77
|
+
_touch(tmp_path / "unrelated.py", "x = 'chip_em_old.gds'\n", 1)
|
|
78
|
+
scripts = model_scripts(str(gds), [str(tmp_path), str(tmp_path)])
|
|
79
|
+
assert [os.path.basename(p) for p in scripts] == ["relative.py", "chip_em.py"]
|
|
80
|
+
|
|
81
|
+
# the script saved after the config: the config is no longer newer
|
|
82
|
+
_touch(tmp_path / "chip_em.py", script_text, 1)
|
|
83
|
+
assert newer_configs(str(tmp_path / "chip_em.py"), str(gds), [str(tmp_path)]) == []
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# GUI test (offscreen) for updating a script edited in place from a *.simcfg of the same layout:
|
|
2
|
+
# - opening a .simcfg when a newer model script for the same GDS exists ("Continue with the script"),
|
|
3
|
+
# - loading a .simcfg while a script is edited in place ("Update the script from it" / "Open it as a new model"),
|
|
4
|
+
# - the info bar notice about a newer .simcfg, with its "Update from it" button.
|
|
5
|
+
# Needs the sibling repo gds2palace_ihp_sg13g2 (GDS and stackup); skipped without it.
|
|
6
|
+
#
|
|
7
|
+
# Run from the repo root: python -m pytest tests
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import shutil
|
|
12
|
+
import sys
|
|
13
|
+
import time
|
|
14
|
+
|
|
15
|
+
import pytest
|
|
16
|
+
|
|
17
|
+
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
|
18
|
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
19
|
+
sys.path.insert(0, os.path.join(HERE, "..", "src"))
|
|
20
|
+
G2P = os.path.join(HERE, "..", "..", "gds2palace_ihp_sg13g2")
|
|
21
|
+
GDS = os.path.join(G2P, "workflow", "inductor_500pH_with_ports.gds")
|
|
22
|
+
XML = os.path.join(G2P, "XML_stackup", "legacy", "SG13G2_200um.xml")
|
|
23
|
+
pytestmark = pytest.mark.skipif(not (os.path.isfile(GDS) and os.path.isfile(XML)),
|
|
24
|
+
reason="needs gds2palace_ihp_sg13g2 next to this repo")
|
|
25
|
+
|
|
26
|
+
from PySide6.QtWidgets import QApplication, QMessageBox # noqa: E402
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def via(n, layer, direction="Z", frm="Metal1", to="TopMetal2"):
|
|
30
|
+
return {"portnumber": n, "source_layernum": layer, "target_layername": None, "from_layername": frm,
|
|
31
|
+
"to_layername": to, "direction": direction, "port_Z0": 50.0, "voltage": 1.0}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def set_age(path, seconds):
|
|
35
|
+
stamp = time.time() - seconds
|
|
36
|
+
os.utime(path, (stamp, stamp))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@pytest.fixture
|
|
40
|
+
def win(monkeypatch):
|
|
41
|
+
app = QApplication.instance() or QApplication([])
|
|
42
|
+
for name, answer in (("information", QMessageBox.Ok), ("warning", QMessageBox.Ok),
|
|
43
|
+
("question", QMessageBox.Yes)):
|
|
44
|
+
monkeypatch.setattr(QMessageBox, name, staticmethod(lambda *a, answer=answer, **k: answer))
|
|
45
|
+
from setupEM.setupEM import MainWindow
|
|
46
|
+
window = MainWindow()
|
|
47
|
+
yield window
|
|
48
|
+
window._script_dirty = False
|
|
49
|
+
window.close()
|
|
50
|
+
app.processEvents()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@pytest.fixture
|
|
54
|
+
def layout(tmp_path):
|
|
55
|
+
"""A setupEM script with its own comment, and a .simcfg exported later with other ports and frequencies."""
|
|
56
|
+
gds = tmp_path / "chip_em.gds"
|
|
57
|
+
shutil.copy(GDS, gds)
|
|
58
|
+
script = (open(os.path.join(HERE, "fixtures", "generated_setupEM_palace.py"), encoding="utf-8").read()
|
|
59
|
+
.replace("settings['GdsFile'] = 'inductor_500pH_with_ports.gds'",
|
|
60
|
+
f"settings['GdsFile'] = {str(gds).replace(os.sep, '/')!r}")
|
|
61
|
+
.replace("settings['SubstrateFile'] = '../XML_stackup/legacy/SG13G2_200um.xml'",
|
|
62
|
+
f"settings['SubstrateFile'] = {os.path.abspath(XML).replace(os.sep, '/')!r}")
|
|
63
|
+
.replace("settings['fstop'] = 100.0e9", "settings['fstop'] = 100.0e9 # my own note"))
|
|
64
|
+
assert "# my own note" in script and str(gds).replace(os.sep, "/") in script
|
|
65
|
+
(tmp_path / "chip_em.py").write_text(script, encoding="utf-8")
|
|
66
|
+
|
|
67
|
+
def write_config(name, ports, age):
|
|
68
|
+
data = {"application": "setupEM", "data_format": "1.0",
|
|
69
|
+
"saved_values": {"unit": 1e-06, "GdsFile": "chip_em.gds", "SubstrateFile": os.path.abspath(XML),
|
|
70
|
+
"cellname": "", "purpose": [0], "fstart": 0.0, "fstop": 7.0, "fstep": 1.0},
|
|
71
|
+
"ports": ports}
|
|
72
|
+
path = tmp_path / name
|
|
73
|
+
path.write_text(json.dumps(data), encoding="utf-8")
|
|
74
|
+
set_age(path, age)
|
|
75
|
+
return str(path)
|
|
76
|
+
|
|
77
|
+
return tmp_path, write_config
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def code_of(win):
|
|
81
|
+
win.save_all_tabs()
|
|
82
|
+
return win.preserve_mode_patch(win.script_model.path)[0]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def port_lines(code):
|
|
86
|
+
return [line for line in code.splitlines() if "simulation_port(" in line and not line.lstrip().startswith("#")]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_open_config_continues_with_script(win, layout, monkeypatch):
|
|
90
|
+
folder, write_config = layout
|
|
91
|
+
# the usual order: Create Model wrote the script, then the layout tool exported again (a newer config)
|
|
92
|
+
set_age(folder / "chip_em.py", 100)
|
|
93
|
+
config = write_config("chip_em.simcfg", [via(1, 201), via(2, 202, "-Z"), via(3, 203)], age=10)
|
|
94
|
+
asked = []
|
|
95
|
+
monkeypatch.setattr(win, "_ask_continue_with_script",
|
|
96
|
+
lambda c, s: asked.append(os.path.basename(s)) or "script")
|
|
97
|
+
win.load_configuration_from_file(config)
|
|
98
|
+
assert asked == ["chip_em.py"]
|
|
99
|
+
assert win.script_model is not None and os.path.basename(win.script_model.path) == "chip_em.py"
|
|
100
|
+
code = code_of(win)
|
|
101
|
+
assert "settings['fstop'] = 100.0e9 # my own note" in code # the script's settings and comment stay
|
|
102
|
+
lines = port_lines(code)
|
|
103
|
+
assert len(lines) == 3
|
|
104
|
+
assert "source_layernum=202" in lines[1] and "'-Z'" in lines[1]
|
|
105
|
+
assert "source_layernum=203" in lines[2]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def test_open_config_without_script_asks_nothing(win, layout, monkeypatch):
|
|
109
|
+
folder, write_config = layout
|
|
110
|
+
os.remove(folder / "chip_em.py") # no model script for this layout yet
|
|
111
|
+
config = write_config("chip_em.simcfg", [via(1, 201)], age=10)
|
|
112
|
+
monkeypatch.setattr(win, "_ask_continue_with_script", lambda c, s: pytest.fail("asked"))
|
|
113
|
+
win.load_configuration_from_file(config)
|
|
114
|
+
assert win.script_model is None and win.user_inputs_file == config
|
|
115
|
+
# Load Config / Import / Edit in place start in the folder of the config, not the working folder
|
|
116
|
+
assert os.path.normcase(win._open_dialog_folder()) == os.path.normcase(str(folder))
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def test_start_from_config(win, layout, monkeypatch):
|
|
120
|
+
folder, write_config = layout
|
|
121
|
+
config = write_config("chip_em.simcfg", [via(1, 201)], age=100)
|
|
122
|
+
set_age(folder / "chip_em.py", 10)
|
|
123
|
+
monkeypatch.setattr(win, "_ask_continue_with_script", lambda c, s: "config")
|
|
124
|
+
win.load_configuration_from_file(config)
|
|
125
|
+
assert win.script_model is None and win.user_inputs_file == config
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def test_load_config_while_editing_in_place(win, layout, monkeypatch):
|
|
129
|
+
folder, write_config = layout
|
|
130
|
+
script = str(folder / "chip_em.py")
|
|
131
|
+
win.edit_py_model(script)
|
|
132
|
+
assert win.script_model is not None
|
|
133
|
+
config = write_config("chip_em.simcfg", [via(1, 201), via(2, 202), via(3, 204)], age=0)
|
|
134
|
+
seen = []
|
|
135
|
+
|
|
136
|
+
def ask(path, data):
|
|
137
|
+
win.save_all_tabs()
|
|
138
|
+
seen.append(win.config_update_differences(data, str(folder)))
|
|
139
|
+
return "update"
|
|
140
|
+
monkeypatch.setattr(win, "_ask_update_script_from_config", ask)
|
|
141
|
+
win.load_configuration_from_file(config)
|
|
142
|
+
assert seen and seen[0] == ["port 3: new, via Metal1 -> TopMetal2 (Z) on layer 204"]
|
|
143
|
+
assert win.script_model is not None
|
|
144
|
+
lines = port_lines(code_of(win))
|
|
145
|
+
assert len(lines) == 3 and "source_layernum=204" in lines[2]
|
|
146
|
+
|
|
147
|
+
monkeypatch.setattr(win, "_ask_update_script_from_config", lambda p, d: "new")
|
|
148
|
+
win._script_dirty = False
|
|
149
|
+
monkeypatch.setattr(win, "confirm_unsaved_script", lambda: True)
|
|
150
|
+
win.load_configuration_from_file(config)
|
|
151
|
+
assert win.script_model is None and win.user_inputs_file == config
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def test_notice_about_newer_config(win, layout):
|
|
155
|
+
folder, write_config = layout
|
|
156
|
+
script = folder / "chip_em.py"
|
|
157
|
+
set_age(script, 100)
|
|
158
|
+
win.edit_py_model(str(script))
|
|
159
|
+
win.hide_info_bar()
|
|
160
|
+
write_config("chip_em.simcfg", [via(1, 201), via(2, 202, "-Z")], age=10)
|
|
161
|
+
win.refresh_config_notice()
|
|
162
|
+
assert not win.info_bar.isHidden() and win.info_bar_button.text() == "Update from it"
|
|
163
|
+
assert "chip_em.simcfg" in win.info_bar_label.text()
|
|
164
|
+
win._info_bar_action_clicked()
|
|
165
|
+
lines = port_lines(code_of(win))
|
|
166
|
+
assert "'-Z'" in lines[1]
|
|
167
|
+
win.hide_info_bar()
|
|
168
|
+
win.refresh_config_notice() # the settings now match the config: no notice
|
|
169
|
+
assert win.info_bar.isHidden()
|
|
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
|
|
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
|