diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index fb01cb47cc..4d8945ab64 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -458,7 +458,7 @@ jobs: run: | . /env/bin/activate uv pip install --upgrade pip - uv pip install -e .[tests] + uv pip install -e .[tests, graphics] - name: Unit Testing and coverage env: diff --git a/doc/changelog.d/1507.added.md b/doc/changelog.d/1507.added.md new file mode 100644 index 0000000000..abe2e0b421 --- /dev/null +++ b/doc/changelog.d/1507.added.md @@ -0,0 +1 @@ +Add helper methods diff --git a/doc/changelog.d/whatsnew.yml b/doc/changelog.d/whatsnew.yml index d6aeecb5ab..ad5919f1b3 100644 --- a/doc/changelog.d/whatsnew.yml +++ b/doc/changelog.d/whatsnew.yml @@ -1,4 +1,36 @@ fragments: +- title: Helper functions for common workflows + version: 0.12.3 + content: | + New helper functions simplify common Mechanical workflows through the + `Helpers` class. + These methods streamline geometry import, material management, visualization, and export operations. + + Available helper functions: + + - **import_geometry()**: Import CAD geometry with flexible options + - **import_materials()**: Load material definitions from XML files + - **export_image()**: Export high-quality images of the current view + - **export_animation()**: Create animations in multiple formats (GIF, MP4, AVI, WMV) + - **display_image()**: Show exported images inline in notebooks + + Usage: + .. code:: python + + from ansys.mechanical.core import App + + app = App() + helpers = app.helpers + + # Import geometry and materials + helpers.import_geometry("bracket.x_t") + helpers.import_materials("steel_aluminum.xml") + + # Configure and export visualization + helpers.export_image("result.png", width=1920, height=1080) + + For complete details, see the `Helpers User Guide `_. + - title: gRPC Security with mTLS, WNUA, and Insecure Transport Modes version: 0.12.0 content: | diff --git a/doc/source/conf.py b/doc/source/conf.py index 86aecd0742..a31c21dd77 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -203,6 +203,7 @@ # Files to ignore "ignore_pattern": "flycheck*", # noqa: E501 "thumbnail_size": (350, 350), + "matplotlib_animations": True, } # -- Options for HTML output ------------------------------------------------- diff --git a/doc/source/user_guide/embedding/helpers.rst b/doc/source/user_guide/embedding/helpers.rst new file mode 100644 index 0000000000..ed876766d3 --- /dev/null +++ b/doc/source/user_guide/embedding/helpers.rst @@ -0,0 +1,329 @@ +.. _ref_embedding_user_guide_helpers: + +Helpers methods +=============== + +The `Helpers <../api/ansys/mechanical/core/embedding/helpers/Helpers.html#ansys.mechanical.core.embedding.helpers.Helpers>`_ class provides +convenient utility methods for common Mechanical operations. These helpers simplify tasks such as +importing geometry and materials, exporting images and animations, configuring views, and +visualizing the project tree structure. + +The Helpers class is accessible through the ``helpers`` attribute of the +`App <../api/ansys/mechanical/core/embedding/app/App.html#ansys.mechanical.core.embedding.app.App>`_ instance: + +.. code:: python + + from ansys.mechanical.core import App + + app = App(globals=globals()) + helpers = app.helpers + +All helper methods are designed to work seamlessly with the embedded Mechanical instance and +provide clear error messages when operations fail. + + +Importing geometry +------------------ + +The ``import_geometry()`` method simplifies importing geometry files into your Mechanical model. +It supports various CAD formats and provides options for processing named selections, material +properties, and coordinate systems. + +**Basic geometry import** + +.. code:: python + + from ansys.mechanical.core import App + + app = App(globals=globals()) + geometry_import = app.helpers.import_geometry("path/to/geometry.x_t") + +**Import with named selections** + +Process named selections from the geometry file: + +.. code:: python + + geometry_import = app.helpers.import_geometry( + "path/to/geometry.pmdb", + process_named_selections=True, + named_selection_key="NS" + ) + +**Import with all options** + +Import geometry with material properties and coordinate systems: + +.. code:: python + + geometry_import = app.helpers.import_geometry( + "path/to/geometry.step", + process_named_selections=True, + named_selection_key="", + process_material_properties=True, + process_coordinate_systems=True, + ) + +**2D analysis** + +For 2D analyses, specify the analysis type using the ``GeometryImportPreference`` enum. +``GeometryImportPreference`` is automatically available as a global variable when +``App(globals=globals())`` is used: + +.. code:: python + + geometry_import = app.helpers.import_geometry( + "path/to/geometry.agdb", + analysis_type=GeometryImportPreference.AnalysisType.Type2D + ) + +.. note:: + The method automatically determines the geometry format and returns the geometry import object. + If the import fails, a ``RuntimeError`` is raised with details about the failure. + +Importing materials +------------------- + +The ``import_materials()`` method imports materials from XML material database files. + +.. code:: python + + from ansys.mechanical.core import App + + app = App(globals=globals()) + app.helpers.import_materials("path/to/materials.xml") + +This method adds the materials to the ``Model.Materials`` collection. If the import fails, +a ``RuntimeError`` is raised. + +Exporting images +---------------- + +The ``export_image()`` method exports high-quality images of your model, geometry, mesh, or results. +It provides extensive control over image resolution, format, and appearance. + +**Basic image export** + +.. code:: python + + from ansys.mechanical.core import App + + app = App(globals=globals()) + # Import geometry first + app.helpers.import_geometry("path/to/geometry.x_t") + + # Export an image of the geometry + app.helpers.export_image( + obj=app.Model.Geometry, + file_path="geometry_image.png" + ) + +**Custom image settings** + +Control image dimensions, resolution, background, and format using Mechanical enums: + +.. code:: python + + from ansys.mechanical.core.embedding.enum_importer import ( + GraphicsBackgroundType, + GraphicsImageExportFormat, + GraphicsResolutionType, + ) + + app.helpers.export_image( + obj=app.Model.Geometry, + file_path="custom_image.jpg", + width=1920, + height=1080, + background=GraphicsBackgroundType.GraphicsAppearanceSetting, + resolution=GraphicsResolutionType.HighResolution, + image_format=GraphicsImageExportFormat.JPG, + ) + +**Export current graphics display** + +Export whatever is currently displayed without specifying an object: + +.. code:: python + + app.helpers.export_image( + file_path="current_view.png", + current_graphics_display=True + ) + +**Supported image formats (``GraphicsImageExportFormat``)** + +- ``GraphicsImageExportFormat.PNG`` - Recommended for technical documentation (lossless, default) +- ``GraphicsImageExportFormat.JPG`` - Good for photographs and presentations +- ``GraphicsImageExportFormat.BMP`` - Uncompressed bitmap +- ``GraphicsImageExportFormat.TIF`` - Tagged image format (lossless) +- ``GraphicsImageExportFormat.EPS`` - Vector format for publications + +**Resolution options (``GraphicsResolutionType``)** + +- ``GraphicsResolutionType.NormalResolution`` - 1:1 pixel ratio (fastest) +- ``GraphicsResolutionType.EnhancedResolution`` - 2:1 pixel ratio (default, good quality) +- ``GraphicsResolutionType.HighResolution`` - 4:1 pixel ratio (best quality, slower) + +Exporting animations +-------------------- + +The ``export_animation()`` method exports animations of results that have multiple time steps +or mode shapes, such as transient analyses or modal analyses. + +.. code:: python + + from ansys.mechanical.core import App + + app = App(globals=globals()) + # Set up and solve an analysis... + analysis = app.Model.AddStaticStructuralAnalysis() + result = analysis.Solution.AddTotalDeformation() + + # After solving, export animation + app.helpers.export_animation( + obj=result, + file_path="deformation.gif" + ) + +**Custom animation settings** + +.. code:: python + + from ansys.mechanical.core.embedding.enum_importer import GraphicsAnimationExportFormat + + app.helpers.export_animation( + obj=result, + file_path="deformation.mp4", + width=1920, + height=1080, + animation_format=GraphicsAnimationExportFormat.MP4, + ) + +**Supported animation formats (``GraphicsAnimationExportFormat``)** + +- ``GraphicsAnimationExportFormat.GIF`` - Widely supported, good for web (default) +- ``GraphicsAnimationExportFormat.AVI`` - Uncompressed video +- ``GraphicsAnimationExportFormat.MP4`` - Compressed video, good for presentations +- ``GraphicsAnimationExportFormat.WMV`` - Windows Media Video format + +.. note:: + Animation export requires that the result has been solved and has multiple steps or modes + to animate. If no ``obj`` is provided, the method falls back to the first active object in + the tree. Attempting to export an animation of unsolved results raises a ``RuntimeError``. + + +Displaying images +----------------- + +The ``display_image()`` method uses ``matplotlib`` to display exported images directly in your +Python environment. This is particularly useful in Jupyter notebooks or interactive sessions. + +.. code:: python + + from ansys.mechanical.core import App + + app = App(globals=globals()) + # Export an image + app.helpers.export_image( + obj=app.Model.Geometry, + file_path="geometry.png" + ) + + # Display it + app.helpers.display_image("geometry.png") + +**Custom display settings** + +.. code:: python + + app.helpers.display_image( + "geometry.png", + figsize=(12, 8), # Figure size in inches + axis="off" # Hide axes completely + ) + +Workflow examples +----------------- + +**Complete geometry visualization workflow** + +.. code:: python + + from ansys.mechanical.core import App + + app = App(globals=globals()) + + # Step 1: Import geometry + app.helpers.import_geometry("bracket.x_t") + + # Step 2: Import materials + app.helpers.import_materials("materials.xml") + + # Step 3: Export image + from ansys.mechanical.core.embedding.enum_importer import GraphicsResolutionType + + app.helpers.export_image( + obj=app.Model.Geometry, + file_path="bracket_iso.png", + width=1920, + height=1080, + resolution=GraphicsResolutionType.EnhancedResolution, + ) + + # Step 4: Display it + app.helpers.display_image("bracket_iso.png") + + + +Error handling +-------------- + +All helper methods raise descriptive exceptions when operations fail: + +**Geometry import errors** + +.. code:: python + + from ansys.mechanical.core import App + + app = App(globals=globals()) + try: + app.helpers.import_geometry("nonexistent.x_t") + except RuntimeError as e: + print(f"Geometry import failed: {e}") + +**Image export errors** + +.. code:: python + + try: + app.helpers.export_image( + obj=app.Model.Geometry, + file_path=None # Missing required parameter + ) + except ValueError as e: + print(f"Invalid parameter: {e}") + +Best practices +-------------- + +1. **Always check paths**: Use absolute paths or ``pathlib.Path`` objects for file operations + to avoid path-related errors. + + +2. **Use appropriate image formats**: PNG for technical documentation, JPG for presentations, + EPS for publications. + +3. **Handle errors gracefully**: Wrap helper method calls in try-except blocks to handle + potential failures gracefully in production scripts. + +4. **Verify imports**: After importing geometry or materials, verify the object state: + + .. code:: python + + geometry_import = app.helpers.import_geometry("part.x_t") + if str(geometry_import.ObjectState) == "FullyDefined": + print("Geometry imported successfully") + else: + print(f"Warning: Geometry state is {geometry_import.ObjectState}") diff --git a/doc/source/user_guide/index.rst b/doc/source/user_guide/index.rst index 7cceef0110..2b402c3afd 100644 --- a/doc/source/user_guide/index.rst +++ b/doc/source/user_guide/index.rst @@ -47,6 +47,7 @@ If you are not sure which mode to use, see :ref:`ref_choose_your_mode`. scripting/overview scripting/threading + .. toctree:: :maxdepth: 1 :hidden: diff --git a/examples/01_basic/bolt_pretension.py b/examples/01_basic/bolt_pretension.py index a199dd9386..8cb289b848 100644 --- a/examples/01_basic/bolt_pretension.py +++ b/examples/01_basic/bolt_pretension.py @@ -39,8 +39,8 @@ from pathlib import Path import typing -from matplotlib import image as mpimg, pyplot as plt -from matplotlib.animation import FuncAnimation +from matplotlib import pyplot as plt +import matplotlib.animation as animation from PIL import Image from ansys.mechanical.core import App @@ -56,12 +56,12 @@ # Import the enums and global variables instead of using app.update_globals(globals()) # or App(globals=globals()) from ansys.mechanical.core.embedding.enum_importer import * # noqa: F403 -from ansys.mechanical.core.embedding.global_importer import GeometryImportPreference, Quantity +from ansys.mechanical.core.embedding.global_importer import Quantity from ansys.mechanical.core.embedding.transaction import Transaction # %% -# Configure graphics for image export -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# Configure view and path for image export +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Set camera orientation graphics = app.Graphics @@ -70,30 +70,9 @@ camera.SetFit() camera.Rotate(180, CameraAxisType.ScreenY) -# Set camera settings for 720p resolution -graphics_image_export_settings = Ansys.Mechanical.Graphics.GraphicsImageExportSettings() -graphics_image_export_settings.Resolution = GraphicsResolutionType.EnhancedResolution -graphics_image_export_settings.Background = GraphicsBackgroundType.White -graphics_image_export_settings.CurrentGraphicsDisplay = False -graphics_image_export_settings.Width = 1280 -graphics_image_export_settings.Height = 720 - -# %% -# Set the geometry import group for the model -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -# Set the model -model = app.Model +# Set the path for the output files (images, gifs, mechdat) +output_path = Path.cwd() / "out" -# Create a geometry import group for the model -geometry_import_group = model.GeometryImportGroup -# Add the geometry import to the group -geometry_import = geometry_import_group.AddGeometryImport() -# Set the geometry import format -geometry_import_format = GeometryImportPreference.Format.Automatic -# Set the geometry import preferences -geometry_import_preferences = Ansys.ACT.Mechanical.Utilities.GeometryImportPreferences() -geometry_import_preferences.ProcessNamedSelections = True # %% # Download and import the geometry @@ -103,8 +82,7 @@ geometry_path = download_file("example_06_bolt_pret_geom.pmdb", "pymechanical", "00_basic") # Import/reload the geometry from the CAD (pmdb) file using the provided preferences -geometry_import.Import(geometry_path, geometry_import_format, geometry_import_preferences) - +geometry_import = app.helpers.import_geometry(geometry_path) # sphinx_gallery_start_ignore # Assert the geometry import was successful assert geometry_import.ObjectState == ObjectState.Solved, "Geometry Import unsuccessful" @@ -124,14 +102,8 @@ # %% # Add materials to the model and import the material files -model_materials = model.Materials -model_materials.Import(copper_material_file_path) -model_materials.Import(steel_material_file_path) - -# sphinx_gallery_start_ignore -# Assert the materials are defined -assert model_materials.ObjectState == ObjectState.FullyDefined, "Materials are not defined" -# sphinx_gallery_end_ignore +app.helpers.import_materials(copper_material_file_path) +app.helpers.import_materials(steel_material_file_path) # %% # Define analysis and unit system @@ -139,6 +111,7 @@ # %% # Add static structural analysis to the model +model = app.Model model.AddStaticStructuralAnalysis() static_structural = model.Analyses[0] static_structural_solution = static_structural.Solution @@ -505,45 +478,14 @@ def add_mesh_sizing(mesh, object_name: str, element_size: Quantity) -> None: mesh.Activate() mesh.GenerateMesh() -# Fit the view to the entire model -camera.SetFit() -# Set the path for the output files (images, gifs, mechdat) -output_path = Path.cwd() / "out" +# Set the image export format, path and export the image mesh_image_path = str(output_path / "mesh.png") -# Set the image export format and export the image -image_export_format = GraphicsImageExportFormat.PNG -graphics.ExportImage(mesh_image_path, image_export_format, graphics_image_export_settings) - - -# %% -# Create a function to display the image using matplotlib -def display_image( - image_path: str, - pyplot_figsize_coordinates: tuple = (16, 9), - plot_xticks: list = [], - plot_yticks: list = [], - plot_axis: str = "off", -): - """Display the image with the specified parameters.""" - # Set the figure size based on the coordinates specified - plt.figure(figsize=pyplot_figsize_coordinates) - - # Read the image from the file into an array - plt.imshow(mpimg.imread(image_path)) - - # Get or set the current tick locations and labels of the x-axis - plt.xticks(plot_xticks) - # Get or set the current tick locations and labels of the y-axis - plt.yticks(plot_yticks) - # Turn off the axis - plt.axis(plot_axis) - # Display the figure - plt.show() - +camera.SetFit() +app.helpers.export_image(mesh, mesh_image_path) # %% # Display the mesh image -display_image(mesh_image_path) +app.helpers.display_image(mesh_image_path) # %% # Analysis settings @@ -635,14 +577,11 @@ def display_image( # Set the image path for the loads and boundary conditions loads_boundary_conditions_image_path = str(output_path / "loads_boundary_conditions.png") # Export the image of the loads and boundary conditions -graphics.ExportImage( - loads_boundary_conditions_image_path, - image_export_format, - graphics_image_export_settings, -) +camera.SetFit() +app.helpers.export_image(bolt_presentation, loads_boundary_conditions_image_path) # Display the image of the loads and boundary conditions -display_image(loads_boundary_conditions_image_path) +app.helpers.display_image(loads_boundary_conditions_image_path) # %% # Insert results @@ -702,126 +641,75 @@ def display_image( # %% # Total deformation -# Activate the object -app.Tree.Activate([total_deformation]) -# Set the camera to fit the model -camera.SetFit() # Set the image name and path for the object image_path = str(output_path / "total_deformation.png") # Export the image of the object -app.Graphics.ExportImage(image_path, image_export_format, graphics_image_export_settings) +camera.SetFit() +app.helpers.export_image(total_deformation, file_path=image_path) # Display the image of the object -display_image(image_path) +app.helpers.display_image(image_path) # %% # Equivalent stress on all bodies -# Activate the object -app.Tree.Activate([equivalent_stress_1]) -# Set the camera to fit the model -camera.SetFit() # Set the image name and path for the object image_path = str(output_path / "equivalent_stress_all_bodies.png") # Export the image of the object -app.Graphics.ExportImage(image_path, image_export_format, graphics_image_export_settings) +camera.SetFit() +app.helpers.export_image(equivalent_stress_1, file_path=image_path) # Display the image of the object -display_image(image_path) +app.helpers.display_image(image_path) # %% # Equivalent stress on the shank -# Activate the object -app.Tree.Activate([equivalent_stress_2]) -# Set the camera to fit the model -camera.SetFit() # Set the image name and path for the object image_path = str(output_path / "equivalent_stress_shank.png") # Export the image of the object -app.Graphics.ExportImage(image_path, image_export_format, graphics_image_export_settings) +camera.SetFit() +app.helpers.export_image(equivalent_stress_2, file_path=image_path) # Display the image of the object -display_image(image_path) +app.helpers.display_image(image_path) # %% -# Export and display the contact status animation -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - -# %% -# Create a function to update the animation frames -def update_animation(frame: int) -> list[mpimg.AxesImage]: - """Update the animation frame for the GIF. - - Parameters - ---------- - frame : int - The frame number to update the animation. - - Returns - ------- - list[mpimg.AxesImage] - A list containing the updated image for the animation. - """ - # Seeks to the given frame in this sequence file - gif.seek(frame) - # Set the image array to the current frame of the GIF - image.set_data(gif.convert("RGBA")) - # Return the updated image - return [image] - - -# %% -# Export and display the contact status animation +# Export the contact status animation +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Get the post contact tool status post_contact_tool_status = post_contact_tool.Children[0] -# Activate the post contact tool status in the tree -app.Tree.Activate([post_contact_tool_status]) - -# Set the camera to fit the model -camera.SetFit() - -# Set the animation export format and settings -animation_export_format = GraphicsAnimationExportFormat.GIF -animation_export_settings = Ansys.Mechanical.Graphics.AnimationExportSettings() -animation_export_settings.Width = 1280 -animation_export_settings.Height = 720 - # Set the path for the contact status GIF contact_status_gif_path = str(output_path / "contact_status.gif") +camera.SetFit() +app.helpers.export_animation(post_contact_tool_status, contact_status_gif_path) -# Export the contact status animation to a GIF file -post_contact_tool_status.ExportAnimation( - contact_status_gif_path, animation_export_format, animation_export_settings -) +# %% +# Display the contact status animation +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Open the GIF file and create an animation gif = Image.open(contact_status_gif_path) -# Set the subplots for the animation and turn off the axis -figure, axes = plt.subplots(figsize=(8, 4)) -axes.axis("off") -# Change the color of the image -image = axes.imshow(gif.convert("RGBA")) - -# Create the animation using the figure, update_animation function, and the GIF frames -# Set the interval between frames to 200 milliseconds and repeat the animation -FuncAnimation( - figure, - update_animation, - frames=range(gif.n_frames), - interval=200, - repeat=True, - blit=True, +fig, ax = plt.subplots(figsize=(8, 4)) +ax.axis("off") +image = ax.imshow(gif.convert("RGBA")) + + +# Animation update function +def update_frame(frame): + """Update the frame for the animation.""" + gif.seek(frame) + image.set_array(gif.convert("RGBA")) + return (image,) + + +# Create and display animation +ani = animation.FuncAnimation( + fig, update_frame, frames=gif.n_frames, interval=200, blit=True, repeat=True ) # Show the animation plt.show() -# %% -# Print the project tree -# ~~~~~~~~~~~~~~~~~~~~~~ - -app.print_tree() # %% # Clean up the project diff --git a/examples/01_basic/cooling_holes_thermal_analysis.py b/examples/01_basic/cooling_holes_thermal_analysis.py index 621d2e4efb..3328f92233 100644 --- a/examples/01_basic/cooling_holes_thermal_analysis.py +++ b/examples/01_basic/cooling_holes_thermal_analysis.py @@ -60,8 +60,6 @@ from pathlib import Path from typing import TYPE_CHECKING -from matplotlib import image as mpimg, pyplot as plt - from ansys.mechanical.core import App from ansys.mechanical.core.examples import delete_downloads, download_file @@ -75,79 +73,10 @@ app = App(globals=globals()) print(app) -# %% -# Create functions to set camera and display images -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - # Set the path for the output files (images, gifs, mechdat) output_path = Path.cwd() / "out" - - -def set_camera_and_display_image( - camera, - graphics, - graphics_image_export_settings, - image_output_path: Path, - image_name: str, -) -> None: - """Set the camera to fit the model and display the image. - - Parameters - ---------- - camera : Ansys.ACT.Common.Graphics.MechanicalCameraWrapper - The camera object to set the view. - graphics : Ansys.ACT.Common.Graphics.MechanicalGraphicsWrapper - The graphics object to export the image. - graphics_image_export_settings : Ansys.Mechanical.Graphics.GraphicsImageExportSettings - The settings for exporting the image. - image_output_path : Path - The path to save the exported image. - image_name : str - The name of the exported image file. - """ - # Set the camera to fit the mesh - camera.SetFit() - # Export the image with the specified settings - image_path = image_output_path / image_name - graphics.ExportImage(str(image_path), image_export_format, graphics_image_export_settings) - # Display the exported image - display_image(image_path) - - -def display_image( - image_path: str, - pyplot_figsize_coordinates: tuple = (16, 9), - plot_xticks: list = [], - plot_yticks: list = [], - plot_axis: str = "off", -) -> None: - """Display the image with the specified parameters. - - Parameters - ---------- - image_path : str - The path to the image file to display. - pyplot_figsize_coordinates : tuple - The size of the figure in inches (width, height). - plot_xticks : list - The x-ticks to display on the plot. - plot_yticks : list - The y-ticks to display on the plot. - plot_axis : str - The axis visibility setting ('on' or 'off'). - """ - # Set the figure size based on the coordinates specified - plt.figure(figsize=pyplot_figsize_coordinates) - # Read the image from the file into an array - plt.imshow(mpimg.imread(image_path)) - # Get or set the current tick locations and labels of the x-axis - plt.xticks(plot_xticks) - # Get or set the current tick locations and labels of the y-axis - plt.yticks(plot_yticks) - # Turn off the axis - plt.axis(plot_axis) - # Display the figure - plt.show() +graphics = app.Graphics +camera = graphics.Camera # %% @@ -160,38 +89,12 @@ def display_image( # Download the material file mat_path = download_file("cooling_holes_material_file.xml", "pymechanical", "embedding") - -# %% -# Configure graphics for image export -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -# Define the graphics and camera objects -graphics = app.Graphics -camera = graphics.Camera - -# Set the camera orientation to the isometric view and set the camera to fit the model -camera.SetSpecificViewOrientation(ViewOrientationType.Iso) -camera.SetFit() - -# Set the image export format and settings -image_export_format = GraphicsImageExportFormat.PNG -settings_720p = Ansys.Mechanical.Graphics.GraphicsImageExportSettings() -settings_720p.Resolution = ( - Ansys.Mechanical.DataModel.Enums.GraphicsResolutionType.EnhancedResolution -) -settings_720p.Background = Ansys.Mechanical.DataModel.Enums.GraphicsBackgroundType.White -settings_720p.Width = 1280 -settings_720p.Height = 720 -settings_720p.CurrentGraphicsDisplay = False - -# Define the model -model = app.Model - # %% # Define Python variables # ~~~~~~~~~~~~~~~~~~~~~~~ # Store all main tree nodes as variables +model = app.Model geometry = model.Geometry mesh = model.Mesh materials = model.Materials @@ -202,21 +105,14 @@ def display_image( # Import the geometry # ~~~~~~~~~~~~~~~~~~~ -# Add the geometry import to the geometry import group -geometry_import_group = model.GeometryImportGroup -geometry_import = geometry_import_group.AddGeometryImport() - -# Set the geometry import format and settings -geometry_import_format = GeometryImportPreference.Format.Automatic -geometry_import_preferences = Ansys.ACT.Mechanical.Utilities.GeometryImportPreferences() -geometry_import_preferences.ProcessNamedSelections = True -geometry_import_preferences.NamedSelectionKey = "" -geometry_import_preferences.ProcessMaterialProperties = True -geometry_import_preferences.ProcessCoordinateSystems = True - # Import the geometry with the specified settings -geometry_import.Import(geometry_path, geometry_import_format, geometry_import_preferences) - +geometry_import = app.helpers.import_geometry( + geometry_path, + process_named_selections=True, + process_coordinate_systems=True, + named_selection_key="", + process_material_properties=True, +) # sphinx_gallery_start_ignore assert str(geometry_import.ObjectState) == "Solved", "Geometry Import unsuccessful" # sphinx_gallery_end_ignore @@ -339,7 +235,10 @@ def display_image( app.Tree.Activate([mesh]) # Set the camera to fit the model and export the image -set_camera_and_display_image(camera, graphics, settings_720p, output_path, "mesh.png") +camera.SetFit() +image_path = output_path / "mesh.png" +app.helpers.export_image(mesh, image_path) +app.helpers.display_image(image_path) # %% # Define analysis @@ -527,14 +426,14 @@ def display_image( # Activate the temperature results for both plates app.Tree.Activate([temp_plot_both_plates]) # Set the extra model display to no wireframe -graphics.ViewOptions.ResultPreference.ExtraModelDisplay = ( +app.Graphics.ViewOptions.ResultPreference.ExtraModelDisplay = ( Ansys.Mechanical.DataModel.MechanicalEnums.Graphics.ExtraModelDisplay.NoWireframe ) # Set the camera to fit the model and export the image image_path = output_path / "temp_plot_both_plates.png" -graphics.ExportImage(str(image_path), image_export_format, settings_720p) +app.helpers.export_image(temp_plot_both_plates, image_path) # Display the exported image -display_image(image_path) +app.helpers.display_image(image_path) # %% # Display the temperature plots for fluid lines @@ -542,11 +441,12 @@ def display_image( # Activate the temperature results for fluid lines app.Tree.Activate([temp_plot_fluidlines]) # Set the camera to fit the model and export the image -# Set the camera to fit the model and export the image image_path = output_path / "temp_plot_fluidlines.png" -graphics.ExportImage(str(image_path), image_export_format, settings_720p) +app.helpers.export_image(temp_plot_fluidlines, image_path) # Display the exported image -display_image(image_path) +app.helpers.display_image(image_path) + + # %% # Clean up the project # ~~~~~~~~~~~~~~~~~~~~ diff --git a/examples/01_basic/fracture_analysis_contact_debonding.py b/examples/01_basic/fracture_analysis_contact_debonding.py index a857918305..42b0cd6f78 100644 --- a/examples/01_basic/fracture_analysis_contact_debonding.py +++ b/examples/01_basic/fracture_analysis_contact_debonding.py @@ -38,7 +38,7 @@ from pathlib import Path from typing import TYPE_CHECKING -from matplotlib import image as mpimg, pyplot as plt +from matplotlib import pyplot as plt from matplotlib.animation import FuncAnimation from PIL import Image @@ -64,15 +64,6 @@ camera = graphics.Camera camera.SetSpecificViewOrientation(ViewOrientationType.Front) -# Set camera settings for 720p resolution -image_export_format = GraphicsImageExportFormat.PNG -graphics_image_export_settings = Ansys.Mechanical.Graphics.GraphicsImageExportSettings() -graphics_image_export_settings.Resolution = GraphicsResolutionType.EnhancedResolution -graphics_image_export_settings.Background = GraphicsBackgroundType.White -graphics_image_export_settings.CurrentGraphicsDisplay = False -graphics_image_export_settings.Width = 1280 -graphics_image_export_settings.Height = 720 - # %% # Create functions to set camera and display images # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -80,94 +71,19 @@ # Set the path for the output files (images, gifs, mechdat) output_path = Path.cwd() / "out" - -def set_camera_and_display_image( - camera, - graphics, - graphics_image_export_settings, - image_output_path: Path, - image_name: str, -) -> None: - """Set the camera to fit the model and display the image. - - Parameters - ---------- - camera : Ansys.ACT.Common.Graphics.MechanicalCameraWrapper - The camera object to set the view. - graphics : Ansys.ACT.Common.Graphics.MechanicalGraphicsWrapper - The graphics object to export the image. - image_output_path : Path - The path to save the exported image. - image_name : str - The name of the exported image file. - """ - # Set the camera to fit the mesh - camera.SetFit() - # Export the mesh image with the specified settings - image_path = image_output_path / image_name - graphics.ExportImage(str(image_path), image_export_format, graphics_image_export_settings) - # Display the exported mesh image - display_image(image_path) - - -def display_image( - image_path: str, - pyplot_figsize_coordinates: tuple = (16, 9), - plot_xticks: list = [], - plot_yticks: list = [], - plot_axis: str = "off", -) -> None: - """Display the image with the specified parameters. - - Parameters - ---------- - image_path : str - The path to the image file to display. - pyplot_figsize_coordinates : tuple - The size of the figure in inches (width, height). - plot_xticks : list - The x-ticks to display on the plot. - plot_yticks : list - The y-ticks to display on the plot. - plot_axis : str - The axis visibility setting ('on' or 'off'). - """ - # Set the figure size based on the coordinates specified - plt.figure(figsize=pyplot_figsize_coordinates) - # Read the image from the file into an array - plt.imshow(mpimg.imread(image_path)) - # Get or set the current tick locations and labels of the x-axis - plt.xticks(plot_xticks) - # Get or set the current tick locations and labels of the y-axis - plt.yticks(plot_yticks) - # Turn off the axis - plt.axis(plot_axis) - # Display the figure - plt.show() - - # %% # Download and import the geometry file # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Set the model -model = app.Model -# Create a geometry import group for the model -geometry_import_group = model.GeometryImportGroup -# Add the geometry import to the group -geometry_import = geometry_import_group.AddGeometryImport() -# Set the geometry import format -geometry_import_format = GeometryImportPreference.Format.Automatic -# Set the geometry import preferences -geometry_import_preferences = Ansys.ACT.Mechanical.Utilities.GeometryImportPreferences() -geometry_import_preferences.ProcessNamedSelections = True -geometry_import_preferences.AnalysisType = GeometryImportPreference.AnalysisType.Type2D # Download the geometry file from the ansys/example-data repository geometry_path = download_file("Contact_Debonding_Example.agdb", "pymechanical", "embedding") -# Import/reload the geometry from the CAD (.agdb) file using the provided preferences -geometry_import.Import(geometry_path, geometry_import_format, geometry_import_preferences) +app.helpers.import_geometry( + geometry_path, analysis_type=GeometryImportPreference.AnalysisType.Type2D +) +# Set the model +model = app.Model # Visualize the model in 3D app.plot() @@ -182,9 +98,8 @@ def display_image( # Add materials to the model and import the material files model_materials = model.Materials -model_materials.Import(mat1_path) -model_materials.Import(mat2_path) - +app.helpers.import_materials(mat1_path) +app.helpers.import_materials(mat2_path) # %% # Add connections to the model # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -251,9 +166,6 @@ def get_child_object(body, child_type, name: str): # Define the contact and contact regions # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# %% -# Activate the contact region - # Get the contact from the connection group contact = get_child_object( connections, Ansys.ACT.Automation.Mechanical.Connections.ConnectionGroup, "Contacts" @@ -349,9 +261,10 @@ def add_sizing( mesh.GenerateMesh() # Display the mesh image -set_camera_and_display_image( - camera, graphics, graphics_image_export_settings, output_path, "mesh.png" -) +camera.SetFit() +image_path = Path(output_path) / "boundary_conditions.png" +app.helpers.export_image(mesh, image_path) +app.helpers.display_image(image_path) # %% # Add a contact debonding object @@ -458,13 +371,10 @@ def add_displacement( static_structural_analysis.Activate() -set_camera_and_display_image( - camera, - graphics, - graphics_image_export_settings, - output_path, - "boundary_conditions.png", -) +camera.SetFit() +image_path = Path(output_path) / "boundary_conditions.png" +app.helpers.export_image(static_structural_analysis, image_path) +app.helpers.display_image(image_path) # %% # Add results to the solution @@ -513,86 +423,47 @@ def add_displacement( # Directional deformation directional_deformation.Activate() -set_camera_and_display_image( - camera, - graphics, - graphics_image_export_settings, - output_path, - "directional_deformation.png", -) +image_path = Path(output_path) / "directional_deformation.png" +app.helpers.export_image(directional_deformation, image_path) +app.helpers.display_image(image_path) # %% # Force reaction force_reaction.Activate() -set_camera_and_display_image( - camera, graphics, graphics_image_export_settings, output_path, "force_reaction.png" -) +image_path = Path(output_path) / "force_reaction.png" +app.helpers.export_image(force_reaction, image_path) +app.helpers.display_image(image_path) # %% # Export the animation # ~~~~~~~~~~~~~~~~~~~~ -# %% -# Create a function to update the animation frame - - -def update_animation(frame: int) -> list[mpimg.AxesImage]: - """Update the animation frame for the GIF. - - Parameters - ---------- - frame : int - The frame number to update the animation. - - Returns - ------- - list[mpimg.AxesImage] - A list containing the updated image for the animation. - """ - # Seeks to the given frame in this sequence file - gif.seek(frame) - # Set the image array to the current frame of the GIF - image.set_data(gif.convert("RGBA")) - # Return the updated image - return [image] +force_reaction_gif_path = output_path / "force_reaction.gif" +app.helpers.export_animation(force_reaction, force_reaction_gif_path) # %% -# Display the animation of the force reaction +# Display the contact status animation +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Set the animation export format and settings -animation_export_format = GraphicsAnimationExportFormat.GIF -animation_export_settings = Ansys.Mechanical.Graphics.AnimationExportSettings() -animation_export_settings.Width = 1280 -animation_export_settings.Height = 720 +# Open the GIF file and create an animation +gif = Image.open(force_reaction_gif_path) +fig, ax = plt.subplots(figsize=(8, 4)) +ax.axis("off") +image = ax.imshow(gif.convert("RGBA")) -# Set the path for the contact status GIF -force_reaction_gif_path = output_path / "force_reaction.gif" -# Export the force reaction animation to a GIF file -force_reaction.ExportAnimation( - str(force_reaction_gif_path), animation_export_format, animation_export_settings -) +# Animation update function +def update_frame(frame): + """Update the frame for the animation.""" + gif.seek(frame) + image.set_array(gif.convert("RGBA")) + return (image,) -# Open the GIF file and create an animation -gif = Image.open(force_reaction_gif_path) -# Set the subplots for the animation and turn off the axis -figure, axes = plt.subplots(figsize=(16, 9)) -axes.axis("off") -# Change the color of the image -image = axes.imshow(gif.convert("RGBA")) - -# Create the animation using the figure, update_animation function, and the GIF frames -# Set the interval between frames to 200 milliseconds and repeat the animation -FuncAnimation( - figure, - update_animation, - frames=range(gif.n_frames), - interval=100, - repeat=True, - blit=True, -) + +# Create and display animation +ani = FuncAnimation(fig, update_frame, frames=gif.n_frames, interval=200, blit=True, repeat=True) # Show the animation plt.show() diff --git a/examples/01_basic/harmonic_acoustics.py b/examples/01_basic/harmonic_acoustics.py index 109edb58e8..f4eb507e7a 100644 --- a/examples/01_basic/harmonic_acoustics.py +++ b/examples/01_basic/harmonic_acoustics.py @@ -38,7 +38,7 @@ from pathlib import Path from typing import TYPE_CHECKING -from matplotlib import image as mpimg, pyplot as plt +from matplotlib import pyplot as plt from matplotlib.animation import FuncAnimation from PIL import Image @@ -55,101 +55,15 @@ app = App(globals=globals()) print(app) -# %% -# Create functions to set camera and display images -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - # Set the path for the output files (images, gifs, mechdat) output_path = Path.cwd() / "out" - -def set_camera_and_display_image( - camera, - graphics, - graphics_image_export_settings, - image_output_path: Path, - image_name: str, - set_fit: bool = False, -) -> None: - """Set the camera to fit the model and display the image. - - Parameters - ---------- - camera : Ansys.ACT.Common.Graphics.MechanicalCameraWrapper - The camera object to set the view. - graphics : Ansys.ACT.Common.Graphics.MechanicalGraphicsWrapper - The graphics object to export the image. - graphics_image_export_settings : Ansys.Mechanical.Graphics.GraphicsImageExportSettings - The settings for exporting the image. - image_output_path : Path - The path to save the exported image. - image_name : str - The name of the exported image file. - """ - if set_fit: - # Set the camera to fit the mesh - camera.SetFit() - # Export the mesh image with the specified settings - image_path = image_output_path / image_name - graphics.ExportImage(str(image_path), image_export_format, graphics_image_export_settings) - # Display the exported mesh image - display_image(image_path) - - -def display_image( - image_path: str, - pyplot_figsize_coordinates: tuple = (16, 9), - plot_xticks: list = [], - plot_yticks: list = [], - plot_axis: str = "off", -) -> None: - """Display the image with the specified parameters. - - Parameters - ---------- - image_path : str - The path to the image file to display. - pyplot_figsize_coordinates : tuple - The size of the figure in inches (width, height). - plot_xticks : list - The x-ticks to display on the plot. - plot_yticks : list - The y-ticks to display on the plot. - plot_axis : str - The axis visibility setting ('on' or 'off'). - """ - # Set the figure size based on the coordinates specified - plt.figure(figsize=pyplot_figsize_coordinates) - # Read the image from the file into an array - plt.imshow(mpimg.imread(image_path)) - # Get or set the current tick locations and labels of the x-axis - plt.xticks(plot_xticks) - # Get or set the current tick locations and labels of the y-axis - plt.yticks(plot_yticks) - # Turn off the axis - plt.axis(plot_axis) - # Display the figure - plt.show() - - -# %% -# Configure graphics for image export -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - +# Set the camera orientation graphics = app.Graphics camera = graphics.Camera # Set the camera orientation to isometric view camera.SetSpecificViewOrientation(ViewOrientationType.Iso) - -# Set the image export format to PNG and configure the export settings -image_export_format = GraphicsImageExportFormat.PNG -settings_720p = Ansys.Mechanical.Graphics.GraphicsImageExportSettings() -settings_720p.Resolution = GraphicsResolutionType.EnhancedResolution -settings_720p.Background = GraphicsBackgroundType.White -settings_720p.Width = 1280 -settings_720p.Height = 720 -settings_720p.CurrentGraphicsDisplay = False camera.Rotate(180, CameraAxisType.ScreenY) # %% @@ -168,15 +82,7 @@ def display_image( # Define the model model = app.Model -# Add the geometry import group and set its preferences -geometry_import = model.GeometryImportGroup.AddGeometryImport() -geometry_import_format = GeometryImportPreference.Format.Automatic -geometry_import_preferences = Ansys.ACT.Mechanical.Utilities.GeometryImportPreferences() -geometry_import_preferences.ProcessNamedSelections = True - -# Import the geometry file with the specified format and preferences -geometry_import.Import(geometry_path, geometry_import_format, geometry_import_preferences) - +app.helpers.import_geometry(geometry_path, process_named_selections=True) # Define the geometry in the model geometry = model.Geometry @@ -210,8 +116,7 @@ def display_image( # %% # Import and assign the materials -mat.Import(mat_path) - +app.helpers.import_materials(mat_path) # Assign the material to the ``geometry.Children`` bodies that are not suppressed for child in range(geometry.Children.Count): if child not in suppressed_indices: @@ -413,10 +318,10 @@ def add_generation_criteria( # Activate the harmonic acoustics analysis harmonic_acoustics.Activate() # Set the camera to fit the mesh and export the image -set_camera_and_display_image( - camera, graphics, settings_720p, output_path, "boundary_conditions.png", set_fit=True -) - +camera.SetFit() +image_path = output_path / "boundary_conditions.png" +app.helpers.export_image(file_path=image_path) +app.helpers.display_image(image_path) # %% # Add results to the harmonic acoustics solution # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -556,94 +461,60 @@ def set_properties( # Display the total acoustic pressure result app.Tree.Activate([acoustic_pressure_result_1]) -set_camera_and_display_image(camera, graphics, settings_720p, output_path, "pressure.png") +image_path = output_path / "pressure.png" +app.helpers.export_image(file_path=image_path) +app.helpers.display_image(image_path) # %% # Display the total acoustic velocity -app.Tree.Activate([acoustic_pressure_result_1]) -set_camera_and_display_image(camera, graphics, settings_720p, output_path, "total_velocity.png") - -# %% -# Display the acoustic sound pressure level - app.Tree.Activate([acoustic_spl]) -set_camera_and_display_image( - camera, graphics, settings_720p, output_path, "sound_pressure_level.png" -) +image_path = output_path / "sound_pressure_level.png" +app.helpers.export_image(file_path=image_path) +app.helpers.display_image(image_path) # %% # Display the acoustic directional velocity app.Tree.Activate([acoustic_directional_velocity_3]) -set_camera_and_display_image( - camera, graphics, settings_720p, output_path, "directional_velocity.png" -) +image_path = output_path / "directional_velocity.png" +app.helpers.export_image(file_path=image_path) +app.helpers.display_image(image_path) # %% # Display the acoustic kinetic energy app.Tree.Activate([acoustic_ke]) -set_camera_and_display_image(camera, graphics, settings_720p, output_path, "kinetic_energy.png") - -# %% -# Create a function to update the animation frames - - -def update_animation(frame: int) -> list[mpimg.AxesImage]: - """Update the animation frame for the GIF. - - Parameters - ---------- - frame : int - The frame number to update the animation. +image_path = output_path / "kinetic_energy.png" +app.helpers.export_image(file_path=image_path) +app.helpers.display_image(image_path) - Returns - ------- - list[mpimg.AxesImage] - A list containing the updated image for the animation. - """ - # Seeks to the given frame in this sequence file - gif.seek(frame) - # Set the image array to the current frame of the GIF - image.set_data(gif.convert("RGBA")) - # Return the updated image - return [image] +# Export the animation of the acoustic pressure result +press_gif = output_path / "press.gif" +app.helpers.export_animation(acoustic_pressure_result_1, press_gif) # %% -# Display the total acoustic pressure animation +# Display the contact status animation +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Set the animation export format to GIF -animation_export_format = Ansys.Mechanical.DataModel.Enums.GraphicsAnimationExportFormat.GIF +# Open the GIF file and create an animation +gif = Image.open(press_gif) +fig, ax = plt.subplots(figsize=(8, 4)) +ax.axis("off") +image = ax.imshow(gif.convert("RGBA")) -# Configure the export settings for the animation -settings_720p = Ansys.Mechanical.Graphics.AnimationExportSettings() -settings_720p.Width = 1280 -settings_720p.Height = 720 -# Export the animation of the acoustic pressure result -press_gif = output_path / "press.gif" -acoustic_pressure_result_1.ExportAnimation(str(press_gif), animation_export_format, settings_720p) +# Animation update function +def update_frame(frame): + """Update the frame for the animation.""" + gif.seek(frame) + image.set_array(gif.convert("RGBA")) + return (image,) -# Open the GIF file and create an animation -gif = Image.open(press_gif) -# Set the subplots for the animation and turn off the axis -figure, axes = plt.subplots(figsize=(16, 9)) -axes.axis("off") -# Change the color of the image -image = axes.imshow(gif.convert("RGBA")) - -# Create the animation using the figure, update_animation function, and the GIF frames -# Set the interval between frames to 200 milliseconds and repeat the animation -ani = FuncAnimation( - figure, - update_animation, - frames=range(gif.n_frames), - interval=200, - repeat=True, - blit=True, -) + +# Create and display animation +ani = FuncAnimation(fig, update_frame, frames=gif.n_frames, interval=200, blit=True, repeat=True) # Show the animation plt.show() diff --git a/examples/01_basic/modal_acoustics_analysis.py b/examples/01_basic/modal_acoustics_analysis.py index cc216b886c..7317f10bd8 100644 --- a/examples/01_basic/modal_acoustics_analysis.py +++ b/examples/01_basic/modal_acoustics_analysis.py @@ -44,7 +44,7 @@ from pathlib import Path from typing import TYPE_CHECKING -from matplotlib import image as mpimg, pyplot as plt +from matplotlib import pyplot as plt from matplotlib.animation import FuncAnimation from PIL import Image @@ -61,105 +61,11 @@ app = App(globals=globals()) print(app) -# %% -# Create functions to set camera and display images -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - # Set the path for the output files (images, gifs, mechdat) output_path = Path.cwd() / "out" - - -def set_camera_and_display_image( - camera, - graphics, - graphics_image_export_settings, - image_output_path: Path, - image_name: str, - set_fit: bool = False, -) -> None: - """Set the camera to fit the model and display the image. - - Parameters - ---------- - camera : Ansys.ACT.Common.Graphics.MechanicalCameraWrapper - The camera object to set the view. - graphics : Ansys.ACT.Common.Graphics.MechanicalGraphicsWrapper - The graphics object to export the image. - graphics_image_export_settings : Ansys.Mechanical.Graphics.GraphicsImageExportSettings - The settings for exporting the image. - image_output_path : Path - The path to save the exported image. - image_name : str - The name of the exported image file. - set_fit: bool, Optional - If True, set the camera to fit the mesh. - If False, do not set the camera to fit the mesh. - """ - if set_fit: - # Set the camera to fit the mesh - camera.SetFit() - # Export the mesh image with the specified settings - image_path = image_output_path / image_name - graphics.ExportImage(str(image_path), image_export_format, graphics_image_export_settings) - # Display the exported mesh image - display_image(image_path) - - -def display_image( - image_path: str, - pyplot_figsize_coordinates: tuple = (16, 9), - plot_xticks: list = [], - plot_yticks: list = [], - plot_axis: str = "off", -) -> None: - """Display the image with the specified parameters. - - Parameters - ---------- - image_path : str - The path to the image file to display. - pyplot_figsize_coordinates : tuple - The size of the figure in inches (width, height). - plot_xticks : list - The x-ticks to display on the plot. - plot_yticks : list - The y-ticks to display on the plot. - plot_axis : str - The axis visibility setting ('on' or 'off'). - """ - # Set the figure size based on the coordinates specified - plt.figure(figsize=pyplot_figsize_coordinates) - # Read the image from the file into an array - plt.imshow(mpimg.imread(image_path)) - # Get or set the current tick locations and labels of the x-axis - plt.xticks(plot_xticks) - # Get or set the current tick locations and labels of the y-axis - plt.yticks(plot_yticks) - # Turn off the axis - plt.axis(plot_axis) - # Display the figure - plt.show() - - -# %% -# Configure the graphics for image export -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - graphics = app.Graphics camera = graphics.Camera -# Set the camera orientation to isometric view -camera.SetSpecificViewOrientation(ViewOrientationType.Iso) - -# Set the image export format and settings -image_export_format = GraphicsImageExportFormat.PNG -settings_720p = Ansys.Mechanical.Graphics.GraphicsImageExportSettings() -settings_720p.Resolution = GraphicsResolutionType.EnhancedResolution -settings_720p.Background = GraphicsBackgroundType.White -settings_720p.Width = 1280 -settings_720p.Height = 720 -settings_720p.CurrentGraphicsDisplay = False - # %% # Download the geometry and material files # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -173,28 +79,14 @@ def display_image( # Import and display the geometry # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Define the model -model = app.Model - -# Add the geometry import group and set its preferences -geometry_import_group = model.GeometryImportGroup -geometry_import = geometry_import_group.AddGeometryImport() -geometry_import_format = GeometryImportPreference.Format.Automatic -geometry_import_preferences = Ansys.ACT.Mechanical.Utilities.GeometryImportPreferences() -geometry_import_preferences.ProcessNamedSelections = True - -# Import the geometry file with the specified format and preferences -geometry_import.Import(geometry_path, geometry_import_format, geometry_import_preferences) - -# Set the camera to fit the model and display the image -set_camera_and_display_image( - camera, graphics, settings_720p, output_path, "geometry.png", set_fit=True -) +app.helpers.import_geometry(geometry_path, process_named_selections=True) +app.plot() # %% # Store all variables necessary for analysis # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +model = app.Model geometry = model.Geometry mesh = model.Mesh named_selections = model.NamedSelections @@ -210,7 +102,7 @@ def display_image( # Set the unit system to Standard MKS app.ExtAPI.Application.ActiveUnitSystem = MechanicalUnitSystem.StandardMKS # Import the water material from the specified XML file -materials.Import(mat_path) +app.helpers.import_materials(mat_path) # %% # Assign material to solid bodies @@ -355,7 +247,11 @@ def set_mesh_properties( # Generate the mesh and display the image mesh.GenerateMesh() -set_camera_and_display_image(camera, graphics, settings_720p, output_path, "mesh.png") + +image_path = output_path / "mesh.png" +camera.SetFit() +app.helpers.export_image(mesh, image_path) +app.helpers.display_image(image_path) # %% # Set up the contact regions in the connection group @@ -526,9 +422,12 @@ def set_contact_region_properties( # Set the location of the fixed support to the geometry entities fixed_support.Location = fvert -# Activate the modal acoustic analysis and display the image +# Activate the modal acoustic analysis and display boundary conditions modal_acst.Activate() -set_camera_and_display_image(camera, graphics, settings_720p, output_path, "geometry.png") +image_path = output_path / "geometry.png" +camera.SetFit() +app.helpers.export_image(modal_acst, image_path) +app.helpers.display_image(image_path) # %% # Add results to the solution @@ -584,16 +483,18 @@ def set_contact_region_properties( # Activate the first total deformation result and display the image app.Tree.Activate([total_deformation_results[0]]) -set_camera_and_display_image( - camera, graphics, settings_720p, output_path, "total_deformation.png", set_fit=True -) +image_path = output_path / "total_deformation.png" +camera.SetFit() +app.helpers.export_image(total_deformation_results[0], image_path) +app.helpers.display_image(image_path) # %% # Activate the acoustic pressure result and display the image -app.Tree.Activate([acoustic_pressure_result]) -set_camera_and_display_image(camera, graphics, settings_720p, output_path, "acoustic_pressure.png") - +image_path = output_path / "acoustic_pressure.png" +camera.SetFit() +app.helpers.export_image(acoustic_pressure_result, image_path) +app.helpers.display_image(image_path) # %% # Display all modal frequency, force reaction, and acoustic pressure values @@ -620,65 +521,33 @@ def set_contact_region_properties( print("Force reaction z-axis : ", force_reaction_1_z) # %% -# Create a function to update the animation frames - - -def update_animation(frame: int) -> list[mpimg.AxesImage]: - """Update the animation frame for the GIF. - - Parameters - ---------- - frame : int - The frame number to update the animation. - - Returns - ------- - list[mpimg.AxesImage] - A list containing the updated image for the animation. - """ - # Seeks to the given frame in this sequence file - gif.seek(frame) - # Set the image array to the current frame of the GIF - image.set_data(gif.convert("RGBA")) - # Return the updated image - return [image] +# Display the total deformation animation +deformation_gif = output_path / "total_deformation_results.gif" +camera.SetFit() +app.helpers.export_animation(total_deformation_results[-1], deformation_gif) # %% -# Play the total deformation animation +# Display the total deformation animation +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Set the animation export format to GIF -animation_export_format = Ansys.Mechanical.DataModel.Enums.GraphicsAnimationExportFormat.GIF +# Open the GIF file and create an animation +gif = Image.open(deformation_gif) +fig, ax = plt.subplots(figsize=(8, 4)) +ax.axis("off") +image = ax.imshow(gif.convert("RGBA")) -# Set the export settings for the animation -settings_720p = Ansys.Mechanical.Graphics.AnimationExportSettings() -settings_720p.Width = 1280 -settings_720p.Height = 720 -# Export the total deformation animation for the last result -deformation_gif = output_path / f"total_deformation_{len(total_deformation_results)}.gif" -total_deformation_results[-1].ExportAnimation( - str(deformation_gif), animation_export_format, settings_720p -) +# Animation update function +def update_frame(frame): + """Update the frame for the animation.""" + gif.seek(frame) + image.set_array(gif.convert("RGBA")) + return (image,) -# Open the GIF file and create an animation -gif = Image.open(deformation_gif) -# Set the subplots for the animation and turn off the axis -figure, axes = plt.subplots(figsize=(16, 9)) -axes.axis("off") -# Change the color of the image -image = axes.imshow(gif.convert("RGBA")) - -# Create the animation using the figure, update_animation function, and the GIF frames -# Set the interval between frames to 200 milliseconds and repeat the animation -FuncAnimation( - figure, - update_animation, - frames=range(gif.n_frames), - interval=100, - repeat=True, - blit=True, -) + +# Create and display animation +ani = FuncAnimation(fig, update_frame, frames=gif.n_frames, interval=200, blit=True, repeat=True) # Show the animation plt.show() diff --git a/examples/01_basic/steady_state_thermal_analysis.py b/examples/01_basic/steady_state_thermal_analysis.py index d5842c700b..fc1bd9598c 100644 --- a/examples/01_basic/steady_state_thermal_analysis.py +++ b/examples/01_basic/steady_state_thermal_analysis.py @@ -40,7 +40,7 @@ from pathlib import Path from typing import TYPE_CHECKING -from matplotlib import image as mpimg, pyplot as plt +from matplotlib import pyplot as plt from matplotlib.animation import FuncAnimation from PIL import Image @@ -57,100 +57,15 @@ app = App(globals=globals()) print(app) -# %% -# Create functions to set camera and display images -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - # Set the path for the output files (images, gifs, mechdat) output_path = Path.cwd() / "out" - -def set_camera_and_display_image( - camera, - graphics, - graphics_image_export_settings, - image_output_path: Path, - image_name: str, -) -> None: - """Set the camera to fit the model and display the image. - - Parameters - ---------- - camera : Ansys.ACT.Common.Graphics.MechanicalCameraWrapper - The camera object to set the view. - graphics : Ansys.ACT.Common.Graphics.MechanicalGraphicsWrapper - The graphics object to export the image. - graphics_image_export_settings : Ansys.Mechanical.Graphics.GraphicsImageExportSettings - The settings for exporting the image. - image_output_path : Path - The path to save the exported image. - image_name : str - The name of the exported image file. - """ - # Set the camera to fit the mesh - camera.SetFit() - # Export the mesh image with the specified settings - image_path = image_output_path / image_name - graphics.ExportImage(str(image_path), image_export_format, graphics_image_export_settings) - # Display the exported mesh image - display_image(image_path) - - -def display_image( - image_path: str, - pyplot_figsize_coordinates: tuple = (16, 9), - plot_xticks: list = [], - plot_yticks: list = [], - plot_axis: str = "off", -) -> None: - """Display the image with the specified parameters. - - Parameters - ---------- - image_path : str - The path to the image file to display. - pyplot_figsize_coordinates : tuple - The size of the figure in inches (width, height). - plot_xticks : list - The x-ticks to display on the plot. - plot_yticks : list - The y-ticks to display on the plot. - plot_axis : str - The axis visibility setting ('on' or 'off'). - """ - # Set the figure size based on the coordinates specified - plt.figure(figsize=pyplot_figsize_coordinates) - # Read the image from the file into an array - plt.imshow(mpimg.imread(image_path)) - # Get or set the current tick locations and labels of the x-axis - plt.xticks(plot_xticks) - # Get or set the current tick locations and labels of the y-axis - plt.yticks(plot_yticks) - # Turn off the axis - plt.axis(plot_axis) - # Display the figure - plt.show() - - -# %% -# Configure graphics for image export -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - +# Set the camera orientation to isometric view graphics = app.Graphics camera = graphics.Camera # Set the camera orientation to isometric view camera.SetSpecificViewOrientation(ViewOrientationType.Iso) -camera.SetFit() - -# Set the image export format and settings -image_export_format = GraphicsImageExportFormat.PNG -settings_720p = Ansys.Mechanical.Graphics.GraphicsImageExportSettings() -settings_720p.Resolution = GraphicsResolutionType.EnhancedResolution -settings_720p.Background = GraphicsBackgroundType.White -settings_720p.Width = 1280 -settings_720p.Height = 720 -settings_720p.CurrentGraphicsDisplay = False # %% # Download the geometry file @@ -163,18 +78,7 @@ def display_image( # Import the geometry # ~~~~~~~~~~~~~~~~~~~ -# Define the model -model = app.Model - -# Add the geometry import group and set its preferences -geometry_import_group = model.GeometryImportGroup -geometry_import = geometry_import_group.AddGeometryImport() -geometry_import_format = GeometryImportPreference.Format.Automatic -geometry_import_preferences = Ansys.ACT.Mechanical.Utilities.GeometryImportPreferences() -geometry_import_preferences.ProcessNamedSelections = True - -# Import the geometry file with the specified format and preferences -geometry_import.Import(geometry_path, geometry_import_format, geometry_import_preferences) +app.helpers.import_geometry(geometry_path, process_named_selections=True) # Visualize the model in 3D app.plot() @@ -184,6 +88,7 @@ def display_image( # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Add a steady state thermal analysis to the model +model = app.Model model.AddSteadyStateThermalAnalysis() # Set the Mechanical unit system to Standard MKS app.ExtAPI.Application.ActiveUnitSystem = MechanicalUnitSystem.StandardMKS @@ -481,8 +386,10 @@ def set_inputs_and_outputs( analysis_settings.CalculateVolumeEnergy = True # Activate the static thermal analysis and display the image -stat_therm.Activate() -set_camera_and_display_image(camera, graphics, settings_720p, output_path, "bc_steady_state.png") +image_path = output_path / "bc_steady_state.png" +camera.SetFit() +app.helpers.export_image(stat_therm, image_path) +app.helpers.display_image(image_path) # %% # Add results @@ -599,102 +506,73 @@ def set_inputs_and_outputs( # ~~~~~~~~~~~~~~~~~~~ # Activate the total body temperature and display the image -app.Tree.Activate([temp_rst]) -set_camera_and_display_image(camera, graphics, settings_720p, output_path, "total_body_temp.png") +image_path = output_path / "total_body_temp.png" +camera.SetFit() +app.helpers.export_image(stat_therm, image_path) +app.helpers.display_image(image_path) # %% # Temperature on part of the body # Activate the temperature on part of the body and display the image -app.Tree.Activate([temp_rst2]) -set_camera_and_display_image(camera, graphics, settings_720p, output_path, "part_temp_body.png") +image_path = output_path / "part_temp_body.png" +camera.SetFit() +app.helpers.export_image(stat_therm, image_path) +app.helpers.display_image(image_path) # %% # Temperature distribution along the specific path # Activate the temperature distribution along the specific path and display the image -app.Tree.Activate([temp_rst3]) -set_camera_and_display_image( - camera, graphics, settings_720p, output_path, "path_temp_distribution.png" -) +image_path = output_path / "path_temp_distribution.png" +camera.SetFit() +app.helpers.export_image(stat_therm, image_path) +app.helpers.display_image(image_path) # %% # Temperature of bottom surface # Activate the temperature of the bottom surface and display the image -app.Tree.Activate([temp_rst4]) -set_camera_and_display_image( - camera, graphics, settings_720p, output_path, "bottom_surface_temp.png" -) +image_path = output_path / "bottom_surface_temp.png" +camera.SetFit() +app.helpers.export_image(stat_therm, image_path) +app.helpers.display_image(image_path) # %% # Export the directional heat flux animation # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -# %% -# Create a function to update the animation frames -def update_animation(frame: int) -> list[mpimg.AxesImage]: - """Update the animation frame for the GIF. - - Parameters - ---------- - frame : int - The frame number to update the animation. - - Returns - ------- - list[mpimg.AxesImage] - A list containing the updated image for the animation. - """ - # Seeks to the given frame in this sequence file - gif.seek(frame) - # Set the image array to the current frame of the GIF - image.set_data(gif.convert("RGBA")) - # Return the updated image - return [image] +# Export the directional heat flux animation as a GIF +directional_heat_flux_gif = output_path / "directional_heat_flux.gif" +app.helpers.export_animation(directional_heat_flux, directional_heat_flux_gif) # %% -# Show the directional heat flux animation +# Display the heat flux animation +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Activate the directional heat flux -app.Tree.Activate([directional_heat_flux]) +# Open the GIF file and create an animation +gif = Image.open(directional_heat_flux_gif) +fig, ax = plt.subplots(figsize=(8, 4)) +ax.axis("off") +image = ax.imshow(gif.convert("RGBA")) -# Set the animation export format and settings -animation_export_format = Ansys.Mechanical.DataModel.Enums.GraphicsAnimationExportFormat.GIF -settings_720p = Ansys.Mechanical.Graphics.AnimationExportSettings() -settings_720p.Width = 1280 -settings_720p.Height = 720 -# Export the directional heat flux animation as a GIF -directional_heat_flux_gif = output_path / "directional_heat_flux.gif" -directional_heat_flux.ExportAnimation( - str(directional_heat_flux_gif), animation_export_format, settings_720p -) +# Animation update function +def update_frame(frame): + """Update the frame for the animation.""" + gif.seek(frame) + image.set_array(gif.convert("RGBA")) + return (image,) -# Open the GIF file and create an animation -gif = Image.open(directional_heat_flux_gif) -# Set the subplots for the animation and turn off the axis -figure, axes = plt.subplots(figsize=(16, 9)) -axes.axis("off") -# Change the color of the image -image = axes.imshow(gif.convert("RGBA")) - -# Create the animation using the figure, update_animation function, and the GIF frames -# Set the interval between frames to 200 milliseconds and repeat the animation -ani = FuncAnimation( - figure, - update_animation, - frames=range(gif.n_frames), - interval=100, - repeat=True, - blit=True, -) + +# Create and display animation +ani = FuncAnimation(fig, update_frame, frames=gif.n_frames, interval=200, blit=True, repeat=True) # Show the animation plt.show() + # %% # Display the output file from the solve # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/examples/01_basic/topology_optimization_cantilever_beam.py b/examples/01_basic/topology_optimization_cantilever_beam.py index bc69179ad0..d90beb05ae 100644 --- a/examples/01_basic/topology_optimization_cantilever_beam.py +++ b/examples/01_basic/topology_optimization_cantilever_beam.py @@ -37,13 +37,15 @@ from pathlib import Path from typing import TYPE_CHECKING -from matplotlib import image as mpimg, pyplot as plt +from matplotlib import pyplot as plt +import matplotlib.animation as animation +from PIL import Image from ansys.mechanical.core import App from ansys.mechanical.core.examples import delete_downloads, download_file if TYPE_CHECKING: - import Ansys + pass # %% # Initialize the embedded application @@ -53,98 +55,19 @@ print(app) # %% -# Create functions to set camera and display images -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# Setup the output path and camera +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Set the path for the output files (images, gifs, mechdat) output_path = Path.cwd() / "out" - -def set_camera_and_display_image( - camera, - graphics, - graphics_image_export_settings, - image_output_path: Path, - image_name: str, -) -> None: - """Set the camera to fit the model and display the image. - - Parameters - ---------- - camera : Ansys.ACT.Common.Graphics.MechanicalCameraWrapper - The camera object to set the view. - graphics : Ansys.ACT.Common.Graphics.MechanicalGraphicsWrapper - The graphics object to export the image. - graphics_image_export_settings : Ansys.Mechanical.Graphics.GraphicsImageExportSettings - The settings for exporting the image. - image_output_path : Path - The path to save the exported image. - image_name : str - The name of the exported image file. - """ - # Set the camera to fit the mesh - camera.SetFit() - # Export the mesh image with the specified settings - image_path = image_output_path / image_name - graphics.ExportImage(str(image_path), image_export_format, graphics_image_export_settings) - # Display the exported mesh image - display_image(image_path) - - -def display_image( - image_path: str, - pyplot_figsize_coordinates: tuple = (16, 9), - plot_xticks: list = [], - plot_yticks: list = [], - plot_axis: str = "off", -) -> None: - """Display the image with the specified parameters. - - Parameters - ---------- - image_path : str - The path to the image file to display. - pyplot_figsize_coordinates : tuple - The size of the figure in inches (width, height). - plot_xticks : list - The x-ticks to display on the plot. - plot_yticks : list - The y-ticks to display on the plot. - plot_axis : str - The axis visibility setting ('on' or 'off'). - """ - # Set the figure size based on the coordinates specified - plt.figure(figsize=pyplot_figsize_coordinates) - # Read the image from the file into an array - plt.imshow(mpimg.imread(image_path)) - # Get or set the current tick locations and labels of the x-axis - plt.xticks(plot_xticks) - # Get or set the current tick locations and labels of the y-axis - plt.yticks(plot_yticks) - # Turn off the axis - plt.axis(plot_axis) - # Display the figure - plt.show() - - -# %% -# Configure graphics for image export - +# Set the camera orientation to the front view graphics = app.Graphics camera = graphics.Camera # Set the camera orientation to the front view camera.SetSpecificViewOrientation(ViewOrientationType.Front) -# Set the image export format and settings -image_export_format = GraphicsImageExportFormat.PNG -settings_720p = Ansys.Mechanical.Graphics.GraphicsImageExportSettings() -settings_720p.Resolution = GraphicsResolutionType.EnhancedResolution -settings_720p.Background = GraphicsBackgroundType.White -settings_720p.Width = 1280 -settings_720p.Height = 720 -settings_720p.CurrentGraphicsDisplay = False - # %% # Import the structural analysis model # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -181,13 +104,18 @@ def display_image( # Activate the total deformation result and display the image struct_sln.Children[1].Activate() -set_camera_and_display_image(camera, graphics, settings_720p, output_path, "total_deformation.png") - +image_path = output_path / "total_deformation.png" +camera.SetFit() +app.helpers.export_image(struct_sln.Children[1], image_path) +app.helpers.display_image(image_path) # %% # Activate the equivalent stress result and display the image struct_sln.Children[2].Activate() -set_camera_and_display_image(camera, graphics, settings_720p, output_path, "equivalent_stress.png") +image_path = output_path / "equivalent_stress.png" +camera.SetFit() +app.helpers.export_image(struct_sln.Children[2], image_path) +app.helpers.display_image(image_path) # %% # Topology optimization @@ -227,9 +155,9 @@ def display_image( # Activate the topology optimization analysis and display the image topology_optimization.Activate() -set_camera_and_display_image( - camera, graphics, settings_720p, output_path, "boundary_conditions.png" -) +camera.SetFit() +app.helpers.export_image(topology_optimization, output_path / "boundary_conditions.png") +app.helpers.display_image(output_path / "boundary_conditions.png") # %% # Solve the solution @@ -270,29 +198,42 @@ def display_image( # Activate the topology density result after smoothing and display the image topology_density.Children[0].Activate() -set_camera_and_display_image( - camera, graphics, settings_720p, output_path, "topo_optimized_smooth.png" -) +image_path = output_path / "topo_optimized_smooth.png" +camera.SetFit() +app.helpers.export_image(topology_density.Children[0], image_path) +app.helpers.display_image(image_path) # %% # Export the animation -app.Tree.Activate([topology_density]) +topology_optimized_gif = output_path / "topology_optimized.gif" +app.helpers.export_animation(topology_density, topology_optimized_gif) -# Set the animation export format and settings -animation_export_format = Ansys.Mechanical.DataModel.Enums.GraphicsAnimationExportFormat.GIF -settings_720p = Ansys.Mechanical.Graphics.AnimationExportSettings() -settings_720p.Width = 1280 -settings_720p.Height = 720 +# %% +# Display the topology optimized animation -# Export the animation of the topology density result -topology_optimized_gif = output_path / "topology_optimized.gif" -topology_density.ExportAnimation( - str(topology_optimized_gif), animation_export_format, settings_720p +# Open the GIF file and create an animation +gif = Image.open(topology_optimized_gif) +fig, ax = plt.subplots(figsize=(8, 4)) +ax.axis("off") +image = ax.imshow(gif.convert("RGBA")) + + +# Animation update function +def update_frame(frame): + """Update the frame for the animation.""" + gif.seek(frame) + image.set_array(gif.convert("RGBA")) + return (image,) + + +# Create and display animation +ani = animation.FuncAnimation( + fig, update_frame, frames=gif.n_frames, interval=200, blit=True, repeat=True ) -# %% -# .. image:: /_static/basic/Topo_opitimized.gif +# Show the animation +plt.show() # %% # Review the results diff --git a/examples/01_basic/valve.py b/examples/01_basic/valve.py index 86accef54b..48c38c551b 100644 --- a/examples/01_basic/valve.py +++ b/examples/01_basic/valve.py @@ -35,7 +35,7 @@ from pathlib import Path from typing import TYPE_CHECKING -from matplotlib import image as mpimg, pyplot as plt +from matplotlib import pyplot as plt from matplotlib.animation import FuncAnimation from PIL import Image @@ -52,100 +52,14 @@ app = App(globals=globals()) print(app) -# %% -# Create functions to set camera and display images -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - # Set the path for the output files (images, gifs, mechdat) output_path = Path.cwd() / "out" - -def set_camera_and_display_image( - camera, - graphics, - graphics_image_export_settings, - image_output_path: Path, - image_name: str, -) -> None: - """Set the camera to fit the model and display the image. - - Parameters - ---------- - camera : Ansys.ACT.Common.Graphics.MechanicalCameraWrapper - The camera object to set the view. - graphics : Ansys.ACT.Common.Graphics.MechanicalGraphicsWrapper - The graphics object to export the image. - graphics_image_export_settings : Ansys.Mechanical.Graphics.GraphicsImageExportSettings - The settings for exporting the image. - image_output_path : Path - The path to save the exported image. - image_name : str - The name of the exported image file. - """ - # Set the camera to fit the mesh - camera.SetFit() - # Export the mesh image with the specified settings - image_path = image_output_path / image_name - graphics.ExportImage(str(image_path), image_export_format, graphics_image_export_settings) - # Display the exported mesh image - display_image(image_path) - - -def display_image( - image_path: str, - pyplot_figsize_coordinates: tuple = (16, 9), - plot_xticks: list = [], - plot_yticks: list = [], - plot_axis: str = "off", -) -> None: - """Display the image with the specified parameters. - - Parameters - ---------- - image_path : str - The path to the image file to display. - pyplot_figsize_coordinates : tuple - The size of the figure in inches (width, height). - plot_xticks : list - The x-ticks to display on the plot. - plot_yticks : list - The y-ticks to display on the plot. - plot_axis : str - The axis visibility setting ('on' or 'off'). - """ - # Set the figure size based on the coordinates specified - plt.figure(figsize=pyplot_figsize_coordinates) - # Read the image from the file into an array - plt.imshow(mpimg.imread(image_path)) - # Get or set the current tick locations and labels of the x-axis - plt.xticks(plot_xticks) - # Get or set the current tick locations and labels of the y-axis - plt.yticks(plot_yticks) - # Turn off the axis - plt.axis(plot_axis) - # Display the figure - plt.show() - - -# %% -# Configure graphics for image export -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# Set camera and graphics graphics = app.Graphics camera = graphics.Camera -# Set the camera orientation to the isometric view -camera.SetSpecificViewOrientation(ViewOrientationType.Iso) - -# Set the image export format and settings -image_export_format = GraphicsImageExportFormat.PNG -settings_720p = Ansys.Mechanical.Graphics.GraphicsImageExportSettings() -settings_720p.Resolution = GraphicsResolutionType.EnhancedResolution -settings_720p.Background = GraphicsBackgroundType.White -settings_720p.Width = 1280 -settings_720p.Height = 720 -settings_720p.CurrentGraphicsDisplay = False - # %% # Download and import the geometry file # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -155,20 +69,12 @@ def display_image( # %% # Import the geometry +# ~~~~~~~~~~~~~~~~~~~ # Define the model model = app.Model -# Add a geometry import to the geometry import group -geometry_import = model.GeometryImportGroup.AddGeometryImport() - -# Set the geometry import settings -geometry_import_format = GeometryImportPreference.Format.Automatic -geometry_import_preferences = Ansys.ACT.Mechanical.Utilities.GeometryImportPreferences() -geometry_import_preferences.ProcessNamedSelections = True - -# Import the geometry file with the specified settings -geometry_import.Import(geometry_path, geometry_import_format, geometry_import_preferences) +app.helpers.import_geometry(geometry_path, process_named_selections=True) # Visualize the model in 3D app.plot() @@ -200,6 +106,7 @@ def display_image( # %% # Define the mesh settings and generate the mesh +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Define the mesh mesh = model.Mesh @@ -210,8 +117,10 @@ def display_image( mesh.GenerateMesh() # Activate the mesh and display the image -app.Tree.Activate([mesh]) -set_camera_and_display_image(camera, graphics, settings_720p, output_path, "mesh.png") +image_path = output_path / "mesh.png" +camera.SetFit() +app.helpers.export_image(mesh, image_path) +app.helpers.display_image(image_path) # %% # Add a static structural analysis and apply boundary conditions @@ -242,13 +151,14 @@ def display_image( pressure.Magnitude.Output.DiscreteValues = [Quantity("0 [Pa]"), Quantity("15 [MPa]")] # Activate the analysis and display the image -analysis.Activate() -set_camera_and_display_image( - camera, graphics, settings_720p, output_path, "boundary_conditions.png" -) +image_path = output_path / "boundary_conditions.png" +camera.SetFit() +app.helpers.export_image(analysis, image_path) +app.helpers.display_image(image_path) # %% # Add results to the analysis solution +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Define the solution for the analysis solution = analysis.Solution @@ -280,75 +190,56 @@ def display_image( # %% # Show the total deformation image +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Activate the total deformation result and display the image app.Tree.Activate([deformation]) -set_camera_and_display_image( - camera, graphics, settings_720p, output_path, "total_deformation_valve.png" -) +image_path = output_path / "total_deformation_valve.png" +camera.SetFit() +app.helpers.export_image(deformation, image_path) +app.helpers.display_image(image_path) # %% # Show the equivalent stress image +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Activate the equivalent stress result and display the image app.Tree.Activate([stress]) -set_camera_and_display_image(camera, graphics, settings_720p, output_path, "stress_valve.png") - - -# %% -# Create a function to update the animation frames -def update_animation(frame: int) -> list[mpimg.AxesImage]: - """Update the animation frame for the GIF. - - Parameters - ---------- - frame : int - The frame number to update the animation. - - Returns - ------- - list[mpimg.AxesImage] - A list containing the updated image for the animation. - """ - # Seeks to the given frame in this sequence file - gif.seek(frame) - # Set the image array to the current frame of the GIF - image.set_data(gif.convert("RGBA")) - # Return the updated image - return [image] +image_path = output_path / "stress_valve.png" +camera.SetFit() +app.helpers.export_image(stress, image_path) +app.helpers.display_image(image_path) # %% # Export the stress animation +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Set the animation export format and settings -animation_export_format = Ansys.Mechanical.DataModel.Enums.GraphicsAnimationExportFormat.GIF -settings_720p = Ansys.Mechanical.Graphics.AnimationExportSettings() -settings_720p.Width = 1280 -settings_720p.Height = 720 - -# Export the animation of the equivalent stress result +camera.SetFit() valve_gif = output_path / "valve.gif" -stress.ExportAnimation(str(valve_gif), animation_export_format, settings_720p) +app.helpers.export_animation(stress, valve_gif) + +# %% +# Display the stress animation +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Open the GIF file and create an animation gif = Image.open(valve_gif) -# Set the subplots for the animation and turn off the axis -figure, axes = plt.subplots(figsize=(16, 9)) -axes.axis("off") -# Change the color of the image -image = axes.imshow(gif.convert("RGBA")) - -# Create the animation using the figure, update_animation function, and the GIF frames -# Set the interval between frames to 200 milliseconds and repeat the animation -FuncAnimation( - figure, - update_animation, - frames=range(gif.n_frames), - interval=100, - repeat=True, - blit=True, -) +fig, ax = plt.subplots(figsize=(8, 4)) +ax.axis("off") +image = ax.imshow(gif.convert("RGBA")) + + +# Animation update function +def update_frame(frame): + """Update the frame for the animation.""" + gif.seek(frame) + image.set_array(gif.convert("RGBA")) + return (image,) + + +# Create and display animation +ani = FuncAnimation(fig, update_frame, frames=gif.n_frames, interval=200, blit=True, repeat=True) # Show the animation plt.show() diff --git a/pyproject.toml b/pyproject.toml index 041b75cc66..77430e4716 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -313,4 +313,4 @@ passenv = * extras = doc commands = sphinx-build -d "{toxworkdir}/doc_doctree" doc/source "{toxinidir}/doc/_build/html" --color -vW -bhtml -""" \ No newline at end of file +""" diff --git a/src/ansys/mechanical/core/embedding/app.py b/src/ansys/mechanical/core/embedding/app.py index a9fe44f321..a31eeecbaa 100644 --- a/src/ansys/mechanical/core/embedding/app.py +++ b/src/ansys/mechanical/core/embedding/app.py @@ -42,6 +42,7 @@ from ansys.mechanical.core.embedding.poster import Poster import ansys.mechanical.core.embedding.shell as shell from ansys.mechanical.core.embedding.ui import launch_ui +from ansys.mechanical.core.embedding.utils import GetterWrapper from ansys.mechanical.core.feature_flags import get_command_line_arguments if typing.TYPE_CHECKING: @@ -136,33 +137,6 @@ def is_initialized() -> bool: return len(INSTANCES) != 0 -class GetterWrapper: - """Wrapper class around an attribute of an object.""" - - def __init__(self, obj, getter): - """Create a new instance of GetterWrapper.""" - # immortal class which provides wrapped object - self.__dict__["_immortal_object"] = obj - # function to get the wrapped object from the immortal class - self.__dict__["_get_wrapped_object"] = getter - - def __getattr__(self, attr): - """Wrap getters to the wrapped object.""" - if attr in self.__dict__: - return getattr(self, attr) - return getattr(self._get_wrapped_object(self._immortal_object), attr) - - def __setattr__(self, attr, value): - """Wrap setters to the wrapped object.""" - if attr in self.__dict__: - setattr(self, attr, value) - setattr(self._get_wrapped_object(self._immortal_object), attr, value) - - def __repr__(self): - """Return the repr of the wrapped object.""" - return repr(self._get_wrapped_object(self._immortal_object)) - - class App: """Mechanical embedding Application. @@ -344,6 +318,9 @@ def __init__( if self.version >= 252: self._license_manager = LicenseManager(self) + # Initialize helpers + self._helpers = None + def __repr__(self): """Get the product info.""" import clr @@ -675,6 +652,15 @@ def license_manager(self): raise RuntimeError("LicenseManager is only available for version 252 and later.") return self._license_manager + @property + def helpers(self): + """Return the Helpers instance.""" + if self._helpers is None: + from ansys.mechanical.core.embedding.helpers import Helpers + + self._helpers = Helpers(self) + return self._helpers + def _share(self, other) -> None: """Shares the state of self with other. @@ -698,6 +684,20 @@ def _share(self, other) -> None: other._version = self._version other._poster = self._poster other._updated_scopes = self._updated_scopes + other._helpers = self._helpers + + # Share logging configuration + other._enable_logging = self._enable_logging + other._log = self._log + other._log_level = self._log_level + + # Share interactive mode and shell state + other._interactive_mode = self._interactive_mode + other._started_interactive_shell = self._started_interactive_shell + + # Share lazy-loaded instances + other._messages = self._messages + other._license_manager = self._license_manager # all events will be handled by the original App instance other._subscribed = False @@ -843,7 +843,6 @@ def print_tree(self, node=None, max_lines=80, lines_count=0, indentation=""): """ if node is None: node = self.DataModel.Project - self._print_tree(node, max_lines, lines_count, indentation) def log_debug(self, message): diff --git a/src/ansys/mechanical/core/embedding/helpers.py b/src/ansys/mechanical/core/embedding/helpers.py new file mode 100644 index 0000000000..d73a501db9 --- /dev/null +++ b/src/ansys/mechanical/core/embedding/helpers.py @@ -0,0 +1,364 @@ +# Copyright (C) 2022 - 2026 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. + +"""Helper functions for Embedded App.""" + + +class Helpers: + """Helper utilities for Mechanical embedding application. + + Parameters + ---------- + app : App + The Mechanical embedding application instance. + + Examples + -------- + >>> from ansys.mechanical.core import App + >>> app = App() + >>> helpers = app.helpers + """ + + def __init__(self, app): + """Initialize the Helpers class with the app instance.""" + self._app = app + + # Import Ansys module for use across helper methods + from ansys.mechanical.core.embedding.global_importer import Ansys + + self.Ansys = Ansys + + def import_geometry( + self, + file_path: str, + process_named_selections: bool = False, + named_selection_key: str = "NS", + process_material_properties: bool = False, + process_coordinate_systems: bool = False, + analysis_type=None, + ): + r"""Import geometry file into the current Mechanical model. + + Returns + ------- + Ansys.ACT.Automation.Mechanical.GeometryImport + The geometry import object. + + Parameters + ---------- + file_path : str + The path to the geometry file to be imported. + process_named_selections : bool, optional + Whether to process named selections during import. Default is False. + named_selection_key : str, optional + Named selection key for filtering. Default is "NS". + process_material_properties : bool, optional + Whether to process material properties during import. Default is False. + process_coordinate_systems : bool, optional + Whether to process coordinate systems during import. Default is False. + analysis_type : GeometryImportPreference.AnalysisType, optional + The analysis type enum for the geometry import. Default is + ``GeometryImportPreference.AnalysisType.Type3D``. + + Examples + -------- + >>> from ansys.mechanical.core import App + >>> app = App() + >>> app.helpers.import_geometry("C:\\path\\to\\geometry.pmdb") + + >>> # Import with 2D analysis type + >>> from ansys.mechanical.core.embedding.enum_importer import GeometryImportPreference + >>> app.helpers.import_geometry( + ... "C:\\path\\to\\geometry.agdb", + ... analysis_type=GeometryImportPreference.AnalysisType.Type2D, + ... ) + + >>> # Import with all options specified + >>> app.helpers.import_geometry( + ... "C:\\path\\to\\geometry.pmdb", + ... process_named_selections=True, + ... named_selection_key="", + ... process_material_properties=True, + ... process_coordinate_systems=True, + ... ) + """ + GeometryImportPreference = self.Ansys.Mechanical.DataModel.Enums.GeometryImportPreference # noqa: N806 + + if analysis_type is None: + analysis_type = GeometryImportPreference.AnalysisType.Type3D + + geometry_import_group = self._app.Model.GeometryImportGroup + geometry_import = geometry_import_group.AddGeometryImport() + geometry_import_format = GeometryImportPreference.Format.Automatic + geometry_import_preferences = ( + self.Ansys.ACT.Mechanical.Utilities.GeometryImportPreferences() + ) + geometry_import_preferences.ProcessNamedSelections = process_named_selections + geometry_import_preferences.NamedSelectionKey = named_selection_key + geometry_import_preferences.ProcessMaterialProperties = process_material_properties + geometry_import_preferences.ProcessCoordinateSystems = process_coordinate_systems + geometry_import_preferences.AnalysisType = analysis_type + + try: + geometry_import.Import(file_path, geometry_import_format, geometry_import_preferences) + self._app.log_info( + f"Imported geometry from {file_path} successfully." + f"Object State: {geometry_import.ObjectState}" + ) + return geometry_import + except Exception as e: + raise RuntimeError(f"Geometry Import unsuccessful: {e}") + + def import_materials(self, file_path: str): + r"""Import materials from a specified material database file. + + Parameters + ---------- + file_path : str + The path to the material database file to be imported. + + Examples + -------- + >>> from ansys.mechanical.core import App + >>> app.helpers.import_materials("C:\\path\\to\\materials.xml") + """ + # Add materials to the model and import the material files + materials = self._app.Model.Materials + + try: + materials.Import(file_path) + self._app.log_info( + f"Imported materials from {file_path} successfully." + f"Object State: {materials.ObjectState}" + ) + except Exception as e: + raise RuntimeError(f"Material Import unsuccessful: {e}") + + def export_image( + self, + obj=None, + file_path: "str | None" = None, + width: int = 1920, + height: int = 1080, + background=None, + resolution=None, + current_graphics_display: bool = False, + image_format=None, + ): + r"""Export an image of the specified object. + + Parameters + ---------- + obj : optional + The object to activate and export. If None, exports the current graphics display. + Can be any Mechanical object such as Geometry, Mesh, or Results. + file_path : str, optional + The path where the image will be saved. + width : int, optional + The width of the exported image in pixels. Default is 1920. + height : int, optional + The height of the exported image in pixels. Default is 1080. + background : GraphicsBackgroundType, optional + Background type for the exported image. Default is + ``GraphicsBackgroundType.White``. + resolution : GraphicsResolutionType, optional + Resolution type for the exported image. Default is + ``GraphicsResolutionType.EnhancedResolution``. + current_graphics_display : bool, optional + Whether to use current graphics display. Default is False. + image_format : GraphicsImageExportFormat, optional + Image format for export. Default is ``GraphicsImageExportFormat.PNG``. + + Examples + -------- + >>> from ansys.mechanical.core import App + >>> app = App() + >>> app.helpers.export_image(app.Model.Geometry, "C:\\path\\to\\geometry.png") + + >>> from ansys.mechanical.core.embedding.enum_importer import ( + ... GraphicsBackgroundType, + ... GraphicsImageExportFormat, + ... GraphicsResolutionType, + ... ) + >>> result = app.Model.Analyses[0].Solution.Children[0] + >>> app.helpers.export_image( + ... result, + ... "C:\\path\\to\\result.jpg", + ... background=GraphicsBackgroundType.GraphicsAppearanceSetting, + ... resolution=GraphicsResolutionType.HighResolution, + ... image_format=GraphicsImageExportFormat.JPG, + ... ) + """ + from pathlib import Path + + from ansys.mechanical.core.embedding.enum_importer import ( + GraphicsBackgroundType, + GraphicsImageExportFormat, + GraphicsResolutionType, + ) + + if file_path is None: + raise ValueError("file_path must be provided for image export.") + + resolved_path = Path(file_path) + if not resolved_path.parent or resolved_path.parent == Path(): + resolved_path = Path.cwd() / resolved_path.name + file_path = str(resolved_path) + + if obj is not None: + self._app.Tree.Activate([obj]) + + if background is None: + background = GraphicsBackgroundType.White + if resolution is None: + resolution = GraphicsResolutionType.EnhancedResolution + if image_format is None: + image_format = GraphicsImageExportFormat.PNG + + graphics_image_export_settings = ( + self.Ansys.Mechanical.Graphics.GraphicsImageExportSettings() + ) + graphics_image_export_settings.Resolution = resolution + graphics_image_export_settings.Background = background + graphics_image_export_settings.CurrentGraphicsDisplay = current_graphics_display + graphics_image_export_settings.Width = width + graphics_image_export_settings.Height = height + + try: + self._app.Graphics.ExportImage(file_path, image_format, graphics_image_export_settings) + self._app.log_info(f"Exported image to {file_path} successfully.") + except Exception as e: + raise RuntimeError(f"Image export unsuccessful: {e}") + + def export_animation( + self, + obj=None, + file_path: "str | None" = None, + width: int = 1280, + height: int = 720, + animation_format=None, + ): + r"""Export an animation of the specified object. + + Parameters + ---------- + obj : optional + The object to activate and export animation. If None, exports animation of the + current graphics display. Can be any Mechanical object that supports animation + such as results with multiple time steps or modal analysis results. + file_path : str, optional + The path where the animation will be saved. If None, raises ValueError. + If only a filename is provided, saves to current working directory. + width : int, optional + The width of the exported animation in pixels. Default is 1280. + height : int, optional + The height of the exported animation in pixels. Default is 720. + animation_format : GraphicsAnimationExportFormat, optional + Animation format for export. Default is ``GraphicsAnimationExportFormat.GIF``. + + Examples + -------- + >>> from ansys.mechanical.core import App + >>> app = App() + >>> result = app.Model.Analyses[0].Solution.Children[0] + >>> app.helpers.export_animation(result, "result_animation.gif") + + >>> from ansys.mechanical.core.embedding.enum_importer import ( + ... GraphicsAnimationExportFormat, + ... ) + >>> app.helpers.export_animation( + ... result, + ... "result_animation.mp4", + ... animation_format=GraphicsAnimationExportFormat.MP4, + ... ) + """ + from pathlib import Path + + from ansys.mechanical.core.embedding.enum_importer import ( + GraphicsAnimationExportFormat, + ) + + if file_path is None: + raise ValueError("file_path must be provided for animation export.") + + resolved_path = Path(file_path) + if not resolved_path.parent or resolved_path.parent == Path(): + resolved_path = Path.cwd() / resolved_path.name + file_path = str(resolved_path) + + if obj is None: + self._app.log_info( + "No object provided for animation export; using first active object." + ) + obj = self._app.Tree.FirstActiveObject + + if animation_format is None: + animation_format = GraphicsAnimationExportFormat.GIF + + animation_export_settings = self.Ansys.Mechanical.Graphics.AnimationExportSettings( + width, height + ) + + try: + self._app.Tree.Activate([obj]) + obj.ExportAnimation(file_path, animation_format, animation_export_settings) + self._app.log_info(f"Exported animation to {file_path} successfully.") + except Exception as e: + raise RuntimeError(f"Animation export unsuccessful: {e}") + + def display_image( + self, + image_path: str, + figsize: tuple = (16, 9), + axis: str = "off", + ): + """Display an image using matplotlib. + + Parameters + ---------- + image_path : str + The path to the image file to display. + figsize : tuple, optional + The size of the figure in inches (width, height). Default is (16, 9). + axis : str, optional + The axis visibility setting ('on' or 'off'). Default is "off". + + Examples + -------- + >>> from ansys.mechanical.core import App + >>> app = App() + >>> app.helpers.export_image(app.Model.Geometry, "geometry.png") + >>> app.helpers.display_image("geometry.png") + + >>> # Display with custom figure size + >>> app.helpers.display_image("result.png", figsize=(10, 6)) + """ + from matplotlib import image as mpimg, pyplot as plt + + # Set the figure size + plt.figure(figsize=figsize) + # Read and display the image + plt.imshow(mpimg.imread(image_path)) + # Turn axis on or off + plt.axis(axis) + # Display the figure + plt.show() diff --git a/src/ansys/mechanical/core/embedding/utils.py b/src/ansys/mechanical/core/embedding/utils.py index f02d678274..4bb15c3fb8 100644 --- a/src/ansys/mechanical/core/embedding/utils.py +++ b/src/ansys/mechanical/core/embedding/utils.py @@ -28,6 +28,33 @@ TEST_HELPER = None +class GetterWrapper: + """Wrapper class around an attribute of an object.""" + + def __init__(self, obj, getter): + """Create a new instance of GetterWrapper.""" + # immortal class which provides wrapped object + self.__dict__["_immortal_object"] = obj + # function to get the wrapped object from the immortal class + self.__dict__["_get_wrapped_object"] = getter + + def __getattr__(self, attr): + """Wrap getters to the wrapped object.""" + if attr in self.__dict__: + return getattr(self, attr) + return getattr(self._get_wrapped_object(self._immortal_object), attr) + + def __setattr__(self, attr, value): + """Wrap setters to the wrapped object.""" + if attr in self.__dict__: + setattr(self, attr, value) + setattr(self._get_wrapped_object(self._immortal_object), attr, value) + + def __repr__(self): + """Return the repr of the wrapped object.""" + return repr(self._get_wrapped_object(self._immortal_object)) + + def _get_test_helper(): global TEST_HELPER if TEST_HELPER is not None: diff --git a/tests/embedding/test_graphics_export.py b/tests/embedding/test_graphics_export.py deleted file mode 100644 index 8e3feb062e..0000000000 --- a/tests/embedding/test_graphics_export.py +++ /dev/null @@ -1,80 +0,0 @@ -# Copyright (C) 2022 - 2026 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. - -"""Graphics export tests.""" - -from pathlib import Path - -import pytest - - -def _is_readable(filepath: str): - filepath = Path(filepath) - try: - with filepath.open("rb") as file: - file.read() - except Exception as e: - assert False, f"Failed to read file {filepath}: {e}" - finally: - filepath.unlink(missing_ok=True) - - -@pytest.mark.embedding -@pytest.mark.parametrize("image_format", ["PNG", "JPG", "BMP"]) -def test_graphics_export_image(printer, embedded_app, image_format, graphics_test_mechdb_file): - """Tests to check image export.""" - printer(f"{image_format} export") - embedded_app.update_globals(globals()) - embedded_app.open(graphics_test_mechdb_file) - image_settings = Ansys.Mechanical.Graphics.GraphicsImageExportSettings() - image_format = getattr(GraphicsImageExportFormat, image_format) - image_settings.Resolution = GraphicsResolutionType.EnhancedResolution - image_settings.Background = GraphicsBackgroundType.White - image_settings.Width = 1280 - image_settings.Height = 720 - dir_deformation = DataModel.GetObjectsByType(DataModelObjectCategory.DeformationResult)[0] - Tree.Activate([dir_deformation]) - ExtAPI.Graphics.Camera.SetFit() - image_file = str(Path.cwd() / f"image.{image_format}") - ExtAPI.Graphics.ExportImage(image_file, image_format, image_settings) - _is_readable(image_file) - - -@pytest.mark.embedding -@pytest.mark.parametrize("animation_format", ["GIF", "AVI", "MP4", "WMV"]) -def test_graphics_export_animation( - printer, embedded_app, animation_format, graphics_test_mechdb_file -): - """Tests to check animation export.""" - printer(f"{animation_format} export") - embedded_app.update_globals(globals()) - embedded_app.open(graphics_test_mechdb_file) - animation_settings = Ansys.Mechanical.Graphics.AnimationExportSettings() - animation_format = getattr(GraphicsAnimationExportFormat, animation_format) - animation_settings.Width = 1280 - animation_settings.Height = 720 - dir_deformation = DataModel.GetObjectsByType(DataModelObjectCategory.DeformationResult)[0] - Tree.Activate([dir_deformation]) - ExtAPI.Graphics.Camera.SetFit() - animation_file = str(Path.cwd() / f"animation.{animation_format}") - dir_deformation.ExportAnimation(animation_file, animation_format, animation_settings) - _is_readable(animation_file) diff --git a/tests/embedding/test_helper.py b/tests/embedding/test_helper.py new file mode 100644 index 0000000000..eeaf81658f --- /dev/null +++ b/tests/embedding/test_helper.py @@ -0,0 +1,288 @@ +# Copyright (C) 2022 - 2026 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. + +"""Tests for embedding helper methods.""" + +from pathlib import Path + +import pytest + + +def _is_readable(filepath): + """Assert that the file is non-empty and readable, then delete it.""" + filepath = Path(filepath) + try: + with filepath.open("rb") as file: + file.read() + except Exception as e: + assert False, f"Failed to read file {filepath}: {e}" + finally: + filepath.unlink(missing_ok=True) + + +@pytest.mark.embedding +def test_helpers_initialization(embedded_app): + """Test that helpers are properly initialized with the app.""" + assert embedded_app.helpers is not None + assert hasattr(embedded_app.helpers, "_app") + assert embedded_app.helpers._app == embedded_app + assert hasattr(embedded_app.helpers, "Ansys") + + +@pytest.mark.embedding +def test_import_geometry(embedded_app, assets, printer): + """Test geometry import with basic and all options.""" + printer("Testing geometry import") + geometry_file = str(Path(assets) / "Eng157.x_t") + + # Test basic import + printer("Testing basic geometry import") + geometry_import = embedded_app.helpers.import_geometry(geometry_file) + assert geometry_import is not None + assert str(geometry_import.ObjectState) in ["FullyDefined", "Solved"] + printer(f"Basic import successful: {geometry_import.ObjectState}") + + # Start fresh for comprehensive test + embedded_app.new() + + # Test with all options enabled + printer("Testing geometry import with all options") + GeometryImportPreference = ( # noqa: N806 + embedded_app.helpers.Ansys.Mechanical.DataModel.Enums.GeometryImportPreference + ) + geometry_import = embedded_app.helpers.import_geometry( + geometry_file, + process_named_selections=True, + named_selection_key="", + process_material_properties=True, + process_coordinate_systems=True, + analysis_type=GeometryImportPreference.AnalysisType.Type3D, + ) + assert geometry_import is not None + assert str(geometry_import.ObjectState) in ["FullyDefined", "Solved"] + printer("Import with all options successful") + + +@pytest.mark.embedding +def test_import_geometry_invalid_file(embedded_app, printer): + """Test geometry import with invalid file path.""" + printer("Testing geometry import with invalid file") + invalid_file = "C:\\nonexistent\\file.x_t" + + with pytest.raises(RuntimeError, match="Geometry Import unsuccessful"): + embedded_app.helpers.import_geometry(invalid_file) + + +@pytest.mark.embedding +def test_import_materials(embedded_app, assets, printer): + """Test material import.""" + printer("Testing material import") + material_file = str(Path(assets) / "eng200_material.xml") + + embedded_app.helpers.import_materials(material_file) + + # Verify materials were imported + materials = embedded_app.Model.Materials + assert materials.Children is not None + # Materials collection doesn't have Count, but import should succeed without error + printer("Materials imported successfully") + + +@pytest.mark.embedding +def test_import_materials_invalid_file(embedded_app, printer): + """Test material import with invalid file.""" + printer("Testing material import with invalid file") + invalid_file = "C:\\nonexistent\\materials.xml" + + with pytest.raises(RuntimeError, match="Material Import unsuccessful"): + embedded_app.helpers.import_materials(invalid_file) + + +@pytest.mark.embedding +def test_export_image(embedded_app, assets, tmp_path, printer): + """Test image export with default and custom settings.""" + printer("Testing image export") + geometry_file = str(Path(assets) / "Eng157.x_t") + embedded_app.helpers.import_geometry(geometry_file) + + # Test with default parameters + printer("Testing image export with defaults") + image_path = tmp_path / "test_image.png" + embedded_app.helpers.export_image( + obj=embedded_app.Model.Geometry, + file_path=str(image_path), + ) + _is_readable(image_path) + printer(f"Default export successful: {image_path}") + + # Test with custom settings using enums + from ansys.mechanical.core.embedding.enum_importer import ( + GraphicsBackgroundType, + GraphicsImageExportFormat, + GraphicsResolutionType, + ) + + printer("Testing image export with custom settings") + image_path_custom = tmp_path / "test_custom.jpg" + embedded_app.helpers.export_image( + obj=embedded_app.Model.Geometry, + file_path=str(image_path_custom), + width=1280, + height=720, + background=GraphicsBackgroundType.GraphicsAppearanceSetting, + resolution=GraphicsResolutionType.HighResolution, + image_format=GraphicsImageExportFormat.JPG, + ) + _is_readable(image_path_custom) + printer("Custom export successful") + + +@pytest.mark.embedding +@pytest.mark.parametrize("image_format", ["PNG", "JPG", "BMP", "TIF", "EPS"]) +def test_export_image_all_formats(embedded_app, assets, tmp_path, printer, image_format): + """Test image export with different formats.""" + printer(f"Testing image export with format: {image_format}") + geometry_file = str(Path(assets) / "Eng157.x_t") + embedded_app.helpers.import_geometry(geometry_file) + + from ansys.mechanical.core.embedding.enum_importer import GraphicsImageExportFormat + + fmt = getattr(GraphicsImageExportFormat, image_format) + image_path = tmp_path / f"test_image.{image_format.lower()}" + embedded_app.helpers.export_image( + obj=embedded_app.Model.Geometry, + file_path=str(image_path), + image_format=fmt, + ) + _is_readable(image_path) + printer(f"Exported {image_format} successfully") + + +@pytest.mark.embedding +def test_export_image_validation_errors(embedded_app, assets, tmp_path, printer): + """Test image export validation errors.""" + printer("Testing image export validation errors") + geometry_file = str(Path(assets) / "Eng157.x_t") + embedded_app.helpers.import_geometry(geometry_file) + + # Test missing file path + printer("Testing missing file path error") + with pytest.raises(ValueError, match="file_path must be provided"): + embedded_app.helpers.export_image(obj=embedded_app.Model.Geometry) + + printer("All validation errors handled correctly") + + +@pytest.mark.embedding +@pytest.mark.parametrize("animation_format", ["GIF", "AVI", "MP4", "WMV"]) +def test_export_animation_basic( + embedded_app, tmp_path, printer, graphics_test_mechdb_file, animation_format +): + """Test animation export with graphics fixture.""" + printer(f"Testing animation export with format: {animation_format}") + + from ansys.mechanical.core.embedding.enum_importer import ( + DataModelObjectCategory, + GraphicsAnimationExportFormat, + ) + + # Open the mechdb file with pre-solved results + embedded_app.open(str(graphics_test_mechdb_file)) + result = embedded_app.DataModel.GetObjectsByType(DataModelObjectCategory.DeformationResult)[0] + assert result is not None + printer(f"Using result: {result.Name}") + + fmt = getattr(GraphicsAnimationExportFormat, animation_format) + animation_path = tmp_path / f"test_animation.{animation_format.lower()}" + embedded_app.helpers.export_animation( + obj=result, + file_path=str(animation_path), + animation_format=fmt, + width=640, + height=480, + ) + _is_readable(animation_path) + printer(f"Exported {animation_format} successfully") + + +@pytest.mark.embedding +def test_export_animation_validation_errors(embedded_app, assets, tmp_path, printer): + """Test animation export validation errors.""" + printer("Testing animation export validation errors") + geometry_file = str(Path(assets) / "Eng157.x_t") + embedded_app.helpers.import_geometry(geometry_file) + + analysis = embedded_app.Model.AddStaticStructuralAnalysis() + result = analysis.Solution.AddTotalDeformation() + + # Test missing file path + printer("Testing missing file path error") + with pytest.raises(ValueError, match="file_path must be provided"): + embedded_app.helpers.export_animation(obj=result) + + printer("All animation validation errors handled correctly") + + +@pytest.mark.embedding +def test_display_image(embedded_app, assets, tmp_path, printer, monkeypatch): + """Test display_image with default and custom settings.""" + printer("Testing display_image") + geometry_file = str(Path(assets) / "Eng157.x_t") + embedded_app.helpers.import_geometry(geometry_file) + + # Mock pyplot.show to avoid displaying during tests + show_called = [] + + def mock_show(): + show_called.append(True) + + import matplotlib.pyplot as plt + + monkeypatch.setattr(plt, "show", mock_show) + + # Test with default settings + printer("Testing display with defaults") + image_path = tmp_path / "test_display.png" + embedded_app.helpers.export_image( + obj=embedded_app.Model.Geometry, + file_path=str(image_path), + ) + embedded_app.helpers.display_image(str(image_path)) + assert len(show_called) == 1 + printer("Default display successful") + Path(image_path).unlink(missing_ok=True) + + # Test with custom settings + printer("Testing display with custom settings") + image_path_custom = tmp_path / "test_display_custom.png" + embedded_app.helpers.export_image( + obj=embedded_app.Model.Geometry, + file_path=str(image_path_custom), + ) + embedded_app.helpers.display_image( + str(image_path_custom), + figsize=(10, 6), + axis="on", + ) + assert len(show_called) == 2 + printer("Custom display successful") + Path(image_path_custom).unlink(missing_ok=True)