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.
Files changed (40) hide show
  1. {setupem-0.14.2/src/setupEM.egg-info → setupem-0.15.0}/PKG-INFO +13 -5
  2. {setupem-0.14.2 → setupem-0.15.0}/README.md +4 -2
  3. setupem-0.15.0/README_pypi.md +54 -0
  4. {setupem-0.14.2 → setupem-0.15.0}/pyproject.toml +3 -0
  5. {setupem-0.14.2 → setupem-0.15.0}/src/setupEM/__init__.py +1 -1
  6. {setupem-0.14.2 → setupem-0.15.0}/src/setupEM/result_viewer.py +8 -3
  7. setupem-0.15.0/src/setupEM/run_with_overrides.py +287 -0
  8. setupem-0.15.0/src/setupEM/script_model.py +1054 -0
  9. setupem-0.15.0/src/setupEM/script_probe.py +275 -0
  10. {setupem-0.14.2 → setupem-0.15.0}/src/setupEM/setupEM.py +175 -68
  11. {setupem-0.14.2 → setupem-0.15.0}/src/setupEM/setupThermal.py +97 -44
  12. {setupem-0.14.2 → setupem-0.15.0}/src/setupEM/setup_common.py +1182 -189
  13. {setupem-0.14.2 → setupem-0.15.0/src/setupEM.egg-info}/PKG-INFO +13 -5
  14. {setupem-0.14.2 → setupem-0.15.0}/src/setupEM.egg-info/SOURCES.txt +7 -1
  15. {setupem-0.14.2 → setupem-0.15.0}/src/setupEM.egg-info/requires.txt +3 -0
  16. setupem-0.15.0/tests/test_run_with_overrides.py +147 -0
  17. setupem-0.15.0/tests/test_script_model.py +535 -0
  18. setupem-0.15.0/tests/test_script_probe.py +164 -0
  19. setupem-0.14.2/README_pypi.md +0 -48
  20. {setupem-0.14.2 → setupem-0.15.0}/LICENSE +0 -0
  21. {setupem-0.14.2 → setupem-0.15.0}/setup.cfg +0 -0
  22. {setupem-0.14.2 → setupem-0.15.0}/src/setupEM/__main__.py +0 -0
  23. {setupem-0.14.2 → setupem-0.15.0}/src/setupEM/data/SG13CMOS5L_200um.xml +0 -0
  24. {setupem-0.14.2 → setupem-0.15.0}/src/setupEM/data/SG13G2_100um.xml +0 -0
  25. {setupem-0.14.2 → setupem-0.15.0}/src/setupEM/data/SG13G2_200um.xml +0 -0
  26. {setupem-0.14.2 → setupem-0.15.0}/src/setupEM/data/SG13G2_FEM_200um.xml +0 -0
  27. {setupem-0.14.2 → setupem-0.15.0}/src/setupEM/data/SG13G2_FEM_200um_passi3D.xml +0 -0
  28. {setupem-0.14.2 → setupem-0.15.0}/src/setupEM/data/SG13G2_nosub.xml +0 -0
  29. {setupem-0.14.2 → setupem-0.15.0}/src/setupEM/field_viewer.py +0 -0
  30. {setupem-0.14.2 → setupem-0.15.0}/src/setupEM/gds_hierarchy_scan.py +0 -0
  31. {setupem-0.14.2 → setupem-0.15.0}/src/setupEM/layout_preview.py +0 -0
  32. {setupem-0.14.2 → setupem-0.15.0}/src/setupEM/momentum_import.py +0 -0
  33. {setupem-0.14.2 → setupem-0.15.0}/src/setupEM/palace_results.py +0 -0
  34. {setupem-0.14.2 → setupem-0.15.0}/src/setupEM/simplify_gds.py +0 -0
  35. {setupem-0.14.2 → setupem-0.15.0}/src/setupEM/stackupEditor.py +0 -0
  36. {setupem-0.14.2 → setupem-0.15.0}/src/setupEM/stackup_writer.py +0 -0
  37. {setupem-0.14.2 → setupem-0.15.0}/src/setupEM/thermal_results.py +0 -0
  38. {setupem-0.14.2 → setupem-0.15.0}/src/setupEM.egg-info/dependency_links.txt +0 -0
  39. {setupem-0.14.2 → setupem-0.15.0}/src/setupEM.egg-info/entry_points.txt +0 -0
  40. {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.14.2
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, 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,54 @@
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 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.)
@@ -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.15.0"
@@ -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
@@ -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()