|
| 1 | +""" pyplots.ai |
| 2 | +line-stress-strain: Engineering Stress-Strain Curve |
| 3 | +Library: letsplot 4.9.0 | Python 3.14.3 |
| 4 | +Quality: 92/100 | Created: 2026-03-20 |
| 5 | +""" |
| 6 | + |
| 7 | +import numpy as np |
| 8 | +import pandas as pd |
| 9 | +from lets_plot import * # noqa: F403 |
| 10 | +from lets_plot.export import ggsave as export_ggsave |
| 11 | + |
| 12 | + |
| 13 | +LetsPlot.setup_html() # noqa: F405 |
| 14 | + |
| 15 | +# Data - Mild steel tensile test simulation |
| 16 | +np.random.seed(42) |
| 17 | + |
| 18 | +# Material properties for mild steel |
| 19 | +youngs_modulus = 210000 # MPa |
| 20 | +yield_strength = 250 # MPa |
| 21 | +uts = 400 # MPa (ultimate tensile strength) |
| 22 | +fracture_strain = 0.35 |
| 23 | +uts_strain = 0.22 |
| 24 | +yield_strain = yield_strength / youngs_modulus # ~0.00119 |
| 25 | + |
| 26 | +# Elastic region (0 to yield) |
| 27 | +n_elastic = 60 |
| 28 | +strain_elastic = np.linspace(0, yield_strain, n_elastic) |
| 29 | +stress_elastic = youngs_modulus * strain_elastic |
| 30 | + |
| 31 | +# Yield plateau (mild steel has a distinct yield point) |
| 32 | +n_plateau = 20 |
| 33 | +strain_plateau = np.linspace(yield_strain, 0.015, n_plateau) |
| 34 | +stress_plateau = yield_strength + np.random.normal(0, 1.5, n_plateau) |
| 35 | + |
| 36 | +# Strain hardening region (from end of plateau to UTS) |
| 37 | +n_hardening = 120 |
| 38 | +strain_hardening = np.linspace(0.015, uts_strain, n_hardening) |
| 39 | +stress_hardening = yield_strength + (uts - yield_strength) * ( |
| 40 | + 1 - np.exp(-8 * (strain_hardening - 0.015) / (uts_strain - 0.015)) |
| 41 | +) |
| 42 | +stress_hardening += np.random.normal(0, 1.0, n_hardening) |
| 43 | + |
| 44 | +# Necking region (UTS to fracture) |
| 45 | +n_necking = 60 |
| 46 | +strain_necking = np.linspace(uts_strain, fracture_strain, n_necking) |
| 47 | +stress_necking = uts - (uts - 280) * ((strain_necking - uts_strain) / (fracture_strain - uts_strain)) ** 1.5 |
| 48 | +stress_necking += np.random.normal(0, 1.5, n_necking) |
| 49 | + |
| 50 | +# Combine all regions |
| 51 | +strain = np.concatenate([strain_elastic, strain_plateau, strain_hardening, strain_necking]) |
| 52 | +stress = np.concatenate([stress_elastic, stress_plateau, stress_hardening, stress_necking]) |
| 53 | + |
| 54 | +df = pd.DataFrame({"strain": strain, "stress": stress}) |
| 55 | + |
| 56 | +# 0.2% offset line for yield point determination |
| 57 | +offset_val = 0.002 |
| 58 | +offset_line_strain = np.linspace(offset_val, offset_val + yield_strength / youngs_modulus + 0.003, 50) |
| 59 | +offset_line_stress = youngs_modulus * (offset_line_strain - offset_val) |
| 60 | +offset_line_stress = np.clip(offset_line_stress, 0, yield_strength + 30) |
| 61 | +df_offset = pd.DataFrame({"strain": offset_line_strain, "stress": offset_line_stress}) |
| 62 | + |
| 63 | +# Key points |
| 64 | +yield_point_strain = offset_val + yield_strength / youngs_modulus |
| 65 | +yield_point_stress = yield_strength |
| 66 | +fracture_stress = stress_necking[-1] |
| 67 | + |
| 68 | +df_points = pd.DataFrame( |
| 69 | + { |
| 70 | + "strain": [yield_point_strain, uts_strain, fracture_strain], |
| 71 | + "stress": [yield_point_stress, uts, fracture_stress], |
| 72 | + "label": [f"Yield Point ({yield_strength} MPa)", f"UTS ({uts} MPa)", f"Fracture ({fracture_stress:.0f} MPa)"], |
| 73 | + "type": ["Yield", "UTS", "Fracture"], |
| 74 | + } |
| 75 | +) |
| 76 | + |
| 77 | +# Consolidated annotations DataFrame |
| 78 | +df_annotations = pd.DataFrame( |
| 79 | + { |
| 80 | + "x": [yield_point_strain + 0.012, uts_strain + 0.015, fracture_strain - 0.045, 0.008, 0.007, 0.005, 0.11, 0.29], |
| 81 | + "y": [yield_point_stress + 15, uts + 10, fracture_stress - 30, 130, 60, 350, 350, 350], |
| 82 | + "label": [ |
| 83 | + f"Yield Point\n({yield_strength} MPa)", |
| 84 | + f"UTS ({uts} MPa)", |
| 85 | + "Fracture", |
| 86 | + f"E = {youngs_modulus // 1000} GPa", |
| 87 | + "0.2% offset", |
| 88 | + "Elastic", |
| 89 | + "Strain Hardening", |
| 90 | + "Necking", |
| 91 | + ], |
| 92 | + "group": ["yield", "uts", "fracture", "modulus", "offset", "region", "region", "region"], |
| 93 | + } |
| 94 | +) |
| 95 | + |
| 96 | +# Colorblind-safe palette: blue, purple, gray (avoids orange/green pair) |
| 97 | +color_yield = "#9467BD" # purple |
| 98 | +color_uts = "#D62728" # red |
| 99 | +color_fracture = "#7F7F7F" # gray |
| 100 | +color_main = "#306998" # Python blue |
| 101 | +color_offset = "#E377C2" # pink |
| 102 | + |
| 103 | +# Segment connector lines from key points to annotations (distinctive lets-plot feature) |
| 104 | +df_segments = pd.DataFrame( |
| 105 | + { |
| 106 | + "x": [yield_point_strain, uts_strain, fracture_strain], |
| 107 | + "y": [yield_point_stress, uts, fracture_stress], |
| 108 | + "xend": [yield_point_strain + 0.011, uts_strain + 0.014, fracture_strain - 0.035], |
| 109 | + "yend": [yield_point_stress + 12, uts + 8, fracture_stress - 22], |
| 110 | + } |
| 111 | +) |
| 112 | + |
| 113 | +# Plot |
| 114 | +plot = ( |
| 115 | + ggplot() |
| 116 | + # Region background bands using geom_rect (distinctive lets-plot feature) |
| 117 | + + geom_rect( |
| 118 | + aes(xmin="xmin", xmax="xmax", ymin="ymin", ymax="ymax", fill="region"), |
| 119 | + data=pd.DataFrame( |
| 120 | + { |
| 121 | + "xmin": [0, 0.015, uts_strain], |
| 122 | + "xmax": [0.015, uts_strain, fracture_strain], |
| 123 | + "ymin": [0, 0, 0], |
| 124 | + "ymax": [460, 460, 460], |
| 125 | + "region": ["Elastic", "Strain Hardening", "Necking"], |
| 126 | + } |
| 127 | + ), |
| 128 | + alpha=0.35, |
| 129 | + ) |
| 130 | + + scale_fill_manual( |
| 131 | + values={ |
| 132 | + "Elastic": "#DAE8FC", |
| 133 | + "Strain Hardening": "#FFF2CC", |
| 134 | + "Necking": "#F8D7DA", |
| 135 | + "Yield": color_yield, |
| 136 | + "UTS": color_uts, |
| 137 | + "Fracture": color_fracture, |
| 138 | + } |
| 139 | + ) |
| 140 | + # Main stress-strain curve with tooltips (distinctive lets-plot feature) |
| 141 | + + geom_line( |
| 142 | + aes(x="strain", y="stress"), |
| 143 | + data=df, |
| 144 | + color=color_main, |
| 145 | + size=2.0, |
| 146 | + tooltips=layer_tooltips() |
| 147 | + .format("strain", ".4f") |
| 148 | + .format("stress", ".1f") |
| 149 | + .line("Strain: @strain") |
| 150 | + .line("Stress: @stress MPa"), |
| 151 | + ) |
| 152 | + # 0.2% offset line |
| 153 | + + geom_line(aes(x="strain", y="stress"), data=df_offset, color=color_offset, size=1.2, linetype="dashed") |
| 154 | + # Segment connectors from points to labels (geom_segment - distinctive feature) |
| 155 | + + geom_segment( |
| 156 | + aes(x="x", y="y", xend="xend", yend="yend"), data=df_segments, color="#999999", size=0.6, linetype="dotted" |
| 157 | + ) |
| 158 | + # Key points with tooltips (distinctive lets-plot feature) |
| 159 | + + geom_point( |
| 160 | + aes(x="strain", y="stress", fill="type"), |
| 161 | + data=df_points, |
| 162 | + color="white", |
| 163 | + size=7, |
| 164 | + shape=21, |
| 165 | + stroke=1.2, |
| 166 | + tooltips=layer_tooltips().line("@type").line("Strain: @strain").line("Stress: @stress MPa"), |
| 167 | + ) |
| 168 | + + guides(fill="none") |
| 169 | + # Annotations - key points |
| 170 | + + geom_text( |
| 171 | + aes(x="x", y="y", label="label"), |
| 172 | + data=df_annotations.query("group == 'yield'"), |
| 173 | + size=11, |
| 174 | + color=color_yield, |
| 175 | + hjust=0, |
| 176 | + ) |
| 177 | + + geom_text( |
| 178 | + aes(x="x", y="y", label="label"), data=df_annotations.query("group == 'uts'"), size=11, color=color_uts, hjust=0 |
| 179 | + ) |
| 180 | + + geom_text( |
| 181 | + aes(x="x", y="y", label="label"), |
| 182 | + data=df_annotations.query("group == 'fracture'"), |
| 183 | + size=11, |
| 184 | + color=color_fracture, |
| 185 | + hjust=0.5, |
| 186 | + ) |
| 187 | + # Elastic modulus annotation |
| 188 | + + geom_text( |
| 189 | + aes(x="x", y="y", label="label"), |
| 190 | + data=df_annotations.query("group == 'modulus'"), |
| 191 | + size=10, |
| 192 | + color=color_main, |
| 193 | + hjust=0, |
| 194 | + fontface="italic", |
| 195 | + ) |
| 196 | + # Offset label |
| 197 | + + geom_text( |
| 198 | + aes(x="x", y="y", label="label"), |
| 199 | + data=df_annotations.query("group == 'offset'"), |
| 200 | + size=9, |
| 201 | + color=color_offset, |
| 202 | + hjust=0, |
| 203 | + fontface="italic", |
| 204 | + ) |
| 205 | + # Region labels |
| 206 | + + geom_text( |
| 207 | + aes(x="x", y="y", label="label"), |
| 208 | + data=df_annotations.query("group == 'region'"), |
| 209 | + size=12, |
| 210 | + color="#666666", |
| 211 | + fontface="italic", |
| 212 | + ) |
| 213 | + # Styling |
| 214 | + + labs( |
| 215 | + x="Engineering Strain", |
| 216 | + y="Engineering Stress (MPa)", |
| 217 | + title="line-stress-strain \u00b7 letsplot \u00b7 pyplots.ai", |
| 218 | + ) |
| 219 | + + scale_x_continuous(breaks=[0, 0.05, 0.10, 0.15, 0.20, 0.25, 0.30, 0.35]) |
| 220 | + + scale_y_continuous(breaks=[0, 50, 100, 150, 200, 250, 300, 350, 400, 450]) |
| 221 | + + ggsize(1600, 900) |
| 222 | + + theme_minimal() |
| 223 | + + theme( |
| 224 | + axis_text=element_text(size=16, color="#555555"), |
| 225 | + axis_title=element_text(size=20, color="#333333"), |
| 226 | + plot_title=element_text(size=24, color="#222222", face="bold"), |
| 227 | + panel_grid_major_x=element_blank(), |
| 228 | + panel_grid_major_y=element_line(color="#E0E0E0", size=0.3), |
| 229 | + panel_grid_minor=element_blank(), |
| 230 | + plot_background=element_rect(fill="#FAFAFA", color="#FAFAFA"), |
| 231 | + panel_background=element_rect(fill="transparent", color="transparent"), |
| 232 | + axis_ticks=element_blank(), |
| 233 | + axis_ticks_length=0, |
| 234 | + plot_margin=[30, 40, 20, 20], |
| 235 | + ) |
| 236 | +) |
| 237 | + |
| 238 | +# Save |
| 239 | +export_ggsave(plot, filename="plot.png", path=".", scale=3) |
| 240 | +export_ggsave(plot, filename="plot.html", path=".") |
0 commit comments