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