Skip to content

Commit 52633bc

Browse files
REFACTOR: Nastran import refactoring (#6236)
Co-authored-by: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com>
1 parent d480be5 commit 52633bc

25 files changed

+429
-458
lines changed

MANIFEST.in

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
11
global-include ansys.aedt.core *.txt *.md *.toml *.json *.png *.xml *.areg *.joblib *.acf *.m *.ipynb *.py_build
2+
include src/ansys/aedt/core/syslib/**/*.so
3+
include src/ansys/aedt/core/syslib/**/*.pyd
4+
include src/ansys/aedt/core/syslib/**/*.license
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Nastran import refactoring

pyproject.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,12 @@ examples = [
132132
"rpyc>=6.0.0,<6.1",
133133
]
134134

135+
[tool.setuptools]
136+
include-package-data = true
137+
138+
[tool.setuptools.package-data]
139+
"ansys.aedt.core.syslib" = ["**/*.so", "**/*.pyd", "**/*.license"]
140+
135141
[tool.setuptools.dynamic]
136142
version = {attr = "ansys.aedt.core.__version__"}
137143

src/ansys/aedt/core/extensions/common/import_nastran.py

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,7 @@
3939
from ansys.aedt.core.extensions.misc import get_process_id
4040
from ansys.aedt.core.extensions.misc import is_student
4141
from ansys.aedt.core.internal.errors import AEDTRuntimeError
42-
from ansys.aedt.core.visualization.advanced.misc import nastran_to_stl
43-
from ansys.aedt.core.visualization.advanced.misc import simplify_stl
42+
from ansys.aedt.core.syslib.nastran_import import nastran_to_stl
4443

4544
PORT = get_port()
4645
VERSION = get_aedt_version()
@@ -53,6 +52,7 @@
5352
"lightweight": False,
5453
"decimate": 0.0,
5554
"planar": True,
55+
"remove_multiple_connections": False,
5656
}
5757
EXTENSION_TITLE = "Import Nastran"
5858

@@ -65,6 +65,7 @@ class ImportNastranExtensionData(ExtensionCommonData):
6565
lightweight: bool = EXTENSION_DEFAULT_ARGUMENTS["lightweight"]
6666
decimate: float = EXTENSION_DEFAULT_ARGUMENTS["decimate"]
6767
planar: bool = EXTENSION_DEFAULT_ARGUMENTS["planar"]
68+
remove_multiple_connections: bool = EXTENSION_DEFAULT_ARGUMENTS["remove_multiple_connections"]
6869

6970

7071
class ImportNastranExtension(ExtensionProjectCommon):
@@ -85,6 +86,7 @@ def __init__(self, withdraw: bool = False):
8586
self.__decimation_text = None
8687
self.__lightweight_var = None
8788
self.__planar_var = None
89+
self.__remove_multiple_connections_var = None
8890

8991
# Add extension content
9092
self.add_extension_content()
@@ -155,6 +157,21 @@ def add_extension_content(self):
155157
name="check_planar_merge",
156158
).grid(row=3, column=1, pady=10, padx=5)
157159

160+
# Remove multiple connections option
161+
ttk.Label(
162+
self.root,
163+
text="Remove multiple connections:",
164+
style="PyAEDT.TLabel",
165+
).grid(row=4, column=0, padx=15, pady=10)
166+
167+
self.__remove_multiple_connections_var = tkinter.IntVar(self.root, name="var_remove_multiple_connections")
168+
ttk.Checkbutton(
169+
self.root,
170+
variable=self.__remove_multiple_connections_var,
171+
style="PyAEDT.TCheckbutton",
172+
name="check_remove_multiple_connections",
173+
).grid(row=4, column=1, pady=10, padx=5)
174+
158175
# Preview button
159176
ttk.Button(
160177
self.root,
@@ -163,7 +180,7 @@ def add_extension_content(self):
163180
command=self.__preview,
164181
style="PyAEDT.TButton",
165182
name="preview_button",
166-
).grid(row=4, column=0, pady=10, padx=10)
183+
).grid(row=5, column=0, pady=10, padx=10)
167184

168185
# Import button
169186
ttk.Button(
@@ -173,10 +190,10 @@ def add_extension_content(self):
173190
command=self.__import_callback,
174191
style="PyAEDT.TButton",
175192
name="import_button",
176-
).grid(row=4, column=1, pady=10, padx=10)
193+
).grid(row=5, column=1, pady=10, padx=10)
177194

178195
def __browse_files(self):
179-
"""Open file dialog to select Nastran or STL file."""
196+
"""Open the file dialog to select Nastran or STL file."""
180197
filename = filedialog.askopenfilename(
181198
initialdir="/",
182199
title="Select a Nastran or STL File",
@@ -205,13 +222,16 @@ def __preview(self):
205222
if file_path_ui.endswith(".nas"):
206223
nastran_to_stl(file_path_ui, decimation=decimate_ui, preview=True)
207224
else:
208-
simplify_stl(file_path_ui, decimation=decimate_ui, preview=True)
225+
from ansys.aedt.core.visualization.advanced.misc import simplify_and_preview_stl
226+
227+
simplify_and_preview_stl(file_path_ui, decimation=decimate_ui, preview=True)
209228

210229
def __import_callback(self):
211230
"""Callback for import button."""
212231
file_path = self.__file_path_text.get("1.0", tkinter.END).strip()
213232
lightweight_val = self.__lightweight_var.get() == 1
214233
planar_val = self.__planar_var.get() == 1
234+
remove_multiple_connections_val = self.__remove_multiple_connections_var.get() == 1
215235

216236
# Validation
217237
if not file_path:
@@ -231,6 +251,7 @@ def __import_callback(self):
231251
decimate=decimate_val,
232252
lightweight=lightweight_val,
233253
planar=planar_val,
254+
remove_multiple_connections=remove_multiple_connections_val,
234255
)
235256
self.root.destroy()
236257

@@ -277,9 +298,12 @@ def main(data: ImportNastranExtensionData):
277298
import_as_light_weight=data.lightweight,
278299
decimation=data.decimate,
279300
enable_planar_merge=str(data.planar),
301+
remove_multiple_connections=data.remove_multiple_connections,
280302
)
281303
else:
282-
outfile = simplify_stl(str(file_path), decimation=data.decimate)
304+
from ansys.aedt.core.visualization.advanced.misc import simplify_and_preview_stl
305+
306+
outfile = simplify_and_preview_stl(str(file_path), decimation=data.decimate)
283307
aedtapp.modeler.import_3d_cad(
284308
outfile,
285309
healing=False,

src/ansys/aedt/core/generic/general_methods.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import itertools
3333
import logging
3434
import os
35+
import platform
3536
import re
3637
import sys
3738
import time
@@ -47,8 +48,11 @@
4748
from ansys.aedt.core.internal.errors import GrpcApiError
4849
from ansys.aedt.core.internal.errors import MethodNotSupportedError
4950

50-
is_linux = os.name == "posix"
51-
is_windows = not is_linux
51+
system = platform.system()
52+
is_linux = system == "Linux"
53+
is_windows = system == "Windows"
54+
is_macos = system == "Darwin"
55+
5256
inside_desktop_ironpython_console = True if "4.0.30319.42000" in sys.version else False
5357

5458
inclusion_list = [

src/ansys/aedt/core/generic/settings.py

Lines changed: 44 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
import uuid
4949
import warnings
5050

51+
from ansys.aedt.core import pyaedt_path
5152
from ansys.aedt.core.generic.scheduler import DEFAULT_CUSTOM_SUBMISSION_STRING
5253
from ansys.aedt.core.generic.scheduler import DEFAULT_NUM_CORES
5354

@@ -112,6 +113,8 @@
112113
"skip_license_check",
113114
"num_cores",
114115
"use_local_example_data",
116+
"pyd_libraries_path",
117+
"pyd_libraries_user_path",
115118
]
116119

117120
ALLOWED_AEDT_ENV_VAR_SETTINGS = [
@@ -184,7 +187,7 @@ def __init__(self):
184187
self.__lsf_queue: Optional[str] = None
185188
self.__custom_lsf_command = DEFAULT_CUSTOM_SUBMISSION_STRING
186189
# Settings related to environment variables that are set before launching a new AEDT session
187-
# This includes those that enable the beta features !
190+
# This includes those that enable the beta features!
188191
self.__aedt_environment_variables: dict[str, str] = {
189192
"ANSYSEM_FEATURE_SF6694_NON_GRAPHICAL_COMMAND_EXECUTION_ENABLE": "1",
190193
"ANSYSEM_FEATURE_SF159726_SCRIPTOBJECT_ENABLE": "1",
@@ -229,6 +232,8 @@ def __init__(self):
229232
self.__block_figure_plot = False
230233
self.__local_example_folder = None
231234
self.__use_local_example_data = False
235+
self.__pyd_libraries_path: Path = Path(pyaedt_path) / "syslib"
236+
self.__pyd_libraries_user_path: Optional[str] = None
232237

233238
# Load local settings if YAML configuration file exists.
234239
pyaedt_settings_path = os.environ.get("PYAEDT_LOCAL_SETTINGS_PATH", "")
@@ -848,6 +853,34 @@ def local_example_folder(self):
848853
def local_example_folder(self, value):
849854
self.__local_example_folder = value
850855

856+
@property
857+
def pyd_libraries_path(self):
858+
if self.__pyd_libraries_user_path is not None:
859+
# If the user path is set, return it
860+
return Path(self.__pyd_libraries_user_path)
861+
return Path(self.__pyd_libraries_path)
862+
863+
@property
864+
def pyd_libraries_user_path(self):
865+
# Get the user path for PyAEDT libraries.
866+
if self.__pyd_libraries_user_path is not None:
867+
return Path(self.__pyd_libraries_user_path)
868+
return None
869+
870+
@pyd_libraries_user_path.setter
871+
def pyd_libraries_user_path(self, val):
872+
if val is None:
873+
# If the user path is None, set it to None
874+
self.__pyd_libraries_user_path = None
875+
else:
876+
lib_path = Path(str(val))
877+
if not lib_path.exists():
878+
# If the user path does not exist, return None
879+
raise ValueError("The user path for PyAEDT libraries does not exist. Please set a valid path.")
880+
else:
881+
# If the user path exists, set it as a Path object
882+
self.__pyd_libraries_user_path = lib_path
883+
851884
# yaml setting file IO methods
852885

853886
def load_yaml_configuration(self, path: Union[Path, str], raise_on_wrong_key: bool = False):
@@ -898,19 +931,17 @@ def write_yaml_configuration(self, path: Union[Path, str]):
898931
configuration_file = Path(path)
899932

900933
data = {}
901-
data["log"] = {}
902-
for key in ALLOWED_LOG_SETTINGS:
903-
value = getattr(self, key)
904-
data["log"][key] = str(value) if isinstance(value, Path) else value
905-
data["lsf"] = {}
906-
for key in ALLOWED_LSF_SETTINGS:
907-
value = getattr(self, key)
908-
data["lsf"][key] = str(value) if isinstance(value, Path) else value
934+
data["log"] = {
935+
key: str(value) if isinstance(value := getattr(self, key), Path) else value for key in ALLOWED_LOG_SETTINGS
936+
}
937+
data["lsf"] = {
938+
key: str(value) if isinstance(value := getattr(self, key), Path) else value for key in ALLOWED_LSF_SETTINGS
939+
}
909940
data["aedt_env_var"] = getattr(self, "aedt_environment_variables")
910-
data["general"] = {}
911-
for key in ALLOWED_GENERAL_SETTINGS:
912-
value = getattr(self, key)
913-
data["general"][key] = str(value) if isinstance(value, Path) else value
941+
data["general"] = {
942+
key: str(value) if isinstance(value := getattr(self, key), Path) else value
943+
for key in ALLOWED_GENERAL_SETTINGS
944+
}
914945

915946
with open(configuration_file, "w") as file:
916947
yaml.safe_dump(data, file, sort_keys=False)

src/ansys/aedt/core/modeler/modeler_3d.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
from ansys.aedt.core.internal.errors import GrpcApiError
3838
from ansys.aedt.core.modeler.cad.primitives_3d import Primitives3D
3939
from ansys.aedt.core.modeler.geometry_operators import GeometryOperators
40-
from ansys.aedt.core.visualization.advanced.misc import nastran_to_stl
40+
from ansys.aedt.core.syslib.nastran_import import nastran_to_stl
4141

4242

4343
class Modeler3D(Primitives3D):
@@ -1031,6 +1031,7 @@ def import_nastran(
10311031
save_only_stl=False,
10321032
preview=False,
10331033
merge_angle=1e-3,
1034+
remove_multiple_connections=False,
10341035
):
10351036
"""Import Nastran file into 3D Modeler by converting the faces to stl and reading it.
10361037
@@ -1062,6 +1063,8 @@ def import_nastran(
10621063
Whether to preview the model in pyvista or skip it.
10631064
merge_angle : float, optional
10641065
Angle in radians for which faces will be considered planar. Default is ``1e-3``.
1066+
remove_multiple_connections : bool, optional
1067+
Whether to remove multiple connections in the mesh. Default is ``False``.
10651068
10661069
Returns
10671070
-------
@@ -1080,6 +1083,7 @@ def import_nastran(
10801083
output_folder=self._app.working_directory,
10811084
enable_planar_merge=enable_planar_merge,
10821085
preview=preview,
1086+
remove_multiple_connections=remove_multiple_connections,
10831087
)
10841088
if save_only_stl:
10851089
return output_stls, nas_to_dict
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# -*- coding: utf-8 -*-
2+
#
3+
# Copyright (C) 2021 - 2025 ANSYS, Inc. and/or its affiliates.
4+
# SPDX-License-Identifier: MIT
5+
#
6+
#
7+
# Permission is hereby granted, free of charge, to any person obtaining a copy
8+
# of this software and associated documentation files (the "Software"), to deal
9+
# in the Software without restriction, including without limitation the rights
10+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11+
# copies of the Software, and to permit persons to whom the Software is
12+
# furnished to do so, subject to the following conditions:
13+
#
14+
# The above copyright notice and this permission notice shall be included in all
15+
# copies or substantial portions of the Software.
16+
#
17+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23+
# SOFTWARE.
24+
25+
import importlib.util
26+
from pathlib import Path
27+
import sys
28+
29+
from ansys.aedt.core.generic.general_methods import is_windows
30+
31+
32+
def load_native_module(module_name: str, base_dir: Path | str) -> object:
33+
"""
34+
Dynamically load a compiled native Python module (.pyd or .so) from a base directory.
35+
Automatically choose the correct file extension based on the platform and Python version.
36+
37+
The module name must end with '_lib' or '_dynload'.
38+
39+
Example:
40+
module_name='nastran_import_lib' on Python 3.11 → loads 'nastran_import_lib_311.so' on Linux
41+
42+
Parameters
43+
----------
44+
module_name: str
45+
The name of the module to load (e.g. 'nastran_import').
46+
47+
base_dir: Path, str
48+
Path to the directory containing the compiled module.
49+
50+
Returns
51+
-------
52+
The loaded module object.
53+
54+
Raises
55+
------
56+
FileNotFoundError: If the compiled module file is not found.
57+
ImportError: If the module cannot be imported.
58+
"""
59+
base_path = Path(base_dir)
60+
61+
# Validate the module name
62+
if not module_name.endswith(("_lib", "_dynload")):
63+
raise ValueError("Module name must end with '_lib' or '_dynload'.")
64+
65+
# Get Python version as suffix (e.g., '311' for Python 3.11)
66+
version_suffix = f"{sys.version_info.major}{sys.version_info.minor}"
67+
68+
if version_suffix not in ["310", "311", "312", "313"]:
69+
raise ValueError(
70+
f"Unsupported Python version for {module_name}: {sys.version_info.major}.{sys.version_info.minor} \n"
71+
f"Supported versions are 3.10, 3.11, 3.12, and 3.13."
72+
)
73+
74+
# Determine the platform-specific extension
75+
ext = ".pyd" if is_windows else ".so"
76+
77+
# Construct the full module path
78+
file_name = f"{module_name}_{version_suffix}{ext}"
79+
module_path = base_path / file_name
80+
81+
if not module_path.is_file():
82+
raise FileNotFoundError(f"Module file not found at: {module_path}")
83+
84+
spec = importlib.util.spec_from_file_location(module_name, str(module_path))
85+
if spec is None or spec.loader is None:
86+
raise ImportError(f"Failed to create import spec for {module_path}")
87+
88+
mod = importlib.util.module_from_spec(spec)
89+
sys.modules[module_name] = mod
90+
spec.loader.exec_module(mod)
91+
92+
return mod

0 commit comments

Comments
 (0)