diff --git a/.flake8 b/.flake8 index ad9ec1672d..a5b4921ec9 100644 --- a/.flake8 +++ b/.flake8 @@ -1,11 +1,11 @@ [flake8] exclude = venv, __init__.py, doc/_build -select = W191, W291, W293, W391, E115, E117, E122, E124, E125, E225, E231, E301, E303, E501, F401, F403 +select = W191, W291, W293, W391, E115, E117, E122, E124, E125, E225, E301, E303, E501, F401, F403 count = True max-complexity = 10 max-line-length = 100 statistics = True -per-file-ignores = +per-file-ignores = src/ansys/meshing/prime/autogen/*: F401, F403, E501 src/ansys/meshing/prime/params/primestructs.py: F401, F403 src/ansys/meshing/prime/internals/error_handling.py: E501 diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index fb22edab7e..763c737366 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -16,7 +16,7 @@ on: env: DOCKER_IMAGE_NAME: ghcr.io/ansys/prime DOCKER_IMAGE_TAG: '25.1.0.dev20' - MAIN_PYTHON_VERSION: '3.10' + MAIN_PYTHON_VERSION: '3.12' PACKAGE_NAME: 'ansys-meshing-prime' PACKAGE_NAMESPACE: 'ansys.meshing.prime' DOCUMENTATION_CNAME: 'prime.docs.pyansys.com' @@ -89,7 +89,6 @@ jobs: runs-on: ubuntu-latest needs: [docs-style] steps: - - name: Login in Github Container registry uses: docker/login-action@v3 with: @@ -101,7 +100,7 @@ jobs: run: docker pull ${{ env.DOCKER_IMAGE_NAME }}:${{ env.DOCKER_IMAGE_TAG }} - name: Setup headless display - uses: pyvista/setup-headless-display-action@v2 + uses: pyvista/setup-headless-display-action@v3 - name: "Run Ansys documentation building action" uses: ansys/actions/doc-build@v8 diff --git a/doc/changelog.d/944.fixed.md b/doc/changelog.d/944.fixed.md new file mode 100644 index 0000000000..d1d2260180 --- /dev/null +++ b/doc/changelog.d/944.fixed.md @@ -0,0 +1 @@ +fix: Error appearing in some examples while documentation building \ No newline at end of file diff --git a/doc/make.bat b/doc/make.bat index 05a0bf9dc2..24f344a4e8 100644 --- a/doc/make.bat +++ b/doc/make.bat @@ -10,6 +10,7 @@ if "%SPHINXBUILD%" == "" ( if "%SPHINXOPTS%" == "" ( set SPHINXOPTS=-j auto ) + set SOURCEDIR=source set BUILDDIR=_build diff --git a/examples/gallery/05_pcb_stacker.py b/examples/gallery/05_pcb_stacker.py index d117bb6893..7994e8045a 100644 --- a/examples/gallery/05_pcb_stacker.py +++ b/examples/gallery/05_pcb_stacker.py @@ -1,187 +1,187 @@ -# Copyright (C) 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. - -""" -.. _ref_pcb: - -========================================== -Mesh a PCB for structural thermal analysis -========================================== - -**Summary**: This example demonstrates how to mesh a printed circuit board -with mainly hexahedral elements for structural thermal simulation using the volume sweeper. - -Objective -~~~~~~~~~~ -This example uses the volume sweeper to mesh the solids of a printed circuit board for a -structural thermal analysis using predominantly hexahedral elements. - -.. image:: ../../../images/pcb_stacker.png - :align: center - :width: 800 - :alt: Thermal structural mesh. - -Procedure -~~~~~~~~~~ -#. Launch an Ansys Prime Server instance and connect the PyPrimeMesh client. -#. Read the CAD geometry. -#. Create a base face, projecting edge loops and imprinting to capture the geometry. -#. Surface mesh the base face with quad elements. -#. Stack the base face mesh through the volumes to create a mainly hexahedral volume mesh. -#. Write the mesh for the structural thermal analysis. -""" - -############################################################################### -# Launch Ansys Prime Server -# ~~~~~~~~~~~~~~~~~~~~~~~~~ -# Import all necessary modules. -# Launch the Ansys Prime Server instance and connect the client. -# Get the client model and instantiate meshing utilities from the ``lucid`` class. - -import os -import tempfile - -import ansys.meshing.prime as prime -from ansys.meshing.prime.graphics import PrimePlotter - -prime_client = prime.launch_prime() -model = prime_client.model -mesh_util = prime.lucid.Mesh(model=model) - -############################################################################### -# Import geometry -# ~~~~~~~~~~~~~~~ -# Download the PCB geometry (PMDAT) file. -# Import the geometry. -# Display the imported geometry. Purple edges indicate that the geometry is -# connected and the topology is shared between the different volumes. -# This means that the mesh is also to be connected between volumes. - -# For Windows OS users, SCDOC files are also available. -# To read the geometry as connected with shared topology, you must use -# the Workbench ``CadReaderRoute``: - -# mesh_util.read( -# file_name=prime.examples.download_pcb_scdoc(), -# cad_reader_route=prime.CadReaderRoute.WORKBENCH, -# ) - -mesh_util.read(file_name=prime.examples.download_pcb_pmdat()) - -display = PrimePlotter() -display.plot(model) -display.show() - -sizing_params = prime.GlobalSizingParams(model=model, min=0.5, max=1.0) -model.set_global_sizing_params(params=sizing_params) - -############################################################################### -# Create base face -# ~~~~~~~~~~~~~~~~ -# Define stacker parameters: -# -# - Set the direction vector for defining stacking. -# - Set the maximum offset size for mesh layers created by the stacker method. -# - Set the base faces to delete after stacking. -# -# Create the base face from the part and volumes. -# Define a label for the generated base faces and display. -# When coloured by zonelet, the display shows the imprints -# on the base face. - -part = model.parts[0] -sweeper = prime.VolumeSweeper(model) - -stacker_params = prime.MeshStackerParams( - model=model, - direction=[0, 1, 0], - max_offset_size=1.0, - delete_base=True, -) - -createbase_results = sweeper.create_base_face( - part_id=part.id, - topo_volume_ids=part.get_topo_volumes(), - params=stacker_params, -) - -base_faces = createbase_results.base_face_ids - -part.add_labels_on_topo_entities(["base_faces"], base_faces) - -scope = prime.ScopeDefinition(model=model, label_expression="base_faces") - -display = PrimePlotter() -display.plot(model, scope=scope) -display.show() - - -############################################################################### -# Surface mesh base face -# ~~~~~~~~~~~~~~~~~~~~~~ -# Quad surface mesh the generated base faces for stacking. - -base_scope = prime.lucid.SurfaceScope( - entity_expression="base_faces", - part_expression=part.name, - scope_evaluation_type=prime.ScopeEvaluationType.LABELS, -) - -mesh_util.surface_mesh(min_size=0.5, scope=base_scope, generate_quads=True) - -display = PrimePlotter() -display.plot(model, scope=scope, update=True) -display.show() - -############################################################################### -# Stack base face -# ~~~~~~~~~~~~~~~~~~~~~~ -# Create a mainly hexahedral volume mesh using the stacker method. -# Display the volume mesh. - -stackbase_results = sweeper.stack_base_face( - part_id=part.id, - base_face_ids=base_faces, - topo_volume_ids=part.get_topo_volumes(), - params=stacker_params, -) - -display = PrimePlotter() -display.plot(model, update=True) -display.show() - -############################################################################### -# Write mesh -# ~~~~~~~~~~ -# Write a CDB file. - -with tempfile.TemporaryDirectory() as temp_folder: - mesh_file = os.path.join(temp_folder, "pcb.cdb") - mesh_util.write(mesh_file) - assert os.path.exists(mesh_file) - print("\nExported file:\n", mesh_file) - -############################################################################### -# Exit PyPrimeMesh -# ~~~~~~~~~~~~~~~~ - -prime_client.exit() +# Copyright (C) 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. + +""" +.. _ref_pcb: + +========================================== +Mesh a PCB for structural thermal analysis +========================================== + +**Summary**: This example demonstrates how to mesh a printed circuit board +with mainly hexahedral elements for structural thermal simulation using the volume sweeper. + +Objective +~~~~~~~~~~ +This example uses the volume sweeper to mesh the solids of a printed circuit board for a +structural thermal analysis using predominantly hexahedral elements. + +.. image:: ../../../images/pcb_stacker.png + :align: center + :width: 800 + :alt: Thermal structural mesh. + +Procedure +~~~~~~~~~~ +#. Launch an Ansys Prime Server instance and connect the PyPrimeMesh client. +#. Read the CAD geometry. +#. Create a base face, projecting edge loops and imprinting to capture the geometry. +#. Surface mesh the base face with quad elements. +#. Stack the base face mesh through the volumes to create a mainly hexahedral volume mesh. +#. Write the mesh for the structural thermal analysis. +""" + +############################################################################### +# Launch Ansys Prime Server +# ~~~~~~~~~~~~~~~~~~~~~~~~~ +# Import all necessary modules. +# Launch the Ansys Prime Server instance and connect the client. +# Get the client model and instantiate meshing utilities from the ``lucid`` class. + +import os +import tempfile + +import ansys.meshing.prime as prime +from ansys.meshing.prime.graphics import PrimePlotter + +prime_client = prime.launch_prime() +model = prime_client.model +mesh_util = prime.lucid.Mesh(model=model) + +############################################################################### +# Import geometry +# ~~~~~~~~~~~~~~~ +# Download the PCB geometry (PMDAT) file. +# Import the geometry. +# Display the imported geometry. Purple edges indicate that the geometry is +# connected and the topology is shared between the different volumes. +# This means that the mesh is also to be connected between volumes. + +# For Windows OS users, SCDOC files are also available. +# To read the geometry as connected with shared topology, you must use +# the Workbench ``CadReaderRoute``: + +# mesh_util.read( +# file_name=prime.examples.download_pcb_scdoc(), +# cad_reader_route=prime.CadReaderRoute.WORKBENCH, +# ) + +mesh_util.read(file_name=prime.examples.download_pcb_pmdat()) + +display = PrimePlotter() +display.plot(model) +display.show() + +sizing_params = prime.GlobalSizingParams(model=model, min=0.5, max=1.0) +model.set_global_sizing_params(params=sizing_params) + +############################################################################### +# Create base face +# ~~~~~~~~~~~~~~~~ +# Define stacker parameters: +# +# - Set the direction vector for defining stacking. +# - Set the maximum offset size for mesh layers created by the stacker method. +# - Set the base faces to delete after stacking. +# +# Create the base face from the part and volumes. +# Define a label for the generated base faces and display. +# When coloured by zonelet, the display shows the imprints +# on the base face. + +part = model.parts[0] +sweeper = prime.VolumeSweeper(model) + +stacker_params = prime.MeshStackerParams( + model=model, + direction=[0, 1, 0], + max_offset_size=1.0, + delete_base=True, +) + +createbase_results = sweeper.create_base_face( + part_id=part.id, + topo_volume_ids=part.get_topo_volumes(), + params=stacker_params, +) + +base_faces = createbase_results.base_face_ids + +part.add_labels_on_topo_entities(["base_faces"], base_faces) + +scope = prime.ScopeDefinition(model=model, label_expression="base_faces") + +display = PrimePlotter() +display.plot(model, scope=scope) +display.show() + + +############################################################################### +# Surface mesh base face +# ~~~~~~~~~~~~~~~~~~~~~~ +# Quad surface mesh the generated base faces for stacking. + +base_scope = prime.lucid.SurfaceScope( + entity_expression="base_faces", + part_expression=part.name, + scope_evaluation_type=prime.ScopeEvaluationType.LABELS, +) + +mesh_util.surface_mesh(min_size=0.5, scope=base_scope, generate_quads=True) + +display = PrimePlotter() +display.plot(model, scope=scope, update=True) +display.show() + +############################################################################### +# Stack base face +# ~~~~~~~~~~~~~~~~~~~~~~ +# Create a mainly hexahedral volume mesh using the stacker method. +# Display the volume mesh. + +stackbase_results = sweeper.stack_base_face( + part_id=part.id, + base_face_ids=base_faces, + topo_volume_ids=part.get_topo_volumes(), + params=stacker_params, +) + +display = PrimePlotter() +display.plot(model, update=True) +display.show() + +############################################################################### +# Write mesh +# ~~~~~~~~~~ +# Write a CDB file. + +with tempfile.TemporaryDirectory() as temp_folder: + mesh_file = os.path.join(temp_folder, "pcb.cdb") + mesh_util.write(mesh_file) + assert os.path.exists(mesh_file) + print("\nExported file:\n", mesh_file) + +############################################################################### +# Exit PyPrimeMesh +# ~~~~~~~~~~~~~~~~ + +prime_client.exit() diff --git a/examples/gallery/07_saddle_bracket.py b/examples/gallery/07_saddle_bracket.py index a8d42d6785..b38df619ae 100644 --- a/examples/gallery/07_saddle_bracket.py +++ b/examples/gallery/07_saddle_bracket.py @@ -1,239 +1,239 @@ -# Copyright (C) 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. - -""" -.. _ref_saddle_thin_hex: - -=============================================== -Mesh a saddle bracket for a structural analysis -=============================================== - -**Summary**: This example demonstrates how to mesh a thin -solid with hexahedral and prism cells. - -Objective -~~~~~~~~~ - -This example creates a mainly hexahedral mesh on a thin solid volume. - -.. image:: ../../../images/saddle_bracket.png - :align: center - :width: 400 - :alt: Thin volume hexahedral mesh. - -Procedure -~~~~~~~~~ -#. Launch Ansys Prime Server. -#. Import the CAD geometry. -#. Quad surface mesh the source face. -#. Surface mesh the remaining unmeshed TopoFaces with tri surface mesh. -#. Delete the topology. -#. Define volume meshing controls to use thin volume meshing. -#. Volume mesh with hexahedral and prism cells. -#. Write a CDB file for use in the APDL solver. -#. Exit the PyPrimeMesh session. - -""" - -############################################################################### -# Launch Ansys Prime Server -# ~~~~~~~~~~~~~~~~~~~~~~~~~ -# Import all necessary modules. -# Launch an instance of Ansys Prime Server. -# Connect the PyPrimeMesh client and get the model. - -import os -import tempfile - -import ansys.meshing.prime as prime -from ansys.meshing.prime.graphics import PrimePlotter - -prime_client = prime.launch_prime() -model = prime_client.model - -mesh_util = prime.lucid.Mesh(model) - -############################################################################### -# Import geometry -# ~~~~~~~~~~~~~~~ -# Download the saddle bracket geometry (FMD) file exported by SpaceClaim. -# Import the geometry and display all. - -# For Windows OS users, scdoc is also available: -# saddle_bracket = prime.examples.download_saddle_bracket_scdoc() - -saddle_bracket = prime.examples.download_saddle_bracket_fmd() - -mesh_util.read(file_name=saddle_bracket) - -print(model) - -display = PrimePlotter() -display.plot(model) -display.show() - -############################################################################### -# Quad mesh source faces -# ~~~~~~~~~~~~~~~~~~~~~~ -# Mesh the source faces for the thin volume control with quads. - -scope = prime.lucid.SurfaceScope( - part_expression="*", - entity_expression="source_thin", - scope_evaluation_type=prime.ScopeEvaluationType.LABELS, -) - -mesh_util.surface_mesh( - scope=scope, - min_size=2.0, - generate_quads=True, -) - -display = PrimePlotter() -display.plot(model, update=True) -display.show() - -############################################################################### -# Surface mesh unmeshed faces -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Mesh unmeshed faces with tri surface mesh. Tri surface mesh on the target -# and side faces is used to show more clearly that the result of the thin -# volume control is a hex mesh that is imprinted up to the side faces. -# All quads could be used for the surface mesh to simplify the process. - -part = model.parts[0] - -all_faces = part.get_topo_faces() -meshed_faces = part.get_topo_faces_of_label_name_pattern( - label_name_pattern="source_thin", - name_pattern_params=prime.NamePatternParams(model), -) - -unmeshed_faces = [face for face in all_faces if face not in meshed_faces] - -part.add_labels_on_topo_entities( - labels=["unmeshed_faces"], - topo_entities=unmeshed_faces, -) - -scope = prime.lucid.SurfaceScope( - part_expression="*", - entity_expression="unmeshed_faces", - scope_evaluation_type=prime.ScopeEvaluationType.LABELS, -) - -mesh_util.surface_mesh( - scope=scope, - min_size=2.0, -) - -display = PrimePlotter() -display.plot(model, update=True) -display.show() - -############################################################################### -# Delete topology -# ~~~~~~~~~~~~~~~ -# Delete topology to leave only the surface mesh. This is necessary for the -# thin volume control to be used. - -part.delete_topo_entities( - prime.DeleteTopoEntitiesParams( - model=model, - delete_geom_zonelets=True, - delete_mesh_zonelets=False, - ) -) - -############################################################################### -# Define volume meshing controls -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Define volume meshing controls to use thin volume meshing. -# Specify source and target faces for the thin volume using imported labels. -# Set the number of layers of cells through the thickness of the thin solid to be 4. -# To create a fully hexahedral and prism mesh the side faces must be imprinted on -# the side faces. If needed, a buffer region at the sides of the volume can be -# defined where the volume fill type used for the volume mesh parameters is -# used to infill. This is useful on more complex geometries, where it provides -# more robustness of the method. To create a buffer region set ``imprint_sides`` -# to ``False`` and specify how many rings of cells to ignore at the sides -# using ``n_ignore_rings``. - -auto_mesh_params = prime.AutoMeshParams(model=model) -thin_vol_ctrls_ids = [] -thin_vol_ctrl = model.control_data.create_thin_volume_control() - -thin_vol_ctrl.set_source_scope( - prime.ScopeDefinition( - model=model, - label_expression="source_thin", - ) -) - -thin_vol_ctrl.set_target_scope( - prime.ScopeDefinition( - model=model, - label_expression="target_thin", - ) -) - -thin_params = prime.ThinVolumeMeshParams( - model=model, - n_layers=4, - imprint_sides=True, -) - -thin_vol_ctrl.set_thin_volume_mesh_params(thin_volume_mesh_params=thin_params) -thin_vol_ctrls_ids.append(thin_vol_ctrl.id) -auto_mesh_params.thin_volume_control_ids = thin_vol_ctrls_ids -auto_mesh_params.volume_fill_type = prime.VolumeFillType.TET - -############################################################################### -# Generate volume mesh -# ~~~~~~~~~~~~~~~~~~~~ -# Volume mesh to obtain hexahedral and prism mesh. -# Print mesh summary. -# Display volume mesh. - -volume_mesh = prime.AutoMesh(model=model) -result_vol = volume_mesh.mesh(part_id=part.id, automesh_params=auto_mesh_params) -print(part.get_summary(prime.PartSummaryParams(model))) - -display = PrimePlotter() -display.plot(model, update=True) -display.show() -############################################################################### -# Write mesh -# ~~~~~~~~~~ -# Write a CDB file for use in the MAPDL solver. - -with tempfile.TemporaryDirectory() as temp_folder: - mesh_file = os.path.join(temp_folder, "saddle_bracket.cdb") - mesh_util.write(mesh_file) - assert os.path.exists(mesh_file) - print("\nExported file:\n", mesh_file) - -############################################################################### -# Exit PyPrimeMesh -# ~~~~~~~~~~~~~~~~ - -prime_client.exit() +# Copyright (C) 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. + +""" +.. _ref_saddle_thin_hex: + +=============================================== +Mesh a saddle bracket for a structural analysis +=============================================== + +**Summary**: This example demonstrates how to mesh a thin +solid with hexahedral and prism cells. + +Objective +~~~~~~~~~ + +This example creates a mainly hexahedral mesh on a thin solid volume. + +.. image:: ../../../images/saddle_bracket.png + :align: center + :width: 400 + :alt: Thin volume hexahedral mesh. + +Procedure +~~~~~~~~~ +#. Launch Ansys Prime Server. +#. Import the CAD geometry. +#. Quad surface mesh the source face. +#. Surface mesh the remaining unmeshed TopoFaces with tri surface mesh. +#. Delete the topology. +#. Define volume meshing controls to use thin volume meshing. +#. Volume mesh with hexahedral and prism cells. +#. Write a CDB file for use in the APDL solver. +#. Exit the PyPrimeMesh session. + +""" + +############################################################################### +# Launch Ansys Prime Server +# ~~~~~~~~~~~~~~~~~~~~~~~~~ +# Import all necessary modules. +# Launch an instance of Ansys Prime Server. +# Connect the PyPrimeMesh client and get the model. + +import os +import tempfile + +import ansys.meshing.prime as prime +from ansys.meshing.prime.graphics import PrimePlotter + +prime_client = prime.launch_prime() +model = prime_client.model + +mesh_util = prime.lucid.Mesh(model) + +############################################################################### +# Import geometry +# ~~~~~~~~~~~~~~~ +# Download the saddle bracket geometry (FMD) file exported by SpaceClaim. +# Import the geometry and display all. + +# For Windows OS users, scdoc is also available: +# saddle_bracket = prime.examples.download_saddle_bracket_scdoc() + +saddle_bracket = prime.examples.download_saddle_bracket_fmd() + +mesh_util.read(file_name=saddle_bracket) + +print(model) + +display = PrimePlotter() +display.plot(model) +display.show() + +############################################################################### +# Quad mesh source faces +# ~~~~~~~~~~~~~~~~~~~~~~ +# Mesh the source faces for the thin volume control with quads. + +scope = prime.lucid.SurfaceScope( + part_expression="*", + entity_expression="source_thin", + scope_evaluation_type=prime.ScopeEvaluationType.LABELS, +) + +mesh_util.surface_mesh( + scope=scope, + min_size=2.0, + generate_quads=True, +) + +display = PrimePlotter() +display.plot(model, update=True) +display.show() + +############################################################################### +# Surface mesh unmeshed faces +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# Mesh unmeshed faces with tri surface mesh. Tri surface mesh on the target +# and side faces is used to show more clearly that the result of the thin +# volume control is a hex mesh that is imprinted up to the side faces. +# All quads could be used for the surface mesh to simplify the process. + +part = model.parts[0] + +all_faces = part.get_topo_faces() +meshed_faces = part.get_topo_faces_of_label_name_pattern( + label_name_pattern="source_thin", + name_pattern_params=prime.NamePatternParams(model), +) + +unmeshed_faces = [face for face in all_faces if face not in meshed_faces] + +part.add_labels_on_topo_entities( + labels=["unmeshed_faces"], + topo_entities=unmeshed_faces, +) + +scope = prime.lucid.SurfaceScope( + part_expression="*", + entity_expression="unmeshed_faces", + scope_evaluation_type=prime.ScopeEvaluationType.LABELS, +) + +mesh_util.surface_mesh( + scope=scope, + min_size=2.0, +) + +display = PrimePlotter() +display.plot(model, update=True) +display.show() + +############################################################################### +# Delete topology +# ~~~~~~~~~~~~~~~ +# Delete topology to leave only the surface mesh. This is necessary for the +# thin volume control to be used. + +part.delete_topo_entities( + prime.DeleteTopoEntitiesParams( + model=model, + delete_geom_zonelets=True, + delete_mesh_zonelets=False, + ) +) + +############################################################################### +# Define volume meshing controls +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# Define volume meshing controls to use thin volume meshing. +# Specify source and target faces for the thin volume using imported labels. +# Set the number of layers of cells through the thickness of the thin solid to be 4. +# To create a fully hexahedral and prism mesh the side faces must be imprinted on +# the side faces. If needed, a buffer region at the sides of the volume can be +# defined where the volume fill type used for the volume mesh parameters is +# used to infill. This is useful on more complex geometries, where it provides +# more robustness of the method. To create a buffer region set ``imprint_sides`` +# to ``False`` and specify how many rings of cells to ignore at the sides +# using ``n_ignore_rings``. + +auto_mesh_params = prime.AutoMeshParams(model=model) +thin_vol_ctrls_ids = [] +thin_vol_ctrl = model.control_data.create_thin_volume_control() + +thin_vol_ctrl.set_source_scope( + prime.ScopeDefinition( + model=model, + label_expression="source_thin", + ) +) + +thin_vol_ctrl.set_target_scope( + prime.ScopeDefinition( + model=model, + label_expression="target_thin", + ) +) + +thin_params = prime.ThinVolumeMeshParams( + model=model, + n_layers=4, + imprint_sides=True, +) + +thin_vol_ctrl.set_thin_volume_mesh_params(thin_volume_mesh_params=thin_params) +thin_vol_ctrls_ids.append(thin_vol_ctrl.id) +auto_mesh_params.thin_volume_control_ids = thin_vol_ctrls_ids +auto_mesh_params.volume_fill_type = prime.VolumeFillType.TET + +############################################################################### +# Generate volume mesh +# ~~~~~~~~~~~~~~~~~~~~ +# Volume mesh to obtain hexahedral and prism mesh. +# Print mesh summary. +# Display volume mesh. + +volume_mesh = prime.AutoMesh(model=model) +result_vol = volume_mesh.mesh(part_id=part.id, automesh_params=auto_mesh_params) +print(part.get_summary(prime.PartSummaryParams(model))) + +display = PrimePlotter() +display.plot(model, update=True) +display.show() +############################################################################### +# Write mesh +# ~~~~~~~~~~ +# Write a CDB file for use in the MAPDL solver. + +with tempfile.TemporaryDirectory() as temp_folder: + mesh_file = os.path.join(temp_folder, "saddle_bracket.cdb") + mesh_util.write(mesh_file) + assert os.path.exists(mesh_file) + print("\nExported file:\n", mesh_file) + +############################################################################### +# Exit PyPrimeMesh +# ~~~~~~~~~~~~~~~~ + +prime_client.exit() diff --git a/examples/gallery/10_wheel_ground_contact_patch.py b/examples/gallery/10_wheel_ground_contact_patch.py index 87e4627867..e11002185a 100644 --- a/examples/gallery/10_wheel_ground_contact_patch.py +++ b/examples/gallery/10_wheel_ground_contact_patch.py @@ -1,233 +1,252 @@ -# Copyright (C) 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. - -""" -.. _ref_contact_patch: - -======================================================================== -Create a contact patch for wrapping between a wheel and ground interface -======================================================================== - -**Summary**: This example demonstrates how to create a contact patch for use with wrapping -to avoid meshing into a narrow contact region between two objects. - -Objective -~~~~~~~~~ -This example uses a contact patch for wrapping to avoid the interface of a wheel with the ground -to improve mesh quality when growing prism layers in the region of the contacting faces. - -.. image:: ../../../images/contact_patch.png - :align: center - :width: 600 - -The preceding image shows the following: - - -* Top left: Wheel/ground interface -* Top right: Addition of contact patch -* Lower left: Grouping tolerance at 4 with multiple contact patches -* Lower right: Grouping tolerance at 20 with merged single contact patch - - - -Procedure -~~~~~~~~~ -#. Launch an Ansys Prime Server instance and instantiate the meshing utilities - from the ``lucid`` class. -#. Import the wheel ground geometry. -#. Convert the topo parts to mesh parts so that the contact patch can be created. -#. Create a contact patch between the wheel and the ground. -#. Extract the fluid region using wrapping. -#. Volume mesh with polyhedral and prism cells. -#. Write a CAS file for use in the Fluent solver. -#. Exit the PyPrimeMesh session. - - -""" - -############################################################################### -# Launch Ansys Prime Server -# ~~~~~~~~~~~~~~~~~~~~~~~~~ -# Import all necessary modules and launch an instance of Ansys Prime Server. -# From the PyPrimeMesh client, get the model. -# Instantiate meshing utilities from the ``lucid`` class. - -import os -import tempfile - -import ansys.meshing.prime as prime -from ansys.meshing.prime.graphics import PrimePlotter - -client = prime.launch_prime() -model = client.model - -mesh_util = prime.lucid.Mesh(model) - -############################################################################### -# Import CAD geometry -# ~~~~~~~~~~~~~~~~~~~ -# Download the wheel ground geometry (FMD) file exported by SpaceClaim. -# Import the CAD geometry. The geometry consists of two topo parts: a wheel and an enclosing box. -# Labels are defined for the ground topo face on the enclosure and for the wheel -# as all the topo faces of the wheel part. - -# For Windows OS users, SCDOC or DSCO is also available. For example: -# wheel_ground_file = prime.examples.download_wheel_ground_scdoc() - -wheel_ground_file = prime.examples.download_wheel_ground_fmd() - -mesh_util.read(wheel_ground_file) - -display = PrimePlotter() -display.plot(model, scope=prime.ScopeDefinition(model, label_expression="ground, wheel")) -display.show() - -print(model) - -############################################################################### -# Convert topo parts to mesh parts -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Convert the faceted geometry of the topology to mesh for all parts as the contact patch -# requires face zonelets from mesh parts as input. - -wheel_part = model.get_part_by_name("wheel_body") -enclosure_part = model.get_part_by_name("enclosure") - -[part.delete_topo_entities(prime.DeleteTopoEntitiesParams(model)) for part in model.parts] - -############################################################################### -# Create a contact patch -# ~~~~~~~~~~~~~~~~~~~~~~ -# To create a contact patch, a direction is needed to define the resulting shape of the patch. -# A new part is created containing the patch. -# A prefix can be specified for the label created for the contact patch face zonelets generated. -# The offset distance determines the thickness and extent of the patch. The source face zonelet is -# offset to intersect the planar target face and the intersection used to define the contact patch. -# Due to the depth of the treads on the wheel, 20.0 is used as the offset to reach the tire surface. -# If multiple contact regions are found, they can be merged by grouping them using the grouping -# tolerance distance. With a grouping tolerance of 4.0, separate contact regions are created for -# some of the treads of the tire, see the image at the top of the example. To merge these contact -# regions into a single patch, the grouping tolerance distance is increased to 20.0, avoiding small -# gaps between contact regions. - -# The face zonelets of the wheel are defined as the source. -# The planar surface must be specified as the target. -# In this instance, the ground provides the planar target. - -source = wheel_part.get_face_zonelets() -target = enclosure_part.get_face_zonelets_of_label_name_pattern( - "ground", prime.NamePatternParams(model) -) - -params = prime.CreateContactPatchParams( - model, - contact_patch_axis=prime.ContactPatchAxis.Z, - offset_distance=20.0, - grouping_tolerance=20.0, - suggested_label_prefix="patch", -) -result = prime.SurfaceUtilities(model).create_contact_patch( - source_zonelets=source, target_zonelets=target, params=params -) -print(result.error_code) -print(model) - -display = PrimePlotter() -display.plot(model, scope=prime.ScopeDefinition(model, label_expression="ground, patch*, wheel")) -display.show() - -############################################################################### -# Wrap the fluid region -# ~~~~~~~~~~~~~~~~~~~~~ -# The largest internal region in this instance is the fluid region around the wheel. -# Intersection loops are created to capture the features at the corners between the -# patch, ground, and wheel. - -model.set_global_sizing_params(prime.GlobalSizingParams(model, min=4.0, max=100.0, growth_rate=1.4)) - -# Create a size control to limit the size of mesh on the wheel. -size_control = model.control_data.create_size_control(prime.SizingType.SOFT) -size_control.set_soft_sizing_params(prime.SoftSizingParams(model=model, max=8.0)) -size_control.set_scope(prime.ScopeDefinition(model=model, label_expression="wheel")) - -wrap_part = mesh_util.wrap( - min_size=4.0, - max_size=100.0, - region_extract=prime.WrapRegion.LARGESTINTERNAL, - create_intersection_loops=True, - wrap_size_controls=[size_control], -) - -display = PrimePlotter() -display.plot( - model, scope=prime.ScopeDefinition(model, label_expression="ground, patch*, wheel"), update=True -) -display.show() - -print(model) - -############################################################################### -# Volume mesh -# ~~~~~~~~~~~ -# Apply five layers of prisms to the wheel, patch, and ground. Mesh with polyhedrals. - -model.set_global_sizing_params(prime.GlobalSizingParams(model, min=4.0, max=100.0, growth_rate=1.4)) -mesh_util.volume_mesh( - volume_fill_type=prime.VolumeFillType.POLY, - prism_layers=5.0, - prism_surface_expression="wheel, patch*, ground", - prism_volume_expression="*", - scope=prime.lucid.VolumeScope(part_expression=wrap_part.name), -) - -display = PrimePlotter() -display.plot( - model, - scope=prime.ScopeDefinition(model, label_expression="!front !side_right !top"), - update=True, -) -display.show() - -mesh_util.create_zones_from_labels() - -wrap_part._print_mesh = True -print(wrap_part) - -############################################################################### -# Write model -# ~~~~~~~~~~~ -# Write a CAS file for use in the Fluent solver. - -with tempfile.TemporaryDirectory() as temp_folder: - wheel_model = os.path.join(temp_folder, "wheel_ground_contact.cas.h5") - prime.FileIO(model).export_fluent_case( - wheel_model, - export_fluent_case_params=prime.ExportFluentCaseParams(model, cff_format=True), - ) - assert os.path.exists(wheel_model) - print(f"Fluent case exported at {wheel_model}") - -############################################################################### -# Exit the PyPrimeMesh session -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -client.exit() +# Copyright (C) 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. + +""" +.. _ref_contact_patch: + +======================================================================== +Create a contact patch for wrapping between a wheel and ground interface +======================================================================== + +**Summary**: This example demonstrates how to create a contact patch for use with wrapping +to avoid meshing into a narrow contact region between two objects. + +Objective +~~~~~~~~~ +This example uses a contact patch for wrapping to avoid the interface of a wheel with the ground +to improve mesh quality when growing prism layers in the region of the contacting faces. + +.. image:: ../../../images/contact_patch.png + :align: center + :width: 600 + +The preceding image shows the following: + + +* Top left: Wheel/ground interface +* Top right: Addition of contact patch +* Lower left: Grouping tolerance at 4 with multiple contact patches +* Lower right: Grouping tolerance at 20 with merged single contact patch + + + +Procedure +~~~~~~~~~ +#. Launch an Ansys Prime Server instance and instantiate the meshing utilities + from the ``lucid`` class. +#. Import the wheel ground geometry. +#. Convert the topo parts to mesh parts so that the contact patch can be created. +#. Create a contact patch between the wheel and the ground. +#. Extract the fluid region using wrapping. +#. Volume mesh with polyhedral and prism cells. +#. Write a CAS file for use in the Fluent solver. +#. Exit the PyPrimeMesh session. + + +""" + +############################################################################### +# Launch Ansys Prime Server +# ~~~~~~~~~~~~~~~~~~~~~~~~~ +# Import all necessary modules and launch an instance of Ansys Prime Server. +# From the PyPrimeMesh client, get the model. +# Instantiate meshing utilities from the ``lucid`` class. + +import os +import tempfile + +import ansys.meshing.prime as prime +from ansys.meshing.prime.graphics import PrimePlotter + +client = prime.launch_prime() +model = client.model + +mesh_util = prime.lucid.Mesh(model) + +############################################################################### +# Import CAD geometry +# ~~~~~~~~~~~~~~~~~~~ +# Download the wheel ground geometry (FMD) file exported by SpaceClaim. +# Import the CAD geometry. The geometry consists of two topo parts: a wheel and an enclosing box. +# Labels are defined for the ground topo face on the enclosure and for the wheel +# as all the topo faces of the wheel part. + +# For Windows OS users, SCDOC or DSCO is also available. For example: +# wheel_ground_file = prime.examples.download_wheel_ground_scdoc() + +wheel_ground_file = prime.examples.download_wheel_ground_fmd() + +mesh_util.read(wheel_ground_file) + + +################### +# Visualize results +# ================= +# .. code-block:: python +# +# display = PrimePlotter() +# display.plot(model, scope=prime.ScopeDefinition(model, label_expression="ground, wheel")) +# display.show() + +print(model) + +############################################################################### +# Convert topo parts to mesh parts +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# Convert the faceted geometry of the topology to mesh for all parts as the contact patch +# requires face zonelets from mesh parts as input. + +wheel_part = model.get_part_by_name("wheel_body") +enclosure_part = model.get_part_by_name("enclosure") + +[part.delete_topo_entities(prime.DeleteTopoEntitiesParams(model)) for part in model.parts] + +############################################################################### +# Create a contact patch +# ~~~~~~~~~~~~~~~~~~~~~~ +# To create a contact patch, a direction is needed to define the resulting shape of the patch. +# A new part is created containing the patch. +# A prefix can be specified for the label created for the contact patch face zonelets generated. +# The offset distance determines the thickness and extent of the patch. The source face zonelet is +# offset to intersect the planar target face and the intersection used to define the contact patch. +# Due to the depth of the treads on the wheel, 20.0 is used as the offset to reach the tire surface. +# If multiple contact regions are found, they can be merged by grouping them using the grouping +# tolerance distance. With a grouping tolerance of 4.0, separate contact regions are created for +# some of the treads of the tire, see the image at the top of the example. To merge these contact +# regions into a single patch, the grouping tolerance distance is increased to 20.0, avoiding small +# gaps between contact regions. + +# The face zonelets of the wheel are defined as the source. +# The planar surface must be specified as the target. +# In this instance, the ground provides the planar target. + +source = wheel_part.get_face_zonelets() +target = enclosure_part.get_face_zonelets_of_label_name_pattern( + "ground", prime.NamePatternParams(model) +) + +params = prime.CreateContactPatchParams( + model, + contact_patch_axis=prime.ContactPatchAxis.Z, + offset_distance=20.0, + grouping_tolerance=20.0, + suggested_label_prefix="patch", +) +result = prime.SurfaceUtilities(model).create_contact_patch( + source_zonelets=source, target_zonelets=target, params=params +) +print(result.error_code) +print(model) + +################### +# Visualize results +# ================= +# .. code-block:: python +# +# display = PrimePlotter() +# display.plot( +# model, scope=prime.ScopeDefinition(model, label_expression="ground, patch*, wheel") +# ) +# display.show() + +############################################################################### +# Wrap the fluid region +# ~~~~~~~~~~~~~~~~~~~~~ +# The largest internal region in this instance is the fluid region around the wheel. +# Intersection loops are created to capture the features at the corners between the +# patch, ground, and wheel. + +model.set_global_sizing_params(prime.GlobalSizingParams(model, min=4.0, max=100.0, growth_rate=1.4)) + +# Create a size control to limit the size of mesh on the wheel. +size_control = model.control_data.create_size_control(prime.SizingType.SOFT) +size_control.set_soft_sizing_params(prime.SoftSizingParams(model=model, max=8.0)) +size_control.set_scope(prime.ScopeDefinition(model=model, label_expression="wheel")) + +wrap_part = mesh_util.wrap( + min_size=4.0, + max_size=100.0, + region_extract=prime.WrapRegion.LARGESTINTERNAL, + create_intersection_loops=True, + wrap_size_controls=[size_control], +) + +######################### +# Open a pyvistaqt window +# ======================= +# .. code-block:: python +# +# display = PrimePlotter() +# display.plot( +# model, +# scope=prime.ScopeDefinition(model, label_expression="ground, patch*, wheel"), update=True +# ) +# display.show() + +print(model) + +############################################################################### +# Volume mesh +# ~~~~~~~~~~~ +# Apply five layers of prisms to the wheel, patch, and ground. Mesh with polyhedrals. + +model.set_global_sizing_params(prime.GlobalSizingParams(model, min=4.0, max=100.0, growth_rate=1.4)) +mesh_util.volume_mesh( + volume_fill_type=prime.VolumeFillType.POLY, + prism_layers=5.0, + prism_surface_expression="wheel, patch*, ground", + prism_volume_expression="*", + scope=prime.lucid.VolumeScope(part_expression=wrap_part.name), +) + +display = PrimePlotter() +display.plot( + model, + scope=prime.ScopeDefinition(model, label_expression="!front !side_right !top"), + update=True, +) +display.show() + +mesh_util.create_zones_from_labels() + +wrap_part._print_mesh = True +print(wrap_part) + +############################################################################### +# Write model +# ~~~~~~~~~~~ +# Write a CAS file for use in the Fluent solver. + +with tempfile.TemporaryDirectory() as temp_folder: + wheel_model = os.path.join(temp_folder, "wheel_ground_contact.cas.h5") + prime.FileIO(model).export_fluent_case( + wheel_model, + export_fluent_case_params=prime.ExportFluentCaseParams(model, cff_format=True), + ) + assert os.path.exists(wheel_model) + print(f"Fluent case exported at {wheel_model}") + +############################################################################### +# Exit the PyPrimeMesh session +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +client.exit() diff --git a/examples/gallery/11_solder_ball.py b/examples/gallery/11_solder_ball.py index 5814f39ae4..3f2605bc52 100644 --- a/examples/gallery/11_solder_ball.py +++ b/examples/gallery/11_solder_ball.py @@ -1,399 +1,399 @@ -# Copyright (C) 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. - -""" -.. _ref_solder_ball_mesh: - -================================= -Mesh a set of solder balls (beta) -================================= - -**Summary**: This example demonstrates how to mesh a set of solder balls with mainly -hexahedral elements. The solder is initially modelled as cylindrical to allow meshing -using stacker and then local match morph controls are applied to recover the -spherical shapes. - -.. note:: This example contains a beta API. The behavior and implementation may change in future. - -Objective -~~~~~~~~~~ -This example uses locally defined match morphing controls to morph a hexahedral mesh, -created using volume sweep, to many spherical solder balls. - -.. image:: ../../../images/solder_ball_mesh_cross_section.png - :align: center - :width: 800 - :alt: Mesh cross section of solder ball model. - -Procedure -~~~~~~~~~~ -#. Import libraries necessary to run the script. -#. Launch an Ansys Prime Server instance. -#. Import stackable simplified CAD geometry with refaceting. -#. Connect the geometry using scaffolding. -#. Mesh with hex dominant elements using the volume sweeper. -#. Import the target CAD geometry for the solders for match morphing. -#. Locally match morph the simplified mesh to the target spherical solders. -#. Delete the target and export the morphed mesh. -#. Exit the PyPrimeMesh session. - - -""" - -############################################################################### -# Import modules -# ~~~~~~~~~~~~~~ -# Import libraries necessary to run the script. - -import os -import tempfile - -import ansys.meshing.prime as prime -from ansys.meshing.prime.graphics import PrimePlotter - -############################################################################### -# Launch Ansys Prime Server -# ~~~~~~~~~~~~~~~~~~~~~~~~~ -# Launch an instance of Ansys Prime Server. -# Connect the PyPrimeMesh client and get the model. - -prime_client = prime.launch_prime() -model = prime_client.model - -############################################################################### -# Import CAD geometry -# ~~~~~~~~~~~~~~~~~~~ -# FMD is exported from SpaceClaim for the geometry. -# Geometry consists of multiple non overlapping and disconnected volumes. -# The model has multiple layers either side of several solder balls with pads -# and contains an infill volume around the solder. During import, the part -# creation type is set to BODY so that each body in the CAD is imported as a -# separate part. Refaceting is specified for more control of the scaffolding -# operation. Consistent faceting for the curved surfaces to be joined can be -# obtained by specifying CadRefacetingMaxEdgeSizeLimit as ABSOLUTE. To avoid -# over refinement of the faceting the max_edge_size is allowed to reach a size -# of 1.0. Labels can be assigned to manage the entities of each volume. - -solder_ball = prime.examples.download_solder_ball_fmd() - -params = prime.ImportCadParams( - model, - append=True, - part_creation_type=prime.PartCreationType.BODY, - refacet=True, - cad_refaceting_params=prime.CadRefacetingParams( - model, - cad_faceter=prime.CadFaceter.PARASOLID, - max_edge_size_limit=prime.CadRefacetingMaxEdgeSizeLimit.ABSOLUTE, - max_edge_size=1.0, - ), -) - -prime.FileIO(model).import_cad(file_name=solder_ball, params=params) - -for part in model.parts: - part.add_labels_on_topo_entities([part.name], part.get_topo_faces()) - part.add_labels_on_topo_entities([part.name], part.get_topo_volumes()) - -# Display the model without the infill so the cylindrical geometry of the solder -# is visible. - -display = PrimePlotter() -display.plot( - model, scope=prime.ScopeDefinition(model=model, label_expression="solder_cyl*,pad*,layer*") -) -display.show() - -############################################################################### -# Connect geometry -# ~~~~~~~~~~~~~~~~ -# Merge all parts into a single part so they can be connected. -# Imprint adjacent topo faces by connecting topo faces and topo edges. -# Mesh all topo faces to allow splitting the imprinted topo faces by mesh regions. -# Delete mesh on topo faces after splitting. -# Merge newly created overlapping topo faces so only a single topo face exists -# between connected volumes. - -part_ids = [part.id for part in model.parts if part.get_topo_faces()] -merge_result = model.merge_parts(part_ids=part_ids, params=prime.MergePartsParams(model)) -print(model) - -merged_part = model.get_part(merge_result.merged_part_id) - -params = prime.ScaffolderParams( - model=model, - absolute_dist_tol=0.01, - intersection_control_mask=prime.IntersectionMask.FACEFACEANDEDGEEDGE, - constant_mesh_size=0.1, -) - -scaffolder = prime.Scaffolder(model, merged_part.id) -res = scaffolder.scaffold_topo_faces_and_beams( - topo_faces=merged_part.get_topo_faces(), topo_beams=[], params=params -) - -prime.lucid.Mesh(model).surface_mesh(min_size=0.1) -prime.Scaffolder(model, merged_part.id).split_topo_faces_by_mesh_region( - merged_part.get_topo_faces() -) - -# This is a beta API. The behavior and implementation may change in future. -result = model.topo_data.delete_mesh_on_topo_faces( - merged_part.get_topo_faces(), prime.DeleteMeshParams(model=model) -) - -scaffolder.merge_overlapping_topo_faces(merged_part.get_topo_faces(), params) - -display = PrimePlotter() -display.plot(model, update=True) -display.show() - -############################################################################### -# Volume sweeper -# ~~~~~~~~~~~~~~ -# Setup a size control to refine the mesh around the solder. -# Setup stacker parameters to define the volume sweep mesh. -# Create the base face to quad surface mesh and use for sweeping. -# Stack the base face to create the volume mesh. -# Delete topology on mesh part to allow use of surface utilities and feature extraction. -# A large lateral defeature tolerance of 0.1 is used to avoid additional topo nodes -# from scaffolding impacting the final mesh. - -model.set_global_sizing_params(prime.GlobalSizingParams(model, min=0.1, max=0.4)) - -size_control = model.control_data.create_size_control(prime.SizingType.SOFT) -size_control.set_scope( - prime.ScopeDefinition(model, part_expression=merged_part.name, label_expression="*solder*") -) -size_control.set_soft_sizing_params(prime.SoftSizingParams(model, max=0.1)) - -stacker_params = prime.MeshStackerParams( - model=model, - direction=[0, 1, 0], - max_offset_size=0.4, - delete_base=True, - lateral_defeature_tolerance=0.1, - stacking_defeature_tolerance=0.01, - size_control_ids=[size_control.id], -) - -sweeper = prime.VolumeSweeper(model) - -createbase_results = sweeper.create_base_face( - part_id=merged_part.id, - topo_volume_ids=merged_part.get_topo_volumes(), - params=stacker_params, -) - -base_faces = createbase_results.base_face_ids -merged_part.add_labels_on_topo_entities(["base_faces"], base_faces) - -scope = prime.ScopeDefinition(model=model, label_expression="base_faces") - -base_scope = prime.lucid.SurfaceScope( - entity_expression="base_faces", - part_expression=merged_part.name, - scope_evaluation_type=prime.ScopeEvaluationType.LABELS, -) - -prime.lucid.Mesh(model).surface_mesh( - min_size=0.1, max_size=0.4, scope=base_scope, generate_quads=True -) - -display = PrimePlotter() -display.plot(model, update=True) -display.show() - -stackbase_results = sweeper.stack_base_face( - part_id=merged_part.id, - base_face_ids=base_faces, - topo_volume_ids=merged_part.get_topo_volumes(), - params=stacker_params, -) - -merged_part.delete_topo_entities( - prime.DeleteTopoEntitiesParams(model, delete_geom_zonelets=True, delete_mesh_zonelets=False) -) - -merged_part._print_mesh = True -print(merged_part) - -display = PrimePlotter() -display.plot(model, update=True) -display.show() - -############################################################################### -# Import sphere geometry for match morphing -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create a single part on CAD import by setting the part creation type to MODEL. -# Convert topology to mesh face zonelets to use surface utilities. - -solder_ball_target = prime.examples.download_solder_ball_target_fmd() - -params = prime.ImportCadParams(model, append=True, part_creation_type=prime.PartCreationType.MODEL) - -prime.FileIO(model).import_cad(file_name=solder_ball_target, params=params) - -imported_cad_part_ids = [part.id for part in model.parts if part.get_topo_faces()] -target_part = model.get_part(imported_cad_part_ids[0]) - -display = PrimePlotter() -display.plot(model, scope=prime.ScopeDefinition(model, part_expression=target_part.name)) -display.show() - -print(model) - -target_part.delete_topo_entities( - prime.DeleteTopoEntitiesParams(model, delete_geom_zonelets=False, delete_mesh_zonelets=False) -) - -############################################################################### -# Match morph the mesh to the spherical solder -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Delete feature edge zonelets on the mesh source. -# Get lists of the source and target face zonelets for the match morph. -# Pair source and target faces based on overlapping bounding box locations. -# For each source face extract feature edges with nodes attached to faces. -# Define edge pairs for each match pair control as a morph boundary condition. -# Match morph solder faces and edges. -# Delete target sphere part and retain the morphed mesh for export. - -merged_part.delete_zonelets(merged_part.get_edge_zonelets()) -cylinder_faces = merged_part.get_face_zonelets_of_label_name_pattern( - "solder_cyl", prime.NamePatternParams(model) -) -sphere_faces = target_part.get_face_zonelets() - -match_pairs = [] - -# When the match morph operation is defined by multiple -# separate local match morph source and targets, -# individual match pairs must be specified for each -# contiguous set of face zonelets. -# Separate one to one boundary condition pairs for the -# connected edge zonelets must also be defined for -# each face match pair to ensure the edges remain rigid. - -# For each solder to be morphed we have a match pair -# for the source and target face zonelets. To ensure -# the edge zonelets remain rigid during the morph, -# boundary condition pairs for each of the two edge -# zonelets need to be created. - -# For each cylindrical face zonelet on the source -# mesh find the corresponding target sphere face zonelet -# to match morph the mesh using the position of their bounding box. -# Labels defined in the CAD model would be a more efficient way to pair. -tolerance = 0.2 -for face in cylinder_faces: - box = prime.SurfaceUtilities(model).get_bounding_box_of_zonelets([face]) - for i in range(len(sphere_faces)): - sphere_box = prime.SurfaceUtilities(model).get_bounding_box_of_zonelets([sphere_faces[i]]) - if ( - (abs(sphere_box.xmin - box.xmin) < tolerance) - and (abs(sphere_box.ymin - box.ymin) < tolerance) - and (abs(sphere_box.zmin - box.zmin) < tolerance) - and (abs(sphere_box.xmax - box.xmax) < tolerance) - and (abs(sphere_box.ymax - box.ymax) < tolerance) - and (abs(sphere_box.zmax - box.zmax) < tolerance) - ): - break - elif i == len(sphere_faces) - 1: - # if no target face is found by the final pass then return an error - prime.PrimeRuntimeError(message=f"Target sphere not found for face {face}.") - match_pair = prime.MatchPair( - model, - source_surfaces=[face], - target_surfaces=[sphere_faces[i]], - target_type=prime.MatchPairTargetType.FACEZONELET, - ) - bc_pairs = [] - - # Once each source and target face zonelet are found and paired, edge zonelets - # are created and used as boundary conditions to ensure they remain rigid. - # Creating edge zonelets not disconnected from the faces ensures that the edge - # zonelet will act as a boundary condition for the morph operation. - # Extracting the edge zonelets from the source face zonelet and using - # them to define both source and target boundary conditions ensures - # the edge zonelets of the solder remain rigid. - result = prime.FeatureExtraction(model).extract_features_on_face_zonelets( - merged_part.id, - [face], - prime.ExtractFeatureParams( - model, - feature_angle=60, - separate_features=True, - separation_angle=60, - replace=False, - disconnect_with_faces=False, - ), - ) - for edge in result.new_edge_zonelets: - bc_pair = prime.BCPair( - model, - int(edge), - int(edge), - type=prime.BCPairType.EDGE, - ) - bc_pairs.append(bc_pair) - match_pair.bc_pairs = bc_pairs - match_pairs.append(match_pair) - -morph = prime.Morpher(model) - -morph_params = prime.MatchMorphParams(model) -bc_params = prime.MorphBCParams(model, morphable_layers=0) -solve_params = prime.MorphSolveParams(model) - -morph.match_morph( - part_id=merged_part.id, - match_pairs=match_pairs, - match_morph_params=morph_params, - bc_params=bc_params, - solve_params=solve_params, -) - -model.delete_parts([target_part.id]) - -display = PrimePlotter() -display.plot( - model, scope=prime.ScopeDefinition(model=model, label_expression="solder*"), update=True -) -display.show() - -############################################################################### -# Export mesh -# ~~~~~~~~~~~ -# Export a CDB file. - -with tempfile.TemporaryDirectory() as temp_folder: - mesh_file = os.path.join(temp_folder, "solder_balls.cdb") - params = prime.ExportMapdlCdbParams(model=model) - prime.FileIO(model=model).export_mapdl_cdb(file_name=mesh_file, params=params) - assert os.path.exists(mesh_file) - print("\nExported file:\n", mesh_file) - -############################################################################### -# Exit PyPrimeMesh -# ~~~~~~~~~~~~~~~~ - -prime_client.exit() +# Copyright (C) 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. + +""" +.. _ref_solder_ball_mesh: + +================================= +Mesh a set of solder balls (beta) +================================= + +**Summary**: This example demonstrates how to mesh a set of solder balls with mainly +hexahedral elements. The solder is initially modelled as cylindrical to allow meshing +using stacker and then local match morph controls are applied to recover the +spherical shapes. + +.. note:: This example contains a beta API. The behavior and implementation may change in future. + +Objective +~~~~~~~~~~ +This example uses locally defined match morphing controls to morph a hexahedral mesh, +created using volume sweep, to many spherical solder balls. + +.. image:: ../../../images/solder_ball_mesh_cross_section.png + :align: center + :width: 800 + :alt: Mesh cross section of solder ball model. + +Procedure +~~~~~~~~~~ +#. Import libraries necessary to run the script. +#. Launch an Ansys Prime Server instance. +#. Import stackable simplified CAD geometry with refaceting. +#. Connect the geometry using scaffolding. +#. Mesh with hex dominant elements using the volume sweeper. +#. Import the target CAD geometry for the solders for match morphing. +#. Locally match morph the simplified mesh to the target spherical solders. +#. Delete the target and export the morphed mesh. +#. Exit the PyPrimeMesh session. + + +""" + +############################################################################### +# Import modules +# ~~~~~~~~~~~~~~ +# Import libraries necessary to run the script. + +import os +import tempfile + +import ansys.meshing.prime as prime +from ansys.meshing.prime.graphics import PrimePlotter + +############################################################################### +# Launch Ansys Prime Server +# ~~~~~~~~~~~~~~~~~~~~~~~~~ +# Launch an instance of Ansys Prime Server. +# Connect the PyPrimeMesh client and get the model. + +prime_client = prime.launch_prime() +model = prime_client.model + +############################################################################### +# Import CAD geometry +# ~~~~~~~~~~~~~~~~~~~ +# FMD is exported from SpaceClaim for the geometry. +# Geometry consists of multiple non overlapping and disconnected volumes. +# The model has multiple layers either side of several solder balls with pads +# and contains an infill volume around the solder. During import, the part +# creation type is set to BODY so that each body in the CAD is imported as a +# separate part. Refaceting is specified for more control of the scaffolding +# operation. Consistent faceting for the curved surfaces to be joined can be +# obtained by specifying CadRefacetingMaxEdgeSizeLimit as ABSOLUTE. To avoid +# over refinement of the faceting the max_edge_size is allowed to reach a size +# of 1.0. Labels can be assigned to manage the entities of each volume. + +solder_ball = prime.examples.download_solder_ball_fmd() + +params = prime.ImportCadParams( + model, + append=True, + part_creation_type=prime.PartCreationType.BODY, + refacet=True, + cad_refaceting_params=prime.CadRefacetingParams( + model, + cad_faceter=prime.CadFaceter.PARASOLID, + max_edge_size_limit=prime.CadRefacetingMaxEdgeSizeLimit.ABSOLUTE, + max_edge_size=1.0, + ), +) + +prime.FileIO(model).import_cad(file_name=solder_ball, params=params) + +for part in model.parts: + part.add_labels_on_topo_entities([part.name], part.get_topo_faces()) + part.add_labels_on_topo_entities([part.name], part.get_topo_volumes()) + +# Display the model without the infill so the cylindrical geometry of the solder +# is visible. + +display = PrimePlotter() +display.plot( + model, scope=prime.ScopeDefinition(model=model, label_expression="solder_cyl*,pad*,layer*") +) +display.show() + +############################################################################### +# Connect geometry +# ~~~~~~~~~~~~~~~~ +# Merge all parts into a single part so they can be connected. +# Imprint adjacent topo faces by connecting topo faces and topo edges. +# Mesh all topo faces to allow splitting the imprinted topo faces by mesh regions. +# Delete mesh on topo faces after splitting. +# Merge newly created overlapping topo faces so only a single topo face exists +# between connected volumes. + +part_ids = [part.id for part in model.parts if part.get_topo_faces()] +merge_result = model.merge_parts(part_ids=part_ids, params=prime.MergePartsParams(model)) +print(model) + +merged_part = model.get_part(merge_result.merged_part_id) + +params = prime.ScaffolderParams( + model=model, + absolute_dist_tol=0.01, + intersection_control_mask=prime.IntersectionMask.FACEFACEANDEDGEEDGE, + constant_mesh_size=0.1, +) + +scaffolder = prime.Scaffolder(model, merged_part.id) +res = scaffolder.scaffold_topo_faces_and_beams( + topo_faces=merged_part.get_topo_faces(), topo_beams=[], params=params +) + +prime.lucid.Mesh(model).surface_mesh(min_size=0.1) +prime.Scaffolder(model, merged_part.id).split_topo_faces_by_mesh_region( + merged_part.get_topo_faces() +) + +# This is a beta API. The behavior and implementation may change in future. +result = model.topo_data.delete_mesh_on_topo_faces( + merged_part.get_topo_faces(), prime.DeleteMeshParams(model=model) +) + +scaffolder.merge_overlapping_topo_faces(merged_part.get_topo_faces(), params) + +display = PrimePlotter() +display.plot(model, update=True) +display.show() + +############################################################################### +# Volume sweeper +# ~~~~~~~~~~~~~~ +# Setup a size control to refine the mesh around the solder. +# Setup stacker parameters to define the volume sweep mesh. +# Create the base face to quad surface mesh and use for sweeping. +# Stack the base face to create the volume mesh. +# Delete topology on mesh part to allow use of surface utilities and feature extraction. +# A large lateral defeature tolerance of 0.1 is used to avoid additional topo nodes +# from scaffolding impacting the final mesh. + +model.set_global_sizing_params(prime.GlobalSizingParams(model, min=0.1, max=0.4)) + +size_control = model.control_data.create_size_control(prime.SizingType.SOFT) +size_control.set_scope( + prime.ScopeDefinition(model, part_expression=merged_part.name, label_expression="*solder*") +) +size_control.set_soft_sizing_params(prime.SoftSizingParams(model, max=0.1)) + +stacker_params = prime.MeshStackerParams( + model=model, + direction=[0, 1, 0], + max_offset_size=0.4, + delete_base=True, + lateral_defeature_tolerance=0.1, + stacking_defeature_tolerance=0.01, + size_control_ids=[size_control.id], +) + +sweeper = prime.VolumeSweeper(model) + +createbase_results = sweeper.create_base_face( + part_id=merged_part.id, + topo_volume_ids=merged_part.get_topo_volumes(), + params=stacker_params, +) + +base_faces = createbase_results.base_face_ids +merged_part.add_labels_on_topo_entities(["base_faces"], base_faces) + +scope = prime.ScopeDefinition(model=model, label_expression="base_faces") + +base_scope = prime.lucid.SurfaceScope( + entity_expression="base_faces", + part_expression=merged_part.name, + scope_evaluation_type=prime.ScopeEvaluationType.LABELS, +) + +prime.lucid.Mesh(model).surface_mesh( + min_size=0.1, max_size=0.4, scope=base_scope, generate_quads=True +) + +display = PrimePlotter() +display.plot(model, update=True) +display.show() + +stackbase_results = sweeper.stack_base_face( + part_id=merged_part.id, + base_face_ids=base_faces, + topo_volume_ids=merged_part.get_topo_volumes(), + params=stacker_params, +) + +merged_part.delete_topo_entities( + prime.DeleteTopoEntitiesParams(model, delete_geom_zonelets=True, delete_mesh_zonelets=False) +) + +merged_part._print_mesh = True +print(merged_part) + +display = PrimePlotter() +display.plot(model, update=True) +display.show() + +############################################################################### +# Import sphere geometry for match morphing +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# Create a single part on CAD import by setting the part creation type to MODEL. +# Convert topology to mesh face zonelets to use surface utilities. + +solder_ball_target = prime.examples.download_solder_ball_target_fmd() + +params = prime.ImportCadParams(model, append=True, part_creation_type=prime.PartCreationType.MODEL) + +prime.FileIO(model).import_cad(file_name=solder_ball_target, params=params) + +imported_cad_part_ids = [part.id for part in model.parts if part.get_topo_faces()] +target_part = model.get_part(imported_cad_part_ids[0]) + +display = PrimePlotter() +display.plot(model, scope=prime.ScopeDefinition(model, part_expression=target_part.name)) +display.show() + +print(model) + +target_part.delete_topo_entities( + prime.DeleteTopoEntitiesParams(model, delete_geom_zonelets=False, delete_mesh_zonelets=False) +) + +############################################################################### +# Match morph the mesh to the spherical solder +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# Delete feature edge zonelets on the mesh source. +# Get lists of the source and target face zonelets for the match morph. +# Pair source and target faces based on overlapping bounding box locations. +# For each source face extract feature edges with nodes attached to faces. +# Define edge pairs for each match pair control as a morph boundary condition. +# Match morph solder faces and edges. +# Delete target sphere part and retain the morphed mesh for export. + +merged_part.delete_zonelets(merged_part.get_edge_zonelets()) +cylinder_faces = merged_part.get_face_zonelets_of_label_name_pattern( + "solder_cyl", prime.NamePatternParams(model) +) +sphere_faces = target_part.get_face_zonelets() + +match_pairs = [] + +# When the match morph operation is defined by multiple +# separate local match morph source and targets, +# individual match pairs must be specified for each +# contiguous set of face zonelets. +# Separate one to one boundary condition pairs for the +# connected edge zonelets must also be defined for +# each face match pair to ensure the edges remain rigid. + +# For each solder to be morphed we have a match pair +# for the source and target face zonelets. To ensure +# the edge zonelets remain rigid during the morph, +# boundary condition pairs for each of the two edge +# zonelets need to be created. + +# For each cylindrical face zonelet on the source +# mesh find the corresponding target sphere face zonelet +# to match morph the mesh using the position of their bounding box. +# Labels defined in the CAD model would be a more efficient way to pair. +tolerance = 0.2 +for face in cylinder_faces: + box = prime.SurfaceUtilities(model).get_bounding_box_of_zonelets([face]) + for i in range(len(sphere_faces)): + sphere_box = prime.SurfaceUtilities(model).get_bounding_box_of_zonelets([sphere_faces[i]]) + if ( + (abs(sphere_box.xmin - box.xmin) < tolerance) + and (abs(sphere_box.ymin - box.ymin) < tolerance) + and (abs(sphere_box.zmin - box.zmin) < tolerance) + and (abs(sphere_box.xmax - box.xmax) < tolerance) + and (abs(sphere_box.ymax - box.ymax) < tolerance) + and (abs(sphere_box.zmax - box.zmax) < tolerance) + ): + break + elif i == len(sphere_faces) - 1: + # if no target face is found by the final pass then return an error + prime.PrimeRuntimeError(message=f"Target sphere not found for face {face}.") + match_pair = prime.MatchPair( + model, + source_surfaces=[face], + target_surfaces=[sphere_faces[i]], + target_type=prime.MatchPairTargetType.FACEZONELET, + ) + bc_pairs = [] + + # Once each source and target face zonelet are found and paired, edge zonelets + # are created and used as boundary conditions to ensure they remain rigid. + # Creating edge zonelets not disconnected from the faces ensures that the edge + # zonelet will act as a boundary condition for the morph operation. + # Extracting the edge zonelets from the source face zonelet and using + # them to define both source and target boundary conditions ensures + # the edge zonelets of the solder remain rigid. + result = prime.FeatureExtraction(model).extract_features_on_face_zonelets( + merged_part.id, + [face], + prime.ExtractFeatureParams( + model, + feature_angle=60, + separate_features=True, + separation_angle=60, + replace=False, + disconnect_with_faces=False, + ), + ) + for edge in result.new_edge_zonelets: + bc_pair = prime.BCPair( + model, + int(edge), + int(edge), + type=prime.BCPairType.EDGE, + ) + bc_pairs.append(bc_pair) + match_pair.bc_pairs = bc_pairs + match_pairs.append(match_pair) + +morph = prime.Morpher(model) + +morph_params = prime.MatchMorphParams(model) +bc_params = prime.MorphBCParams(model, morphable_layers=0) +solve_params = prime.MorphSolveParams(model) + +morph.match_morph( + part_id=merged_part.id, + match_pairs=match_pairs, + match_morph_params=morph_params, + bc_params=bc_params, + solve_params=solve_params, +) + +model.delete_parts([target_part.id]) + +display = PrimePlotter() +display.plot( + model, scope=prime.ScopeDefinition(model=model, label_expression="solder*"), update=True +) +display.show() + +############################################################################### +# Export mesh +# ~~~~~~~~~~~ +# Export a CDB file. + +with tempfile.TemporaryDirectory() as temp_folder: + mesh_file = os.path.join(temp_folder, "solder_balls.cdb") + params = prime.ExportMapdlCdbParams(model=model) + prime.FileIO(model=model).export_mapdl_cdb(file_name=mesh_file, params=params) + assert os.path.exists(mesh_file) + print("\nExported file:\n", mesh_file) + +############################################################################### +# Exit PyPrimeMesh +# ~~~~~~~~~~~~~~~~ + +prime_client.exit() diff --git a/pyproject.toml b/pyproject.toml index 32893e9d00..0bfa448c77 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,7 +46,7 @@ doc = [ "numpydoc==1.8.0", "sphinx==8.1.3", "sphinx_design==0.6.1", - "pyvista==0.44.1", + "pyvista==0.44.2", "sphinx-autodoc-typehints==2.5.0", "sphinx-copybutton==0.5.2", "sphinx-gallery==0.18.0", diff --git a/src/ansys/meshing/prime/internals/client.py b/src/ansys/meshing/prime/internals/client.py index 076ed65fb3..8f9fc69be5 100644 --- a/src/ansys/meshing/prime/internals/client.py +++ b/src/ansys/meshing/prime/internals/client.py @@ -75,6 +75,7 @@ def __init__( self._local = local self._process = server_process self._comm = None + self._has_exited = False if not local: try: from ansys.meshing.prime.internals.grpc_communicator import ( @@ -111,6 +112,10 @@ def __init__( f'Failed to load prime_communicator with message: {err.msg}' ) + def __del__(self): + """In case unexpected termination, close the connection.""" + self.exit() + @property def model(self): """Get model associated with the client.""" @@ -154,25 +159,27 @@ def exit(self): >>> print(result) >>> prime_client.exit() # Sever connection with server and kill the server. """ - if self._comm is not None: - self._comm.close() - self._comm = None - if self._process is not None: - assert self._local == False - terminate_process(self._process) - self._process = None - - if config.using_container(): - container_name = getattr(self, 'container_name') - utils.stop_prime_github_container(container_name) - elif config.has_pim(): - self.remote_instance.delete() - self.pim_client.close() - - clear_examples = bool(int(os.environ.get('PYPRIMEMESH_CLEAR_EXAMPLES', '1'))) - if clear_examples: - download_manager = examples.DownloadManager() - download_manager.clear_download_cache() + if not self._has_exited: + if self._comm is not None: + self._comm.close() + self._comm = None + if self._process is not None: + assert self._local == False + terminate_process(self._process) + self._process = None + + if config.using_container(): + container_name = getattr(self, 'container_name') + utils.stop_prime_github_container(container_name) + elif config.has_pim(): + self.remote_instance.delete() + self.pim_client.close() + + clear_examples = bool(int(os.environ.get('PYPRIMEMESH_CLEAR_EXAMPLES', '1'))) + if clear_examples: + download_manager = examples.DownloadManager() + download_manager.clear_download_cache() + self._has_exited = True def __enter__(self): """Open client."""