|
| 1 | +""" pyplots.ai |
| 2 | +line-stress-strain: Engineering Stress-Strain Curve |
| 3 | +Library: plotnine 0.15.3 | Python 3.14.3 |
| 4 | +Quality: 90/100 | Created: 2026-03-20 |
| 5 | +""" |
| 6 | + |
| 7 | +import numpy as np |
| 8 | +import pandas as pd |
| 9 | +from plotnine import ( |
| 10 | + aes, |
| 11 | + annotate, |
| 12 | + coord_cartesian, |
| 13 | + element_blank, |
| 14 | + element_line, |
| 15 | + element_rect, |
| 16 | + element_text, |
| 17 | + geom_line, |
| 18 | + geom_point, |
| 19 | + geom_segment, |
| 20 | + geom_text, |
| 21 | + ggplot, |
| 22 | + labs, |
| 23 | + scale_color_identity, |
| 24 | + scale_fill_identity, |
| 25 | + scale_linetype_identity, |
| 26 | + scale_size_identity, |
| 27 | + scale_x_continuous, |
| 28 | + scale_y_continuous, |
| 29 | + theme, |
| 30 | + theme_minimal, |
| 31 | +) |
| 32 | + |
| 33 | + |
| 34 | +# Data - Mild steel stress-strain curve |
| 35 | +np.random.seed(42) |
| 36 | + |
| 37 | +youngs_modulus = 210000 # MPa |
| 38 | +yield_stress = 250 # MPa |
| 39 | +uts = 400 # MPa |
| 40 | +fracture_strain = 0.35 |
| 41 | +necking_strain = 0.22 |
| 42 | + |
| 43 | +# Elastic region (0 to yield) |
| 44 | +elastic_strain = np.linspace(0, yield_stress / youngs_modulus, 40) |
| 45 | +elastic_stress = youngs_modulus * elastic_strain |
| 46 | + |
| 47 | +# Yield plateau (short flat region for mild steel) |
| 48 | +plateau_strain = np.linspace(elastic_strain[-1], 0.025, 15) |
| 49 | +plateau_stress = np.full_like(plateau_strain, yield_stress) |
| 50 | + |
| 51 | +# Strain hardening (power law) |
| 52 | +hardening_strain = np.linspace(0.025, necking_strain, 80) |
| 53 | +hardening_stress = yield_stress + (uts - yield_stress) * ((hardening_strain - 0.025) / (necking_strain - 0.025)) ** 0.45 |
| 54 | + |
| 55 | +# Necking to fracture (stress decreases) |
| 56 | +necking_strain_vals = np.linspace(necking_strain, fracture_strain, 40) |
| 57 | +necking_stress = ( |
| 58 | + uts - (uts - 320) * ((necking_strain_vals - necking_strain) / (fracture_strain - necking_strain)) ** 1.3 |
| 59 | +) |
| 60 | + |
| 61 | +# Combine all regions |
| 62 | +strain = np.concatenate([elastic_strain, plateau_strain[1:], hardening_strain[1:], necking_strain_vals[1:]]) |
| 63 | +stress = np.concatenate([elastic_stress, plateau_stress[1:], hardening_stress[1:], necking_stress[1:]]) |
| 64 | + |
| 65 | +df = pd.DataFrame({"strain": strain, "stress": stress}) |
| 66 | + |
| 67 | +# 0.2% offset line data - extended for better visibility |
| 68 | +offset = 0.002 |
| 69 | +offset_strain_start = offset |
| 70 | +offset_strain_end = (yield_stress + 50) / youngs_modulus + offset |
| 71 | + |
| 72 | +# Key points |
| 73 | +yield_point_strain = yield_stress / youngs_modulus + offset |
| 74 | +yield_point_stress = yield_stress |
| 75 | +uts_strain = necking_strain |
| 76 | +uts_stress = uts |
| 77 | +fracture_strain_pt = fracture_strain |
| 78 | +fracture_stress_pt = necking_stress[-1] |
| 79 | + |
| 80 | +df_points = pd.DataFrame( |
| 81 | + { |
| 82 | + "strain": [yield_point_strain, uts_strain, fracture_strain_pt], |
| 83 | + "stress": [yield_point_stress, uts_stress, fracture_stress_pt], |
| 84 | + "label": ["Yield Point\n(0.2% offset)", "UTS", "Fracture"], |
| 85 | + "color": ["#C0392B", "#C0392B", "#C0392B"], |
| 86 | + "size": [6.0, 6.0, 6.0], |
| 87 | + } |
| 88 | +) |
| 89 | + |
| 90 | +# Region labels - repositioned for clarity |
| 91 | +df_regions = pd.DataFrame( |
| 92 | + { |
| 93 | + "strain": [0.005, 0.015, 0.13, 0.29], |
| 94 | + "stress": [410, 215, 310, 370], |
| 95 | + "label": ["Elastic", "Yield\nPlateau", "Strain\nHardening", "Necking"], |
| 96 | + "color": ["#5D6D7E", "#5D6D7E", "#5D6D7E", "#5D6D7E"], |
| 97 | + } |
| 98 | +) |
| 99 | + |
| 100 | +# Region boundary strains for shading |
| 101 | +elastic_end = yield_stress / youngs_modulus |
| 102 | +plateau_end = 0.025 |
| 103 | + |
| 104 | +# Plot using plotnine grammar of graphics with layered composition |
| 105 | +plot = ( |
| 106 | + ggplot() |
| 107 | + # Region shading using annotate("rect") - plotnine-distinctive feature |
| 108 | + + annotate("rect", xmin=0, xmax=elastic_end, ymin=0, ymax=440, alpha=0.15, fill="#3498DB") |
| 109 | + + annotate("rect", xmin=elastic_end, xmax=plateau_end, ymin=0, ymax=440, alpha=0.15, fill="#2ECC71") |
| 110 | + + annotate("rect", xmin=plateau_end, xmax=necking_strain, ymin=0, ymax=440, alpha=0.12, fill="#F39C12") |
| 111 | + + annotate("rect", xmin=necking_strain, xmax=fracture_strain, ymin=0, ymax=440, alpha=0.12, fill="#E74C3C") |
| 112 | + # Main stress-strain curve |
| 113 | + + geom_line(df, aes(x="strain", y="stress"), color="#306998", size=2.8) |
| 114 | + # 0.2% offset line using geom_segment - plotnine-distinctive |
| 115 | + + geom_segment( |
| 116 | + aes(x=offset_strain_start, xend=offset_strain_end, y=0, yend=yield_stress + 50), |
| 117 | + color="#C0392B", |
| 118 | + size=1.2, |
| 119 | + linetype="dashed", |
| 120 | + ) |
| 121 | + # Offset label near the line |
| 122 | + + annotate("text", x=0.012, y=60, label="0.2% offset", size=11, color="#C0392B", fontstyle="italic") |
| 123 | + # Key points with identity scales for direct aesthetic mapping |
| 124 | + + geom_point(df_points, aes(x="strain", y="stress", color="color", size="size"), fill="#C0392B") |
| 125 | + + scale_color_identity() |
| 126 | + + scale_size_identity() |
| 127 | + # Point labels - larger text |
| 128 | + + geom_text( |
| 129 | + df_points, aes(x="strain", y="stress", label="label"), nudge_y=32, size=15, color="#2C3E50", fontweight="bold" |
| 130 | + ) |
| 131 | + # Region labels with identity color scale - larger text |
| 132 | + + geom_text(df_regions, aes(x="strain", y="stress", label="label", color="color"), size=14, fontstyle="italic") |
| 133 | + + scale_fill_identity() |
| 134 | + + scale_linetype_identity() |
| 135 | + # Modulus annotation - larger and repositioned |
| 136 | + + annotate( |
| 137 | + "text", x=0.03, y=140, label=f"E = {youngs_modulus // 1000} GPa", size=16, color="#306998", fontweight="bold" |
| 138 | + ) |
| 139 | + + labs(x="Engineering Strain", y="Engineering Stress (MPa)", title="line-stress-strain · plotnine · pyplots.ai") |
| 140 | + + scale_x_continuous(breaks=np.arange(0, 0.40, 0.05)) |
| 141 | + + scale_y_continuous(breaks=np.arange(0, 500, 50)) |
| 142 | + # Coordinate control - plotnine-distinctive |
| 143 | + + coord_cartesian(xlim=(0, 0.38), ylim=(0, 460)) |
| 144 | + + theme_minimal() |
| 145 | + + theme( |
| 146 | + figure_size=(16, 9), |
| 147 | + plot_title=element_text(size=26, weight="bold", color="#1A2530"), |
| 148 | + axis_title=element_text(size=22, color="#2C3E50", weight="bold"), |
| 149 | + axis_text=element_text(size=16, color="#555555"), |
| 150 | + panel_grid_major_x=element_blank(), |
| 151 | + panel_grid_minor=element_blank(), |
| 152 | + panel_grid_major_y=element_line(color="#E8E8E8", size=0.4, alpha=0.5), |
| 153 | + plot_background=element_rect(fill="white", color="white"), |
| 154 | + panel_background=element_rect(fill="white", color="white"), |
| 155 | + ) |
| 156 | +) |
| 157 | + |
| 158 | +# Save |
| 159 | +plot.save("plot.png", dpi=300, verbose=False) |
0 commit comments