Skip to content

REFACTOR: Nastran import refactoring #6236

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 39 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
e2f195a
modified the misc.py to separate the import_nastran.py
Alberto-DM Jun 4, 2025
4bf5687
changed function name simplify_stl to simplify_and_preview_stl
Alberto-DM Jun 4, 2025
5d92f4a
working import of the pyd
Alberto-DM Jun 4, 2025
35ca005
created the proper folder structure
Alberto-DM Jun 5, 2025
38c74a6
recompiled library
Alberto-DM Jun 5, 2025
0ca818d
changed import of nastran_to_stl
Alberto-DM Jun 5, 2025
65d01e6
added initialize_helpers
Alberto-DM Jun 5, 2025
27dbc6a
Merge branch 'main' into nastran_import_refactoring
Alberto-DM Jun 5, 2025
b252738
chore: adding changelog file 6236.miscellaneous.md [dependabot-skip]
pyansys-ci-bot Jun 5, 2025
88e0d5f
adding new settings to ALLOWED_GENERAL_SETTINGS
Alberto-DM Jun 6, 2025
bbe2646
Merge remote-tracking branch 'origin/nastran_import_refactoring' into…
Alberto-DM Jun 6, 2025
da45451
Merge branch 'main' into nastran_import_refactoring
Alberto-DM Jun 6, 2025
86050dc
added control for the input file existence.
Alberto-DM Jun 6, 2025
f85dd68
testing setuptools option
Alberto-DM Jun 6, 2025
35b52f8
testing setuptools option
Alberto-DM Jun 6, 2025
533b9a2
testing setuptools option
Alberto-DM Jun 6, 2025
69858b5
testing setuptools option
Alberto-DM Jun 6, 2025
aafbaae
testing setuptools option
Alberto-DM Jun 6, 2025
113ebfb
testing setuptools option
Alberto-DM Jun 6, 2025
11dde73
testing setuptools option
Alberto-DM Jun 6, 2025
be11264
testing setuptools option
Alberto-DM Jun 6, 2025
aa78057
testing setuptools option
Alberto-DM Jun 6, 2025
ff08d9b
final setuptools configuration
Alberto-DM Jun 6, 2025
c3b9c54
Merge branch 'main' into nastran_import_refactoring
Alberto-DM Jun 6, 2025
12e5093
added python versions for linux
Alberto-DM Jun 10, 2025
6b5f57b
fixed version check
Alberto-DM Jun 10, 2025
b87087c
correctly compiled libraries
Alberto-DM Jun 11, 2025
f7f7636
Merge branch 'main' into nastran_import_refactoring
Alberto-DM Jun 11, 2025
c9323ea
added windows libraries for all python versions
Alberto-DM Jun 13, 2025
1d32a14
updated extension
Alberto-DM Jun 13, 2025
b852084
fixed label
Alberto-DM Jun 13, 2025
5e9c74b
fixed docstring
Alberto-DM Jun 13, 2025
c45d9a2
updated import_nastran extension tests
Alberto-DM Jun 13, 2025
4074ee1
fixed bug in the UI
Alberto-DM Jun 13, 2025
c372c4d
Merge branch 'main' into nastran_import_refactoring
Alberto-DM Jun 13, 2025
5c3b982
fixed write_yaml_configuration to support Path objects. Fixed typo in…
Alberto-DM Jun 16, 2025
db041a8
fixed unit test
Alberto-DM Jun 16, 2025
41241e8
remove commented code
Alberto-DM Jun 16, 2025
fb8d62d
Merge branch 'main' into nastran_import_refactoring
Alberto-DM Jun 16, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
global-include ansys.aedt.core *.txt *.md *.toml *.json *.png *.xml *.areg *.joblib *.acf *.m *.ipynb *.py_build
include src/ansys/aedt/core/syslib/**/*.so
include src/ansys/aedt/core/syslib/**/*.pyd
include src/ansys/aedt/core/syslib/**/*.license
1 change: 1 addition & 0 deletions doc/changelog.d/6236.miscellaneous.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Nastran import refactoring
6 changes: 6 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,12 @@ examples = [
"scikit-rf>=0.30.0,<1.8",
]

[tool.setuptools]
include-package-data = true

[tool.setuptools.package-data]
"ansys.aedt.core.syslib" = ["**/*.so", "**/*.pyd", "**/*.license"]

[tool.setuptools.dynamic]
version = {attr = "ansys.aedt.core.__version__"}

Expand Down
39 changes: 32 additions & 7 deletions src/ansys/aedt/core/extensions/project/import_nastran.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
from ansys.aedt.core.extensions.misc import get_port
from ansys.aedt.core.extensions.misc import get_process_id
from ansys.aedt.core.extensions.misc import is_student
from ansys.aedt.core.visualization.advanced.misc import nastran_to_stl
from ansys.aedt.core.syslib.nastran_import import nastran_to_stl


@dataclass
Expand All @@ -45,14 +45,21 @@
lightweight: bool = False
planar: bool = True
file_path: str = ""
remove_multiple_connections: bool = False


PORT = get_port()
VERSION = get_aedt_version()
AEDT_PROCESS_ID = get_process_id()
IS_STUDENT = is_student()
EXTENSION_TITLE = "Import Nastran or STL file"
EXTENSION_DEFAULT_ARGUMENTS = {"decimate": 0.0, "lightweight": False, "planar": True, "file_path": ""}
EXTENSION_DEFAULT_ARGUMENTS = {
"decimate": 0.0,
"lightweight": False,
"planar": True,
"file_path": "",
"remove_multiple_connections": False,
}

result = None

Expand All @@ -78,6 +85,7 @@
title="Select a Nastran or stl File",
filetypes=(("Nastran", "*.nas"), ("STL", "*.stl"), ("all files", "*.*")),
)
text.delete("1.0", tkinter.END)
text.insert(tkinter.END, filename)

b1 = ttk.Button(root, text="...", width=10, command=browseFiles, style="PyAEDT.TButton", name="browse_button")
Expand All @@ -103,6 +111,17 @@
check3 = ttk.Checkbutton(root, variable=planar, style="PyAEDT.TCheckbutton", name="check_planar_merge")
check3.grid(row=3, column=1, pady=10, padx=5)

label = ttk.Label(root, text="Remove multiple connections:", style="PyAEDT.TLabel")
label.grid(row=4, column=0, pady=10)
remove_multiple_connections = tkinter.IntVar(root, value=0)
check4 = ttk.Checkbutton(
root,
variable=remove_multiple_connections,
style="PyAEDT.TCheckbutton",
name="check_remove_multiple_connections",
)
check4.grid(row=4, column=1, pady=10, padx=5)

def toggle_theme():
if root.theme == "light":
set_dark_theme()
Expand Down Expand Up @@ -145,6 +164,7 @@
lightweight=True if light.get() == 1 else False,
planar=True if planar.get() == 1 else False,
file_path=text.get("1.0", tkinter.END).strip(),
remove_multiple_connections=True if remove_multiple_connections.get() == 1 else False,
)
root.destroy()

Expand All @@ -160,9 +180,9 @@
if file_path_ui.endswith(".nas"):
nastran_to_stl(file_path_ui, decimation=decimate_ui, preview=True)
else:
from ansys.aedt.core.visualization.advanced.misc import simplify_stl
from ansys.aedt.core.visualization.advanced.misc import simplify_and_preview_stl

Check warning on line 183 in src/ansys/aedt/core/extensions/project/import_nastran.py

View check run for this annotation

Codecov / codecov/patch

src/ansys/aedt/core/extensions/project/import_nastran.py#L183

Added line #L183 was not covered by tests

simplify_stl(file_path_ui, decimation=decimate_ui, preview=True)
simplify_and_preview_stl(file_path_ui, decimation=decimate_ui, preview=True)

Check warning on line 185 in src/ansys/aedt/core/extensions/project/import_nastran.py

View check run for this annotation

Codecov / codecov/patch

src/ansys/aedt/core/extensions/project/import_nastran.py#L185

Added line #L185 was not covered by tests

b2 = ttk.Button(root, text="Preview", width=40, command=preview, style="PyAEDT.TButton", name="preview_button")
b2.grid(row=5, column=0, pady=10, padx=10)
Expand All @@ -178,6 +198,7 @@
lightweight = extension_args["lightweight"]
decimate = extension_args["decimate"]
planar = extension_args["planar"]
remove_multiple_connections = extension_args["remove_multiple_connections"]

if file_path.is_file():
app = ansys.aedt.core.Desktop(
Expand All @@ -198,12 +219,16 @@

if file_path.suffix == ".nas":
aedtapp.modeler.import_nastran(
str(file_path), import_as_light_weight=lightweight, decimation=decimate, enable_planar_merge=str(planar)
str(file_path),
import_as_light_weight=lightweight,
decimation=decimate,
enable_planar_merge=str(planar),
remove_multiple_connections=remove_multiple_connections,
)
else:
from ansys.aedt.core.visualization.advanced.misc import simplify_stl
from ansys.aedt.core.visualization.advanced.misc import simplify_and_preview_stl

outfile = simplify_stl(str(file_path), decimation=decimate)
outfile = simplify_and_preview_stl(str(file_path), decimation=decimate)
aedtapp.modeler.import_3d_cad(
outfile, healing=False, create_lightweigth_part=lightweight, merge_planar_faces=planar
)
Expand Down
8 changes: 6 additions & 2 deletions src/ansys/aedt/core/generic/general_methods.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import itertools
import logging
import os
import platform
import re
import sys
import time
Expand All @@ -47,8 +48,11 @@
from ansys.aedt.core.internal.errors import GrpcApiError
from ansys.aedt.core.internal.errors import MethodNotSupportedError

is_linux = os.name == "posix"
is_windows = not is_linux
system = platform.system()
is_linux = system == "Linux"
is_windows = system == "Windows"
is_macos = system == "Darwin"

inside_desktop = True if "4.0.30319.42000" in sys.version else False

inclusion_list = [
Expand Down
54 changes: 49 additions & 5 deletions src/ansys/aedt/core/generic/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,16 @@

import logging
import os
from pathlib import Path
import time
from typing import Any
from typing import List
from typing import Optional
from typing import Union
import uuid

from ansys.aedt.core import pyaedt_path

is_linux = os.name == "posix"

# Settings allowed to be updated using a YAML configuration file.
Expand Down Expand Up @@ -102,6 +105,8 @@
"remote_rpc_session_temp_folder",
"block_figure_plot",
"skip_license_check",
"pyd_libraries_path",
"pyd_libraries_user_path",
]
ALLOWED_AEDT_ENV_VAR_SETTINGS = [
"ANSYSEM_FEATURE_F335896_MECHANICAL_STRUCTURAL_SOLN_TYPE_ENABLE",
Expand Down Expand Up @@ -172,7 +177,7 @@
self.__lsf_queue: Optional[str] = None
self.__custom_lsf_command: Optional[str] = None
# Settings related to environment variables that are set before launching a new AEDT session
# This includes those that enable the beta features !
# This includes those that enable the beta features!
self.__aedt_environment_variables: dict[str, str] = {
"ANSYSEM_FEATURE_SF6694_NON_GRAPHICAL_COMMAND_EXECUTION_ENABLE": "1",
"ANSYSEM_FEATURE_SF159726_SCRIPTOBJECT_ENABLE": "1",
Expand Down Expand Up @@ -214,6 +219,8 @@
self.__time_tick = time.time()
self.__pyaedt_server_path = ""
self.__block_figure_plot = False
self.__pyd_libraries_path: Path = Path(pyaedt_path) / "syslib"
self.__pyd_libraries_user_path: Optional[str] = None

# Load local settings if YAML configuration file exists.
pyaedt_settings_path = os.environ.get("PYAEDT_LOCAL_SETTINGS_PATH", "")
Expand Down Expand Up @@ -785,6 +792,36 @@
def skip_license_check(self, value):
self.__skip_license_check = value

@property
def pyd_libraries_path(self):
if self.__pyd_libraries_user_path is not None:
# If the user path is set, return it
return Path(self.__pyd_libraries_user_path)

Check warning on line 799 in src/ansys/aedt/core/generic/settings.py

View check run for this annotation

Codecov / codecov/patch

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

Added line #L799 was not covered by tests
return Path(self.__pyd_libraries_path)

@property
def pyd_libraries_user_path(self):
# Get the user path for PyAEDT libraries.
if self.__pyd_libraries_user_path is not None:
return Path(self.__pyd_libraries_user_path)

Check warning on line 806 in src/ansys/aedt/core/generic/settings.py

View check run for this annotation

Codecov / codecov/patch

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

Added line #L806 was not covered by tests
return None

@pyd_libraries_user_path.setter
def pyd_libraries_user_path(self, val):
if val is None:

Check warning on line 811 in src/ansys/aedt/core/generic/settings.py

View check run for this annotation

Codecov / codecov/patch

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

Added line #L811 was not covered by tests
# If the user path is None, set it to None
self.__pyd_libraries_user_path = None

Check warning on line 813 in src/ansys/aedt/core/generic/settings.py

View check run for this annotation

Codecov / codecov/patch

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

Added line #L813 was not covered by tests
else:
lib_path = Path(str(val))
if not lib_path.exists():

Check warning on line 816 in src/ansys/aedt/core/generic/settings.py

View check run for this annotation

Codecov / codecov/patch

src/ansys/aedt/core/generic/settings.py#L815-L816

Added lines #L815 - L816 were not covered by tests
# If the user path does not exist, return None
raise ValueError("The user path for PyAEDT libraries does not exist. Please set a valid path.")

Check warning on line 818 in src/ansys/aedt/core/generic/settings.py

View check run for this annotation

Codecov / codecov/patch

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

Added line #L818 was not covered by tests
else:
# If the user path exists, set it as a Path object
self.__pyd_libraries_user_path = lib_path

Check warning on line 821 in src/ansys/aedt/core/generic/settings.py

View check run for this annotation

Codecov / codecov/patch

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

Added line #L821 was not covered by tests

# yaml setting file IO methods

def load_yaml_configuration(self, path: str, raise_on_wrong_key: bool = False):
"""Update default settings from a YAML configuration file."""
import yaml
Expand Down Expand Up @@ -825,15 +862,22 @@
raise KeyError("An environment variable key is not part of the allowed keys.")
self.aedt_environment_variables = settings

def writte_yaml_configuration(self, path: str):
def write_yaml_configuration(self, path: str):
"""Write the current settings into a YAML configuration file."""
import yaml

data = {}
data["log"] = {key: getattr(self, key) for key in ALLOWED_LOG_SETTINGS}
data["lsf"] = {key: getattr(self, key) for key in ALLOWED_LSF_SETTINGS}
data["log"] = {
key: str(value) if isinstance(value := getattr(self, key), Path) else value for key in ALLOWED_LOG_SETTINGS
}
data["lsf"] = {
key: str(value) if isinstance(value := getattr(self, key), Path) else value for key in ALLOWED_LSF_SETTINGS
}
data["aedt_env_var"] = getattr(self, "aedt_environment_variables")
data["general"] = {key: getattr(self, key) for key in ALLOWED_GENERAL_SETTINGS}
data["general"] = {
key: str(value) if isinstance(value := getattr(self, key), Path) else value
for key in ALLOWED_GENERAL_SETTINGS
}

with open(path, "w") as file:
yaml.safe_dump(data, file, sort_keys=False)
Expand Down
6 changes: 5 additions & 1 deletion src/ansys/aedt/core/modeler/modeler_3d.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
from ansys.aedt.core.internal.errors import GrpcApiError
from ansys.aedt.core.modeler.cad.primitives_3d import Primitives3D
from ansys.aedt.core.modeler.geometry_operators import GeometryOperators
from ansys.aedt.core.visualization.advanced.misc import nastran_to_stl
from ansys.aedt.core.syslib.nastran_import import nastran_to_stl


class Modeler3D(Primitives3D):
Expand Down Expand Up @@ -1029,6 +1029,7 @@ def import_nastran(
save_only_stl=False,
preview=False,
merge_angle=1e-3,
remove_multiple_connections=False,
):
"""Import Nastran file into 3D Modeler by converting the faces to stl and reading it.

Expand Down Expand Up @@ -1060,6 +1061,8 @@ def import_nastran(
Whether to preview the model in pyvista or skip it.
merge_angle : float, optional
Angle in radians for which faces will be considered planar. Default is ``1e-3``.
remove_multiple_connections : bool, optional
Whether to remove multiple connections in the mesh. Default is ``False``.

Returns
-------
Expand All @@ -1078,6 +1081,7 @@ def import_nastran(
output_folder=self._app.working_directory,
enable_planar_merge=enable_planar_merge,
preview=preview,
remove_multiple_connections=remove_multiple_connections,
)
if save_only_stl:
return output_stls, nas_to_dict
Expand Down
92 changes: 92 additions & 0 deletions src/ansys/aedt/core/syslib/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2021 - 2025 ANSYS, Inc. and/or its affiliates.
# SPDX-License-Identifier: MIT
#
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

import importlib.util
from pathlib import Path
import sys

from ansys.aedt.core.generic.general_methods import is_windows


def load_native_module(module_name: str, base_dir: Path | str) -> object:
"""
Dynamically load a compiled native Python module (.pyd or .so) from a base directory.
Automatically choose the correct file extension based on the platform and Python version.

The module name must end with '_lib' or '_dynload'.

Example:
module_name='nastran_import_lib' on Python 3.11 → loads 'nastran_import_lib_311.so' on Linux

Parameters
----------
module_name: str
The name of the module to load (e.g. 'nastran_import').

base_dir: Path, str
Path to the directory containing the compiled module.

Returns
-------
The loaded module object.

Raises
------
FileNotFoundError: If the compiled module file is not found.
ImportError: If the module cannot be imported.
"""
base_path = Path(base_dir)

# Validate the module name
if not module_name.endswith(("_lib", "_dynload")):
raise ValueError("Module name must end with '_lib' or '_dynload'.")

Check warning on line 63 in src/ansys/aedt/core/syslib/__init__.py

View check run for this annotation

Codecov / codecov/patch

src/ansys/aedt/core/syslib/__init__.py#L63

Added line #L63 was not covered by tests

# Get Python version as suffix (e.g., '311' for Python 3.11)
version_suffix = f"{sys.version_info.major}{sys.version_info.minor}"

if version_suffix not in ["310", "311", "312", "313"]:
raise ValueError(

Check warning on line 69 in src/ansys/aedt/core/syslib/__init__.py

View check run for this annotation

Codecov / codecov/patch

src/ansys/aedt/core/syslib/__init__.py#L69

Added line #L69 was not covered by tests
f"Unsupported Python version for {module_name}: {sys.version_info.major}.{sys.version_info.minor} \n"
f"Supported versions are 3.10, 3.11, 3.12, and 3.13."
)

# Determine the platform-specific extension
ext = ".pyd" if is_windows else ".so"

# Construct the full module path
file_name = f"{module_name}_{version_suffix}{ext}"
module_path = base_path / file_name

if not module_path.is_file():
raise FileNotFoundError(f"Module file not found at: {module_path}")

Check warning on line 82 in src/ansys/aedt/core/syslib/__init__.py

View check run for this annotation

Codecov / codecov/patch

src/ansys/aedt/core/syslib/__init__.py#L82

Added line #L82 was not covered by tests

spec = importlib.util.spec_from_file_location(module_name, str(module_path))
if spec is None or spec.loader is None:
raise ImportError(f"Failed to create import spec for {module_path}")

Check warning on line 86 in src/ansys/aedt/core/syslib/__init__.py

View check run for this annotation

Codecov / codecov/patch

src/ansys/aedt/core/syslib/__init__.py#L86

Added line #L86 was not covered by tests

mod = importlib.util.module_from_spec(spec)
sys.modules[module_name] = mod
spec.loader.exec_module(mod)

return mod
Loading