Chris Parmer — home

Stress-strain curve

Mechanical Engineering — AISI 1018 steel tensile test

Example from the compendium of canonical charts

Mechanical Engineering — Stress-strain curve (AISI 1018 steel tensile test)

Python Code

"""Mechanical Engineering — Stress-strain curve (AISI 1018 steel tensile test)."""
import tempfile


import numpy as np
import plotly.graph_objects as go
import requests
import warnings

warnings.filterwarnings("ignore")
requests.packages.urllib3.disable_warnings()  # type: ignore


URL = (
    "https://github.com/ProfessorKazarinoff/Engineering-Materials-Programming/"
    "raw/main/Engineering-Materials-Programming-Book/07-Mechanical-Properties/"
    "Steel1018_raw_data.xls"
)


def load_excel():
    try:
        import pandas as pd

        print("  downloading xls ...")
        r = requests.get(URL, verify=False, timeout=60)
        r.raise_for_status()

        with tempfile.NamedTemporaryFile(suffix=".xls", delete=False) as f:
            f.write(r.content)
            tmp = f.name

        # Try the named sheet first
        try:
            df = pd.read_excel(tmp, sheet_name="Curve_Table")
        except Exception:
            df = pd.read_excel(tmp, sheet_name=0)

        print(f"  columns: {list(df.columns)}")
        print(f"  shape: {df.shape}")
        return df
    except Exception as e:
        print(f"  excel load failed ({e})")
        return None


def find_col(df, candidates):
    for cand in candidates:
        for col in df.columns:
            if cand.lower() in str(col).lower():
                return col
    return None


def generate():
    import pandas as pd

    df = load_excel()
    synthetic = False

    if df is not None:
        force_col = find_col(df, ["force", "load", "lb"])
        ext_col   = find_col(df, ["ext", "extension", "strain", "%"])

        if force_col is None or ext_col is None:
            print(f"  could not find force/ext columns in {list(df.columns)}")
            df = None

    if df is None:
        print("  using synthetic stress-strain curve for 1018 steel")
        synthetic = True

        # Piecewise synthetic curve for 1018 steel
        # Elastic: E = 200 GPa, yield ≈ 370 MPa, UTS ≈ 440 MPa, fracture ≈ 350 MPa
        strain_elastic = np.linspace(0, 0.00185, 50)   # up to ~370 MPa
        stress_elastic = 200e3 * strain_elastic         # MPa (E=200 GPa)

        # Work-hardening: yield to UTS
        strain_wh = np.linspace(0.00185, 0.20, 150)
        stress_wh = 370 + (440 - 370) * (
            1 - np.exp(-10 * (strain_wh - 0.00185))
        )

        # Necking/softening: UTS to fracture
        strain_neck = np.linspace(0.20, 0.30, 50)
        stress_neck = 440 * (1 - 1.5 * (strain_neck - 0.20))
        stress_neck = np.clip(stress_neck, 340, 440)

        strain_all = np.concatenate([strain_elastic, strain_wh, strain_neck])
        stress_all = np.concatenate([stress_elastic, stress_wh, stress_neck])
        rng = np.random.default_rng(7)
        stress_all += rng.normal(0, 1.5, len(stress_all))

        force_col = None
        ext_col   = None
    else:
        # Convert real data
        import pandas as pd
        df[force_col] = pd.to_numeric(df[force_col], errors="coerce")
        df[ext_col]   = pd.to_numeric(df[ext_col],   errors="coerce")
        df = df.dropna(subset=[force_col, ext_col])

        # Specimen geometry
        A0 = np.pi * (0.505 / 2) ** 2   # in²  (0.505 inch diameter round)

        stress_psi = df[force_col].values / A0
        stress_all = stress_psi * 6894.76 / 1e6   # psi → MPa

        strain_all = df[ext_col].values / 100       # % → fraction
        print(f"  stress range: {stress_all.min():.0f} – {stress_all.max():.0f} MPa")
        print(f"  strain range: {strain_all.min():.4f} – {strain_all.max():.4f}")

    # --- Key properties ---
    uts_idx = int(np.argmax(stress_all))
    UTS     = stress_all[uts_idx]
    UTS_strain = strain_all[uts_idx]

    # Elastic modulus from linear region (0 to ~50% of max stress)
    mask_elastic = stress_all < 0.5 * UTS
    if mask_elastic.sum() > 3:
        from numpy.polynomial import polynomial as P
        coeffs = np.polyfit(strain_all[mask_elastic], stress_all[mask_elastic], 1)
        E_GPa = coeffs[0] / 1e3   # MPa/fraction → GPa
    else:
        E_GPa = 200.0

    # 0.2% offset yield: find intersection of (stress vs strain) with offset line
    offset_line = E_GPa * 1e3 * (strain_all - 0.002)
    diff = stress_all - offset_line
    # Sign changes
    sign_changes = np.where(np.diff(np.sign(diff)))[0]
    if len(sign_changes) > 0:
        idx_yield = sign_changes[0] + 1
    else:
        idx_yield = int(np.argmin(np.abs(diff)))

    Sy       = stress_all[idx_yield]
    Sy_strain = strain_all[idx_yield]

    print(f"  E ≈ {E_GPa:.0f} GPa, Sy ≈ {Sy:.0f} MPa, UTS ≈ {UTS:.0f} MPa")

    fig = go.Figure()

    # Main stress-strain curve
    fig.add_trace(go.Scatter(
        x=strain_all,
        y=stress_all,
        mode="lines",
        name="Steel 1018" + (" (synthetic)" if synthetic else ""),
        line=dict(color=VIOLET, width=2),
        hovertemplate="ε=%{x:.4f}<br>σ=%{y:.1f} MPa<extra></extra>",
    ))

    # 0.2% offset line
    strain_offset = np.linspace(0, strain_all.max() * 0.6, 100)
    stress_offset = E_GPa * 1e3 * (strain_offset - 0.002)
    fig.add_trace(go.Scatter(
        x=strain_offset,
        y=stress_offset,
        mode="lines",
        name="0.2% offset line",
        line=dict(color=TEAL, width=1.5, dash="dash"),
    ))

    # Yield point marker
    fig.add_trace(go.Scatter(
        x=[Sy_strain],
        y=[Sy],
        mode="markers+text",
        name=f"Yield Sy = {Sy:.0f} MPa",
        marker=dict(color=TEAL, size=10, symbol="circle"),
        text=[f"  Sy={Sy:.0f} MPa"],
        textposition="middle right",
        textfont=dict(size=10, color=TEAL),
    ))

    # UTS marker
    fig.add_trace(go.Scatter(
        x=[UTS_strain],
        y=[UTS],
        mode="markers+text",
        name=f"UTS = {UTS:.0f} MPa",
        marker=dict(color=PINK, size=10, symbol="circle"),
        text=[f"  UTS={UTS:.0f} MPa"],
        textposition="middle right",
        textfont=dict(size=10, color=PINK),
    ))

    fig.update_layout(
        xaxis=dict(title="Engineering strain (ε)", tickformat=".3f"),
        yaxis=dict(title="Engineering stress (MPa)"),
        legend=dict(x=0.02, y=0.98, xanchor="left", yanchor="top", font=dict(size=10)),
        margin=dict(t=20, b=60, l=70, r=40),
        annotations=[
            dict(
                text=f"E ≈ {E_GPa:.0f} GPa",
                x=0.01, y=0.5,
                xref="paper", yref="paper",
                showarrow=False,
                font=dict(size=10, color=MUTED),
                xanchor="left",
            )
        ],
    )

    return fig


if __name__ == "__main__":
    generate()

Made with Plotly