Skip to content
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

Docs/imported solid model #697

Merged
merged 19 commits into from
Nov 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
267 changes: 267 additions & 0 deletions examples/modeling_features/021-imported-solid-model.py
Original file line number Diff line number Diff line change
@@ -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`.
8 changes: 7 additions & 1 deletion src/ansys/acp/core/_tree_objects/cad_geometry.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down
16 changes: 14 additions & 2 deletions src/ansys/acp/core/_tree_objects/imported_solid_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
roosre marked this conversation as resolved.
Show resolved Hide resolved
self.external_path = self._server_wrapper.auto_upload(path)
with wrap_grpc_errors():
self._get_stub().Refresh( # type: ignore
Expand Down
3 changes: 3 additions & 0 deletions src/ansys/acp/core/_tree_objects/solid_element_set.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
8 changes: 8 additions & 0 deletions src/ansys/acp/core/extras/example_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -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"
),
Expand Down
16 changes: 13 additions & 3 deletions tests/unittests/test_imported_solid_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
3 changes: 3 additions & 0 deletions tests/unittests/test_solid_element_set.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,)
Loading