|
| 1 | +""" pyplots.ai |
| 2 | +line-stress-strain: Engineering Stress-Strain Curve |
| 3 | +Library: seaborn 0.13.2 | Python 3.14.3 |
| 4 | +Quality: 90/100 | Created: 2026-03-20 |
| 5 | +""" |
| 6 | + |
| 7 | +import matplotlib.pyplot as plt |
| 8 | +import numpy as np |
| 9 | +import pandas as pd |
| 10 | +import seaborn as sns |
| 11 | + |
| 12 | + |
| 13 | +# Data — Mild steel tensile test simulation |
| 14 | +np.random.seed(42) |
| 15 | + |
| 16 | +youngs_modulus = 200000 # MPa |
| 17 | +yield_stress = 250 # MPa |
| 18 | +uts = 400 # MPa |
| 19 | +fracture_strain = 0.35 |
| 20 | +uts_strain = 0.22 |
| 21 | +yield_strain = yield_stress / youngs_modulus |
| 22 | + |
| 23 | +# Elastic region |
| 24 | +strain_elastic = np.linspace(0, yield_strain, 60) |
| 25 | +stress_elastic = youngs_modulus * strain_elastic |
| 26 | + |
| 27 | +# Strain hardening (quadratic rise to UTS) |
| 28 | +strain_hardening = np.linspace(yield_strain, uts_strain, 200) |
| 29 | +t = (strain_hardening - yield_strain) / (uts_strain - yield_strain) |
| 30 | +stress_hardening = yield_stress + (uts - yield_stress) * (2 * t - t**2) |
| 31 | + |
| 32 | +# Necking region (UTS to fracture — stress drops) |
| 33 | +strain_necking = np.linspace(uts_strain, fracture_strain, 100) |
| 34 | +t_neck = (strain_necking - uts_strain) / (fracture_strain - uts_strain) |
| 35 | +fracture_stress = 320 |
| 36 | +stress_necking = uts - (uts - fracture_stress) * t_neck**1.5 |
| 37 | + |
| 38 | +# Build DataFrame with region labels for seaborn hue-based plotting |
| 39 | +df_elastic = pd.DataFrame({"strain": strain_elastic, "stress": stress_elastic, "region": "Elastic"}) |
| 40 | +df_hardening = pd.DataFrame({"strain": strain_hardening, "stress": stress_hardening, "region": "Strain Hardening"}) |
| 41 | +df_necking = pd.DataFrame({"strain": strain_necking, "stress": stress_necking, "region": "Necking"}) |
| 42 | +df = pd.concat([df_elastic, df_hardening, df_necking], ignore_index=True) |
| 43 | + |
| 44 | +# 0.2% offset yield point |
| 45 | +offset = 0.002 |
| 46 | +offset_line_strain = np.linspace(offset, offset + yield_stress / youngs_modulus + 0.003, 50) |
| 47 | +offset_line_stress = youngs_modulus * (offset_line_strain - offset) |
| 48 | +offset_line_stress = np.clip(offset_line_stress, 0, yield_stress + 20) |
| 49 | + |
| 50 | +yield_offset_strain = offset + yield_stress / youngs_modulus |
| 51 | +yield_offset_stress = yield_stress |
| 52 | + |
| 53 | +# Define seaborn palette and style |
| 54 | +region_palette = {"Elastic": "#306998", "Strain Hardening": "#E07B39", "Necking": "#C0392B"} |
| 55 | +sns.set_style("whitegrid", {"grid.linestyle": "-", "grid.alpha": 0.15, "grid.linewidth": 0.8}) |
| 56 | +sns.set_context( |
| 57 | + "talk", |
| 58 | + rc={ |
| 59 | + "axes.titlesize": 24, |
| 60 | + "axes.labelsize": 20, |
| 61 | + "xtick.labelsize": 16, |
| 62 | + "ytick.labelsize": 16, |
| 63 | + "legend.fontsize": 13, |
| 64 | + "font.weight": "medium", |
| 65 | + }, |
| 66 | +) |
| 67 | + |
| 68 | +fig, ax = plt.subplots(figsize=(16, 9)) |
| 69 | + |
| 70 | +# Plot stress-strain curve using seaborn lineplot with hue for regions |
| 71 | +sns.lineplot( |
| 72 | + data=df, x="strain", y="stress", hue="region", palette=region_palette, linewidth=3.5, ax=ax, legend=True, sort=False |
| 73 | +) |
| 74 | + |
| 75 | +# 0.2% offset line |
| 76 | +ax.plot(offset_line_strain, offset_line_stress, linestyle="--", linewidth=2, color="#808080", zorder=3) |
| 77 | + |
| 78 | +# Mark critical points using seaborn scatterplot for consistent styling |
| 79 | +critical_points = pd.DataFrame( |
| 80 | + { |
| 81 | + "strain": [yield_offset_strain, uts_strain, fracture_strain], |
| 82 | + "stress": [yield_offset_stress, uts, fracture_stress], |
| 83 | + "point": ["Yield Point", "UTS", "Fracture"], |
| 84 | + } |
| 85 | +) |
| 86 | +point_palette = {"Yield Point": "#E07B39", "UTS": "#C0392B", "Fracture": "#2C3E50"} |
| 87 | +point_markers = {"Yield Point": "o", "UTS": "o", "Fracture": "X"} |
| 88 | +sns.scatterplot( |
| 89 | + data=critical_points, |
| 90 | + x="strain", |
| 91 | + y="stress", |
| 92 | + hue="point", |
| 93 | + style="point", |
| 94 | + palette=point_palette, |
| 95 | + markers=point_markers, |
| 96 | + s=200, |
| 97 | + edgecolor="white", |
| 98 | + linewidth=1.5, |
| 99 | + ax=ax, |
| 100 | + zorder=5, |
| 101 | + legend=True, |
| 102 | +) |
| 103 | + |
| 104 | +# Annotations for critical points — yield moved right to reduce left-side clutter |
| 105 | +ax.annotate( |
| 106 | + "Yield Point\n(0.2% offset)", |
| 107 | + xy=(yield_offset_strain, yield_offset_stress), |
| 108 | + xytext=(0.06, yield_offset_stress - 30), |
| 109 | + fontsize=14, |
| 110 | + fontweight="bold", |
| 111 | + color="#E07B39", |
| 112 | + arrowprops={"arrowstyle": "-|>", "color": "#E07B39", "lw": 1.5}, |
| 113 | + ha="left", |
| 114 | + va="top", |
| 115 | +) |
| 116 | + |
| 117 | +ax.annotate( |
| 118 | + f"UTS = {uts} MPa", |
| 119 | + xy=(uts_strain, uts), |
| 120 | + xytext=(uts_strain + 0.03, uts + 30), |
| 121 | + fontsize=14, |
| 122 | + fontweight="bold", |
| 123 | + color="#C0392B", |
| 124 | + arrowprops={"arrowstyle": "-|>", "color": "#C0392B", "lw": 1.5}, |
| 125 | + ha="left", |
| 126 | + va="bottom", |
| 127 | +) |
| 128 | + |
| 129 | +ax.annotate( |
| 130 | + "Fracture", |
| 131 | + xy=(fracture_strain, fracture_stress), |
| 132 | + xytext=(fracture_strain - 0.01, fracture_stress - 60), |
| 133 | + fontsize=14, |
| 134 | + fontweight="bold", |
| 135 | + color="#2C3E50", |
| 136 | + arrowprops={"arrowstyle": "-|>", "color": "#2C3E50", "lw": 1.5}, |
| 137 | + ha="center", |
| 138 | + va="top", |
| 139 | +) |
| 140 | + |
| 141 | +# Elastic modulus annotation — positioned to the right to avoid left-side clutter |
| 142 | +ax.annotate( |
| 143 | + f"E = {youngs_modulus // 1000} GPa", |
| 144 | + xy=(yield_strain * 0.5, youngs_modulus * yield_strain * 0.5), |
| 145 | + xytext=(0.05, 60), |
| 146 | + fontsize=13, |
| 147 | + fontstyle="italic", |
| 148 | + color="#306998", |
| 149 | + arrowprops={"arrowstyle": "-|>", "color": "#306998", "lw": 1.2}, |
| 150 | + ha="left", |
| 151 | + va="center", |
| 152 | +) |
| 153 | + |
| 154 | +# Region shading using axvspan |
| 155 | +region_shading = [ |
| 156 | + (0, yield_strain, "#306998"), |
| 157 | + (yield_strain, uts_strain, "#E07B39"), |
| 158 | + (uts_strain, fracture_strain, "#C0392B"), |
| 159 | +] |
| 160 | +for x0, x1, color in region_shading: |
| 161 | + ax.axvspan(x0, x1, alpha=0.06, color=color, zorder=0) |
| 162 | + |
| 163 | +# Style using seaborn's despine |
| 164 | +sns.despine(ax=ax) |
| 165 | +ax.xaxis.grid(False) |
| 166 | +ax.set_xlabel("Engineering Strain") |
| 167 | +ax.set_ylabel("Engineering Stress (MPa)") |
| 168 | +ax.set_title("line-stress-strain · seaborn · pyplots.ai", fontweight="medium") |
| 169 | +ax.set_xlim(-0.01, fracture_strain + 0.03) |
| 170 | +ax.set_ylim(-10, uts + 80) |
| 171 | + |
| 172 | +# Consolidate legend: combine region lines and critical point markers |
| 173 | +handles, labels = ax.get_legend_handles_labels() |
| 174 | +# Reorder: regions first, then critical points |
| 175 | +ax.legend(handles=handles, labels=labels, fontsize=13, loc="center right", framealpha=0.9, edgecolor="#cccccc") |
| 176 | + |
| 177 | +# Save |
| 178 | +plt.tight_layout() |
| 179 | +plt.savefig("plot.png", dpi=300, bbox_inches="tight") |
0 commit comments