diff --git a/examples/modeling_features/021-imported-solid-model.py b/examples/modeling_features/021-imported-solid-model.py new file mode 100644 index 0000000000..ab5de525c9 --- /dev/null +++ b/examples/modeling_features/021-imported-solid-model.py @@ -0,0 +1,267 @@ +# Copyright (C) 2022 - 2024 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. + +""" +.. _imported_solid_model_example: + +Imported Solid model +==================== + +This example guides you through the definition of an :class:`.ImportedSolidModel` +which allows to map the layup onto an external solid mesh. +In contrast to the :class:`.SolidModel`, the raw solid mesh of +:class:`.ImportedSolidModel` is loaded from an external source, such as a CDB file. +In this example, the layup is applied onto a t-joint which consists of different +parts such as shell, stringer, and bonding skins. +The example only shows the PyACP part of the setup. For a complete composite analysis, +see :ref:`pymapdl_workflow_example`. + +This example starts from an ACP model with layup. It shows how to: + +- Create an :class:`.ImportedSolidModel` from an external mesh. +- Define the :class:`.LayupMappingObject` to apply the layup onto the solid mesh. +- Scope plies to specific parts of the solid mesh. +- Visualize the mapped layup. + +It is recommended to look at the Ansys help for all the details. This example shows the +basic setup only. +""" + +# %% +# Import the standard library and third-party dependencies. +import pathlib +import tempfile + +import pyvista + +# %% +# Import the PyACP dependencies. +from ansys.acp.core import ElementTechnology, LayupMappingRosetteSelectionMethod, launch_acp +from ansys.acp.core.extras import ExampleKeys, get_example_file + +# sphinx_gallery_thumbnail_number = 3 + + +# %% +# Start ACP and load the model +# ---------------------------- +# %% +# Get the example file from the server. +tempdir = tempfile.TemporaryDirectory() +WORKING_DIR = pathlib.Path(tempdir.name) +input_file = get_example_file(ExampleKeys.IMPORTED_SOLID_MODEL_ACPH5, WORKING_DIR) + +# %% +# Launch the PyACP server and connect to it. +acp = launch_acp() + +# %% +# Load the model from an acph5 file +model = acp.import_model(input_file) + +# %% +# Import external solid model +# --------------------------- +# +# Get the solid mesh file and create an ImportedSolidModel, +# load the initial mesh and show the raw mesh without any mapping. +solid_mesh_file = get_example_file(ExampleKeys.IMPORTED_SOLID_MODEL_SOLID_MESH, WORKING_DIR) +imported_solid_model = model.create_imported_solid_model( + name="Imported Solid Model", +) +imported_solid_model.refresh(path=solid_mesh_file, format="ansys:h5") +imported_solid_model.import_initial_mesh() +model.solid_mesh.to_pyvista().plot(show_edges=True) + +# %% +# The solid element sets are used as target for the mapping later. +# Here is the full list and one is visualized. +imported_solid_model.solid_element_sets.keys() + +solid_eset_mesh = imported_solid_model.solid_element_sets[ + "mapping_target bonding skin right" +].solid_mesh +plotter = pyvista.Plotter() +plotter.add_mesh(solid_eset_mesh.to_pyvista()) +plotter.add_mesh(model.solid_mesh.to_pyvista(), opacity=0.2, show_edges=False) +plotter.show() + +# %% +# Add mapping objects +# ------------------- +# +# Link the layup (plies) of the top skin of the sandwich +# with the corresponding named selections of the solid mesh +# and show the updated solid model. +solid_esets = imported_solid_model.solid_element_sets + +imported_solid_model.create_layup_mapping_object( + name="sandwich skin top", + element_technology=ElementTechnology.LAYERED_ELEMENT, + shell_element_sets=[model.element_sets["els_sandwich_skin_top"]], + entire_solid_mesh=False, + solid_element_sets=[solid_esets["mapping_target sandwich skin top"]], +) + +model.update() +model.solid_mesh.to_pyvista().plot(show_edges=True) + +# %% +# Add other mapping objects +imported_solid_model.create_layup_mapping_object( + name="sandwich skin bottom", + element_technology=ElementTechnology.LAYERED_ELEMENT, + shell_element_sets=[model.element_sets["els_sandwich_skin_bottom"]], + entire_solid_mesh=False, + solid_element_sets=[ + imported_solid_model.solid_element_sets["mapping_target sandwich skin bottom"] + ], +) + +imported_solid_model.create_layup_mapping_object( + name="stringer", + element_technology=ElementTechnology.LAYERED_ELEMENT, + shell_element_sets=[model.element_sets["els_stringer_skin_left"]], + entire_solid_mesh=False, + solid_element_sets=[ + solid_esets[v] + for v in [ + "mapping_target stringer honeycomb", + "mapping_target stringer skin left", + "mapping_target stringer skin right", + ] + ], +) + +imported_solid_model.create_layup_mapping_object( + name="bonding skin", + element_technology=ElementTechnology.LAYERED_ELEMENT, + shell_element_sets=[ + model.element_sets[v] for v in ["els_bonding_skin_left", "els_bonding_skin_right"] + ], + entire_solid_mesh=False, + solid_element_sets=[ + solid_esets[v] + for v in ["mapping_target bonding skin left", "mapping_target bonding skin right"] + ], +) + +# %% +# Show intermediate result +model.update() +model.solid_mesh.to_pyvista().plot(show_edges=True) + +# %% +# The mapping can also be done for specific plies +# as shown for the core materials. +imported_solid_model.create_layup_mapping_object( + name="foam", + element_technology=ElementTechnology.LAYERED_ELEMENT, + shell_element_sets=[ + model.element_sets[v] for v in ["els_foam_core_left", "els_foam_core_right"] + ], + select_all_plies=False, + sequences=[model.modeling_groups["MG foam_core"]], + entire_solid_mesh=False, + solid_element_sets=[solid_esets["mapping_target foam core"]], + delete_lost_elements=False, + filler_material=model.materials["SAN Foam (81 kg m^-3)"], + rosettes=[model.rosettes["Global Coordinate System"]], + rosette_selection_method=LayupMappingRosetteSelectionMethod.MINIMUM_DISTANCE, +) + +imported_solid_model.create_layup_mapping_object( + name="honeycomb", + element_technology=ElementTechnology.LAYERED_ELEMENT, + shell_element_sets=[ + model.element_sets[v] for v in ["els_honeycomb_left", "els_honeycomb_right"] + ], + select_all_plies=False, + sequences=[model.modeling_groups["MG honeycomb_core"]], + entire_solid_mesh=False, + solid_element_sets=[solid_esets["mapping_target sandwich honeycomb"]], + delete_lost_elements=False, + filler_material=model.materials["Honeycomb"], + rosettes=[model.rosettes["Global Coordinate System"]], + rosette_selection_method=LayupMappingRosetteSelectionMethod.MINIMUM_DISTANCE, +) +model.update() +model.solid_mesh.to_pyvista().plot(show_edges=True) + +# %% +# Add filler mapping objects where the solid mesh is "filled" +# with a single material. No plies from the layup are used here. + +imported_solid_model.create_layup_mapping_object( + name="resin", + element_technology=ElementTechnology.LAYERED_ELEMENT, + shell_element_sets=[], + entire_solid_mesh=False, + solid_element_sets=[ + solid_esets[v] for v in ["mapping_target adhesive", "mapping_target adhesive stringer root"] + ], + delete_lost_elements=False, + filler_material=model.materials["Resin Epoxy"], + rosettes=[model.rosettes["Global Coordinate System"]], + rosette_selection_method=LayupMappingRosetteSelectionMethod.MINIMUM_DISTANCE, +) + +# %% +# Show final solid mesh with mapped layup +model.update() +model.solid_mesh.to_pyvista().plot(show_edges=True) + + +# %% +# Show extent and thickness of mapped plies +# ----------------------------------------- +# +# Use :func:`.print_model` to get the list of plies. After identifying the ply +# of interest, for example the thickness can be visualized. Note that +# only ply-wise data of :class:`.AnalysisPly` can be visualized on the +# solid mesh. :class:`.ProductionPly` and :class:`.ModelingPly` cannot +# be visualized on the solid mesh. +ap = ( + model.modeling_groups["MG bonding_skin_right"] + .modeling_plies["ModelingPly.26"] + .production_plies["ProductionPly.33"] + .analysis_plies["P1L1__ModelingPly.26"] +) +thickness_data = ap.elemental_data.thickness +thickness_pyvista_mesh = thickness_data.get_pyvista_mesh(mesh=ap.solid_mesh) # type: ignore +plotter = pyvista.Plotter() +plotter.add_mesh(thickness_pyvista_mesh) +plotter.add_mesh(model.solid_mesh.to_pyvista(), opacity=0.2, show_edges=False) +plotter.show() + + +# %% +# Other features +# -------------- +# +# The :class:`.CutOffGeometry` can be used in combination witt the :class:`.ImportedSolidModel` +# as well. See example :ref:`solid_model_example` for more details. +# More plotting capabilities are shown in the example :ref:`solid_model_example` as well. +# +# The solid mesh can be exported as CDB for MAPDL or to PyMechanical for further analysis. +# These workflows are shown in :ref:`pymapdl_workflow_example` and +# :ref:`pymechanical_solid_example`. diff --git a/src/ansys/acp/core/_tree_objects/cad_geometry.py b/src/ansys/acp/core/_tree_objects/cad_geometry.py index 04762d901a..a801b6d3f3 100644 --- a/src/ansys/acp/core/_tree_objects/cad_geometry.py +++ b/src/ansys/acp/core/_tree_objects/cad_geometry.py @@ -180,7 +180,13 @@ def visualization_mesh(self) -> TriangleMesh: ) def refresh(self, path: PATH) -> None: - """Reload the geometry from its external source.""" + """Reload the geometry from its external source. + + Parameters + ---------- + path : + Path of the new input file. + """ self.external_path = self._server_wrapper.auto_upload(path) stub = cast(cad_geometry_pb2_grpc.ObjectServiceStub, self._get_stub()) with wrap_grpc_errors(): diff --git a/src/ansys/acp/core/_tree_objects/imported_solid_model.py b/src/ansys/acp/core/_tree_objects/imported_solid_model.py index bca3e1ec96..55f534b985 100644 --- a/src/ansys/acp/core/_tree_objects/imported_solid_model.py +++ b/src/ansys/acp/core/_tree_objects/imported_solid_model.py @@ -354,8 +354,20 @@ def _create_stub(self) -> imported_solid_model_pb2_grpc.ObjectServiceStub: layup_mapping_object_pb2_grpc.ObjectServiceStub, ) - def refresh(self, path: _PATH) -> None: - """Re-import the solid model from the external file.""" + def refresh(self, path: _PATH, format: SolidModelImportFormat | None = None) -> None: # type: ignore + """ + Re-import the solid model from the external file. + + Parameters + ---------- + path : + Path of the new input file. + format : + Switch format of the input file. Optional, uses the current format of the + imported solid model if not specified. + """ + if format is not None: + self.format = format self.external_path = self._server_wrapper.auto_upload(path) with wrap_grpc_errors(): self._get_stub().Refresh( # type: ignore diff --git a/src/ansys/acp/core/_tree_objects/solid_element_set.py b/src/ansys/acp/core/_tree_objects/solid_element_set.py index cafbf1d6c2..501beee85b 100644 --- a/src/ansys/acp/core/_tree_objects/solid_element_set.py +++ b/src/ansys/acp/core/_tree_objects/solid_element_set.py @@ -31,6 +31,7 @@ from .._utils.property_protocols import ReadOnlyProperty from ._elemental_or_nodal_data import ElementalData, NodalData from ._grpc_helpers.property_helper import grpc_data_property_read_only, mark_grpc_properties +from ._mesh_data import solid_mesh_property from .base import IdTreeObject, ReadOnlyTreeObject from .enums import status_type_from_pb from .object_registry import register @@ -76,3 +77,5 @@ def _create_stub(self) -> solid_element_set_pb2_grpc.ObjectServiceStub: element_labels: ReadOnlyProperty[tuple[int, ...]] = grpc_data_property_read_only( "properties.element_labels", from_protobuf=to_tuple_from_1D_array ) + + solid_mesh = solid_mesh_property diff --git a/src/ansys/acp/core/extras/example_helpers.py b/src/ansys/acp/core/extras/example_helpers.py index aa67ae47ae..cc59e277b3 100644 --- a/src/ansys/acp/core/extras/example_helpers.py +++ b/src/ansys/acp/core/extras/example_helpers.py @@ -67,6 +67,8 @@ class ExampleKeys(Enum): CLASS40_AGDB = auto() CLASS40_CDB = auto() MATERIALS_XML = auto() + IMPORTED_SOLID_MODEL_ACPH5 = auto() + IMPORTED_SOLID_MODEL_SOLID_MESH = auto() SNAP_TO_GEOMETRY = auto() CUT_OFF_GEOMETRY_SOLID_MODEL = auto() @@ -102,6 +104,12 @@ class ExampleKeys(Enum): ExampleKeys.CLASS40_AGDB: _ExampleLocation(directory="class40", filename="class40.agdb"), ExampleKeys.CLASS40_CDB: _ExampleLocation(directory="class40", filename="class40.cdb"), ExampleKeys.MATERIALS_XML: _ExampleLocation(directory="materials", filename="materials.engd"), + ExampleKeys.IMPORTED_SOLID_MODEL_ACPH5: _ExampleLocation( + directory="imported_solid_model", filename="t-joint-ACP-Pre.acph5" + ), + ExampleKeys.IMPORTED_SOLID_MODEL_SOLID_MESH: _ExampleLocation( + directory="imported_solid_model", filename="t-joint.solid.h5" + ), ExampleKeys.SNAP_TO_GEOMETRY: _ExampleLocation( directory="geometries", filename="snap_to_geometry.stp" ), diff --git a/tests/unittests/test_imported_solid_model.py b/tests/unittests/test_imported_solid_model.py index f676459ea9..16e7229880 100644 --- a/tests/unittests/test_imported_solid_model.py +++ b/tests/unittests/test_imported_solid_model.py @@ -250,11 +250,21 @@ def test_import_initial_mesh(acp_instance, parent_object): model.update() with tempfile.TemporaryDirectory() as tmp_dir: - out_path = pathlib.Path(tmp_dir) / f"out_file.h5" - solid_model.export(path=out_path, format=pyacp.SolidModelExportFormat.ANSYS_H5) + out_path_h5 = pathlib.Path(tmp_dir) / f"out_file.h5" + solid_model.export(path=out_path_h5, format=pyacp.SolidModelExportFormat.ANSYS_H5) + out_path_cdb = pathlib.Path(tmp_dir) / f"out_file.cdb" + solid_model.export(path=out_path_cdb, format=pyacp.SolidModelExportFormat.ANSYS_CDB) imported_solid_model = model.create_imported_solid_model( - external_path=acp_instance.upload_file(out_path), + external_path=acp_instance.upload_file(out_path_h5), format=pyacp.SolidModelImportFormat.ANSYS_H5, ) imported_solid_model.import_initial_mesh() + + # refresh from external source with the same format + imported_solid_model.refresh(out_path_h5) + imported_solid_model.import_initial_mesh() + + # refresh from external source where the format is different + imported_solid_model.refresh(out_path_cdb, format=pyacp.SolidModelImportFormat.ANSYS_CDB) + imported_solid_model.import_initial_mesh() diff --git a/tests/unittests/test_solid_element_set.py b/tests/unittests/test_solid_element_set.py index 82e4556d30..c277288cdd 100644 --- a/tests/unittests/test_solid_element_set.py +++ b/tests/unittests/test_solid_element_set.py @@ -89,3 +89,6 @@ def test_properties(self, parent_object, properties): ref_values = properties[solid_element_set.id] for prop, value in ref_values.items(): assert getattr(solid_element_set, prop) == value + + assert solid_element_set.solid_mesh is not None + assert solid_element_set.solid_mesh.element_labels == (2,)