cenos-py 0.1.1__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.
- cenos_py-0.1.1/LICENSE +21 -0
- cenos_py-0.1.1/PKG-INFO +92 -0
- cenos_py-0.1.1/README.md +67 -0
- cenos_py-0.1.1/pyproject.toml +45 -0
- cenos_py-0.1.1/src/cenos_py/__init__.py +33 -0
- cenos_py-0.1.1/src/cenos_py/_generated_case_methods.py +687 -0
- cenos_py-0.1.1/src/cenos_py/backend_service.py +197 -0
- cenos_py-0.1.1/src/cenos_py/logger.py +38 -0
- cenos_py-0.1.1/src/cenos_py/misc_functions.py +66 -0
- cenos_py-0.1.1/src/cenos_py/property_data.py +68 -0
- cenos_py-0.1.1/src/cenos_py/py.typed +0 -0
- cenos_py-0.1.1/src/cenos_py/testing_utils.py +27 -0
cenos_py-0.1.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 CENOS
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
cenos_py-0.1.1/PKG-INFO
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cenos-py
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Python API for using CENOS applications
|
|
5
|
+
Author: Rinalds
|
|
6
|
+
Author-email: Rinalds <rp@cenos-platform.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Science/Research
|
|
11
|
+
Classifier: Topic :: Scientific/Engineering
|
|
12
|
+
Classifier: Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)
|
|
13
|
+
Classifier: Topic :: Scientific/Engineering :: Physics
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Requires-Dist: websocket-client>=1.9.0
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Project-URL: Homepage, https://cenos-platform.com
|
|
23
|
+
Project-URL: Repository, https://github.com/CENOS-Platform/cenos-py
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# CENOS Python package
|
|
27
|
+
|
|
28
|
+
This is a simple package that allows to interface with the cenos backend via a python API.
|
|
29
|
+
|
|
30
|
+
## Quick-start
|
|
31
|
+
|
|
32
|
+
Install this package
|
|
33
|
+
```
|
|
34
|
+
pip install cenos-py
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## How it works
|
|
38
|
+
|
|
39
|
+
While you use CENOS normally using the GUI, most of your actions are written out to a `logs/python_trace_script.py` file inside of the case itself. It will look something like this:
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from cenos_py import CenosCase, setCenosLocation
|
|
43
|
+
|
|
44
|
+
setCenosLocation(r"C:\source\cenos")
|
|
45
|
+
|
|
46
|
+
case = CenosCase("INDUCTION")
|
|
47
|
+
case.set_pre_processor('geomWizard', load_recent=True)
|
|
48
|
+
case.update_template_shape_type(1, 'workpieceTube')
|
|
49
|
+
case.update_template_property(1, 'diameter3', 32)
|
|
50
|
+
case.update_template_property(1, 'diameter4', 10)
|
|
51
|
+
case.update_tab_property('simulation', 'tend', 3)
|
|
52
|
+
case.update_tab_property('simulation', 'isAdaptive', True)
|
|
53
|
+
case.assign_group_material('workpiece', 'alloy_34Cr4')
|
|
54
|
+
case.update_physics_property('physicsThermal', 'boundary', 'workpiece_surface', 'hr', 0.9)
|
|
55
|
+
case.entered_meshing_window()
|
|
56
|
+
case.set_meshing_global_density('rough')
|
|
57
|
+
case.set_meshing_grading_surface(0.29)
|
|
58
|
+
case.set_meshing_domain_element_size('workpiece', 2)
|
|
59
|
+
case.set_meshing_domain_skin_layer('workpiece', True, 4.6, 10, 1.4)
|
|
60
|
+
case.generate_mesh()
|
|
61
|
+
case.calculate()
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
You can use this code in your script to create a CENOS case from scratch for every run.
|
|
65
|
+
|
|
66
|
+
(Recommended) Alternatively you can use a script that simply opens an existing case and modifies only the variables that you wish to control, for example:
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
from pathlib import Path
|
|
70
|
+
|
|
71
|
+
from cenos_py import CenosCase, setCenosLocation
|
|
72
|
+
|
|
73
|
+
setCenosLocation(r"C:\Users\user\AppData\Local\Programs\CENOS Induction Heating beta")
|
|
74
|
+
|
|
75
|
+
case = CenosCase("INDUCTION")
|
|
76
|
+
|
|
77
|
+
your_case_path = r"C:\Users\rp\CENOS\your_case"
|
|
78
|
+
|
|
79
|
+
for i in [20, 30, 40, 50]:
|
|
80
|
+
case.open(your_case_path)
|
|
81
|
+
|
|
82
|
+
# Create a copy first
|
|
83
|
+
case.save_case_as(r"C:\Users\rp\CENOS\your_case_{i}")
|
|
84
|
+
|
|
85
|
+
case.update_template_property(1, "workpiece_height", i)
|
|
86
|
+
case.calculate()
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
|
cenos_py-0.1.1/README.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# CENOS Python package
|
|
2
|
+
|
|
3
|
+
This is a simple package that allows to interface with the cenos backend via a python API.
|
|
4
|
+
|
|
5
|
+
## Quick-start
|
|
6
|
+
|
|
7
|
+
Install this package
|
|
8
|
+
```
|
|
9
|
+
pip install cenos-py
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## How it works
|
|
13
|
+
|
|
14
|
+
While you use CENOS normally using the GUI, most of your actions are written out to a `logs/python_trace_script.py` file inside of the case itself. It will look something like this:
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
from cenos_py import CenosCase, setCenosLocation
|
|
18
|
+
|
|
19
|
+
setCenosLocation(r"C:\source\cenos")
|
|
20
|
+
|
|
21
|
+
case = CenosCase("INDUCTION")
|
|
22
|
+
case.set_pre_processor('geomWizard', load_recent=True)
|
|
23
|
+
case.update_template_shape_type(1, 'workpieceTube')
|
|
24
|
+
case.update_template_property(1, 'diameter3', 32)
|
|
25
|
+
case.update_template_property(1, 'diameter4', 10)
|
|
26
|
+
case.update_tab_property('simulation', 'tend', 3)
|
|
27
|
+
case.update_tab_property('simulation', 'isAdaptive', True)
|
|
28
|
+
case.assign_group_material('workpiece', 'alloy_34Cr4')
|
|
29
|
+
case.update_physics_property('physicsThermal', 'boundary', 'workpiece_surface', 'hr', 0.9)
|
|
30
|
+
case.entered_meshing_window()
|
|
31
|
+
case.set_meshing_global_density('rough')
|
|
32
|
+
case.set_meshing_grading_surface(0.29)
|
|
33
|
+
case.set_meshing_domain_element_size('workpiece', 2)
|
|
34
|
+
case.set_meshing_domain_skin_layer('workpiece', True, 4.6, 10, 1.4)
|
|
35
|
+
case.generate_mesh()
|
|
36
|
+
case.calculate()
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
You can use this code in your script to create a CENOS case from scratch for every run.
|
|
40
|
+
|
|
41
|
+
(Recommended) Alternatively you can use a script that simply opens an existing case and modifies only the variables that you wish to control, for example:
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from pathlib import Path
|
|
45
|
+
|
|
46
|
+
from cenos_py import CenosCase, setCenosLocation
|
|
47
|
+
|
|
48
|
+
setCenosLocation(r"C:\Users\user\AppData\Local\Programs\CENOS Induction Heating beta")
|
|
49
|
+
|
|
50
|
+
case = CenosCase("INDUCTION")
|
|
51
|
+
|
|
52
|
+
your_case_path = r"C:\Users\rp\CENOS\your_case"
|
|
53
|
+
|
|
54
|
+
for i in [20, 30, 40, 50]:
|
|
55
|
+
case.open(your_case_path)
|
|
56
|
+
|
|
57
|
+
# Create a copy first
|
|
58
|
+
case.save_case_as(r"C:\Users\rp\CENOS\your_case_{i}")
|
|
59
|
+
|
|
60
|
+
case.update_template_property(1, "workpiece_height", i)
|
|
61
|
+
case.calculate()
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "cenos-py"
|
|
3
|
+
version = "0.1.1"
|
|
4
|
+
description = "Python API for using CENOS applications"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
authors = [
|
|
9
|
+
{ name = "Rinalds", email = "rp@cenos-platform.com" }
|
|
10
|
+
]
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
dependencies = [
|
|
13
|
+
"websocket-client>=1.9.0",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
|
|
19
|
+
"Intended Audience :: Science/Research",
|
|
20
|
+
"Topic :: Scientific/Engineering",
|
|
21
|
+
"Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)",
|
|
22
|
+
"Topic :: Scientific/Engineering :: Physics",
|
|
23
|
+
|
|
24
|
+
"Programming Language :: Python :: 3",
|
|
25
|
+
"Programming Language :: Python :: 3.10",
|
|
26
|
+
"Programming Language :: Python :: 3.11",
|
|
27
|
+
"Programming Language :: Python :: 3.12",
|
|
28
|
+
"Programming Language :: Python :: 3.13",
|
|
29
|
+
"Programming Language :: Python :: 3.14",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "https://cenos-platform.com"
|
|
34
|
+
Repository = "https://github.com/CENOS-Platform/cenos-py"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
[build-system]
|
|
38
|
+
requires = ["uv_build>=0.11.24,<0.12.0"]
|
|
39
|
+
build-backend = "uv_build"
|
|
40
|
+
|
|
41
|
+
[dependency-groups]
|
|
42
|
+
dev = [
|
|
43
|
+
"pytest>=9.1.1",
|
|
44
|
+
"pytest-cov>=7.1.0",
|
|
45
|
+
]
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from os import PathLike
|
|
2
|
+
from typing import Literal
|
|
3
|
+
|
|
4
|
+
from cenos_py import misc_functions
|
|
5
|
+
from cenos_py._generated_case_methods import _GeneratedCaseMethods
|
|
6
|
+
|
|
7
|
+
from .backend_service import BackendInstance, start_backend
|
|
8
|
+
|
|
9
|
+
setCenosLocation = misc_functions.setCenosLocation
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CenosCase(_GeneratedCaseMethods):
|
|
13
|
+
def _start_backend(self, app_name: str):
|
|
14
|
+
assert misc_functions._cenos_location is not None, (
|
|
15
|
+
"Please call setCenosLocation to specify the location of your cenos installation before creating a case."
|
|
16
|
+
)
|
|
17
|
+
if not app_name:
|
|
18
|
+
app_name = misc_functions.get_installed_app_name(
|
|
19
|
+
misc_functions._cenos_location
|
|
20
|
+
)
|
|
21
|
+
self.backend: BackendInstance = start_backend(
|
|
22
|
+
misc_functions._cenos_location, app_name
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
def __init__(self, app_name: str | None = None):
|
|
26
|
+
self._start_backend(app_name)
|
|
27
|
+
self.backend.call("newCase")
|
|
28
|
+
|
|
29
|
+
def open(self, case_path: str | PathLike[str]):
|
|
30
|
+
self.backend.call("openCase", {"caseDir": str(case_path)})
|
|
31
|
+
|
|
32
|
+
def stop(self) -> None:
|
|
33
|
+
self.backend.stop()
|
|
@@ -0,0 +1,687 @@
|
|
|
1
|
+
# This file is auto-generated using cenos_py_function_generator.py in the main cenos repo
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from cenos_py.property_data import build_property_data
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class _GeneratedCaseMethods:
|
|
10
|
+
"""Auto-generated wrapper methods for CenosCase."""
|
|
11
|
+
|
|
12
|
+
def generate_template_geometry(self) -> None:
|
|
13
|
+
"""Generates and returns geometryData.
|
|
14
|
+
Kind of depricated now, because geometry is generated when update_template_properties is called."""
|
|
15
|
+
self.backend.call("generateTemplateGeometry")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def receive_pre_processor_geometry(self, pre_proc: str, geom_data_json: str, geom_package: str | None) -> None:
|
|
19
|
+
"""FreeCAD/Salome calls this command then the user sends geometry/mesh to CENOS."""
|
|
20
|
+
self.backend.call(
|
|
21
|
+
"receivePreprocessorGeometry",
|
|
22
|
+
{"preprocessorName": pre_proc, "geometryDataFile": geom_data_json, "geom_package": geom_package},
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def merge_groups(self, groups_to_merge: list[str]) -> None:
|
|
27
|
+
"""Merge groups for role selection."""
|
|
28
|
+
self.backend.call(
|
|
29
|
+
"mergeGroups",
|
|
30
|
+
{"names": groups_to_merge},
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def unmerge_groups(self, groups_to_unmerge: list[str]) -> None:
|
|
35
|
+
"""Unmerge groups for role selection."""
|
|
36
|
+
self.backend.call(
|
|
37
|
+
"unmergeGroups",
|
|
38
|
+
{"names": groups_to_unmerge},
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def set_object_label(self, obj_to_rename: str, label: str) -> None:
|
|
43
|
+
"""Set the label of a boundary, domain or lumped element"""
|
|
44
|
+
self.backend.call(
|
|
45
|
+
"setLabel",
|
|
46
|
+
{"name": obj_to_rename, "label": label},
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def scale_geometry(self, scale: float) -> None:
|
|
51
|
+
"""Scale geometry by a factor. If mesh existed, then delete it."""
|
|
52
|
+
self.backend.call(
|
|
53
|
+
"scaleGeometry",
|
|
54
|
+
{"scale": scale},
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def save_gerber_to_step(self) -> None:
|
|
59
|
+
"""Save Gerber files to step"""
|
|
60
|
+
self.backend.call("saveGerberToStep")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def replace_cad_files(self, new_files: list[str]) -> None:
|
|
64
|
+
"""Called when user re-import CAD files in the FromCAD workflow"""
|
|
65
|
+
self.backend.call(
|
|
66
|
+
"replaceCadFiles",
|
|
67
|
+
{"fileNames": new_files},
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def replace_gerber_files(self, new_files: list[Path]) -> None:
|
|
72
|
+
"""Called when user re-imports Gerber files."""
|
|
73
|
+
self.backend.call(
|
|
74
|
+
"replaceGerberFiles",
|
|
75
|
+
{"new_files": new_files},
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def calculate(self, validate: bool = True) -> None:
|
|
80
|
+
"""Called when user clicks 'RUN'"""
|
|
81
|
+
self.backend.call(
|
|
82
|
+
"calculate",
|
|
83
|
+
{"validate": validate},
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def update_variables(self, new_variables: dict[str, float]) -> None:
|
|
88
|
+
"""Called when user changes and updates 'geometry variables'"""
|
|
89
|
+
self.backend.call(
|
|
90
|
+
"updateGeometryVariables",
|
|
91
|
+
{"variables": new_variables},
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def run_salome_as_mesher(self) -> None:
|
|
96
|
+
"""Runs salome as mesher, waits for user to complete everything and close salome."""
|
|
97
|
+
self.backend.call("runSalomeAsMesher")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def new_case(self, test_mode: object = False) -> None:
|
|
101
|
+
"""Creates a new case with empty physics block."""
|
|
102
|
+
self.backend.call(
|
|
103
|
+
"newCase",
|
|
104
|
+
{"test_mode": test_mode},
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def save_case(self) -> None:
|
|
109
|
+
"""Saves case at the current location"""
|
|
110
|
+
self.backend.call("saveCase")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def save_case_as(self, new_case_path: str | os.PathLike[str], without_results: bool = False) -> None:
|
|
114
|
+
"""Moves and saves case to a new location."""
|
|
115
|
+
self.backend.call(
|
|
116
|
+
"saveCaseAs",
|
|
117
|
+
{"caseDir": str(new_case_path), "without_results": without_results},
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def compress_case(self, should_include_results: bool = True, custom_filename: str | None = None) -> None:
|
|
122
|
+
"""Compresses the case directory for sharing.
|
|
123
|
+
|
|
124
|
+
Delegates to misc_worker.compress_case() for actual implementation."""
|
|
125
|
+
self.backend.call(
|
|
126
|
+
"compressCase",
|
|
127
|
+
{"shouldIncludeResults": should_include_results, "customFilename": custom_filename},
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def compress_logs(self, custom_filename: str | None = None) -> None:
|
|
132
|
+
"""Compresses log files for sharing.
|
|
133
|
+
|
|
134
|
+
Delegates to misc_worker.compress_logs() for actual implementation."""
|
|
135
|
+
self.backend.call(
|
|
136
|
+
"compressLogs",
|
|
137
|
+
{"custom_filename": custom_filename},
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def set_pre_processor(self, pre_proc_key: str, load_recent: object = False) -> None:
|
|
142
|
+
"""Sets the preprocessor and in case of templates generates default geometry and physics."""
|
|
143
|
+
self.backend.call(
|
|
144
|
+
"setPreprocessor",
|
|
145
|
+
{"preProcessorKey": pre_proc_key, "load_recent": load_recent},
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def read_cad_files(self, files: list[str]) -> None:
|
|
150
|
+
"""FromCAD CAD file import function. Creates physics as well."""
|
|
151
|
+
self.backend.call(
|
|
152
|
+
"readCadFiles",
|
|
153
|
+
{"fileNames": files},
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def load_parametric_cad_files(self, files: list[str]) -> None:
|
|
158
|
+
"""When user imports many CAD files using the 'parametric' button"""
|
|
159
|
+
self.backend.call(
|
|
160
|
+
"loadParametricCadFiles",
|
|
161
|
+
{"fileNames": files},
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def read_parametric_gerber_files(self, folders: list[str]) -> None:
|
|
166
|
+
"""Reads parametric gerber files and returs layer data to be edited by user"""
|
|
167
|
+
self.backend.call(
|
|
168
|
+
"readParametricGerberFiles",
|
|
169
|
+
{"fileNames": folders},
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def load_parametric_gerber_files(self, folders: list[str], layer_data: list[dict[str, float, str]]) -> None:
|
|
174
|
+
"""Only adds the parametric case entries to the case."""
|
|
175
|
+
self.backend.call(
|
|
176
|
+
"loadParametricGerberFiles",
|
|
177
|
+
{"folders": folders, "layerData": layer_data},
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def load_gerber_2(self, gdo_files: list[str | os.PathLike[str]]) -> None:
|
|
182
|
+
"""Loads Gerber files."""
|
|
183
|
+
self.backend.call(
|
|
184
|
+
"readGerberFiles",
|
|
185
|
+
{"fileNames": str(gdo_files)},
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def load_gerber_3(self, layer_data: list[dict[str, float, str]]) -> None:
|
|
190
|
+
"""Performs the actual loading of Gerber files after user has done all the GUI part.
|
|
191
|
+
Seperate for case trace."""
|
|
192
|
+
self.backend.call(
|
|
193
|
+
"loadGerberFiles",
|
|
194
|
+
{"layerData": layer_data},
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def read_shape_from_python_script(self, python_file: str) -> None:
|
|
199
|
+
"""FromPythonScript CAD file import function. Creates physics as well."""
|
|
200
|
+
self.backend.call(
|
|
201
|
+
"readPythonGeomFile",
|
|
202
|
+
{"python_file": python_file},
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def add_air(self, add_air_choice: bool, terminals: list[str]) -> None:
|
|
207
|
+
"""Adds air and updates physics."""
|
|
208
|
+
self.backend.call(
|
|
209
|
+
"addAir",
|
|
210
|
+
{"addAirChoice": add_air_choice, "terminals": terminals},
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def set_geometry_type(self, geom_type_key: str) -> None:
|
|
215
|
+
"""Sets the geometry type."""
|
|
216
|
+
self.backend.call(
|
|
217
|
+
"setGeometryType",
|
|
218
|
+
{"geometryTypeKey": geom_type_key},
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def reset_geometry_type(self) -> None:
|
|
223
|
+
"""When the user clicks the 'x' in the role window all roles, domain merges, etc. must be reset."""
|
|
224
|
+
self.backend.call("resetGeometryType")
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def add_role_entry(self, role_key: str) -> None:
|
|
228
|
+
"""Adds a new empty role entry in the role selection window and return the new id."""
|
|
229
|
+
self.backend.call(
|
|
230
|
+
"addRoleEntry",
|
|
231
|
+
{"key": role_key},
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def remove_role_entry(self, role_entry_id: int) -> None:
|
|
236
|
+
"""User deletes role entry in the role selection window."""
|
|
237
|
+
self.backend.call(
|
|
238
|
+
"removeRoleEntry",
|
|
239
|
+
{"id": role_entry_id},
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def assign_role(self, entry_id: int, object_type: str, objects: list[str]) -> None:
|
|
244
|
+
"""Assigns role in the role selection window."""
|
|
245
|
+
self.backend.call(
|
|
246
|
+
"assignRole",
|
|
247
|
+
{"id": entry_id, "type": object_type, "names": objects},
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def assign_domain_role_py(self, domain_names: list[str], role_key: str) -> None:
|
|
252
|
+
"""Role assignment function for python scripts."""
|
|
253
|
+
self.backend.call(
|
|
254
|
+
"assignDomainRolePy",
|
|
255
|
+
{"domainNames": domain_names, "roleKey": role_key},
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def assign_boundary_role_py(self, boundary_names: list[str], role_key: str) -> None:
|
|
260
|
+
"""Role assignment function for python scripts."""
|
|
261
|
+
self.backend.call(
|
|
262
|
+
"assignBoundaryRolePy",
|
|
263
|
+
{"boundaryNames": boundary_names, "roleKey": role_key},
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def assign_lumped_element(self, entry_id: int, points: list[dict[str, list[float]]]) -> None:
|
|
268
|
+
"""Specifies the points and edges for a given lumped element role entry."""
|
|
269
|
+
self.backend.call(
|
|
270
|
+
"assignLumpedElement",
|
|
271
|
+
{"id": entry_id, "points": points},
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def unassign_lumped_element(self, entry_id: int) -> None:
|
|
276
|
+
"""Unassigns the role entry and removes the lumped element from GeometryData"""
|
|
277
|
+
self.backend.call(
|
|
278
|
+
"unassignLumpedElement",
|
|
279
|
+
{"id": entry_id},
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def entered_meshing_window(self) -> None:
|
|
284
|
+
"""Called whenever user enters meshing window"""
|
|
285
|
+
self.backend.call("enteredMeshing")
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def set_manual_meshing_params(self, mesh_params: dict) -> None:
|
|
289
|
+
"""Called whenever the user changes any of the manual meshing parameters."""
|
|
290
|
+
self.backend.call(
|
|
291
|
+
"setMeshingParams",
|
|
292
|
+
{"meshParams": mesh_params},
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def set_meshing_global_density(self, density: str) -> None:
|
|
297
|
+
"""Called whenever the user changes the global density (rough, average, fine)"""
|
|
298
|
+
self.backend.call(
|
|
299
|
+
"setMeshingGlobalDensity",
|
|
300
|
+
{"density": density},
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def set_meshing_global_netgen_param(self, param_key: str, param_value: int | float | str) -> None:
|
|
305
|
+
"""Global netgen param"""
|
|
306
|
+
self.backend.call(
|
|
307
|
+
"setMeshingGlobalNetgenParam",
|
|
308
|
+
{"paramKey": param_key, "paramValue": param_value},
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def set_meshing_grading_surface(self, value: float) -> None:
|
|
313
|
+
"""Sets the slider value"""
|
|
314
|
+
self.backend.call(
|
|
315
|
+
"setMeshingGradingSurface",
|
|
316
|
+
{"value": value},
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def set_meshing_grading_volume(self, value: float) -> None:
|
|
321
|
+
"""Sets the slider value"""
|
|
322
|
+
self.backend.call(
|
|
323
|
+
"setMeshingGradingVolume",
|
|
324
|
+
{"value": value},
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def set_meshing_curvature_safety(self, value: float) -> None:
|
|
329
|
+
"""Sets the slider value"""
|
|
330
|
+
self.backend.call(
|
|
331
|
+
"setMeshingCurvatureSafety",
|
|
332
|
+
{"value": value},
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def set_meshing_corner_mesh_refinement(self, value: float) -> None:
|
|
337
|
+
"""Sets the corner mesh refinement value."""
|
|
338
|
+
self.backend.call(
|
|
339
|
+
"setMeshingCornerMeshRefinement",
|
|
340
|
+
{"value": value},
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def set_meshing_domain_element_size(self, domain_name: str, value: float) -> None:
|
|
345
|
+
"""Sets the domain element size value."""
|
|
346
|
+
self.backend.call(
|
|
347
|
+
"setMeshingDomainElementSize",
|
|
348
|
+
{"domainName": domain_name, "value": value},
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def set_meshing_domain_skin_layer(self, domain_name: str, enabled: bool, total_thickness: float, layer_count: int, stretch_factor: float) -> None:
|
|
353
|
+
"""Sets skin layer params"""
|
|
354
|
+
self.backend.call(
|
|
355
|
+
"setMeshingDomainSkinLayer",
|
|
356
|
+
{"domainName": domain_name, "enabled": enabled, "totalThickness": total_thickness, "layerCount": layer_count, "stretchFactor": stretch_factor},
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def set_meshing_domain_refinement_size(self, domain_name: str, entity_name: str, value: float) -> None:
|
|
361
|
+
"""Sets the element size for an existing refinement."""
|
|
362
|
+
self.backend.call(
|
|
363
|
+
"setMeshingDomainRefinementSize",
|
|
364
|
+
{"domainName": domain_name, "entityName": entity_name, "elemSize": value},
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def add_meshing_domain_refinement(self, domain_name: str, entity_name: str, elem_size: float) -> None:
|
|
369
|
+
"""Adds a new refinement to the domain"""
|
|
370
|
+
self.backend.call(
|
|
371
|
+
"addMeshingDomainRefinement",
|
|
372
|
+
{"domainName": domain_name, "entityName": entity_name, "elemSize": elem_size},
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def remove_meshing_domain_refinement(self, domain_name: str, entity_name: str) -> None:
|
|
377
|
+
"""Removes a refinement from the specified domain"""
|
|
378
|
+
self.backend.call(
|
|
379
|
+
"removeMeshingDomainRefinement",
|
|
380
|
+
{"domainName": domain_name, "entityName": entity_name},
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def reset_manual_meshing_params(self) -> None:
|
|
385
|
+
"""Resets all of the MeshParams to the defaults."""
|
|
386
|
+
self.backend.call("resetMeshingParams")
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def reset_manual_meshing_domain_params(self, domain_name: str) -> None:
|
|
390
|
+
"""Resets the parameters for the specified domain."""
|
|
391
|
+
self.backend.call(
|
|
392
|
+
"resetDomainMeshParams",
|
|
393
|
+
{"name": domain_name},
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def generate_mesh(self) -> None:
|
|
398
|
+
"""Generates mesh based on the case state."""
|
|
399
|
+
self.backend.call("generateMesh")
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def use_existing_mesh(self) -> None:
|
|
403
|
+
"""Specifies that the current mesh if fine despite changes in the physics which would affect the mesh."""
|
|
404
|
+
self.backend.call("useExistingMesh")
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def generate_pdf_report(self) -> None:
|
|
408
|
+
"""Generates a PDF report for the case. Returns path to the pdf file"""
|
|
409
|
+
self.backend.call("generatePdfReport")
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def update_tab_property(self, tab_key: str, property_key: str, value: str | int | float | bool | list[tuple[float, float]], argument: str | None = None) -> None:
|
|
413
|
+
"""Updates a single property on a physics tab (e.g. simulation tab)."""
|
|
414
|
+
_property_data = build_property_data(property_key, value, argument)
|
|
415
|
+
self.backend.call(
|
|
416
|
+
"updateTabProperty",
|
|
417
|
+
{"tabKey": tab_key, "property": _property_data},
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def update_physics_property(self, physics_key: str, group_type: str, group_name: str, property_key: str, value: str | int | float | bool | list[tuple[float, float]], argument: str | None = None) -> None:
|
|
422
|
+
"""Updates a single property on a physics domain or boundary."""
|
|
423
|
+
_property_data = build_property_data(property_key, value, argument)
|
|
424
|
+
self.backend.call(
|
|
425
|
+
"updatePhysicsProperty",
|
|
426
|
+
{"physicsKey": physics_key, "groupType": group_type, "groupName": group_name, "property": _property_data},
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def update_domain_property(self, domain_name: str, property_key: str, value: str | int | float | bool | list[tuple[float, float]], argument: str | None = None) -> None:
|
|
431
|
+
"""Updates a single domain property."""
|
|
432
|
+
_property_data = build_property_data(property_key, value, argument)
|
|
433
|
+
self.backend.call(
|
|
434
|
+
"updateDomainProperty",
|
|
435
|
+
{"domainName": domain_name, "property": _property_data},
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def update_material_property(self, material_name: str, property_key: str, value: str | int | float | bool | list[tuple[float, float]], argument: str | None = None) -> None:
|
|
440
|
+
"""Updates a single material property."""
|
|
441
|
+
_property_data = build_property_data(property_key, value, argument)
|
|
442
|
+
self.backend.call(
|
|
443
|
+
"updateMaterialProperty",
|
|
444
|
+
{"materialName": material_name, "property": _property_data},
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def update_motion_property(self, motion_name: str, property_key: str, value: str | int | float | bool | list[tuple[float, float]], argument: str | None = None) -> None:
|
|
449
|
+
"""Updates a single motion property."""
|
|
450
|
+
_property_data = build_property_data(property_key, value, argument)
|
|
451
|
+
self.backend.call(
|
|
452
|
+
"updateMotionProperty",
|
|
453
|
+
{"motionName": motion_name, "property": _property_data},
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def clear_group_material(self, group_name: str) -> None:
|
|
458
|
+
"""Clears the assigned material for a domain or boundary group."""
|
|
459
|
+
self.backend.call(
|
|
460
|
+
"clearGroupMaterial",
|
|
461
|
+
{"groupName": group_name},
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def assign_group_material(self, group_name: str, material_key: str) -> None:
|
|
466
|
+
"""Assigns a library material to a domain or boundary group."""
|
|
467
|
+
self.backend.call(
|
|
468
|
+
"assignGroupMaterial",
|
|
469
|
+
{"groupName": group_name, "materialKey": material_key},
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def assign_group_material_custom(self, group_name: str, material_data: dict) -> None:
|
|
474
|
+
"""Assigns a custom material to a domain or boundary group."""
|
|
475
|
+
self.backend.call(
|
|
476
|
+
"assignGroupMaterialCustom",
|
|
477
|
+
{"groupName": group_name, "material": material_data},
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def set_domain_type(self, domain_name: str, physics_key: str, type_key: str) -> None:
|
|
482
|
+
"""Sets the physics domain type for a domain."""
|
|
483
|
+
self.backend.call(
|
|
484
|
+
"setDomainType",
|
|
485
|
+
{"domainName": domain_name, "physicsKey": physics_key, "typeKey": type_key},
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def set_boundary_type(self, boundary_name: str, physics_key: str, type_key: str) -> None:
|
|
490
|
+
"""Sets the physics boundary type for a boundary."""
|
|
491
|
+
self.backend.call(
|
|
492
|
+
"setBoundaryType",
|
|
493
|
+
{"boundaryName": boundary_name, "physicsKey": physics_key, "typeKey": type_key},
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def set_domain_physics_enabled(self, domain_name: str, physics_key: str, enabled: bool) -> None:
|
|
498
|
+
"""Enables or disables physics for a domain."""
|
|
499
|
+
self.backend.call(
|
|
500
|
+
"setDomainPhysicsEnabled",
|
|
501
|
+
{"domainName": domain_name, "physicsKey": physics_key, "enabled": enabled},
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def set_motion_enabled(self, motion_name: str, enabled: bool) -> None:
|
|
506
|
+
"""Enables or disables a motion."""
|
|
507
|
+
self.backend.call(
|
|
508
|
+
"setMotionEnabled",
|
|
509
|
+
{"name": motion_name, "enabled": enabled},
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def set_motion_type(self, motion_name: str, motion_type: str) -> None:
|
|
514
|
+
"""Sets the motion type for a motion."""
|
|
515
|
+
self.backend.call(
|
|
516
|
+
"setMotionType",
|
|
517
|
+
{"name": motion_name, "type": motion_type},
|
|
518
|
+
)
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def set_motion_domains(self, motion_name: str, domain_names: list[str]) -> None:
|
|
522
|
+
"""Sets the domains associated with a motion."""
|
|
523
|
+
self.backend.call(
|
|
524
|
+
"setMotionDomains",
|
|
525
|
+
{"name": motion_name, "domains": domain_names},
|
|
526
|
+
)
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def update_template_property(self, domain_id: int, property_key: str, value: str | int | float | bool | list[tuple[float, float]], argument: str | None = None) -> None:
|
|
530
|
+
"""Updates a single template property value and regenerates geometry.
|
|
531
|
+
If geometry was generated successfully, returns True, otherwise False."""
|
|
532
|
+
_property_data = build_property_data(property_key, value, argument)
|
|
533
|
+
self.backend.call(
|
|
534
|
+
"updateTemplateProperty",
|
|
535
|
+
{"domainId": domain_id, "property": _property_data},
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def update_template_shape_type(self, domain_id: int, new_shape_type: str) -> None:
|
|
540
|
+
"""Updates a template's shape type and regenerates geometry.
|
|
541
|
+
If geometry was generated successfully, returns True, otherwise False."""
|
|
542
|
+
self.backend.call(
|
|
543
|
+
"updateTemplateShapeType",
|
|
544
|
+
{"domainId": domain_id, "type": new_shape_type},
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
def add_template_domain(self, template_tab_key: str) -> None:
|
|
549
|
+
"""Called when user click the plus button in the template tabs row to create new template domain."""
|
|
550
|
+
self.backend.call(
|
|
551
|
+
"addTemplateDomain",
|
|
552
|
+
{"geometryGroupKey": template_tab_key},
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
def remove_template_domain(self, domain_id: int) -> None:
|
|
557
|
+
"""Called when user clicks delete on of the template tabs."""
|
|
558
|
+
self.backend.call(
|
|
559
|
+
"removeTemplateDomain",
|
|
560
|
+
{"id": domain_id},
|
|
561
|
+
)
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
def enable_parametric_study(self) -> None:
|
|
565
|
+
"""Enabled parametric study and adds the current case as one of the entries."""
|
|
566
|
+
self.backend.call("enableParametricStudy")
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
def disable_parametric_study(self) -> None:
|
|
570
|
+
"""Disables parametric study. All the configuration that user entered is still present, just not visible."""
|
|
571
|
+
self.backend.call("disableParametricStudy")
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
def add_parametric_case(self) -> None:
|
|
575
|
+
"""Adds a new parametric case entry with the same values as the last parametric entry."""
|
|
576
|
+
self.backend.call("addParametricCase")
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
def add_parametric_case_input_files(self, files: list[str]) -> None:
|
|
580
|
+
"""User has decided to add some more files from the parametric study window."""
|
|
581
|
+
self.backend.call(
|
|
582
|
+
"addParametricCaseInputFiles",
|
|
583
|
+
{"files": files},
|
|
584
|
+
)
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
def remove_parametric_case(self, parametric_case_id: int) -> None:
|
|
588
|
+
"""Remove parametric case."""
|
|
589
|
+
self.backend.call(
|
|
590
|
+
"removeParametricCase",
|
|
591
|
+
{"id": parametric_case_id},
|
|
592
|
+
)
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
def add_parametric_column(self, variable_name: str) -> None:
|
|
596
|
+
"""Add a new parametric column for the variable and sets the base value for all cases."""
|
|
597
|
+
self.backend.call(
|
|
598
|
+
"addParametricColumn",
|
|
599
|
+
{"variableName": variable_name},
|
|
600
|
+
)
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
def remove_parametric_column(self, variable_name: str) -> None:
|
|
604
|
+
"""Remove the parametric column and removed the property from all cases."""
|
|
605
|
+
self.backend.call(
|
|
606
|
+
"removeParametricColumn",
|
|
607
|
+
{"variableName": variable_name},
|
|
608
|
+
)
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
def update_parametric_case_properties(self, pcase_id: int, variable_name: str, value: float) -> None:
|
|
612
|
+
"""Set the property value for a given parametric case."""
|
|
613
|
+
self.backend.call(
|
|
614
|
+
"updateParametricCaseProperty",
|
|
615
|
+
{"id": pcase_id, "variableName": variable_name, "variableValue": value},
|
|
616
|
+
)
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
def run_parametric_cases(self, recalculate_all: bool) -> None:
|
|
620
|
+
"""Runs a single parametric study case."""
|
|
621
|
+
self.backend.call(
|
|
622
|
+
"runParametricCases",
|
|
623
|
+
{"recalculateAll": recalculate_all},
|
|
624
|
+
)
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
def add_motion(self) -> None:
|
|
628
|
+
"""Adds motion to case."""
|
|
629
|
+
self.backend.call("addMotion")
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
def remove_motion(self, motion_name: str) -> None:
|
|
633
|
+
"""Adds motion to case."""
|
|
634
|
+
self.backend.call(
|
|
635
|
+
"removeMotion",
|
|
636
|
+
{"name": motion_name},
|
|
637
|
+
)
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
def set_physics_block_key(self, block_key: str) -> None:
|
|
641
|
+
"""Adds motion to case."""
|
|
642
|
+
self.backend.call(
|
|
643
|
+
"setPhysicsBlockKey",
|
|
644
|
+
{"blockKey": block_key},
|
|
645
|
+
)
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
def renormalize_results(self, new_port_data: dict[str, dict[str, float | int | str]], sub_case: str | None = None) -> None:
|
|
649
|
+
"""In ANTENNAS, the results can be renormalized with different port parameters.
|
|
650
|
+
Example new_port_data:
|
|
651
|
+
{
|
|
652
|
+
"discrete_port1": {
|
|
653
|
+
"resistance": 50,
|
|
654
|
+
"reactance": 50,
|
|
655
|
+
"reactive_component": "reactance",
|
|
656
|
+
"capacitance": 0,
|
|
657
|
+
"capacitance_arrangement": "serial",
|
|
658
|
+
"phase": 0,
|
|
659
|
+
"power": 10,
|
|
660
|
+
"port_id": 1,
|
|
661
|
+
}
|
|
662
|
+
}"""
|
|
663
|
+
self.backend.call(
|
|
664
|
+
"renormalizeResults",
|
|
665
|
+
{"portData": new_port_data, "sub_case": sub_case},
|
|
666
|
+
)
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
def periodic_array_calculation(self, periodic_array_data: dict[str, float | int | str]) -> None:
|
|
670
|
+
"""In ANTENNAS, the periodic array farfield calculation is done here.
|
|
671
|
+
Example:
|
|
672
|
+
periodic_array_data = {
|
|
673
|
+
"first_direction": "X",
|
|
674
|
+
"second_direction": "Y",
|
|
675
|
+
"elements_x": 4,
|
|
676
|
+
"elements_y": 1,
|
|
677
|
+
"phase_shift_x": 0,
|
|
678
|
+
"phase_shift_y": 0,
|
|
679
|
+
"spacing_x": 0.1, # in meters
|
|
680
|
+
"spacing_y": 0.1, # in meters
|
|
681
|
+
"continuous": False
|
|
682
|
+
}"""
|
|
683
|
+
self.backend.call(
|
|
684
|
+
"periodicArrayCalculation",
|
|
685
|
+
{"periodicArrayData": periodic_array_data},
|
|
686
|
+
)
|
|
687
|
+
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import queue
|
|
4
|
+
import socket
|
|
5
|
+
import subprocess
|
|
6
|
+
import threading
|
|
7
|
+
import time
|
|
8
|
+
import uuid
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
import websocket
|
|
13
|
+
|
|
14
|
+
from .logger import logger
|
|
15
|
+
|
|
16
|
+
STARTUP_TIMEOUT = 60 # seconds
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _find_free_port() -> int:
|
|
20
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
21
|
+
s.bind(("127.0.0.1", 0))
|
|
22
|
+
return s.getsockname()[1]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _build_env(cenos_location: Path, app_name: str) -> dict[str, str]:
|
|
26
|
+
platform_files = cenos_location / "platform-files"
|
|
27
|
+
backend_bin = cenos_location / "backend" / "bin"
|
|
28
|
+
|
|
29
|
+
python_path = os.pathsep.join(
|
|
30
|
+
[
|
|
31
|
+
str(platform_files),
|
|
32
|
+
str(
|
|
33
|
+
platform_files
|
|
34
|
+
/ "cenos"
|
|
35
|
+
/ "viewer"
|
|
36
|
+
/ "cenos-viewer"
|
|
37
|
+
/ "prod-build"
|
|
38
|
+
/ "backend"
|
|
39
|
+
),
|
|
40
|
+
str(cenos_location / "app" / app_name),
|
|
41
|
+
str(backend_bin / "pygetdp"),
|
|
42
|
+
str(backend_bin / "lib" / "netgen-root" / "lib" / "site-packages"),
|
|
43
|
+
]
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
env = os.environ.copy()
|
|
47
|
+
env.update(
|
|
48
|
+
{
|
|
49
|
+
"CENOS_APP": app_name,
|
|
50
|
+
"PYTHONPATH": python_path,
|
|
51
|
+
"TEMP": env.get("TEMP") or env.get("TMP") or r"C:\temp",
|
|
52
|
+
"CENOS_PLATFORM_FILES": str(platform_files),
|
|
53
|
+
"PYTHONIOENCODING": "utf-8",
|
|
54
|
+
"PYTHONASYNCIODEBUG": "0",
|
|
55
|
+
}
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
return env
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _pipe_reader(pipe, line_queue: queue.Queue) -> None:
|
|
62
|
+
"""Read lines from a pipe in a background thread and push them onto a queue."""
|
|
63
|
+
try:
|
|
64
|
+
for line in pipe:
|
|
65
|
+
line_queue.put(line)
|
|
66
|
+
finally:
|
|
67
|
+
line_queue.put(None)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class BackendInstance:
|
|
71
|
+
def __init__(self, process: subprocess.Popen, ws: websocket.WebSocket) -> None:
|
|
72
|
+
self._process = process
|
|
73
|
+
self._ws = ws
|
|
74
|
+
|
|
75
|
+
def call(self, command: str, args: dict[str, Any] | None = None) -> Any:
|
|
76
|
+
"""Send a command and block until the response arrives. Returns response data."""
|
|
77
|
+
guid = str(uuid.uuid4())
|
|
78
|
+
message = {
|
|
79
|
+
"guid": guid,
|
|
80
|
+
"command": command,
|
|
81
|
+
"args": args or {},
|
|
82
|
+
}
|
|
83
|
+
self._ws.send(json.dumps(message))
|
|
84
|
+
logger.debug(f"Sent: {message}")
|
|
85
|
+
|
|
86
|
+
while True:
|
|
87
|
+
resp = self._ws.recv()
|
|
88
|
+
if not resp:
|
|
89
|
+
continue
|
|
90
|
+
|
|
91
|
+
response = json.loads(resp)
|
|
92
|
+
|
|
93
|
+
typ = response.get("type")
|
|
94
|
+
|
|
95
|
+
if typ == "log":
|
|
96
|
+
logger.info(f"Backend log: {response['data']['message']}")
|
|
97
|
+
if response["data"]["showToUser"]:
|
|
98
|
+
print(response["data"]["message"])
|
|
99
|
+
continue
|
|
100
|
+
if typ == "progress":
|
|
101
|
+
logger.info(f"Backend progress: {response['data']['text']}")
|
|
102
|
+
continue
|
|
103
|
+
if typ == "popup":
|
|
104
|
+
leastChangeKey = response["data"]["leastChangeButtonKey"]
|
|
105
|
+
callback_message = {
|
|
106
|
+
"guid": response["guid"],
|
|
107
|
+
"command": "callback",
|
|
108
|
+
"args": {"button": leastChangeKey},
|
|
109
|
+
}
|
|
110
|
+
self._ws.send(json.dumps(callback_message))
|
|
111
|
+
logger.info(
|
|
112
|
+
f"Backend popup: leastChangeButtonKey enforced: '{leastChangeKey}'"
|
|
113
|
+
)
|
|
114
|
+
continue
|
|
115
|
+
|
|
116
|
+
if typ == "success":
|
|
117
|
+
return response.get("data")
|
|
118
|
+
elif typ == "cancel":
|
|
119
|
+
return None
|
|
120
|
+
elif typ == "error":
|
|
121
|
+
data = response.get("data") or {}
|
|
122
|
+
msg = data.get("message") or data.get("description") or "Unknown error"
|
|
123
|
+
raise RuntimeError(f"Backend error for '{command}': {msg}")
|
|
124
|
+
else:
|
|
125
|
+
raise RuntimeError(f"Unexpected response type '{typ}' for '{command}'")
|
|
126
|
+
|
|
127
|
+
def stop(self) -> None:
|
|
128
|
+
"""Close the WebSocket and terminate the backend process."""
|
|
129
|
+
try:
|
|
130
|
+
self._ws.close()
|
|
131
|
+
except Exception:
|
|
132
|
+
pass
|
|
133
|
+
if self._process.poll() is None:
|
|
134
|
+
self._process.terminate()
|
|
135
|
+
self._process.wait()
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def start_backend(cenos_location: Path, app_name: str) -> BackendInstance:
|
|
139
|
+
python_exe = cenos_location / "backend" / "bin" / "python.exe"
|
|
140
|
+
server_script = cenos_location / "platform-files" / "cenos" / "server.py"
|
|
141
|
+
port = _find_free_port()
|
|
142
|
+
|
|
143
|
+
process = subprocess.Popen(
|
|
144
|
+
[str(python_exe), str(server_script), str(port)],
|
|
145
|
+
cwd=str(server_script.parent),
|
|
146
|
+
env=_build_env(cenos_location, app_name),
|
|
147
|
+
stdout=subprocess.PIPE,
|
|
148
|
+
stderr=subprocess.PIPE,
|
|
149
|
+
text=True,
|
|
150
|
+
encoding="utf-8",
|
|
151
|
+
)
|
|
152
|
+
logger.info(f"Backend process started (pid={process.pid}, port={port})")
|
|
153
|
+
|
|
154
|
+
# Read stdout in a background thread so readline() never blocks the main thread.
|
|
155
|
+
stdout_queue: queue.Queue[str | None] = queue.Queue()
|
|
156
|
+
threading.Thread(
|
|
157
|
+
target=_pipe_reader, args=(process.stdout, stdout_queue), daemon=True
|
|
158
|
+
).start()
|
|
159
|
+
|
|
160
|
+
deadline = time.monotonic() + STARTUP_TIMEOUT
|
|
161
|
+
while True:
|
|
162
|
+
if time.monotonic() > deadline:
|
|
163
|
+
process.terminate()
|
|
164
|
+
raise TimeoutError(
|
|
165
|
+
f"Backend did not signal ready within {STARTUP_TIMEOUT}s"
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
if process.poll() is not None:
|
|
169
|
+
# Drain any remaining queued lines before giving up
|
|
170
|
+
remaining = []
|
|
171
|
+
try:
|
|
172
|
+
while True:
|
|
173
|
+
remaining.append(stdout_queue.get_nowait() or "")
|
|
174
|
+
except queue.Empty:
|
|
175
|
+
pass
|
|
176
|
+
raise RuntimeError(
|
|
177
|
+
f"Backend exited (code {process.returncode}) before signalling ready.\n"
|
|
178
|
+
+ "".join(remaining)
|
|
179
|
+
+ process.stderr.read()
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
try:
|
|
183
|
+
line = stdout_queue.get(timeout=0.1)
|
|
184
|
+
except queue.Empty:
|
|
185
|
+
continue
|
|
186
|
+
|
|
187
|
+
if line is None: # pipe closed
|
|
188
|
+
break
|
|
189
|
+
logger.debug(f"Backend stdout: {line.rstrip()}")
|
|
190
|
+
if "CENOS server is started" in line:
|
|
191
|
+
break
|
|
192
|
+
|
|
193
|
+
ws = websocket.WebSocket()
|
|
194
|
+
ws.connect(f"ws://localhost:{port}?id=gui")
|
|
195
|
+
logger.info(f"WebSocket connected on port {port}")
|
|
196
|
+
|
|
197
|
+
return BackendInstance(process, ws)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import tempfile
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from cenos_py import misc_functions
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def setup_logger() -> logging.Logger:
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
logger.setLevel(logging.DEBUG)
|
|
12
|
+
|
|
13
|
+
if logger.hasHandlers():
|
|
14
|
+
logger.handlers.clear()
|
|
15
|
+
|
|
16
|
+
formatter = logging.Formatter(
|
|
17
|
+
"%(asctime)s - %(levelname)s - %(message)s",
|
|
18
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
if misc_functions.is_editable_install():
|
|
22
|
+
console_handler = logging.StreamHandler()
|
|
23
|
+
console_handler.setLevel(logging.DEBUG)
|
|
24
|
+
console_handler.setFormatter(formatter)
|
|
25
|
+
logger.addHandler(console_handler)
|
|
26
|
+
|
|
27
|
+
log_dir = Path(tempfile.gettempdir()) / "cenos"
|
|
28
|
+
log_dir.mkdir(parents=True, exist_ok=True)
|
|
29
|
+
log_filename = log_dir / f"{datetime.now().strftime('%Y-%m-%d')}.cenos-py.log"
|
|
30
|
+
file_handler = logging.FileHandler(log_filename, encoding="utf-8")
|
|
31
|
+
file_handler.setLevel(logging.DEBUG)
|
|
32
|
+
file_handler.setFormatter(formatter)
|
|
33
|
+
logger.addHandler(file_handler)
|
|
34
|
+
|
|
35
|
+
return logger
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
logger = setup_logger()
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import importlib.metadata
|
|
2
|
+
import sys
|
|
3
|
+
from os import PathLike
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
_cenos_location: Path | None = None
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def is_editable_install() -> bool:
|
|
10
|
+
try:
|
|
11
|
+
dist = importlib.metadata.distribution("cenos-py")
|
|
12
|
+
except importlib.metadata.PackageNotFoundError:
|
|
13
|
+
return False
|
|
14
|
+
|
|
15
|
+
pkg_name = dist.metadata["Name"].replace("-", "_")
|
|
16
|
+
|
|
17
|
+
for p in sys.path:
|
|
18
|
+
path = Path(p).resolve()
|
|
19
|
+
if "site-packages" in path.parts:
|
|
20
|
+
continue
|
|
21
|
+
if (path / pkg_name).is_dir() or (path / f"{pkg_name}.py").exists():
|
|
22
|
+
return True
|
|
23
|
+
|
|
24
|
+
return False
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def is_dev_version(cenos_installation_path: Path) -> bool:
|
|
28
|
+
return not (cenos_installation_path / "resources").exists()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get_resources_path(cenos_installation_path: Path) -> Path:
|
|
32
|
+
if is_dev_version(cenos_installation_path):
|
|
33
|
+
return cenos_installation_path
|
|
34
|
+
else:
|
|
35
|
+
return cenos_installation_path / "resources"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def setCenosLocation(path: str | PathLike) -> None:
|
|
39
|
+
"""Set the Cenos software installation location."""
|
|
40
|
+
path = Path(path)
|
|
41
|
+
if not path.exists():
|
|
42
|
+
raise ValueError(f"Path '{path}' does not exist")
|
|
43
|
+
has_platform_files = (path / "platform-files").exists()
|
|
44
|
+
has_resources = (path / "resources").exists()
|
|
45
|
+
|
|
46
|
+
if not has_platform_files and not has_resources:
|
|
47
|
+
raise ValueError(
|
|
48
|
+
f"Path '{path}' does not appear to contains the expected cenos files, please check the path"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
global _cenos_location
|
|
52
|
+
_cenos_location = get_resources_path(path)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def get_installed_app_name(_cenos_location: Path):
|
|
56
|
+
available_apps = []
|
|
57
|
+
for dir in (_cenos_location / "app").iterdir():
|
|
58
|
+
if dir.is_dir():
|
|
59
|
+
available_apps.append(dir.name)
|
|
60
|
+
|
|
61
|
+
if len(available_apps) > 1:
|
|
62
|
+
# Dev mode
|
|
63
|
+
raise ValueError(
|
|
64
|
+
"Please specify the app that should be used in the CenosCase constructor."
|
|
65
|
+
)
|
|
66
|
+
return available_apps[0]
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Helpers for building PropertyDataDict payloads sent to the cenos backend.
|
|
2
|
+
|
|
3
|
+
This module is hand-written (not generated) and is imported by the
|
|
4
|
+
auto-generated `_generated_case_methods.py`. Keeping it separate avoids
|
|
5
|
+
duplicating the same conversion logic in every generated property-setter
|
|
6
|
+
method.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
from typing import Literal, TypedDict
|
|
11
|
+
|
|
12
|
+
# TODO: adjust this import path if case_worker's location changes.
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class PropertyDataDictValueTable(TypedDict):
|
|
16
|
+
arg: float
|
|
17
|
+
value: float
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class PropertyDataDict(TypedDict, total=False):
|
|
21
|
+
"""The format in which physics, template or other property updates are called.
|
|
22
|
+
All possible structure examples:
|
|
23
|
+
{'key': 'I_3D', 'value': 2000}
|
|
24
|
+
{'key': 'I_3D', 'valueType': 'table', 'argument': 'Time'}
|
|
25
|
+
{'key': 'I_3D', 'valueTable': [{'arg': 0, 'value': 2000}, {'arg': 5, 'value': 4000}], 'argument': 'time'}
|
|
26
|
+
{'key': 'workpiece_diameter', 'valueType': 'scalar', 'value': 23}
|
|
27
|
+
{'key': 'isHollow', 'valueType': 'scalar', 'value': True}
|
|
28
|
+
{'key': 'algorithm', 'value': 'accurate'}
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
key: str
|
|
32
|
+
valueType: Literal["scalar", "table"]
|
|
33
|
+
value: str | int | float | bool
|
|
34
|
+
valueTable: list[PropertyDataDictValueTable]
|
|
35
|
+
argument: str
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def build_property_data(
|
|
39
|
+
key: str,
|
|
40
|
+
value: str | int | float | bool | list[tuple[float, float]],
|
|
41
|
+
argument: str | None = None,
|
|
42
|
+
) -> PropertyDataDict:
|
|
43
|
+
"""Build a PropertyDataDict from ergonomic (key, value, argument) arguments.
|
|
44
|
+
|
|
45
|
+
- argument is None:
|
|
46
|
+
-> {"key": key, "value": value}
|
|
47
|
+
- argument is set and value is a list of (arg, value) pairs:
|
|
48
|
+
-> {"key": key, "valueType": "table", "valueTable": [...], "argument": argument}
|
|
49
|
+
- argument is set and value is a scalar (str/int/float/bool):
|
|
50
|
+
-> {"key": key, "valueType": "scalar", "value": value, "argument": argument}
|
|
51
|
+
"""
|
|
52
|
+
if argument is None:
|
|
53
|
+
return {"key": key, "value": value}
|
|
54
|
+
|
|
55
|
+
if isinstance(value, list):
|
|
56
|
+
return {
|
|
57
|
+
"key": key,
|
|
58
|
+
"valueType": "table",
|
|
59
|
+
"valueTable": [{"arg": arg, "value": val} for arg, val in value],
|
|
60
|
+
"argument": argument,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
"key": key,
|
|
65
|
+
"valueType": "scalar",
|
|
66
|
+
"value": value,
|
|
67
|
+
"argument": argument,
|
|
68
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import shutil
|
|
2
|
+
import zipfile
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def unzip_case_to_temp(case_name: str) -> str:
|
|
7
|
+
zip_path = Path(__file__).parent.parent.parent / "examples" / "assets" / case_name
|
|
8
|
+
|
|
9
|
+
if not zip_path.exists():
|
|
10
|
+
raise FileNotFoundError(f"Zip file not found: {zip_path}")
|
|
11
|
+
|
|
12
|
+
temp_dir = Path(__file__).parent.parent.parent / "examples" / "temp"
|
|
13
|
+
|
|
14
|
+
if temp_dir.exists():
|
|
15
|
+
shutil.rmtree(temp_dir)
|
|
16
|
+
|
|
17
|
+
temp_dir.mkdir(parents=True, exist_ok=True)
|
|
18
|
+
|
|
19
|
+
with zipfile.ZipFile(zip_path, "r") as zip_ref:
|
|
20
|
+
zip_ref.extractall(temp_dir)
|
|
21
|
+
|
|
22
|
+
extracted_items = list(temp_dir.iterdir())
|
|
23
|
+
assert len(extracted_items) == 1 and extracted_items[0].is_dir(), (
|
|
24
|
+
"Expected exactly one directory in the extracted zip"
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
return str(extracted_items[0])
|