|
1 | 1 | """ anyplot.ai |
2 | 2 | sn-curve-basic: S-N Curve (Wöhler Curve) |
3 | 3 | Library: plotnine 0.15.4 | Python 3.13.13 |
4 | | -Quality: 85/100 | Updated: 2026-05-20 |
| 4 | +Quality: 91/100 | Updated: 2026-05-20 |
5 | 5 | """ |
6 | 6 |
|
7 | 7 | import os |
|
11 | 11 | from plotnine import ( |
12 | 12 | aes, |
13 | 13 | annotate, |
| 14 | + element_blank, |
14 | 15 | element_line, |
15 | 16 | element_rect, |
16 | 17 | element_text, |
17 | 18 | geom_hline, |
18 | 19 | geom_line, |
19 | 20 | geom_point, |
| 21 | + geom_ribbon, |
| 22 | + geom_rug, |
20 | 23 | ggplot, |
21 | 24 | labs, |
22 | 25 | scale_shape_manual, |
|
43 | 46 | yield_strength = 350 |
44 | 47 | endurance_limit = 250 |
45 | 48 |
|
46 | | -# Generate realistic S-N curve data with scatter |
47 | 49 | # Using Basquin equation: S = A * N^b |
48 | | -A = 1200 # Fatigue strength coefficient |
49 | | -b = -0.12 # Fatigue strength exponent |
| 50 | +A = 1200 |
| 51 | +b = -0.12 |
| 52 | +sigma_logN = 0.3 # Log-normal scatter in cycles |
50 | 53 |
|
51 | 54 | stress_levels = np.array([500, 450, 400, 375, 350, 325, 300, 280, 270, 260, 255]) |
52 | 55 |
|
|
57 | 60 | for s in stress_levels: |
58 | 61 | n_expected = (s / A) ** (1 / b) |
59 | 62 | n_tests = np.random.randint(3, 6) |
60 | | - scatter = np.random.lognormal(0, 0.3, n_tests) |
| 63 | + scatter = np.random.lognormal(0, sigma_logN, n_tests) |
61 | 64 | n_values = n_expected * scatter |
62 | 65 | cycles.extend(n_values) |
63 | 66 | stress.extend([s] * n_tests) |
|
72 | 75 |
|
73 | 76 | df = pd.DataFrame({"cycles": cycles, "stress": stress, "type": point_type}) |
74 | 77 |
|
75 | | -# Create Basquin fit line |
| 78 | +# Basquin fit line with ±2σ scatter band (converts log-N scatter to stress bounds) |
76 | 79 | fit_cycles = np.logspace(2, 7, 100) |
77 | 80 | fit_stress = A * fit_cycles**b |
78 | | -df_fit = pd.DataFrame({"cycles": fit_cycles, "stress": fit_stress}) |
| 81 | +fit_stress_upper = fit_stress * np.exp(2 * sigma_logN * abs(b)) |
| 82 | +fit_stress_lower = fit_stress * np.exp(-2 * sigma_logN * abs(b)) |
| 83 | +df_fit = pd.DataFrame( |
| 84 | + {"cycles": fit_cycles, "stress": fit_stress, "stress_upper": fit_stress_upper, "stress_lower": fit_stress_lower} |
| 85 | +) |
79 | 86 |
|
80 | 87 | anyplot_theme = theme( |
| 88 | + figure_size=(8, 4.5), |
81 | 89 | plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG), |
82 | 90 | panel_background=element_rect(fill=PAGE_BG), |
83 | 91 | panel_grid_major=element_line(color=INK, size=0.3, alpha=0.10), |
84 | 92 | panel_grid_minor=element_line(color=INK, size=0.2, alpha=0.05), |
85 | | - panel_border=element_rect(color=INK_SOFT, fill=None), |
| 93 | + panel_border=element_blank(), |
| 94 | + axis_line_x=element_blank(), |
| 95 | + axis_line_y=element_line(color=INK_SOFT), |
86 | 96 | axis_title=element_text(color=INK, size=10), |
87 | 97 | axis_text=element_text(color=INK_SOFT, size=8), |
88 | | - axis_line=element_line(color=INK_SOFT), |
89 | 98 | plot_title=element_text(color=INK, size=12), |
90 | 99 | legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT), |
91 | 100 | legend_text=element_text(color=INK_SOFT, size=8), |
92 | 101 | legend_title=element_text(color=INK, size=9), |
| 102 | + plot_margin=0.05, |
93 | 103 | ) |
94 | 104 |
|
95 | 105 | plot = ( |
96 | 106 | ggplot() |
| 107 | + # ±2σ scatter band via geom_ribbon — idiomatic plotnine uncertainty visualization |
| 108 | + + geom_ribbon(df_fit, aes(x="cycles", ymin="stress_lower", ymax="stress_upper"), fill=OKABE_ITO[0], alpha=0.12) |
97 | 109 | # Basquin fit line |
98 | 110 | + geom_line(df_fit, aes(x="cycles", y="stress"), color=OKABE_ITO[0], size=1.2, alpha=0.85) |
99 | 111 | # Data points — shape mapped to type for runout vs failure distinction |
100 | | - + geom_point(df, aes(x="cycles", y="stress", shape="type"), color=OKABE_ITO[0], size=2.5, alpha=0.75) |
101 | | - # Reference lines with Okabe-Ito colors |
| 112 | + + geom_point(df, aes(x="cycles", y="stress", shape="type"), color=OKABE_ITO[0], size=4.2, alpha=0.80) |
| 113 | + # geom_rug exposes marginal data density along both axes — distinctive plotnine feature |
| 114 | + + geom_rug(df, aes(x="cycles", y="stress"), color=OKABE_ITO[0], alpha=0.35, size=0.5) |
| 115 | + # Reference lines with Okabe-Ito colors; endurance limit slightly thicker as focal point |
102 | 116 | + geom_hline(yintercept=ultimate_strength, linetype="dashed", color=OKABE_ITO[1], size=0.9, alpha=0.85) |
103 | 117 | + geom_hline(yintercept=yield_strength, linetype="dashed", color=OKABE_ITO[2], size=0.9, alpha=0.85) |
104 | | - + geom_hline(yintercept=endurance_limit, linetype="dashed", color=OKABE_ITO[3], size=0.9, alpha=0.85) |
105 | | - # Reference line labels with matching colors |
| 118 | + + geom_hline(yintercept=endurance_limit, linetype="dashed", color=OKABE_ITO[3], size=1.2, alpha=0.90) |
| 119 | + # Reference line labels with matching colors; shifted right to reduce left-edge crowding |
106 | 120 | + annotate( |
107 | 121 | "text", |
108 | | - x=1.2e2, |
109 | | - y=ultimate_strength + 18, |
| 122 | + x=3e2, |
| 123 | + y=ultimate_strength + 20, |
110 | 124 | label="Ultimate Strength (550 MPa)", |
111 | | - size=8, |
| 125 | + size=10, |
112 | 126 | color=OKABE_ITO[1], |
113 | 127 | ha="left", |
114 | 128 | ) |
115 | 129 | + annotate( |
116 | | - "text", x=1.2e2, y=yield_strength + 18, label="Yield Strength (350 MPa)", size=8, color=OKABE_ITO[2], ha="left" |
| 130 | + "text", x=3e2, y=yield_strength + 20, label="Yield Strength (350 MPa)", size=10, color=OKABE_ITO[2], ha="left" |
117 | 131 | ) |
118 | 132 | + annotate( |
119 | | - "text", |
120 | | - x=1.2e2, |
121 | | - y=endurance_limit - 22, |
122 | | - label="Endurance Limit (250 MPa)", |
123 | | - size=8, |
124 | | - color=OKABE_ITO[3], |
125 | | - ha="left", |
| 133 | + "text", x=3e2, y=endurance_limit - 25, label="Endurance Limit (250 MPa)", size=10, color=OKABE_ITO[3], ha="left" |
126 | 134 | ) |
127 | 135 | # Logarithmic scales |
128 | 136 | + scale_x_log10() |
|
136 | 144 | title="sn-curve-basic · python · plotnine · anyplot.ai", |
137 | 145 | ) |
138 | 146 | + theme_minimal() |
139 | | - + theme(figure_size=(8, 4.5)) |
140 | 147 | + anyplot_theme |
141 | 148 | ) |
142 | 149 |
|
|
0 commit comments