Skip to content

Commit

Permalink
FEAT: added options to decimate and to group when importing nastran f…
Browse files Browse the repository at this point in the history
…ile. (#4698)

Co-authored-by: maxcapodi78 <Shark78>
Co-authored-by: Kathy Pippert <[email protected]>
  • Loading branch information
maxcapodi78 and PipKat authored May 22, 2024
1 parent 34c5e21 commit fabce85
Show file tree
Hide file tree
Showing 8 changed files with 325 additions and 59 deletions.
Binary file added _unittest/example_models/T20/sphere.stl
Binary file not shown.
7 changes: 6 additions & 1 deletion _unittest/test_20_HFSS.py
Original file line number Diff line number Diff line change
Expand Up @@ -1403,7 +1403,12 @@ def test_59_test_nastran(self):

cads = self.aedtapp.modeler.import_nastran(example_project)
assert len(cads) > 0
assert self.aedtapp.modeler.import_nastran(example_project2)
assert self.aedtapp.modeler.import_nastran(example_project2, decimation=0.5)
example_project = os.path.join(local_path, "../_unittest/example_models", test_subfolder, "sphere.stl")
from pyaedt.modules.solutions import simplify_stl

out = simplify_stl(example_project, decimation=0.8, aggressiveness=5)
assert os.path.exists(out)

def test_60_set_variable(self):
self.aedtapp.variable_manager.set_variable("var_test", expression="123")
Expand Down
24 changes: 23 additions & 1 deletion pyaedt/modeler/cad/Primitives.py
Original file line number Diff line number Diff line change
Expand Up @@ -4612,6 +4612,11 @@ def import_3d_cad(
separate_disjoints_lumped_object=False,
import_free_surfaces=False,
point_coicidence_tolerance=1e-6,
heal_stl=True,
reduce_stl=False,
reduce_percentage=0,
reduce_error=0,
merge_planar_faces=True,
):
"""Import a CAD model.
Expand Down Expand Up @@ -4643,6 +4648,16 @@ def import_3d_cad(
Either to import free surfaces parts. The default is ``False``.
point_coicidence_tolerance : float, optional
Tolerance on point. Default is ``1e-6``.
heal_stl : bool, optional
Whether to heal the stl file on import or not. Default is ``True``.
reduce_stl : bool, optional
Whether to reduce the stl file on import or not. Default is ``True``.
reduce_percentage : int, optional
Stl reduce percentage. Default is ``0``.
reduce_error : int, optional
Stl error percentage during reduce operation. Default is ``0``.
merge_planar_faces : bool, optional
Stl automatic planar face merge during import. Default is ``True``.
Returns
-------
Expand All @@ -4668,7 +4683,14 @@ def import_3d_cad(
vArg1.append("GroupByAssembly:="), vArg1.append(group_by_assembly)
vArg1.append("CreateGroup:="), vArg1.append(create_group)
vArg1.append("STLFileUnit:="), vArg1.append("Auto")
vArg1.append("MergeFacesAngle:="), vArg1.append(-1)
vArg1.append("MergeFacesAngle:="), vArg1.append(
0.02 if input_file.endswith(".stl") and merge_planar_faces else -1
)
if input_file.endswith(".stl"):
vArg1.append("HealSTL:="), vArg1.append(heal_stl)
vArg1.append("ReduceSTL:="), vArg1.append(reduce_stl)
vArg1.append("ReduceMaxError:="), vArg1.append(reduce_error)
vArg1.append("ReducePercentage:="), vArg1.append(reduce_percentage)
vArg1.append("PointCoincidenceTol:="), vArg1.append(point_coicidence_tolerance)
vArg1.append("CreateLightweightPart:="), vArg1.append(create_lightweigth_part)
vArg1.append("ImportMaterialNames:="), vArg1.append(import_materials)
Expand Down
194 changes: 150 additions & 44 deletions pyaedt/modeler/modeler3d.py
Original file line number Diff line number Diff line change
Expand Up @@ -877,7 +877,15 @@ def objects_in_bounding_box(self, bounding_box, check_solids=True, check_lines=T
return objects

@pyaedt_function_handler()
def import_nastran(self, file_path, import_lines=True, lines_thickness=0, import_as_light_weight=False, **kwargs):
def import_nastran(
self,
file_path,
import_lines=True,
lines_thickness=0,
import_as_light_weight=False,
decimation=0,
group_parts=True,
):
"""Import Nastran file into 3D Modeler by converting the faces to stl and reading it. The solids are
translated directly to AEDT format.
Expand All @@ -892,15 +900,26 @@ def import_nastran(self, file_path, import_lines=True, lines_thickness=0, import
Every line will be parametrized with a design variable called ``xsection_linename``.
import_as_light_weight : bool, optional
Import the stl generatated as light weight. It works only on SBR+ and HFSS Regions. Default is ``False``.
decimation : float, optional
Fraction of the original mesh to remove before creating the stl file. If set to ``0.9``,
this function tries to reduce the data set to 10% of its
original size and removes 90% of the input triangles.
group_parts : bool, optional
Whether to group imported parts by object ID. The default is ``True``.
Returns
-------
List of :class:`pyaedt.modeler.Object3d.Object3d`
"""
autosave = (
True if self._app.odesktop.GetRegistryInt("Desktop/Settings/ProjectOptions/DoAutoSave") == 1 else False
)
self._app.odesktop.EnableAutoSave(False)

def _write_solid_stl(triangle, nas_to_dict):
def _write_solid_stl(triangle, pp):
try:
points = [nas_to_dict["Points"][id] for id in triangle]
# points = [nas_to_dict["Points"][id] for id in triangle]
points = [pp[i] for i in triangle]
except KeyError:
return
fc = GeometryOperators.get_polygon_centroid(points)
Expand All @@ -927,14 +946,13 @@ def _write_solid_stl(triangle, nas_to_dict):
f.write(" endloop\n")
f.write(" endfacet\n")

nas_to_dict = {"Points": {}, "PointsId": {}, "Triangles": {}, "Lines": {}, "Solids": {}}
nas_to_dict = {"Points": [], "PointsId": {}, "Triangles": {}, "Lines": {}, "Solids": {}}

self.logger.reset_timer()
self.logger.info("Loading file")
el_ids = []
pid = 0
with open_file(file_path, "r") as f:
lines = f.read().splitlines()
id = 0
for lk in range(len(lines)):
line = lines[lk]
line_type = line[:8].strip()
Expand All @@ -956,11 +974,15 @@ def _write_solid_stl(triangle, nas_to_dict):
if "-" in n3[1:] and "e" not in n3[1:].lower():
n3 = n3[0] + n3[1:].replace("-", "e-")
if line_type == "GRID":
nas_to_dict["Points"][grid_id] = [float(n1), float(n2), float(n3)]
nas_to_dict["PointsId"][grid_id] = grid_id
id += 1
nas_to_dict["PointsId"][grid_id] = pid
nas_to_dict["Points"].append([float(n1), float(n2), float(n3)])
pid += 1
else:
tri = [int(n1), int(n2), int(n3)]
tri = [
nas_to_dict["PointsId"][int(n1)],
nas_to_dict["PointsId"][int(n2)],
nas_to_dict["PointsId"][int(n3)],
]
nas_to_dict["Triangles"][tria_id].append(tri)

elif line_type in ["GRID*", "CTRIA3*"]:
Expand All @@ -984,13 +1006,17 @@ def _write_solid_stl(triangle, nas_to_dict):
n3 = n3[0] + n3[1:].replace("-", "e-")
if line_type == "GRID*":
try:
nas_to_dict["Points"][grid_id] = [float(n1), float(n2), float(n3)]
nas_to_dict["Points"].append([float(n1), float(n2), float(n3)])
except Exception: # nosec
continue
nas_to_dict["PointsId"][grid_id] = id
id += 1
nas_to_dict["PointsId"][grid_id] = pid
pid += 1
else:
tri = (int(n1), int(n2), int(n3))
tri = [
nas_to_dict["PointsId"][int(n1)],
nas_to_dict["PointsId"][int(n2)],
nas_to_dict["PointsId"][int(n3)],
]
nas_to_dict["Triangles"][tria_id].append(tri)

elif line_type in [
Expand All @@ -1014,7 +1040,12 @@ def _write_solid_stl(triangle, nas_to_dict):
from itertools import combinations

for k in list(combinations(n, 3)):
tri = [int(k[0]), int(k[1]), int(k[2])]
# tri = [int(k[0]), int(k[1]), int(k[2])]
tri = [
nas_to_dict["PointsId"][int(k[0])],
nas_to_dict["PointsId"][int(k[1])],
nas_to_dict["PointsId"][int(k[2])],
]
tri.sort()
tri = tuple(tri)
nas_to_dict["Solids"][el_id].append(tri)
Expand All @@ -1040,20 +1071,33 @@ def _write_solid_stl(triangle, nas_to_dict):

if line_type == "CTETRA*":
for k in list(combinations(n, 3)):
tri = [int(k[0]), int(k[1]), int(k[2])]
# tri = [int(k[0]), int(k[1]), int(k[2])]
tri = [
nas_to_dict["PointsId"][int(k[0])],
nas_to_dict["PointsId"][int(k[1])],
nas_to_dict["PointsId"][int(k[2])],
]
tri.sort()
tri = tuple(tri)
nas_to_dict["Solids"][el_id].append(tri)
else:
spli1 = [n[0], n[1], n[2], n[4]]
for k in list(combinations(spli1, 3)):
tri = [int(k[0]), int(k[1]), int(k[2])]
tri = [
nas_to_dict["PointsId"][int(k[0])],
nas_to_dict["PointsId"][int(k[1])],
nas_to_dict["PointsId"][int(k[2])],
]
tri.sort()
tri = tuple(tri)
nas_to_dict["Solids"][el_id].append(tri)
spli1 = [n[0], n[2], n[3], n[4]]
for k in list(combinations(spli1, 3)):
tri = [int(k[0]), int(k[1]), int(k[2])]
tri = [
nas_to_dict["PointsId"][int(k[0])],
nas_to_dict["PointsId"][int(k[1])],
nas_to_dict["PointsId"][int(k[2])],
]
tri.sort()
tri = tuple(tri)
nas_to_dict["Solids"][el_id].append(tri)
Expand All @@ -1063,48 +1107,107 @@ def _write_solid_stl(triangle, nas_to_dict):
n1 = int(line[24:32])
n2 = int(line[32:40])
if obj_id in nas_to_dict["Lines"]:
nas_to_dict["Lines"][obj_id].append([n1, n2])
nas_to_dict["Lines"][obj_id].append(
[nas_to_dict["PointsId"][int(n1)], nas_to_dict["PointsId"][int(n2)]]
)
else:
nas_to_dict["Lines"][obj_id] = [[n1, n2]]
nas_to_dict["Lines"][obj_id] = [
[nas_to_dict["PointsId"][int(n1)], nas_to_dict["PointsId"][int(n2)]]
]

self.logger.info_timer("File loaded")
self.logger.info("File loaded")
objs_before = [i for i in self.object_names]

if nas_to_dict["Triangles"] or nas_to_dict["Solids"] or nas_to_dict["Lines"]:
self.logger.reset_timer()
self.logger.info("Creating STL file with detected faces")
f = open(os.path.join(self._app.working_directory, self._app.design_name + "_test.stl"), "w")
output_stl = ""
if nas_to_dict["Triangles"]:
output_stl = os.path.join(self._app.working_directory, self._app.design_name + "_tria.stl")
f = open(output_stl, "w")

def decimate(points_in, faces_in, points_out, faces_out):
if 0 < decimation < 1:
aggressivity = 3
if 0.7 > decimation > 0.3:
aggressivity = 5
elif decimation >= 0.7:
aggressivity = 7
points_out, faces_out = fast_simplification.simplify(
points_in, faces_in, decimation, agg=aggressivity
)

return points_out, faces_out

for tri_id, triangles in nas_to_dict["Triangles"].items():
tri_out = triangles
p_out = nas_to_dict["Points"][::]
if decimation > 0 and len(triangles) > 20:
try:
import fast_simplification

p_out, tri_out = decimate(nas_to_dict["Points"], tri_out, p_out, tri_out)
except Exception:
self.logger.error("Package fast-decimation is needed to perform model simplification.")
self.logger.error("Please install it using pip.")
f.write("solid Sheet_{}\n".format(tri_id))
for triangle in triangles:
_write_solid_stl(triangle, nas_to_dict)

for triangle in tri_out:
_write_solid_stl(triangle, p_out)
f.write("endsolid\n")
if nas_to_dict["Triangles"]:
f.close()
output_solid = ""
if nas_to_dict["Solids"]:
output_solid = os.path.join(self._app.working_directory, self._app.design_name + "_solids.stl")
f = open(output_solid, "w")
for solidid, solid_triangles in nas_to_dict["Solids"].items():
f.write("solid Solid_{}\n".format(solidid))
import pandas as pd

df = pd.Series(solid_triangles)
undulicated_values = df.drop_duplicates(keep=False).to_list()
for triangle in undulicated_values:
_write_solid_stl(triangle, nas_to_dict)
tri_out = df.drop_duplicates(keep=False).to_list()
p_out = nas_to_dict["Points"][::]
if decimation > 0 and len(solid_triangles) > 20:
try:
import fast_simplification

p_out, tri_out = decimate(nas_to_dict["Points"], tri_out, p_out, tri_out)
except Exception:
self.logger.error("Package fast-decimation is needed to perform model simplification.")
self.logger.error("Please install it using pip.")
for triangle in tri_out:
_write_solid_stl(triangle, p_out)
f.write("endsolid\n")
f.close()
self.logger.info_timer("STL file created")
if output_solid:
f.close()
self.logger.info("STL file created")
self._app.odesktop.CloseAllWindows()
self.logger.reset_timer()
self.logger.info("Importing STL in 3D Modeler")
self.import_3d_cad(
os.path.join(self._app.working_directory, self._app.design_name + "_test.stl"),
create_lightweigth_part=import_as_light_weight,
)
for el in nas_to_dict["Solids"].keys():
obj_names = [i for i in self.object_names if i.startswith("Solid_{}".format(el))]
self.create_group(obj_names, group_name=str(el))
for el in nas_to_dict["Triangles"].keys():
obj_names = [i for i in self.object_names if i.startswith("Sheet_{}".format(el))]
self.create_group(obj_names, group_name=str(el))
self.logger.info_timer("Model imported")

if import_lines:
if output_stl:
self.import_3d_cad(
output_stl,
create_lightweigth_part=import_as_light_weight,
healing=False,
)
if output_solid:
self.import_3d_cad(
output_solid,
create_lightweigth_part=import_as_light_weight,
healing=False,
)
self.logger.info("Model imported")

if group_parts:
for el in nas_to_dict["Solids"].keys():
obj_names = [i for i in self.object_names if i.startswith("Solid_{}".format(el))]
self.create_group(obj_names, group_name=str(el))
objs = self.object_names[::]
for el in nas_to_dict["Triangles"].keys():
obj_names = [i for i in objs if i == "Sheet_{}".format(el) or i.startswith("Sheet_{}_".format(el))]
self.create_group(obj_names, group_name=str(el))
self.logger.info("Parts grouped")

if import_lines and nas_to_dict["Lines"]:
for line_name, lines in nas_to_dict["Lines"].items():
if lines_thickness:
self._app["x_section_{}".format(line_name)] = lines_thickness
Expand Down Expand Up @@ -1133,10 +1236,13 @@ def _write_solid_stl(triangle, nas_to_dict):
out_poly = self.unite(polys, purge=not lines_thickness)
if not lines_thickness and out_poly:
self.generate_object_history(out_poly)
self.logger.info("Lines imported")

objs_after = [i for i in self.object_names]
new_objects = [self[i] for i in objs_after if i not in objs_before]
self._app.oproject.SetActiveDesign(self._app.design_name)
self._app.odesktop.EnableAutoSave(autosave)
self.logger.info_timer("Nastran model correctly imported.")
return new_objects

@pyaedt_function_handler()
Expand Down
Loading

0 comments on commit fabce85

Please sign in to comment.