setupEM 0.14.2__tar.gz → 0.15.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.14.2/src/setupEM.egg-info → setupem-0.15.0}/PKG-INFO +13 -5
- {setupem-0.14.2 → setupem-0.15.0}/README.md +4 -2
- setupem-0.15.0/README_pypi.md +54 -0
- {setupem-0.14.2 → setupem-0.15.0}/pyproject.toml +3 -0
- {setupem-0.14.2 → setupem-0.15.0}/src/setupEM/__init__.py +1 -1
- {setupem-0.14.2 → setupem-0.15.0}/src/setupEM/result_viewer.py +8 -3
- setupem-0.15.0/src/setupEM/run_with_overrides.py +287 -0
- setupem-0.15.0/src/setupEM/script_model.py +1054 -0
- setupem-0.15.0/src/setupEM/script_probe.py +275 -0
- {setupem-0.14.2 → setupem-0.15.0}/src/setupEM/setupEM.py +175 -68
- {setupem-0.14.2 → setupem-0.15.0}/src/setupEM/setupThermal.py +97 -44
- {setupem-0.14.2 → setupem-0.15.0}/src/setupEM/setup_common.py +1182 -189
- {setupem-0.14.2 → setupem-0.15.0/src/setupEM.egg-info}/PKG-INFO +13 -5
- {setupem-0.14.2 → setupem-0.15.0}/src/setupEM.egg-info/SOURCES.txt +7 -1
- {setupem-0.14.2 → setupem-0.15.0}/src/setupEM.egg-info/requires.txt +3 -0
- setupem-0.15.0/tests/test_run_with_overrides.py +147 -0
- setupem-0.15.0/tests/test_script_model.py +535 -0
- setupem-0.15.0/tests/test_script_probe.py +164 -0
- setupem-0.14.2/README_pypi.md +0 -48
- {setupem-0.14.2 → setupem-0.15.0}/LICENSE +0 -0
- {setupem-0.14.2 → setupem-0.15.0}/setup.cfg +0 -0
- {setupem-0.14.2 → setupem-0.15.0}/src/setupEM/__main__.py +0 -0
- {setupem-0.14.2 → setupem-0.15.0}/src/setupEM/data/SG13CMOS5L_200um.xml +0 -0
- {setupem-0.14.2 → setupem-0.15.0}/src/setupEM/data/SG13G2_100um.xml +0 -0
- {setupem-0.14.2 → setupem-0.15.0}/src/setupEM/data/SG13G2_200um.xml +0 -0
- {setupem-0.14.2 → setupem-0.15.0}/src/setupEM/data/SG13G2_FEM_200um.xml +0 -0
- {setupem-0.14.2 → setupem-0.15.0}/src/setupEM/data/SG13G2_FEM_200um_passi3D.xml +0 -0
- {setupem-0.14.2 → setupem-0.15.0}/src/setupEM/data/SG13G2_nosub.xml +0 -0
- {setupem-0.14.2 → setupem-0.15.0}/src/setupEM/field_viewer.py +0 -0
- {setupem-0.14.2 → setupem-0.15.0}/src/setupEM/gds_hierarchy_scan.py +0 -0
- {setupem-0.14.2 → setupem-0.15.0}/src/setupEM/layout_preview.py +0 -0
- {setupem-0.14.2 → setupem-0.15.0}/src/setupEM/momentum_import.py +0 -0
- {setupem-0.14.2 → setupem-0.15.0}/src/setupEM/palace_results.py +0 -0
- {setupem-0.14.2 → setupem-0.15.0}/src/setupEM/simplify_gds.py +0 -0
- {setupem-0.14.2 → setupem-0.15.0}/src/setupEM/stackupEditor.py +0 -0
- {setupem-0.14.2 → setupem-0.15.0}/src/setupEM/stackup_writer.py +0 -0
- {setupem-0.14.2 → setupem-0.15.0}/src/setupEM/thermal_results.py +0 -0
- {setupem-0.14.2 → setupem-0.15.0}/src/setupEM.egg-info/dependency_links.txt +0 -0
- {setupem-0.14.2 → setupem-0.15.0}/src/setupEM.egg-info/entry_points.txt +0 -0
- {setupem-0.14.2 → setupem-0.15.0}/src/setupEM.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: setupEM
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.15.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
|
|
@@ -23,6 +23,8 @@ Requires-Dist: gdspy
|
|
|
23
23
|
Requires-Dist: meshio
|
|
24
24
|
Requires-Dist: pyvista
|
|
25
25
|
Requires-Dist: pyvistaqt
|
|
26
|
+
Provides-Extra: test
|
|
27
|
+
Requires-Dist: pytest; extra == "test"
|
|
26
28
|
Dynamic: license-file
|
|
27
29
|
|
|
28
30
|
# Python GUI for gds2palace
|
|
@@ -38,6 +40,16 @@ https://github.com/VolkerMuehlhaus/setupEM
|
|
|
38
40
|
|
|
39
41
|
## Recent changes
|
|
40
42
|
|
|
43
|
+
# What's New - October 29, 2026
|
|
44
|
+
|
|
45
|
+
**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**.
|
|
46
|
+
|
|
47
|
+
**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.
|
|
48
|
+
|
|
49
|
+
**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.
|
|
50
|
+
|
|
51
|
+
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.
|
|
52
|
+
|
|
41
53
|
# What's New - October 21, 2026
|
|
42
54
|
|
|
43
55
|
**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.
|
|
@@ -48,10 +60,6 @@ Fix: with **Merge via arrays** (and no fill removal or merging per layer), the s
|
|
|
48
60
|
|
|
49
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".
|
|
50
62
|
|
|
51
|
-
# What's New - October 18, 2026
|
|
52
|
-
|
|
53
|
-
**Touchstone files with different port impedances:** the option line of the combined `.sNp` file now has one Z0 per port, in port order. Before, ports with the same Z0 as port 1 were left out.
|
|
54
|
-
|
|
55
63
|
Full history: [CHANGES.md](https://github.com/VolkerMuehlhaus/setupEM/blob/main/doc/CHANGES.md)
|
|
56
64
|
|
|
57
65
|
---
|
|
@@ -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
|
|
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.
|
|
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,54 @@
|
|
|
1
|
+
# Python GUI for gds2palace
|
|
2
|
+
|
|
3
|
+

|
|
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 29, 2026
|
|
15
|
+
|
|
16
|
+
**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**.
|
|
17
|
+
|
|
18
|
+
**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.
|
|
19
|
+
|
|
20
|
+
**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.
|
|
21
|
+
|
|
22
|
+
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.
|
|
23
|
+
|
|
24
|
+
# What's New - October 21, 2026
|
|
25
|
+
|
|
26
|
+
**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.
|
|
27
|
+
|
|
28
|
+
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
|
+
|
|
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
|
+
Full history: [CHANGES.md](https://github.com/VolkerMuehlhaus/setupEM/blob/main/doc/CHANGES.md)
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
**Note:** the `setupEM` PyPI package itself requires:
|
|
39
|
+
|
|
40
|
+
- gds2palace>=0.9.0
|
|
41
|
+
- gds_prepare_for_EM>=1.6.0
|
|
42
|
+
- PySide6
|
|
43
|
+
- shiboken6
|
|
44
|
+
- scipy
|
|
45
|
+
- requests
|
|
46
|
+
- scikit-rf
|
|
47
|
+
- matplotlib
|
|
48
|
+
- numpy
|
|
49
|
+
- gdspy
|
|
50
|
+
- meshio
|
|
51
|
+
- pyvista
|
|
52
|
+
- pyvistaqt
|
|
53
|
+
|
|
54
|
+
(Other Python modules mentioned above are only needed to run standalone helper scripts in this repository, not to use the installed package.)
|
|
@@ -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
|
-
|
|
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
|
|
649
|
-
if
|
|
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
|
|
@@ -0,0 +1,287 @@
|
|
|
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
|
+
"""Run a gds2palace model script with some settings[] values overridden.
|
|
20
|
+
|
|
21
|
+
python run_with_overrides.py model.py --set preview_only=True --set no_preview=False
|
|
22
|
+
python run_with_overrides.py model.py --source unsaved.py --set preview_only=True
|
|
23
|
+
python run_with_overrides.py model.py --record models.json --set no_preview=True
|
|
24
|
+
|
|
25
|
+
Used by setupEM's edit-in-place mode, so Preview / Create Mesh don't have to
|
|
26
|
+
write their control flags into the user's script. The script runs unchanged,
|
|
27
|
+
as __main__ with its own __file__ and its own folder first on sys.path,
|
|
28
|
+
exactly like "python model.py". The overrides are applied to the settings dict
|
|
29
|
+
when the script calls the workflow's create_model() (also through
|
|
30
|
+
create_palace(), create_elmer() or create_elmer_thermal()). That function is
|
|
31
|
+
patched when the workflow module is imported, from wherever the script
|
|
32
|
+
imports it (installed package or a local copy put on sys.path by the script).
|
|
33
|
+
|
|
34
|
+
A script may build several models, e.g. a parameter sweep calling
|
|
35
|
+
create_palace() in a loop. --record writes every model it creates (run folder,
|
|
36
|
+
name, solver, stackup variable overrides, plain settings values) to a JSON
|
|
37
|
+
file, so setupEM knows which models to run. --first-only stops the script
|
|
38
|
+
right after its first model is built (a preview only needs one). --no-solver
|
|
39
|
+
keeps a script with start_simulation = True from starting the solver itself
|
|
40
|
+
(setupEM's Start Simulation does that).
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
import argparse
|
|
44
|
+
import ast
|
|
45
|
+
import importlib.abc
|
|
46
|
+
import importlib.machinery
|
|
47
|
+
import json
|
|
48
|
+
import os
|
|
49
|
+
import runpy
|
|
50
|
+
import sys
|
|
51
|
+
|
|
52
|
+
WORKFLOW_MODULE_SUFFIX = "util_simulation_setup"
|
|
53
|
+
STACKUP_MODULE_SUFFIX = "util_stackup_reader"
|
|
54
|
+
|
|
55
|
+
# settings values that are workflow objects or setupEM's own run control, not
|
|
56
|
+
# parameters of a model
|
|
57
|
+
_NOT_PARAMETERS = {"sim_path", "model_basename", "preview_only", "no_preview", "no_gui"}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class _Run:
|
|
61
|
+
"""What this run of the script does and records."""
|
|
62
|
+
|
|
63
|
+
def __init__(self, overrides, record=None, first_only=False, gui_first_only=False):
|
|
64
|
+
self.overrides = overrides
|
|
65
|
+
self.record = record
|
|
66
|
+
self.first_only = first_only
|
|
67
|
+
self.gui_first_only = gui_first_only
|
|
68
|
+
self.models = []
|
|
69
|
+
self.models_started = 0
|
|
70
|
+
self.variable_overrides = None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# programs that start the solver; a script with start_simulation = True runs
|
|
74
|
+
# one of these (e.g. run_command = ['./run_sim']) after building each model
|
|
75
|
+
_SOLVER_PROGRAMS = ("run_sim", "run_elmer", "run_palace", "elmersolver", "palace")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _starts_solver(command):
|
|
79
|
+
if isinstance(command, (list, tuple)):
|
|
80
|
+
words = [str(w) for w in command]
|
|
81
|
+
else:
|
|
82
|
+
words = str(command).replace("&&", " ").replace(";", " ").split()
|
|
83
|
+
for word in words:
|
|
84
|
+
name = os.path.basename(word.strip("'\"")).lower()
|
|
85
|
+
if name.endswith((".bat", ".sh", ".exe")):
|
|
86
|
+
name = name.rsplit(".", 1)[0]
|
|
87
|
+
if name.startswith(_SOLVER_PROGRAMS[:4]) or name == "palace":
|
|
88
|
+
return True
|
|
89
|
+
return False
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _skip_solver_starts():
|
|
93
|
+
"""setupEM starts the solver itself (Start Simulation, one model after the
|
|
94
|
+
other): while it runs a script, the script's own solver start is skipped
|
|
95
|
+
instead of running every simulation inside Create Mesh."""
|
|
96
|
+
import subprocess
|
|
97
|
+
|
|
98
|
+
def wrap(original, result):
|
|
99
|
+
def call(*args, **kwargs):
|
|
100
|
+
command = args[0] if args else kwargs.get("args", kwargs.get("command", ""))
|
|
101
|
+
if _starts_solver(command):
|
|
102
|
+
print(f"setupEM: not starting {command!r} from the script, Start Simulation runs the solver.")
|
|
103
|
+
return result(command)
|
|
104
|
+
return original(*args, **kwargs)
|
|
105
|
+
return call
|
|
106
|
+
|
|
107
|
+
subprocess.run = wrap(subprocess.run, lambda c: subprocess.CompletedProcess(c, 0, "", ""))
|
|
108
|
+
subprocess.call = wrap(subprocess.call, lambda c: 0)
|
|
109
|
+
subprocess.check_call = wrap(subprocess.check_call, lambda c: 0)
|
|
110
|
+
os.system = wrap(os.system, lambda c: 0)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _parse_value(text):
|
|
114
|
+
try:
|
|
115
|
+
return ast.literal_eval(text)
|
|
116
|
+
except (ValueError, SyntaxError):
|
|
117
|
+
return text
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _plain(value):
|
|
121
|
+
# JSON-able numbers / strings / bools, and lists or dicts of those
|
|
122
|
+
if isinstance(value, (bool, int, float, str)) or value is None:
|
|
123
|
+
return True
|
|
124
|
+
if isinstance(value, (list, tuple)):
|
|
125
|
+
return all(_plain(v) for v in value)
|
|
126
|
+
if isinstance(value, dict):
|
|
127
|
+
return all(isinstance(k, str) and _plain(v) for k, v in value.items())
|
|
128
|
+
return False
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _record_model(run, settings, result):
|
|
132
|
+
if run.record is None:
|
|
133
|
+
return
|
|
134
|
+
if settings.get("elmer_thermal"):
|
|
135
|
+
solver = "elmer_thermal"
|
|
136
|
+
elif settings.get("elmer"):
|
|
137
|
+
solver = "elmer"
|
|
138
|
+
else:
|
|
139
|
+
solver = "palace"
|
|
140
|
+
config_name = result[0] if isinstance(result, tuple) and result and isinstance(result[0], str) else None
|
|
141
|
+
sim_path = settings.get("sim_path")
|
|
142
|
+
run.models.append({
|
|
143
|
+
"sim_path": os.path.abspath(sim_path).replace("\\", "/") if isinstance(sim_path, str) else None,
|
|
144
|
+
"model_basename": settings.get("model_basename"),
|
|
145
|
+
"solver": solver,
|
|
146
|
+
"config": config_name,
|
|
147
|
+
"variable_overrides": run.variable_overrides if _plain(run.variable_overrides) else None,
|
|
148
|
+
"settings": {k: v for k, v in settings.items() if k not in _NOT_PARAMETERS and _plain(v)},
|
|
149
|
+
})
|
|
150
|
+
# written after every model, so a run that stops halfway still lists what it built
|
|
151
|
+
os.makedirs(os.path.dirname(os.path.abspath(run.record)), exist_ok=True)
|
|
152
|
+
with open(run.record, "w", encoding="utf-8") as f:
|
|
153
|
+
json.dump(run.models, f, indent=1)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _patch(module, run):
|
|
157
|
+
name = getattr(module, "__name__", "").split(".")[-1]
|
|
158
|
+
if name == STACKUP_MODULE_SUFFIX:
|
|
159
|
+
original_read = getattr(module, "read_substrate", None)
|
|
160
|
+
if original_read is None or getattr(original_read, "_setupEM_run", False):
|
|
161
|
+
return
|
|
162
|
+
|
|
163
|
+
def read_substrate(*args, **kwargs):
|
|
164
|
+
# remember the stackup variables of the model being set up (a sweep
|
|
165
|
+
# parameter like Temp_Celsius often lives here)
|
|
166
|
+
run.variable_overrides = kwargs.get("variable_overrides", args[1] if len(args) > 1 else None)
|
|
167
|
+
return original_read(*args, **kwargs)
|
|
168
|
+
|
|
169
|
+
read_substrate._setupEM_run = True
|
|
170
|
+
module.read_substrate = read_substrate
|
|
171
|
+
return
|
|
172
|
+
|
|
173
|
+
original = getattr(module, "create_model", None)
|
|
174
|
+
if original is None or getattr(original, "_setupEM_run", False):
|
|
175
|
+
return
|
|
176
|
+
|
|
177
|
+
def create_model(*args, **kwargs):
|
|
178
|
+
run.models_started += 1
|
|
179
|
+
settings = kwargs.get("settings")
|
|
180
|
+
if settings is None:
|
|
181
|
+
settings = next((a for a in reversed(args) if isinstance(a, dict)), None)
|
|
182
|
+
if settings is not None:
|
|
183
|
+
settings.update(run.overrides)
|
|
184
|
+
if run.gui_first_only and run.models_started > 1:
|
|
185
|
+
# a sweep: the gmsh window only for the first model, the others
|
|
186
|
+
# are built without one (no_gui only controls the windows)
|
|
187
|
+
settings["no_gui"] = True
|
|
188
|
+
print(f"setupEM: model {run.models_started} is built without a gmsh window.")
|
|
189
|
+
result = original(*args, **kwargs)
|
|
190
|
+
if settings is not None:
|
|
191
|
+
_record_model(run, settings, result)
|
|
192
|
+
if run.first_only:
|
|
193
|
+
# a preview is done once the first model is built: the rest of the
|
|
194
|
+
# script would only set up further models (a sweep) or start the
|
|
195
|
+
# solver (start_simulation = True)
|
|
196
|
+
print("\nsetupEM preview: done after the first model, the rest of the script is not run.")
|
|
197
|
+
sys.stdout.flush()
|
|
198
|
+
sys.stderr.flush()
|
|
199
|
+
# a plain exit could be caught by the script's own try/except
|
|
200
|
+
os._exit(0)
|
|
201
|
+
return result
|
|
202
|
+
|
|
203
|
+
create_model._setupEM_run = True
|
|
204
|
+
module.create_model = create_model
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
class _PatchingLoader(importlib.abc.Loader):
|
|
208
|
+
def __init__(self, loader, run):
|
|
209
|
+
self.loader = loader
|
|
210
|
+
self.run = run
|
|
211
|
+
|
|
212
|
+
def create_module(self, spec):
|
|
213
|
+
return self.loader.create_module(spec)
|
|
214
|
+
|
|
215
|
+
def exec_module(self, module):
|
|
216
|
+
self.loader.exec_module(module)
|
|
217
|
+
_patch(module, self.run)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class _PatchingFinder(importlib.abc.MetaPathFinder):
|
|
221
|
+
def __init__(self, run):
|
|
222
|
+
self.run = run
|
|
223
|
+
|
|
224
|
+
def find_spec(self, fullname, path, target=None):
|
|
225
|
+
if fullname.split(".")[-1] not in (WORKFLOW_MODULE_SUFFIX, STACKUP_MODULE_SUFFIX):
|
|
226
|
+
return None
|
|
227
|
+
for finder in sys.meta_path:
|
|
228
|
+
if finder is self or not hasattr(finder, "find_spec"):
|
|
229
|
+
continue
|
|
230
|
+
spec = finder.find_spec(fullname, path, target)
|
|
231
|
+
if spec is not None:
|
|
232
|
+
if spec.loader is not None and hasattr(spec.loader, "exec_module"):
|
|
233
|
+
spec.loader = _PatchingLoader(spec.loader, self.run)
|
|
234
|
+
return spec
|
|
235
|
+
return None
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def main(argv=None):
|
|
239
|
+
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
|
240
|
+
parser.add_argument("script", help="model script to run")
|
|
241
|
+
parser.add_argument("--set", action="append", default=[], metavar="KEY=VALUE",
|
|
242
|
+
help="settings[KEY] = VALUE (a Python literal) when the model is created")
|
|
243
|
+
parser.add_argument("--source", metavar="FILE",
|
|
244
|
+
help="run the code in FILE as if it were the script (same __file__, folder and "
|
|
245
|
+
"output paths), e.g. unsaved changes for a preview")
|
|
246
|
+
parser.add_argument("--record", metavar="JSON",
|
|
247
|
+
help="write the models the script creates (run folders, parameters) to this file")
|
|
248
|
+
parser.add_argument("--first-only", action="store_true",
|
|
249
|
+
help="stop right after the script has built its first model")
|
|
250
|
+
parser.add_argument("--no-solver", action="store_true",
|
|
251
|
+
help="don't let the script start the solver itself (start_simulation = True)")
|
|
252
|
+
parser.add_argument("--gui-first-only", action="store_true",
|
|
253
|
+
help="show gmsh windows only for the first model the script builds")
|
|
254
|
+
args = parser.parse_args(argv)
|
|
255
|
+
|
|
256
|
+
overrides = {}
|
|
257
|
+
for item in args.set:
|
|
258
|
+
key, sep, value = item.partition("=")
|
|
259
|
+
if not sep or not key:
|
|
260
|
+
parser.error(f"--set expects KEY=VALUE, got {item!r}")
|
|
261
|
+
overrides[key.strip()] = _parse_value(value.strip())
|
|
262
|
+
run = _Run(overrides, record=args.record, first_only=args.first_only, gui_first_only=args.gui_first_only)
|
|
263
|
+
|
|
264
|
+
script = os.path.abspath(args.script)
|
|
265
|
+
for module in list(sys.modules.values()):
|
|
266
|
+
if getattr(module, "__name__", "").split(".")[-1] in (WORKFLOW_MODULE_SUFFIX, STACKUP_MODULE_SUFFIX):
|
|
267
|
+
_patch(module, run)
|
|
268
|
+
sys.meta_path.insert(0, _PatchingFinder(run))
|
|
269
|
+
if args.no_solver:
|
|
270
|
+
_skip_solver_starts()
|
|
271
|
+
|
|
272
|
+
# same environment as "python model.py"
|
|
273
|
+
sys.argv = [script]
|
|
274
|
+
sys.path[0] = os.path.dirname(script)
|
|
275
|
+
if args.source:
|
|
276
|
+
# other code, but run as the script itself: __file__ decides the output
|
|
277
|
+
# folder names (utilities.get_basename(__file__)) and relative paths
|
|
278
|
+
with open(args.source, encoding="utf-8") as f:
|
|
279
|
+
code = compile(f.read(), script, "exec")
|
|
280
|
+
exec(code, {"__name__": "__main__", "__file__": script, "__builtins__": __builtins__,
|
|
281
|
+
"__package__": None, "__spec__": None, "__doc__": None})
|
|
282
|
+
else:
|
|
283
|
+
runpy.run_path(script, run_name="__main__")
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
if __name__ == "__main__":
|
|
287
|
+
main()
|