An S-N curve (also known as a Wöhler curve) visualizes the relationship between alternating stress amplitude and the number of cycles to failure for a material under fatigue loading. Both axes typically use logarithmic scales, with stress on the y-axis and cycle count on the x-axis. This plot is fundamental for predicting material fatigue life and identifying key material properties such as ultimate strength, yield strength, and endurance limit.

""" anyplot.ai
sn-curve-basic: S-N Curve (Wöhler Curve)
Library: seaborn 0.13.2 | Python 3.13.13
Quality: 90/100 | Updated: 2026-05-20
"""
import os
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns
# Theme tokens
THEME = os.getenv("ANYPLOT_THEME", "light")
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
BRAND = "#009E73" # Okabe-Ito pos 1 — data & fit
C2 = "#C475FD" # Okabe-Ito pos 2 — Ultimate Strength
C3 = "#4467A3" # Okabe-Ito pos 3 — Yield Strength
C4 = "#BD8233" # Okabe-Ito pos 4 — Endurance Limit
sns.set_theme(
style="ticks",
rc={
"figure.facecolor": PAGE_BG,
"axes.facecolor": PAGE_BG,
"axes.edgecolor": INK_SOFT,
"axes.labelcolor": INK,
"text.color": INK,
"xtick.color": INK_SOFT,
"ytick.color": INK_SOFT,
"grid.color": INK,
"grid.alpha": 0.10,
"legend.facecolor": ELEVATED_BG,
"legend.edgecolor": INK_SOFT,
},
)
# Data — structural steel fatigue test (Basquin equation)
np.random.seed(42)
ultimate_strength = 450
yield_strength = 350
endurance_limit = 200
stress_levels = np.array([400, 350, 320, 300, 280, 260, 240, 220, 210, 205])
A = 1200 # Basquin material constant
b = -0.12 # Basquin exponent
cycles_list = []
stress_list = []
for stress in stress_levels:
n_specimens = np.random.randint(3, 6)
base_cycles = (stress / A) ** (1 / b)
scatter_factors = np.random.lognormal(0, 0.3, n_specimens)
cycles_list.extend(base_cycles * scatter_factors)
stress_list.extend([stress] * n_specimens)
cycles_arr = np.array(cycles_list)
stress_arr = np.array(stress_list)
# Bootstrap S-N fits for seaborn CI band — seaborn-distinctive statistical layer
fit_cycles_grid = np.logspace(3, 7.7, 25) # matches xlim 1e3–5e7
n_boot = 250
boot_rows = []
for _ in range(n_boot):
idx = np.random.choice(len(cycles_arr), len(cycles_arr), replace=True)
log_c = np.log10(cycles_arr[idx])
log_s = np.log10(stress_arr[idx])
coeffs = np.polyfit(log_c, log_s, 1)
if coeffs[0] < 0: # keep only physically meaningful fits (negative slope)
for c in fit_cycles_grid:
s = 10 ** (coeffs[0] * np.log10(c) + coeffs[1])
if 100 < s < 1000:
boot_rows.append({"cycles": c, "stress": s})
boot_df = pd.DataFrame(boot_rows)
# Plot
fig, ax = plt.subplots(figsize=(8, 4.5), dpi=400, facecolor=PAGE_BG)
ax.set_facecolor(PAGE_BG)
# Infinite life region: subtle shading below endurance limit
ax.axhspan(150, endurance_limit, alpha=0.07, color=C4, zorder=0)
# S-N fit line with 95% prediction interval — seaborn statistical CI band
sns.lineplot(
data=boot_df,
x="cycles",
y="stress",
estimator="mean",
errorbar=("pi", 95),
color=BRAND,
linewidth=2.5,
ax=ax,
label="S-N Curve Fit (95% PI)",
err_kws={"alpha": 0.18},
zorder=3,
)
# Test data scatter
sns.scatterplot(
x=cycles_arr, y=stress_arr, s=130, color=BRAND, alpha=0.75, edgecolor="none", ax=ax, zorder=5, label="Test Data"
)
# Reference lines — endurance limit thicker and solid as critical design threshold
ax.axhline(
y=ultimate_strength, color=C2, linewidth=1.8, linestyle="--", label=f"Ultimate Strength ({ultimate_strength} MPa)"
)
ax.axhline(y=yield_strength, color=C3, linewidth=1.8, linestyle="--", label=f"Yield Strength ({yield_strength} MPa)")
ax.axhline(
y=endurance_limit,
color=C4,
linewidth=2.8,
linestyle="-",
label=f"Endurance Limit ({endurance_limit} MPa)",
zorder=4,
)
# Style
ax.set_xscale("log")
ax.set_yscale("log")
ax.set_xlim(1e3, 5e7)
ax.set_ylim(150, 600)
ax.set_xlabel("Number of Cycles to Failure (N)", fontsize=10, fontweight="medium", color=INK)
ax.set_ylabel("Stress Amplitude (MPa)", fontsize=10, fontweight="medium", color=INK)
ax.set_title("sn-curve-basic · python · seaborn · anyplot.ai", fontsize=12, fontweight="medium", color=INK)
ax.tick_params(axis="both", labelsize=8, colors=INK_SOFT)
# Major gridlines only — which='both' on log scale generates ~18 lines per axis (too dense)
ax.grid(True, alpha=0.10, linewidth=0.8, color=INK, which="major")
ax.grid(True, alpha=0.04, linewidth=0.4, color=INK, which="minor")
ax.spines["top"].set_visible(False)
ax.spines["right"].set_visible(False)
ax.spines["left"].set_color(INK_SOFT)
ax.spines["bottom"].set_color(INK_SOFT)
ax.legend(loc="lower left", fontsize=8, framealpha=0.95, facecolor=ELEVATED_BG, edgecolor=INK_SOFT)
# Controlled margins for polished spacing — avoids bbox_inches='tight' canvas drift
fig.subplots_adjust(left=0.12, right=0.97, top=0.93, bottom=0.13)
# Save — bbox_inches must stay default (None) to preserve 3200×1800 canvas
plt.savefig(f"plot-{THEME}.png", dpi=400, facecolor=PAGE_BG)
Part of S-N Curve (Wöhler Curve) on anyplot.ai.