diff --git a/_unittest/test_20_HFSS.py b/_unittest/test_20_HFSS.py index ae8d68895f2..40e06ceb5c3 100644 --- a/_unittest/test_20_HFSS.py +++ b/_unittest/test_20_HFSS.py @@ -1403,12 +1403,16 @@ def test_59_test_nastran(self): cads = self.aedtapp.modeler.import_nastran(example_project) assert len(cads) > 0 + stl = self.aedtapp.modeler.import_nastran(example_project, decimation=0.3, preview=True, save_only_stl=True) + assert os.path.exists(stl) 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) + out = simplify_stl(example_project, decimation=0.8) assert os.path.exists(out) + out = simplify_stl(example_project, decimation=0.8, preview=True) + assert out def test_60_set_variable(self): self.aedtapp.variable_manager.set_variable("var_test", expression="123") diff --git a/pyaedt/modeler/modeler3d.py b/pyaedt/modeler/modeler3d.py index 2d94ff78274..59698477647 100644 --- a/pyaedt/modeler/modeler3d.py +++ b/pyaedt/modeler/modeler3d.py @@ -877,78 +877,7 @@ 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, - decimation=0, - group_parts=True, - enable_planar_merge="True", - ): - """Import Nastran file into 3D Modeler by converting the faces to stl and reading it. The solids are - translated directly to AEDT format. - - Parameters - ---------- - file_path : str - Path to .nas file. - import_lines : bool, optional - Whether to import the lines or only triangles. Default is ``True``. - lines_thickness : float, optional - Whether to thicken lines after creation and it's default value. - 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``. - enable_planar_merge : str, optional - Whether to enable or not planar merge. It can be ``"True"``, ``"False"`` or ``"Auto"``. - ``"Auto"`` will disable the planar merge if stl contains more than 50000 triangles. - - 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, pp): - try: - # 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) - v1 = points[0] - v2 = points[1] - cv1 = GeometryOperators.v_points(fc, v1) - cv2 = GeometryOperators.v_points(fc, v2) - if cv2[0] == cv1[0] == 0.0 and cv2[1] == cv1[1] == 0.0: - n = [0, 0, 1] - elif cv2[0] == cv1[0] == 0.0 and cv2[2] == cv1[2] == 0.0: - n = [0, 1, 0] - elif cv2[1] == cv1[1] == 0.0 and cv2[2] == cv1[2] == 0.0: - n = [1, 0, 0] - else: - n = GeometryOperators.v_cross(cv1, cv2) - - normal = GeometryOperators.normalize_vector(n) - if normal: - f.write(" facet normal {} {} {}\n".format(normal[0], normal[1], normal[2])) - f.write(" outer loop\n") - f.write(" vertex {} {} {}\n".format(points[0][0], points[0][1], points[0][2])) - f.write(" vertex {} {} {}\n".format(points[1][0], points[1][1], points[1][2])) - f.write(" vertex {} {} {}\n".format(points[2][0], points[2][1], points[2][2])) - f.write(" endloop\n") - f.write(" endfacet\n") + def _parse_nastran(self, file_path): nas_to_dict = {"Points": [], "PointsId": {}, "Triangles": {}, "Lines": {}, "Solids": {}} @@ -1155,103 +1084,209 @@ def _write_solid_stl(triangle, pp): ] self.logger.info("File loaded") - objs_before = [i for i in self.object_names] + return nas_to_dict - if nas_to_dict["Triangles"] or nas_to_dict["Solids"] or nas_to_dict["Lines"]: - self.logger.info("Creating STL file with detected faces") - output_stl = "" - enable_stl_merge = False if enable_planar_merge == "False" or enable_planar_merge is False else True - 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 - ) + @pyaedt_function_handler() + def _write_stl(self, nas_to_dict, decimation, enable_planar_merge): + def _write_solid_stl(triangle, pp): + try: + # points = [nas_to_dict["Points"][id] for id in triangle] + points = [pp[i] for i in triangle] + except KeyError: # pragma: no cover + return + fc = GeometryOperators.get_polygon_centroid(points) + v1 = points[0] + v2 = points[1] + cv1 = GeometryOperators.v_points(fc, v1) + cv2 = GeometryOperators.v_points(fc, v2) + if cv2[0] == cv1[0] == 0.0 and cv2[1] == cv1[1] == 0.0: + n = [0, 0, 1] # pragma: no cover + elif cv2[0] == cv1[0] == 0.0 and cv2[2] == cv1[2] == 0.0: + n = [0, 1, 0] # pragma: no cover + elif cv2[1] == cv1[1] == 0.0 and cv2[2] == cv1[2] == 0.0: + n = [1, 0, 0] # pragma: no cover + else: + n = GeometryOperators.v_cross(cv1, cv2) + + normal = GeometryOperators.normalize_vector(n) + if normal: + f.write(" facet normal {} {} {}\n".format(normal[0], normal[1], normal[2])) + f.write(" outer loop\n") + f.write(" vertex {} {} {}\n".format(points[0][0], points[0][1], points[0][2])) + f.write(" vertex {} {} {}\n".format(points[1][0], points[1][1], points[1][2])) + f.write(" vertex {} {} {}\n".format(points[2][0], points[2][1], points[2][2])) + f.write(" endloop\n") + f.write(" endfacet\n") + + self.logger.info("Creating STL file with detected faces") + enable_stl_merge = False if enable_planar_merge == "False" or enable_planar_merge is False else True + 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, stl_id): + fin = [[3] + list(i) for i in faces_in] + mesh = pv.PolyData(points_in, faces=fin) + new_mesh = mesh.decimate_pro(decimation, preserve_topology=True, boundary_vertex_deletion=False) + points_out = list(new_mesh.points) + faces_out = [i[1:] for i in new_mesh.faces.reshape(-1, 4) if i[0] == 3] + self.logger.info( + "Final decimation on object {}: {}%".format( + stl_id, 100 * (len(faces_in) - len(faces_out)) / len(faces_in) + ) + ) + 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 pyvista as pv + + p_out, tri_out = decimate(nas_to_dict["Points"], tri_out, tri_id) + except Exception: + self.logger.error("Package pyvista is needed to perform model simplification.") + self.logger.error("Please install it using pip.") + f.write("solid Sheet_{}\n".format(tri_id)) + if enable_planar_merge == "Auto" and len(tri_out) > 50000: + enable_stl_merge = False + for triangle in tri_out: + _write_solid_stl(triangle, p_out) + f.write("endsolid\n") + + 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) + 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 pyvista as pv + + p_out, tri_out = decimate(nas_to_dict["Points"], tri_out, solidid) + except Exception: + self.logger.error("Package pyvista is needed to perform model simplification.") + self.logger.error("Please install it using pip.") + if enable_planar_merge == "Auto" and len(tri_out) > 50000: + enable_stl_merge = False + for triangle in tri_out: + _write_solid_stl(triangle, p_out) + f.write("endsolid\n") + f.close() + self.logger.info("STL file created") + return output_stl, enable_stl_merge + + @pyaedt_function_handler() + def import_nastran( + self, + file_path, + import_lines=True, + lines_thickness=0, + import_as_light_weight=False, + decimation=0, + group_parts=True, + enable_planar_merge="True", + save_only_stl=False, + preview=False, + ): + """Import Nastran file into 3D Modeler by converting the faces to stl and reading it. The solids are + translated directly to AEDT format. + + Parameters + ---------- + file_path : str + Path to .nas file. + import_lines : bool, optional + Whether to import the lines or only triangles. Default is ``True``. + lines_thickness : float, optional + Whether to thicken lines after creation and it's default value. + 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``. + enable_planar_merge : str, optional + Whether to enable or not planar merge. It can be ``"True"``, ``"False"`` or ``"Auto"``. + ``"Auto"`` will disable the planar merge if stl contains more than 50000 triangles. + save_only_stl : bool, optional + Whether to import the model in HFSS or only generate the stl file. + preview : bool, optional + Whether to preview the model in pyvista or skip it. - return points_out, faces_out + 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) - for tri_id, triangles in nas_to_dict["Triangles"].items(): + nas_to_dict = self._parse_nastran(file_path) + + objs_before = [i for i in self.object_names] + if not (nas_to_dict["Triangles"] or nas_to_dict["Solids"] or nas_to_dict["Lines"]): + self.logger.error("Failed to import file. Check the model and retry") + return False + output_stl, enable_stl_merge = self._write_stl(nas_to_dict, decimation, enable_planar_merge) + if preview: + import pyvista as pv + + pl = pv.Plotter(shape=(1, 2)) + dargs = dict(show_edges=True, color=True) + p_out = nas_to_dict["Points"][::] + for triangles in nas_to_dict["Triangles"].values(): 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)) - if enable_planar_merge == "Auto" and len(tri_out) > 50000: - enable_stl_merge = False - for triangle in tri_out: - _write_solid_stl(triangle, p_out) - f.write("endsolid\n") - if nas_to_dict["Triangles"]: - f.close() - output_solid = "" - enable_solid_merge = False if enable_planar_merge == "False" or enable_planar_merge is False else True - 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)) + fin = [[3] + list(i) for i in tri_out] + pl.add_mesh(pv.PolyData(p_out, faces=fin), **dargs) + for triangles in nas_to_dict["Solids"].values(): import pandas as pd - df = pd.Series(solid_triangles) + df = pd.Series(triangles) 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.") - if enable_planar_merge == "Auto" and len(tri_out) > 50000: - enable_solid_merge = False - for triangle in tri_out: - _write_solid_stl(triangle, p_out) - f.write("endsolid\n") - if output_solid: - f.close() - self.logger.info("STL file created") - self._app.odesktop.CloseAllWindows() - self.logger.info("Importing STL in 3D Modeler") + fin = [[3] + list(i) for i in tri_out] + pl.add_mesh(pv.PolyData(p_out, faces=fin), **dargs) + pl.add_text("Input mesh", font_size=24) + pl.reset_camera() + pl.subplot(0, 1) if output_stl: - self.import_3d_cad( - output_stl, - create_lightweigth_part=import_as_light_weight, - healing=False, - merge_planar_faces=enable_stl_merge, - ) - if output_solid: - self.import_3d_cad( - output_solid, - create_lightweigth_part=import_as_light_weight, - healing=False, - merge_planar_faces=enable_solid_merge, - ) - 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") + pl.add_mesh(pv.read(output_stl), **dargs) + pl.add_text("Decimated mesh", font_size=24) + pl.reset_camera() + pl.link_views() + if "PYTEST_CURRENT_TEST" not in os.environ: + pl.show() + self.logger.info("STL files created in {}".format(output_stl)) + if save_only_stl: + return output_stl + + self._app.odesktop.CloseAllWindows() + self.logger.info("Importing STL in 3D Modeler") + if output_stl: + self.import_3d_cad( + output_stl, + create_lightweigth_part=import_as_light_weight, + healing=False, + merge_planar_faces=enable_stl_merge, + ) + 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(): diff --git a/pyaedt/modules/solutions.py b/pyaedt/modules/solutions.py index 9d7f190012e..415507460a6 100644 --- a/pyaedt/modules/solutions.py +++ b/pyaedt/modules/solutions.py @@ -7,7 +7,6 @@ import shutil import sys import time -import warnings from pyaedt import is_ironpython from pyaedt import pyaedt_function_handler @@ -51,7 +50,7 @@ plt = None -def simplify_stl(input_file, output_file=None, decimation=0.5, aggressiveness=7): +def simplify_stl(input_file, output_file=None, decimation=0.5, preview=False): """Import and simplify a stl file using pyvista and fast-simplification. Parameters @@ -64,30 +63,30 @@ def simplify_stl(input_file, output_file=None, decimation=0.5, aggressiveness=7) Fraction of the original mesh to remove before creating the stl file. If set to ``0.9``, this function will try to reduce the data set to 10% of its original size and will remove 90% of the input triangles. - aggressiveness : int, optional - Controls how aggressively to decimate the mesh. A value of 10 - will result in a fast decimation at the expense of mesh - quality and shape. A value of 0 will attempt to preserve the - original mesh geometry at the expense of time. Setting a low - value may result in being unable to reach the - ``decimation``. Returns ------- str Full path to output stl. """ - try: - import fast_simplification - except Exception: - warnings.warn("Package fast-decimation is needed to perform model simplification.") - warnings.warn("Please install it using pip.") - return False mesh = pv.read(input_file) if not output_file: output_file = os.path.splitext(input_file)[0] + "_output.stl" - simple = fast_simplification.simplify_mesh(mesh, target_reduction=decimation, agg=aggressiveness, verbose=True) + simple = mesh.decimate_pro(decimation, preserve_topology=True, boundary_vertex_deletion=False) simple.save(output_file) + if preview: + pl = pv.Plotter(shape=(1, 2)) + dargs = dict(show_edges=True, color=True) + pl.add_mesh(mesh, **dargs) + pl.add_text("Input mesh", font_size=24) + pl.reset_camera() + pl.subplot(0, 1) + pl.add_mesh(simple, **dargs) + pl.add_text("Decimated mesh", font_size=24) + pl.reset_camera() + pl.link_views() + if "PYTEST_CURRENT_TEST" not in os.environ: + pl.show() return output_file diff --git a/pyproject.toml b/pyproject.toml index fbc659194ff..ac2e1437211 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,7 +49,6 @@ tests = [ "pytest-cov>=4.0.0,<5.1", "pytest-xdist>=3.5.0,<3.7", "pyvista>=0.38.0,<0.44", - "fast-simplification>=0.1.7", # Never directly imported but required when loading ML related file see #4713 "scikit-learn>=1.0.0,<1.6", "scikit-rf>=0.30.0,<1.1", @@ -81,7 +80,6 @@ doc = [ "pypandoc>=1.10.0,<1.14", #"pytest-sphinx", "pyvista>=0.38.0,<0.44", - "fast-simplification>=0.1.7", "recommonmark", "scikit-rf>=0.30.0,<1.1", "Sphinx==5.3.0; python_version == '3.7'",