I-Beam Analysis with Automated Report Generation#

This comprehensive example demonstrates how to perform a complete structural analysis of an I-beam using PyMAPDL and automatically generate detailed engineering reports. The example showcases advanced PyMAPDL capabilities including parameterized modeling, result extraction, and automated documentation generation.

What this example demonstrates:

  • Parameterized I-beam geometry creation

  • Material property definition and assignment

  • Fully constrained beam at both ends

  • Distributed load applied using nodal forces

  • Comprehensive result extraction and postprocessing

  • Automated report generation in Markdown and Word formats

  • Analytical verification of finite element results

Introduction#

Engineering analysis often requires not only obtaining results but also documenting the analysis process, assumptions, and findings in a professional report format. This example shows how PyMAPDL can be used to create a complete analysis workflow that includes automated report generation.

The analysis focuses on a fully constrained I-beam subjected to a uniformly distributed load, a fundamental structural engineering problem. The results are verified against analytical solutions and presented in professional report formats.

Problem Definition#

Analyze a I-beam with the following characteristics:

Geometry:

  • Length: 5.0 meters

  • I-section with typical structural steel proportions

  • Fully constrained at both ends

Loading:

  • Uniformly distributed load of 50 kN/m (downward)

  • Static analysis (no dynamic effects)

Material:

  • Structural steel (S355 grade)

  • Linear elastic behavior assumed

Objectives:

  • Determine maximum displacement and location

  • Calculate maximum bending stress

  • Verify safety against yielding

  • Generate comprehensive documentation

Model Setup and Parametrization#

The example begins by defining all analysis parameters in dictionaries for easy modification and reuse:

# I-beam geometric parameters (all dimensions in mm)
beam_params = {
    "length": 5000.0,  # Total beam length (5 meters)
    "flange_width": 200.0,  # Width of top and bottom flanges
    "web_height": 400.0,  # Height of the web (total beam height)
    "flange_thickness": 20.0,  # Thickness of flanges
    "web_thickness": 12.0,  # Thickness of web
}
beam_params.update({k: v for k, v in kwargs.items() if k in beam_params})

# Material properties for structural steel (S355)
material_props = {
    "elastic_modulus": 200000.0,  # Young's modulus in MPa (200 GPa = 200,000 MPa)
    "poisson_ratio": 0.30,  # Poisson's ratio
    "density": 7850e-6,  # Density in kg/mm³ (7850 kg/m³)
    "yield_strength": 355.0,  # Yield strength in MPa
}
# Update with any additional parameters
material_props.update({k: v for k, v in kwargs.items() if k in material_props})

# Loading and boundary conditions
load_params = {
    "distributed_load": -50.0,  # Distributed load in N/mm (50 kN/m downward)
    "safety_factor": 2.0,  # Design safety factor
}
load_params.update({k: v for k, v in kwargs.items() if k in load_params})

print(f"Beam Length: {beam_params['length']} mm")
print(
    f"I-Section: {beam_params['flange_width']}x{beam_params['web_height']}x{beam_params['web_thickness']}"
)
print(f"Material: Steel E = {material_props['elastic_modulus']} MPa")
print(f"Load: {load_params['distributed_load']} N/mm")

This parameterized approach allows for:

  • Easy modification of beam dimensions

  • Material property changes

  • Load case variations

  • Design optimization studies

Preprocessing - Geometry and Meshing#

Element Type Selection#

The analysis uses BEAM188 elements, which are:

  • 3D linear finite strain beam elements

  • Suitable for thin to moderately thick beam structures

  • Capable of handling large deflection effects (if needed)

  • Provide comprehensive stress output

# Define element type - BEAM188 (3D linear finite strain beam)
mapdl.et(1, "BEAM188")
mapdl.keyopt(1, 4, 1)  # Enable transverse shear stress output
mapdl.keyopt(1, 6, 1)  # Enable stress output at intermediate stations

Cross-Section Definition#

The I-beam cross-section is defined using PyMAPDL’s section commands:

The SECDATA command defines the I-beam geometry where:

  • First two parameters: top and bottom flange widths

  • Third parameter: total beam height

  • Fourth and fifth parameters: flange thicknesses

  • Sixth parameter: web thickness

Node and Element Generation#

Nodes are created along the beam length with uniform spacing:

# Create nodes along the beam length
num_elements = 20  # Number of elements for discretization
beam_params["num_elements"] = num_elements  # Store for report

node_spacing = beam_params["length"] / num_elements

# Generate nodes from 0 to beam length
for i in range(num_elements + 1):
    x_coord = i * node_spacing
    mapdl.n(i + 1, x_coord, 0, 0)

# Create beam elements connecting consecutive nodes
for i in range(num_elements):
    mapdl.e(i + 1, i + 2)

The mesh uses 20 elements along the beam length, providing sufficient resolution for accurate results while maintaining computational efficiency.

Boundary Conditions and Loading#

Fully Constrained End Conditions#

Fully clamped boundary conditions on the ends are implemented as:

Distributed Load Application#

The uniformly distributed load is applied as equivalent nodal forces:

# Apply distributed load as nodal forces
# Convert distributed load to equivalent nodal forces using trapezoidal rule
# For uniformly distributed load: interior nodes get w*L/n, end nodes get w*L/(2*n)

nodal_force = (
    load_params["distributed_load"] * node_spacing
)  # Force per interior node
end_nodal_force = nodal_force / 2  # Force per end node

print("Applying distributed load as nodal forces:")
print(f"End nodes (1, {last_node}): {end_nodal_force} N each")
print(f"Interior nodes (2 to {num_elements}): {nodal_force} N each")
print(f"Node spacing: {node_spacing} mm")

# Apply forces to all nodes
mapdl.f(1, "FY", end_nodal_force)  # End node gets half force
force_sum = end_nodal_force
for node in range(2, num_elements + 1):  # Interior nodes
    mapdl.f(node, "FY", nodal_force)
    force_sum += nodal_force
mapdl.f(last_node, "FY", end_nodal_force)  # End node gets half force

This approach:

  • Converts the continuous load to discrete nodal forces

  • Maintains load equilibrium

  • Provides accurate representation of the distributed loading

Alternatively, the load can be applied using the SFBEAM command for surface loads on BEAM and PIPE elements.

Solution Process#

The static structural solution is configured and executed:

# Enter solution processor and solve
print("\n-- Solving MAPDL model --")
mapdl.solution()
mapdl.antype("STATIC")  # Static structural analysis
mapdl.nlgeom("OFF")  # Linear analysis (small deflection)

print("Solving...")
mapdl.solve()
mapdl.finish()

print("Solution completed successfully")

The solution uses:

  • Static analysis type (no time-dependent effects)

  • Linear analysis (small deflection theory)

  • Direct solver for optimal accuracy

Post-Processing and Result Extraction#

Displacement Results#

The maximum displacement is extracted using the post_processing module:

# Get nodal displacements
# Extract displacement results - Y direction only
displacements = mapdl.post_processing.nodal_displacement("Y")
max_displacement = np.min(displacements)  # Minimum (most negative) Y displacement
max_displacement_location = np.argmin(displacements) + 1

Moment Results#

The moment at both nodes are extracted using ETABLE command, with SMISC items:

# Get moment values
# Create element tables
mapdl.etable("MOMY_I", "SMISC", 3)  # Moment Y at node I
mapdl.etable("MOMY_J", "SMISC", 16)  # Moment Y at node J

max_moment_i = mapdl.get_array("ELEM", "", "ETAB", "MOMY_I").max()
max_moment_j = mapdl.get_array("ELEM", "", "ETAB", "MOMY_J").max()
max_moment_fem = max(max_moment_i, max_moment_j)

Bending Stress Results#

Again ETABLE is used to retrieve MAPDL values:

# Get bending stress values
# Bending stress on the element +Y side of the beam
mapdl.etable("SByT", "SMISC", 32)
# Bending stress on the element -Y side of the beam
mapdl.etable("SByB", "SMISC", 33)

bending_stress_top = mapdl.get_array("ELEM", "", "ETAB", "SByT").max()
bending_stress_bottom = mapdl.get_array("ELEM", "", "ETAB", "SByB").max()
max_stress_fem = max(bending_stress_top, bending_stress_bottom)

Bending Strain Results#

# Get bending strain values
# Bending strain on the element +Y side of the beam
mapdl.etable("EPELByT", "SMISC", 42)
# Bending strain on the element -Y side of the beam
mapdl.etable("EPELByB", "SMISC", 43)

bending_strain_top = mapdl.get_array("ELEM", "", "ETAB", "EPELByT").max()
bending_strain_bottom = mapdl.get_array("ELEM", "", "ETAB", "EPELByB").max()
max_strain_fem = max(bending_strain_top, bending_strain_bottom)

Report Generation Features#

Markdown Report#

The Markdown report includes:

  • Executive summary with key results

  • Detailed model description with parameter tables

  • Comprehensive results presentation

  • Analytical verification section

  • Professional formatting with tables and equations

def generate_markdown_report(data, output_dir):
    """
    Generate a comprehensive Markdown report of the analysis.

    Parameters
    ----------
    data : dict
        Analysis data dictionary
    output_dir : Path
        Directory to save the report

    Returns
    -------
    str
        Path to generated Markdown file
    """

    timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")

    markdown_content = f"""# {data['name']}

- **Generated:** {timestamp}
- **Analysis Type:** Static Structural Analysis
- **Software:** PyMAPDL (Ansys MAPDL)

## Executive Summary

This report presents the results of a static structural analysis of a fully
constrained I-beam at both ends, subjected to a uniformly distributed load.
The analysis was performed using finite element methods via PyMAPDL.

### Key Results

- **Maximum Displacement:** {data['results']['max_displacement']:.2f} mm
- **Maximum Stress:** {data['results']['max_stress']:.1f} MPa
- **Safety Factor:** {data['results']['safety_factor']:.2f}
- **Total Applied Load:** {data['results']['total_load']:.1f} kN

## Model Description

### Geometry

The analyzed structure is an I-beam with the following dimensions:

| Parameter | Value | Unit |
|-----------|-------|------|
| Length | {data['beam_params']['length']:.0f} | mm |
| Flange Width | {data['beam_params']['flange_width']:.0f} | mm |
| Web Height | {data['beam_params']['web_height']:.0f} | mm |
| Flange Thickness | {data['beam_params']['flange_thickness']:.0f} | mm |
| Web Thickness | {data['beam_params']['web_thickness']:.0f} | mm |


![Beam section plot](ibeam_section.png)

*Figure 1: I-beam cross-section showing key dimensions*


![Beam elements plot](geometry.png)

*Figure 2: Beam showing elements*

### Material Properties

The beam is modeled using structural steel with the following properties:

| Property | Value | Unit |
|----------|-------|------|
| Elastic Modulus | {data['material_props']['elastic_modulus']:.0f} | MPa |
| Poisson's Ratio | {data['material_props']['poisson_ratio']:.2f} | - |
| Density | {data['material_props']['density']*1e9:.0f} | kg/m³ |
| Yield Strength | {data['material_props']['yield_strength']:.0f} | MPa |

### Loading and Boundary Conditions

- **Support Type:** Fully constrained at both ends.
- **Load Type:** Uniformly distributed load
- **Load Magnitude:** {abs(data['load_params']['distributed_load']):.0f} N/mm

## Analysis Results

### Displacement Results

- **Maximum vertical displacement:** {data['results']['max_displacement']:.2f} mm
- **Location:** Node {data['results']['max_displacement_node']} (approximately mid-span)

### Stress Results

- **Maximum bending stress:** {data['results']['max_stress']:.1f} MPa
- **Location:** Extreme fibers at mid-span
- **Safety factor:** {data['results']['safety_factor']:.2f}

### Section Properties

| Property | Value | Unit |
|----------|-------|------|
| Cross-sectional Area | {data['section_props']['Area']:.0f} | mm² |
| Moment of Inertia (Iyy) | {data['section_props']['Iyy']:.0f} | mm⁴ |
| Section Modulus | {data['section_props']['Section_modulus']:.0f} | mm³ |

### Analysis Plots

The following plots visualize the analysis results:

#### Displacement Contours

![Displacement Plot](displacement_plot.png)

*Figure 3: Y-direction displacement contours showing maximum deformation at mid-span*

#### Stress Distribution

![Stress Plot](stress_plot.png)

*Figure 4: Equivalent stress distribution showing maximum stress at beam extreme fibers*

## Conclusions

1. **Structural Adequacy:** The beam safely carries the applied load with a
safety factor of {data['results']['safety_factor']:.2f}.

2. **Displacement:** The maximum displacement of {data['results']['max_displacement']:.2f} mm is within acceptable limits for most structural applications.

3. **Recommendation:** The I-beam design is adequate for the specified loading conditions.

## Analysis Details

- **Element Type:** BEAM188 (3D linear finite strain beam)
- **Number of Elements:** {data["beam_params"]["num_elements"]}
- **Solution Type:** Static linear analysis
- **Convergence:** Solution converged successfully

---
*This report was automatically generated using PyMAPDL*
"""

    # Write to file
    report_file = output_dir / "ibeam_analysis_report.md"
    with open(report_file, "w", encoding="utf-8") as f:
        f.write(markdown_content)

    return str(report_file)

Word Document Report#

The Word document report provides:

  • Professional document formatting

  • Tables for organized data presentation

  • Structured sections and headings

  • Executive summary format

def generate_word_report(data, output_dir):
    """
    Generate a Word document report of the analysis.

    Parameters
    ----------
    data : dict
        Analysis data dictionary
    output_dir : Path
        Directory to save the report

    Returns
    -------
    str
        Path to generated Word file
    """

    from docx import Document
    from docx.enum.text import WD_ALIGN_PARAGRAPH
    from docx.shared import Inches

    # Create new document
    doc = Document()

    # Title
    title = doc.add_heading(data["name"], 0)
    title.alignment = WD_ALIGN_PARAGRAPH.CENTER

    # Metadata
    doc.add_paragraph(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
    doc.add_paragraph("Analysis Type: Static Structural Analysis")
    doc.add_paragraph("Software: PyMAPDL (Ansys MAPDL)")

    # Executive Summary
    doc.add_heading("Executive Summary", level=1)
    doc.add_paragraph(
        "This report presents the results of a static structural analysis of a "
        "fully constrained I-beam at both ends, subjected to a uniformly distributed load."
    )

    # Key Results table
    doc.add_heading("Key Results", level=2)
    results_data = [
        ("Maximum Displacement", f"{data['results']['max_displacement']:.2f} mm"),
        ("Maximum Stress", f"{data['results']['max_stress']:.1f} MPa"),
        ("Safety Factor", f"{data['results']['safety_factor']:.2f}"),
        ("Total Applied Load", f"{data['results']['total_load']:.1f} kN"),
    ]

    table = doc.add_table(rows=len(results_data), cols=2)
    table.style = "Table Grid"

    for i, (param, value) in enumerate(results_data):
        table.rows[i].cells[0].text = param
        table.rows[i].cells[1].text = value

    # Model Description
    doc.add_heading("Model Description", level=1)

    # Geometry table
    doc.add_heading("Geometry", level=2)
    geom_data = [
        ("Length", f"{data['beam_params']['length']:.0f}", "mm"),
        ("Flange Width", f"{data['beam_params']['flange_width']:.0f}", "mm"),
        ("Web Height", f"{data['beam_params']['web_height']:.0f}", "mm"),
        ("Flange Thickness", f"{data['beam_params']['flange_thickness']:.0f}", "mm"),
        ("Web Thickness", f"{data['beam_params']['web_thickness']:.0f}", "mm"),
    ]

    # Create table with header row + data rows
    geom_table = doc.add_table(rows=len(geom_data) + 1, cols=3)
    geom_table.style = "Table Grid"

    # Headers
    geom_table.rows[0].cells[0].text = "Parameter"
    geom_table.rows[0].cells[1].text = "Value"
    geom_table.rows[0].cells[2].text = "Unit"

    for i, (param, value, unit) in enumerate(geom_data, 1):
        geom_table.rows[i].cells[0].text = param
        geom_table.rows[i].cells[1].text = value
        geom_table.rows[i].cells[2].text = unit

    if "plot_files" in data and data["plot_files"]:
        # Add beam section plot
        if "section" in data["plot_files"]:
            section_plot_path = Path(data["plot_files"]["section"])
            if section_plot_path.exists():
                doc.add_heading("Beam Section Plot", level=3)
                doc.add_picture(str(section_plot_path), width=Inches(4))
                doc.add_paragraph(
                    "Figure 1: I-beam cross-section showing key dimensions",
                    style="Caption",
                )

        if "geometry" in data["plot_files"]:
            geometry_plot_path = Path(data["plot_files"]["geometry"])
            if geometry_plot_path.exists():
                doc.add_heading("Beam Geometry Plot", level=3)
                doc.add_picture(str(geometry_plot_path), width=Inches(4))
                doc.add_paragraph("Figure 2: I-beam geometry", style="Caption")

    # Results section
    doc.add_heading("Analysis Results", level=1)

    doc.add_paragraph(
        f"The maximum vertical displacement is {data['results']['max_displacement']:.2f} mm, "
        f"occurring at approximately mid-span."
    )

    doc.add_paragraph(
        f"The maximum bending stress is {data['results']['max_stress']:.1f} MPa, "
        f"resulting in a safety factor of {data['results']['safety_factor']:.2f} "
        f"against yielding."
    )

    # Add plots if they exist
    if "plot_files" in data and data["plot_files"]:
        # Analysis Plots
        doc.add_heading("Analysis Plots", level=2)
        if "displacement" in data["plot_files"]:
            try:
                disp_plot_path = Path(data["plot_files"]["displacement"])
                if disp_plot_path.exists():
                    doc.add_paragraph("Displacement Contours:", style="Heading 3")

                    doc.add_picture(str(disp_plot_path), width=Inches(6))
                    doc.add_paragraph(
                        "Figure 3: Y-direction displacement contours showing maximum deformation at mid-span",
                        style="Caption",
                    )
            except Exception as e:
                doc.add_paragraph(f"Displacement plot could not be loaded: {e}")

        if "stress" in data["plot_files"]:
            try:
                stress_plot_path = Path(data["plot_files"]["stress"])
                if stress_plot_path.exists():
                    doc.add_paragraph("Stress Distribution:", style="Heading 3")

                    doc.add_picture(str(stress_plot_path), width=Inches(6))
                    doc.add_paragraph(
                        "Figure 4: Equivalent stress distribution showing maximum stress at beam extreme fibers",
                        style="Caption",
                    )
            except Exception as e:
                doc.add_paragraph(f"Stress plot could not be loaded: {e}")

    # Conclusions
    doc.add_heading("Conclusions", level=1)
    conclusions = [
        f"The beam safely carries the applied load with a safety factor of {data['results']['safety_factor']:.2f}.",
        f"The maximum displacement of {data['results']['max_displacement']:.2f} mm is within acceptable limits.",
        "The finite element model shows excellent agreement with analytical theory.",
        "The I-beam design is adequate for the specified loading conditions.",
    ]

    for conclusion in conclusions:
        p = doc.add_paragraph()
        p.add_run(f"• {conclusion}")

    # Save document
    word_file = output_dir / "ibeam_analysis_report.docx"
    doc.save(str(word_file))

    return str(word_file)

Note: Word report generation requires the python-docx package:

pip install python-docx

Plot Generation#

The example includes the capability to generate analysis plots:

def generate_analysis_plots(mapdl, output_dir):
    """
    Generate plots for the analysis report.

    Parameters
    ----------
    mapdl : ansys.mapdl.core.Mapdl
        MAPDL instance
    output_dir : Path
        Directory to save plots

    Returns
    -------
    dict
        Dictionary of generated plot file paths
    """

    plot_files = {}

    try:
        # Beam section plot
        mapdl.prep7()
        ibeam_plot_file = output_dir / "ibeam_section.png"
        mapdl.secplot(1, savefig=str(ibeam_plot_file))

        if ibeam_plot_file.exists():
            plot_files["section"] = str(ibeam_plot_file)
            print(f"Generated I-beam section plot: {ibeam_plot_file}")

        eplot_file = output_dir / "geometry.png"
        mapdl.run("eshape,1,1")  # Using run to avoid warning
        mapdl.eplot(1, graphics_backend=GraphicsBackend.MAPDL, savefig=str(eplot_file))

        if eplot_file.exists():
            plot_files["geometry"] = str(eplot_file)
            print(f"Generated element plot: {eplot_file}")

        mapdl.post1()
        mapdl.set("LAST")

        # Displacement plot
        disp_plot_file = output_dir / "displacement_plot.png"
        mapdl.plnsol("U", "Y", savefig=str(disp_plot_file))
        if disp_plot_file.exists():
            plot_files["displacement"] = str(disp_plot_file)
            print(f"Generated displacement plot: {disp_plot_file}")

        # Stress plot
        stress_plot_file = output_dir / "stress_plot.png"
        mapdl.plesol("S", "EQV", savefig=str(stress_plot_file))
        if stress_plot_file.exists():
            plot_files["stress"] = str(stress_plot_file)
            print(f"Generated stress plot: {stress_plot_file}")

    except Exception as e:
        print(f"Warning: Could not generate plots: {e}")
        print(
            "This may be due to graphics/display limitations in the current environment."
        )
        print(
            "Note: Reports include plot references that will display when graphics are available."
        )

    return plot_files

Generated plots include:

  • Displacement contours showing deformed shape

  • Stress contours highlighting critical regions

  • Node and element plots for model verification

Running the Example#

Basic Execution#

To run the complete analysis and generate reports:

from beam_with_report import create_ibeam_analysis_and_report

# Run complete analysis
results = create_ibeam_analysis_and_report()

Or execute the script directly:

python beam_with_report.py

Output Files#

The analysis generates several output files in the beam_analysis_output directory:

  • ibeam_analysis_report.md - Markdown report

  • ibeam_analysis_report.docx - Word document

  • displacement_plot.png - Displacement visualization

  • stress_plot.png - Stress visualization

Customization Options#

Parameter Modification#

Easily modify analysis parameters by changing the dictionaries:

# Modify beam dimensions
beam_params["length"] = 8000.0  # Change to 8 meters
beam_params["web_height"] = 500.0  # Increase height

# Change material
material_props["elastic_modulus"] = 200000.0  # Different steel grade

# Modify loading
load_params["distributed_load"] = -75.0  # Increase load intensity

Multiple Load Cases#

The framework can be extended for multiple load cases:

load_cases = [
    {"name": "Service Load", "distributed_load": -50.0},
    {"name": "Ultimate Load", "distributed_load": -75.0},
    {"name": "Wind Load", "distributed_load": -25.0},
]

for case in load_cases:
    # Run analysis for each case
    # Generate separate reports
    results = create_ibeam_analysis_and_report(**case)

This parameterized approach enables design optimization studies.

Advanced Analysis Options#

The example can be extended for more sophisticated analyses.

Nonlinear Analysis#

# Enable geometric nonlinearity
mapdl.nlgeom("ON")

# Material nonlinearity
mapdl.mp("BISO", 1, yield_strength, hardening_modulus)

Dynamic Analysis#

# Modal analysis
mapdl.antype("MODAL")
mapdl.modopt("LANB", 10)  # First 10 modes

Additional Report Features#

PDF Generation#

If you install pandoc, you can convert the Markdown report to a PDF file as follows:

# Convert Markdown to PDF using pandoc
import subprocess

subprocess.run(
    ["pandoc", "ibeam_analysis_report.md", "-o", "ibeam_analysis_report.pdf"]
)

Excel Export#

You can export results to Excel using pandas. This is quite useful when reporting parametric studies:

import pandas as pd

load_cases = [
    {"name": "Service Load", "distributed_load": -50.0},
    {"name": "Ultimate Load", "distributed_load": -75.0},
    {"name": "Wind Load", "distributed_load": -25.0},
]

df_result = pd.DataFrame()

for case in load_cases:
    # Run analysis for each case
    # Generate separate reports
    results = create_ibeam_analysis_and_report(**case)

    # Create DataFrame from results
    df_result = pd.concat(
        [
            df_result,
            pd.DataFrame(
                {
                    "Load Case": case["name"],
                    "Max Displacement (mm)": results["max_displacement"],
                    "Max Bending Moment (Nm)": results["max_moment"],
                    "Max Bending Stress (MPa)": results["max_stress"],
                    "Max Bending Strain": results["max_strain"],
                    "Safety Factor": results["safety_factor"],
                }
            ),
        ],
        ignore_index=True,
    )

# Save to Excel
df_result.to_excel("ibeam_load_cases_analysis_results.xlsx", index=False)

Email Reports#

Using Python you can send the generated reports via email:

# The following code is not tested
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.base import MIMEBase
from email import encoders
import os
from pathlib import Path


def send_analysis_report_email(
    smtp_server="smtp.gmail.com",
    smtp_port=587,
    sender_email="your-email@gmail.com",
    sender_password="your-app-password",
    recipient_emails=["recipient@example.com"],
    subject="I-Beam Analysis Report",
    body_text="",
    attachments=None,
):
    """
    Send analysis reports via email with attachments

    Parameters
    ----------
    smtp_server : str
       SMTP server address (e.g., 'smtp.gmail.com', 'smtp.outlook.com')
    smtp_port : int
       SMTP port (587 for TLS, 465 for SSL)
    sender_email : str
       Sender's email address
    sender_password : str
       Sender's email password or app-specific password
    recipient_emails : list
       List of recipient email addresses
    subject : str
       Email subject line
    body_text : str
       Email body content
    attachments : list
       List of file paths to attach
    """

    # Create message container
    msg = MIMEMultipart()
    msg["From"] = sender_email
    msg["To"] = ", ".join(recipient_emails)
    msg["Subject"] = subject

    # Add body to email
    msg.attach(MIMEText(body_text, "plain"))

    # Add attachments
    if attachments:
        for file_path in attachments:
            if os.path.isfile(file_path):
                with open(file_path, "rb") as attachment:
                    # Instance of MIMEBase and named as part
                    part = MIMEBase("application", "octet-stream")
                    part.set_payload(attachment.read())

                # Encode file in ASCII characters to send by email
                encoders.encode_base64(part)

                # Add header as key/value pair to attachment part
                filename = os.path.basename(file_path)
                part.add_header(
                    "Content-Disposition",
                    f"attachment; filename= {filename}",
                )

                # Attach the part to message
                msg.attach(part)

    try:
        # Create SMTP session
        server = smtplib.SMTP(smtp_server, smtp_port)
        server.starttls()  # Enable security
        server.login(sender_email, sender_password)

        # Send email
        text = msg.as_string()
        server.sendmail(sender_email, recipient_emails, text)
        server.quit()

        print(f"Email sent successfully to {', '.join(recipient_emails)}")
        return True

    except Exception as e:
        print(f"Error sending email: {e}")
        return False

Conclusion#

This example demonstrates a complete PyMAPDL workflow that goes beyond simple analysis to include professional documentation and reporting. It provides a foundation for developing robust engineering analysis tools and workflows using PyMAPDL by combining:

  • Parameterized modeling

  • Comprehensive result extraction

  • Analytical verification

  • Automated report generation

The approach shown here can be adapted for various structural analysis problems and extended with additional features as needed for specific engineering applications.

References#

Additional files#

  • beam_with_report.py: Complete Python script.

Examples of the generated report files are: