{"spec_id":"sn-curve-basic","library":"matplotlib","language":"python","code":"\"\"\" anyplot.ai\nsn-curve-basic: S-N Curve (Wöhler Curve)\nLibrary: matplotlib 3.10.9 | Python 3.13.13\nQuality: 86/100 | Updated: 2026-05-20\n\"\"\"\n\nimport os\n\nimport matplotlib.pyplot as plt\nimport numpy as np\nfrom matplotlib.ticker import FixedLocator, FuncFormatter\n\n\n# Theme tokens\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nELEVATED_BG = \"#FFFDF6\" if THEME == \"light\" else \"#242420\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\nBRAND = \"#009E73\"  # Okabe-Ito #1 — test data points\nCOLOR_FIT = \"#C475FD\"  # Okabe-Ito #2 — Basquin fit line\nCOLOR_ULT = \"#4467A3\"  # Okabe-Ito #3 — Ultimate Strength\nCOLOR_YLD = \"#BD8233\"  # Okabe-Ito #4 — Yield Strength\nCOLOR_END = \"#AE3030\"  # Okabe-Ito #5 — Endurance Limit\n\n# Data: Simulated fatigue test results for structural steel specimens\nnp.random.seed(42)\n\nstress_levels = np.array([450, 400, 350, 320, 300, 280, 260, 250, 240, 230, 220, 210])\n\ncycles_data = []\nstress_data = []\n\nfor s_level in stress_levels:\n    # Basquin equation: N = (S/A)^(-1/b)\n    A, b = 1200, 0.12\n    N_mean = (s_level / A) ** (-1 / b)\n    n_samples = np.random.randint(2, 5)\n    for _ in range(n_samples):\n        cycle_scatter = np.exp(np.random.normal(0, 0.3))\n        cycles_data.append(N_mean * cycle_scatter)\n        stress_data.append(s_level + np.random.normal(0, 5))\n\ncycles = np.array(cycles_data)\nstress = np.array(stress_data)\n\n# Basquin log-linear fit\ncoeffs = np.polyfit(np.log10(cycles), np.log10(stress), 1)\nfit_cycles = np.logspace(2, 8, 100)\nfit_stress = 10 ** (coeffs[0] * np.log10(fit_cycles) + coeffs[1])\n\n# Material property reference values (typical structural steel, MPa)\nultimate_strength = 500\nyield_strength = 350\nendurance_limit = 200\n\n# Plot\nfig, ax = plt.subplots(figsize=(8, 4.5), dpi=400, facecolor=PAGE_BG)\nax.set_facecolor(PAGE_BG)\n\n# Fatigue region shading — axhspan highlights the three life regimes\nax.axhspan(120, endurance_limit, alpha=0.07, color=BRAND, zorder=0)\nax.axhspan(endurance_limit, yield_strength, alpha=0.05, color=COLOR_FIT, zorder=0)\nax.axhspan(yield_strength, 650, alpha=0.05, color=COLOR_ULT, zorder=0)\n\nax.scatter(\n    cycles, stress, s=100, color=BRAND, alpha=0.75, edgecolors=PAGE_BG, linewidths=0.5, label=\"Test Data\", zorder=5\n)\n\nax.plot(fit_cycles, fit_stress, color=COLOR_FIT, linewidth=2.0, label=\"Basquin Fit\", zorder=4)\n\nax.axhline(\n    y=ultimate_strength,\n    color=COLOR_ULT,\n    linestyle=\"--\",\n    linewidth=1.5,\n    label=f\"Ultimate Strength ({ultimate_strength} MPa)\",\n    zorder=3,\n)\nax.axhline(\n    y=yield_strength,\n    color=COLOR_YLD,\n    linestyle=\"--\",\n    linewidth=1.5,\n    label=f\"Yield Strength ({yield_strength} MPa)\",\n    zorder=3,\n)\nax.axhline(\n    y=endurance_limit,\n    color=COLOR_END,\n    linestyle=\"--\",\n    linewidth=2.5,\n    label=f\"Endurance Limit ({endurance_limit} MPa)\",\n    zorder=3,\n)\n\nax.set_xscale(\"log\")\nax.set_yscale(\"log\")\nax.xaxis.grid(False)\nax.set_xlim(1e2, 1e8)\nax.set_ylim(120, 650)\n\n# Style\nax.set_xlabel(\"Number of Cycles to Failure (N)\", fontsize=10, color=INK)\nax.set_ylabel(\"Stress Amplitude (MPa)\", fontsize=10, color=INK)\nax.set_title(\"sn-curve-basic · python · matplotlib · anyplot.ai\", fontsize=12, fontweight=\"medium\", color=INK)\nax.tick_params(axis=\"both\", labelsize=8, colors=INK_SOFT)\nfor spine in (\"left\", \"bottom\"):\n    ax.spines[spine].set_color(INK_SOFT)\nax.spines[\"top\"].set_visible(False)\nax.spines[\"right\"].set_visible(False)\n\n# Explicit MPa tick positions — more reliable than FuncFormatter on log scale\ny_ticks = [150, 200, 250, 300, 350, 400, 500, 600]\nax.yaxis.set_major_locator(FixedLocator(y_ticks))\nax.yaxis.set_major_formatter(FuncFormatter(lambda x, _: str(int(x))))\n\nax.yaxis.grid(True, which=\"major\", alpha=0.15, linewidth=0.7, color=INK)\n\nleg = ax.legend(fontsize=8, loc=\"upper right\")\nif leg:\n    leg.get_frame().set_facecolor(ELEVATED_BG)\n    leg.get_frame().set_edgecolor(INK_SOFT)\n    plt.setp(leg.get_texts(), color=INK_SOFT)\n\n# Fatigue region labels\nREGION_BBOX = {\"boxstyle\": \"round,pad=0.2\", \"facecolor\": PAGE_BG, \"alpha\": 0.75, \"edgecolor\": \"none\"}\nax.annotate(\n    \"Low-Cycle\\nFatigue\", xy=(4e2, 420), fontsize=8, ha=\"center\", color=INK_MUTED, style=\"italic\", bbox=REGION_BBOX\n)\nax.annotate(\n    \"High-Cycle\\nFatigue\", xy=(1e5, 420), fontsize=8, ha=\"center\", color=INK_MUTED, style=\"italic\", bbox=REGION_BBOX\n)\nax.annotate(\"Infinite Life\", xy=(5e7, 182), fontsize=8, ha=\"center\", color=INK_MUTED, style=\"italic\", bbox=REGION_BBOX)\n\nplt.tight_layout()\n\n# Save — no bbox_inches=\"tight\" to preserve exact 3200×1800 canvas\nplt.savefig(f\"plot-{THEME}.png\", dpi=400, facecolor=PAGE_BG)\n"}