setupEM 0.14.2__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.
Files changed (45) hide show
  1. setupem-0.16.0/PKG-INFO +97 -0
  2. {setupem-0.14.2 → setupem-0.16.0}/README.md +4 -2
  3. setupem-0.16.0/README_pypi.md +68 -0
  4. {setupem-0.14.2 → setupem-0.16.0}/pyproject.toml +4 -1
  5. {setupem-0.14.2 → setupem-0.16.0}/src/setupEM/__init__.py +1 -1
  6. setupem-0.16.0/src/setupEM/config_update.py +179 -0
  7. {setupem-0.14.2 → setupem-0.16.0}/src/setupEM/result_viewer.py +8 -3
  8. setupem-0.16.0/src/setupEM/run_with_overrides.py +287 -0
  9. setupem-0.16.0/src/setupEM/script_model.py +1054 -0
  10. setupem-0.16.0/src/setupEM/script_probe.py +275 -0
  11. {setupem-0.14.2 → setupem-0.16.0}/src/setupEM/setupEM.py +210 -70
  12. {setupem-0.14.2 → setupem-0.16.0}/src/setupEM/setupThermal.py +97 -44
  13. {setupem-0.14.2 → setupem-0.16.0}/src/setupEM/setup_common.py +1348 -193
  14. setupem-0.16.0/src/setupEM.egg-info/PKG-INFO +97 -0
  15. {setupem-0.14.2 → setupem-0.16.0}/src/setupEM.egg-info/SOURCES.txt +10 -1
  16. {setupem-0.14.2 → setupem-0.16.0}/src/setupEM.egg-info/requires.txt +4 -1
  17. setupem-0.16.0/tests/test_config_update.py +83 -0
  18. setupem-0.16.0/tests/test_config_update_gui.py +169 -0
  19. setupem-0.16.0/tests/test_run_with_overrides.py +147 -0
  20. setupem-0.16.0/tests/test_script_model.py +535 -0
  21. setupem-0.16.0/tests/test_script_probe.py +164 -0
  22. setupem-0.14.2/PKG-INFO +0 -75
  23. setupem-0.14.2/README_pypi.md +0 -48
  24. setupem-0.14.2/src/setupEM.egg-info/PKG-INFO +0 -75
  25. {setupem-0.14.2 → setupem-0.16.0}/LICENSE +0 -0
  26. {setupem-0.14.2 → setupem-0.16.0}/setup.cfg +0 -0
  27. {setupem-0.14.2 → setupem-0.16.0}/src/setupEM/__main__.py +0 -0
  28. {setupem-0.14.2 → setupem-0.16.0}/src/setupEM/data/SG13CMOS5L_200um.xml +0 -0
  29. {setupem-0.14.2 → setupem-0.16.0}/src/setupEM/data/SG13G2_100um.xml +0 -0
  30. {setupem-0.14.2 → setupem-0.16.0}/src/setupEM/data/SG13G2_200um.xml +0 -0
  31. {setupem-0.14.2 → setupem-0.16.0}/src/setupEM/data/SG13G2_FEM_200um.xml +0 -0
  32. {setupem-0.14.2 → setupem-0.16.0}/src/setupEM/data/SG13G2_FEM_200um_passi3D.xml +0 -0
  33. {setupem-0.14.2 → setupem-0.16.0}/src/setupEM/data/SG13G2_nosub.xml +0 -0
  34. {setupem-0.14.2 → setupem-0.16.0}/src/setupEM/field_viewer.py +0 -0
  35. {setupem-0.14.2 → setupem-0.16.0}/src/setupEM/gds_hierarchy_scan.py +0 -0
  36. {setupem-0.14.2 → setupem-0.16.0}/src/setupEM/layout_preview.py +0 -0
  37. {setupem-0.14.2 → setupem-0.16.0}/src/setupEM/momentum_import.py +0 -0
  38. {setupem-0.14.2 → setupem-0.16.0}/src/setupEM/palace_results.py +0 -0
  39. {setupem-0.14.2 → setupem-0.16.0}/src/setupEM/simplify_gds.py +0 -0
  40. {setupem-0.14.2 → setupem-0.16.0}/src/setupEM/stackupEditor.py +0 -0
  41. {setupem-0.14.2 → setupem-0.16.0}/src/setupEM/stackup_writer.py +0 -0
  42. {setupem-0.14.2 → setupem-0.16.0}/src/setupEM/thermal_results.py +0 -0
  43. {setupem-0.14.2 → setupem-0.16.0}/src/setupEM.egg-info/dependency_links.txt +0 -0
  44. {setupem-0.14.2 → setupem-0.16.0}/src/setupEM.egg-info/entry_points.txt +0 -0
  45. {setupem-0.14.2 → setupem-0.16.0}/src/setupEM.egg-info/top_level.txt +0 -0
@@ -0,0 +1,97 @@
1
+ Metadata-Version: 2.4
2
+ Name: setupEM
3
+ Version: 0.16.0
4
+ Summary: Python tool for configuration of gds2palace workflow with GUI.
5
+ Author-email: Volker Muehlhaus <volker@muehlhaus.com>
6
+ License-Expression: GPL-3.0-or-later
7
+ Project-URL: Homepage, https://github.com/VolkerMuehlhaus/setupEM
8
+ Project-URL: Repository, https://github.com/VolkerMuehlhaus/setupEM
9
+ Project-URL: Changelog, https://github.com/VolkerMuehlhaus/setupEM/blob/main/doc/CHANGES.md
10
+ Requires-Python: >=3.9
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: gds2palace>=0.9.3
14
+ Requires-Dist: gds_prepare_for_EM>=1.6.0
15
+ Requires-Dist: PySide6
16
+ Requires-Dist: shiboken6
17
+ Requires-Dist: scipy
18
+ Requires-Dist: requests
19
+ Requires-Dist: scikit-rf
20
+ Requires-Dist: matplotlib
21
+ Requires-Dist: numpy
22
+ Requires-Dist: gdspy
23
+ Requires-Dist: meshio
24
+ Requires-Dist: pyvista
25
+ Requires-Dist: pyvistaqt
26
+ Provides-Extra: test
27
+ Requires-Dist: pytest; extra == "test"
28
+ Dynamic: license-file
29
+
30
+ # Python GUI for gds2palace
31
+
32
+ ![Intro](https://raw.githubusercontent.com/VolkerMuehlhaus/setupEM/main/doc/png/setupEM_banner.png)
33
+
34
+ ## Install
35
+
36
+ pip install setupEM
37
+
38
+ **Full documentation, installation guide, and usage walkthrough:**
39
+ https://github.com/VolkerMuehlhaus/setupEM
40
+
41
+ ## Recent changes
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
+
61
+ # What's New - October 29, 2026
62
+
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**.
64
+
65
+ **File > Import from \*.py model** now runs a gds2palace script to read it, without reading the GDSII file and without meshing. The run stops where the script builds the model, so its solver start never runs. The import now gets exactly what gds2palace would get, also values and ports the script computes. Before, ports made in a loop (as in many gds2palace MCP scripts) were left out, and the imported model was missing them. The script's own code before building the model does run, e.g. a script that generates its GDSII file writes it. If the run fails, the import reads the script text as before, and the bar below the menu says why.
66
+
67
+ **File menu:** the three Recent submenus are now one **Open Recent** submenu, with sections for configs, imported \*.py models and \*.py models edited in place. A file reopens the way it was opened. The menu is grouped as open, recent, save, preferences. **Load Default Config** and **Save as Default Config** are removed: defaults for new models are set in **File > Preferences**. An existing `default.simcfg` / `default.tsimcfg` in your home directory still opens with **Load Config**, and **Save Config** can write it.
68
+
69
+ Import and edit in place now read a stackup given as `utilities.get_stackup_path("SG13G2_FEM_200um.xml")`, the way the gds2palace MCP server writes it (needs gds2palace 0.9.1 or later). Before, an imported model using that syntax had no stackup file.
70
+
71
+ # What's New - October 21, 2026
72
+
73
+ **Tools > Simplify GDS:** the simplification code moved to gds_prepare_for_EM (module `gds_simplify_for_em`), where the gds2palace MCP server uses it too. setupEM now requires gds_prepare_for_EM 1.6.0 or later; `pip install --upgrade setupEM` updates it.
74
+
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.
76
+
77
+ Full history: [CHANGES.md](https://github.com/VolkerMuehlhaus/setupEM/blob/main/doc/CHANGES.md)
78
+
79
+ ---
80
+
81
+ **Note:** the `setupEM` PyPI package itself requires:
82
+
83
+ - gds2palace>=0.9.3
84
+ - gds_prepare_for_EM>=1.6.0
85
+ - PySide6
86
+ - shiboken6
87
+ - scipy
88
+ - requests
89
+ - scikit-rf
90
+ - matplotlib
91
+ - numpy
92
+ - gdspy
93
+ - meshio
94
+ - pyvista
95
+ - pyvistaqt
96
+
97
+ (Other Python modules mentioned above are only needed to run standalone helper scripts in this repository, not to use the installed package.)
@@ -269,9 +269,11 @@ Behind the scenes, the setupEM user interface created Python model code for gds2
269
269
  <img src="./doc/png/code1.png" alt="code" width="700">
270
270
 
271
271
  ## File menu
272
- In the setupEM File menu, you can save and load simulation configurations, and you can also save and load a user defined "Default Config" configuration. This includes the choice of simulation target directory and all other settings. Configurations are stored in a JSON file with file extension ".simcfg". The "Default Config" will be stored to the user home diretory.
272
+ In the setupEM File menu, you can save and load simulation configurations. This includes the choice of simulation target directory and all other settings. Configurations are stored in a JSON file with file extension ".simcfg". Default values for new models are set in File > Preferences.
273
273
 
274
- Using "File > Import from *.py model", you can load settings from existing simulation model code, e.g. the examples included in the gds2palace repository. This import is based on detecting known keywords, with or without the settings[] syntax, and also works for openEMS Python models. Note that openEMS substrates model the MIM differently, and parameter "refined_cellsize" will usually be smaller in openEMS simulation, so you need to adjust these settings.
274
+ Using "File > Import from *.py model", you can load settings from existing simulation model code, e.g. the examples included in the gds2palace repository. A gds2palace script is run to read it, without reading the GDSII file and without meshing, so also values and ports the script computes are imported. If that fails, and for openEMS Python models, the import detects known keywords in the script text, with or without the settings[] syntax. Note that openEMS substrates model the MIM differently, and parameter "refined_cellsize" will usually be smaller in openEMS simulation, so you need to adjust these settings.
275
+
276
+ Using "File > Edit *.py model in place", setupEM edits a gds2palace model script directly, e.g. one created by the gds2palace MCP server or written by hand: it writes only the values you change back into the script, its loops, comments and custom code stay.
275
277
 
276
278
  If you are on the "Code" tab, you can also export the Python model code using "File > Export to *.py model". This option is only required if you want to save the model code **without** running it. Buttons "Create mesh and model file" and "Run Palace" on the "Create Model" tab will also save the model code to the target directory, and run it from there.
277
279
 
@@ -0,0 +1,68 @@
1
+ # Python GUI for gds2palace
2
+
3
+ ![Intro](https://raw.githubusercontent.com/VolkerMuehlhaus/setupEM/main/doc/png/setupEM_banner.png)
4
+
5
+ ## Install
6
+
7
+ pip install setupEM
8
+
9
+ **Full documentation, installation guide, and usage walkthrough:**
10
+ https://github.com/VolkerMuehlhaus/setupEM
11
+
12
+ ## Recent changes
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
+
32
+ # What's New - October 29, 2026
33
+
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**.
35
+
36
+ **File > Import from \*.py model** now runs a gds2palace script to read it, without reading the GDSII file and without meshing. The run stops where the script builds the model, so its solver start never runs. The import now gets exactly what gds2palace would get, also values and ports the script computes. Before, ports made in a loop (as in many gds2palace MCP scripts) were left out, and the imported model was missing them. The script's own code before building the model does run, e.g. a script that generates its GDSII file writes it. If the run fails, the import reads the script text as before, and the bar below the menu says why.
37
+
38
+ **File menu:** the three Recent submenus are now one **Open Recent** submenu, with sections for configs, imported \*.py models and \*.py models edited in place. A file reopens the way it was opened. The menu is grouped as open, recent, save, preferences. **Load Default Config** and **Save as Default Config** are removed: defaults for new models are set in **File > Preferences**. An existing `default.simcfg` / `default.tsimcfg` in your home directory still opens with **Load Config**, and **Save Config** can write it.
39
+
40
+ Import and edit in place now read a stackup given as `utilities.get_stackup_path("SG13G2_FEM_200um.xml")`, the way the gds2palace MCP server writes it (needs gds2palace 0.9.1 or later). Before, an imported model using that syntax had no stackup file.
41
+
42
+ # What's New - October 21, 2026
43
+
44
+ **Tools > Simplify GDS:** the simplification code moved to gds_prepare_for_EM (module `gds_simplify_for_em`), where the gds2palace MCP server uses it too. setupEM now requires gds_prepare_for_EM 1.6.0 or later; `pip install --upgrade setupEM` updates it.
45
+
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.
47
+
48
+ Full history: [CHANGES.md](https://github.com/VolkerMuehlhaus/setupEM/blob/main/doc/CHANGES.md)
49
+
50
+ ---
51
+
52
+ **Note:** the `setupEM` PyPI package itself requires:
53
+
54
+ - gds2palace>=0.9.3
55
+ - gds_prepare_for_EM>=1.6.0
56
+ - PySide6
57
+ - shiboken6
58
+ - scipy
59
+ - requests
60
+ - scikit-rf
61
+ - matplotlib
62
+ - numpy
63
+ - gdspy
64
+ - meshio
65
+ - pyvista
66
+ - pyvistaqt
67
+
68
+ (Other Python modules mentioned above are only needed to run standalone helper scripts in this repository, not to use the installed package.)
@@ -12,7 +12,7 @@ authors = [
12
12
  { name = "Volker Muehlhaus", email = "volker@muehlhaus.com" }
13
13
  ]
14
14
  dependencies = [
15
- "gds2palace>=0.9.0",
15
+ "gds2palace>=0.9.3",
16
16
  "gds_prepare_for_EM>=1.6.0",
17
17
  "PySide6",
18
18
  "shiboken6",
@@ -28,6 +28,9 @@ dependencies = [
28
28
  ]
29
29
  requires-python = ">=3.9"
30
30
 
31
+ [project.optional-dependencies]
32
+ test = ["pytest"]
33
+
31
34
  [project.urls]
32
35
  Homepage = "https://github.com/VolkerMuehlhaus/setupEM"
33
36
  Repository = "https://github.com/VolkerMuehlhaus/setupEM"
@@ -24,4 +24,4 @@ A Python tool for EM setup using gds2palace.
24
24
  from .setupEM import main
25
25
 
26
26
  __all__ = ["main"]
27
- __version__ = "0.14.2"
27
+ __version__ = "0.16.0"
@@ -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
@@ -636,7 +636,12 @@ class ResultViewerWindow(QDialog):
636
636
  # regardless of which solver produced it.
637
637
  saved_values = self.MainWindow.saved_values
638
638
  model_basename = saved_values.get('model_basename', '') if isinstance(saved_values, dict) else ''
639
- current_model_dir = f"{model_basename}_data" if model_basename else None
639
+ current_model_dirs = {f"{model_basename}_data"} if model_basename else set()
640
+ # a script edited in place may create several models (e.g. a parameter
641
+ # sweep), each with its own run folder name: all of them are "this model"
642
+ script_runs = getattr(self.MainWindow, "script_runs", None) or []
643
+ if getattr(self.MainWindow, "script_model", None) is not None and script_runs:
644
+ current_model_dirs = {os.path.basename(os.path.normpath(run["sim_path"])) for run in script_runs}
640
645
 
641
646
  result = []
642
647
  for path in files:
@@ -645,8 +650,8 @@ class ResultViewerWindow(QDialog):
645
650
  continue
646
651
  if not include_deembedded and '_deembedded' in name:
647
652
  continue
648
- if not include_all_models and current_model_dir:
649
- if current_model_dir not in os.path.normpath(path).split(os.sep):
653
+ if not include_all_models and current_model_dirs:
654
+ if not current_model_dirs & set(os.path.normpath(path).split(os.sep)):
650
655
  continue
651
656
  result.append(path)
652
657
  return result