{"spec_id":"probability-weibull","library":"pygal","language":"python","code":"\"\"\" anyplot.ai\nprobability-weibull: Weibull Probability Plot for Reliability Analysis\nLibrary: pygal 3.1.0 | Python 3.13.13\nQuality: 83/100 | Updated: 2026-06-07\n\"\"\"\n\nimport os\nimport sys\n\n\n# Remove the script's own directory so pygal.py doesn't shadow the installed pygal package\n_here = os.path.dirname(os.path.abspath(__file__))\nif _here in sys.path:\n    sys.path.remove(_here)\n\nimport numpy as np\nimport pygal\nfrom pygal.style import Style\nfrom scipy import stats\n\n\n# Theme tokens\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n# Imprint categorical palette — Weibull fit takes position 1 (brand green)\nIMPRINT = (\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\")\n\n# Data — turbine blade fatigue-life (hours) with failures and suspensions\nnp.random.seed(42)\nn_failures = 18\nn_censored = 5\nbeta_true = 2.5\neta_true = 8000\n\nfailure_times = np.sort(stats.weibull_min.rvs(beta_true, scale=eta_true, size=n_failures))\ncensored_times = np.sort(np.random.uniform(2000, 9000, n_censored))\n\nall_times = np.concatenate([failure_times, censored_times])\nis_failure = np.concatenate([np.ones(n_failures), np.zeros(n_censored)])\n\nsort_idx = np.argsort(all_times)\nall_times = all_times[sort_idx]\nis_failure = is_failure[sort_idx]\n\n# Median rank plotting positions (i-0.3)/(n+0.4) for failures only\nfailure_ranks = np.cumsum(is_failure)\ntotal_failures = failure_ranks[-1]\nmedian_ranks = (failure_ranks - 0.3) / (total_failures + 0.4)\n\nfailure_mask = is_failure.astype(bool)\nfailure_x = all_times[failure_mask]\nfailure_prob = median_ranks[failure_mask]\n\n# Weibull linearization: ln(-ln(1-F)) for y-axis, ln(time) for x-axis\nweibull_y_failures = np.log(-np.log(1.0 - failure_prob))\nln_x_failures = np.log(failure_x)\n\n# Fit line using least squares on linearized data\nslope, intercept, r_value, _, _ = stats.linregress(ln_x_failures, weibull_y_failures)\nbeta_est = slope\neta_est = np.exp(-intercept / beta_est)\n\n# Fitted line spanning full data range\nx_fit_range = np.linspace(np.log(min(all_times) * 0.7), np.log(max(all_times) * 1.3), 100)\ny_fit_line = slope * x_fit_range + intercept\n\n# Censored points — placed on the fitted line at their log-time\ncensored_x = all_times[~failure_mask]\nln_censored_x = np.log(censored_x)\ncensored_y_on_line = slope * ln_censored_x + intercept\n\n# 63.2% reference line (characteristic life)\nref_y = np.log(-np.log(1 - 0.632))\n\n# B10 life: F=0.10 → time where 10% of units have failed\nb10_y = np.log(-np.log(1 - 0.10))\nb10_ln_x = (b10_y - intercept) / slope\nb10_hours = np.exp(b10_ln_x)\n\n# Characteristic life intersection on fitted line\neta_ln_x = (ref_y - intercept) / slope\n\n# Axis tick values\nx_tick_values = [1000, 2000, 3000, 5000, 7000, 10000, 15000]\nx_tick_ln = [np.log(v) for v in x_tick_values]\n\nprob_levels = [0.01, 0.05, 0.10, 0.20, 0.50, 0.632, 0.80, 0.90, 0.95, 0.99]\ny_tick_weibull = [np.log(-np.log(1.0 - p)) for p in prob_levels]\ny_tick_labels = [f\"{p * 100:.1f}%\" if p == 0.632 else f\"{p * 100:.0f}%\" for p in prob_levels]\n\n# Axis bounds\nx_min_ln = np.log(800)\nx_max_ln = np.log(18000)\ny_min_w = np.log(-np.log(1 - 0.008))\ny_max_w = np.log(-np.log(1 - 0.993))\n\n# Title fontsize scaled for title length (baseline 66 at 67 chars, floor 44)\ntitle_str = \"Turbine Blade Fatigue Life · probability-weibull · python · pygal · anyplot.ai\"\ntitle_font_size = max(44, round(66 * 67 / len(title_str)))\n\n# Style — Imprint palette + theme-adaptive chrome\ncustom_style = Style(\n    background=PAGE_BG,\n    plot_background=PAGE_BG,\n    foreground=INK,\n    foreground_strong=INK,\n    foreground_subtle=INK_MUTED,\n    colors=(\n        IMPRINT[0],  # fit line — brand green (Imprint pos 1)\n        IMPRINT[2],  # failures — blue (Imprint pos 3)\n        IMPRINT[1],  # censored — lavender (Imprint pos 2, lighter read than blue)\n        INK_MUTED,  # 63.2% reference — theme-adaptive neutral\n        IMPRINT[3],  # η marker — ochre (Imprint pos 4)\n        IMPRINT[4],  # B10 marker — matte red (Imprint pos 5, reliability threshold)\n    ),\n    font_family=\"DejaVu Sans, Helvetica, Arial, sans-serif\",\n    title_font_family=\"DejaVu Sans, Helvetica, Arial, sans-serif\",\n    title_font_size=title_font_size,\n    label_font_size=56,\n    major_label_font_size=44,\n    legend_font_size=44,\n    legend_font_family=\"DejaVu Sans, Helvetica, Arial, sans-serif\",\n    value_font_size=36,\n    tooltip_font_size=36,\n    tooltip_font_family=\"DejaVu Sans, Helvetica, Arial, sans-serif\",\n    opacity=0.92,\n    opacity_hover=1.0,\n    stroke_opacity=1,\n    stroke_opacity_hover=1,\n)\n\n# Chart configuration\nchart = pygal.XY(\n    width=3200,\n    height=1800,\n    style=custom_style,\n    title=title_str,\n    x_title=\"Time to Failure (hours, log scale)\",\n    y_title=\"Cumulative Failure Probability\",\n    show_legend=True,\n    legend_at_bottom=True,\n    legend_at_bottom_columns=3,\n    legend_box_size=30,\n    stroke=False,\n    dots_size=10,\n    show_x_guides=True,\n    show_y_guides=True,\n    margin_bottom=100,\n    margin_left=90,\n    margin_right=60,\n    margin_top=60,\n    truncate_legend=-1,\n    range=(y_min_w, y_max_w),\n    xrange=(x_min_ln, x_max_ln),\n    print_values=False,\n    print_zeroes=False,\n    js=[],\n    x_labels=[float(v) for v in x_tick_ln],\n    y_labels=[float(v) for v in y_tick_weibull],\n    x_value_formatter=lambda x: f\"{np.exp(x):,.0f}h\",\n    value_formatter=lambda y: f\"{(1 - np.exp(-np.exp(y))) * 100:.1f}%\",\n    tooltip_border_radius=10,\n    tooltip_fancy_mode=True,\n    dynamic_print_values=True,\n)\n\n# Override label display for axes\nchart.y_labels = [{\"value\": float(v), \"label\": lbl} for v, lbl in zip(y_tick_weibull, y_tick_labels, strict=True)]\nchart.x_labels = [{\"value\": float(v), \"label\": f\"{int(t):,}\"} for v, t in zip(x_tick_ln, x_tick_values, strict=True)]\n\n# Fitted line — brand green (Imprint position 1)\nfit_points = [(float(x), float(y)) for x, y in zip(x_fit_range, y_fit_line, strict=True)]\nchart.add(\n    f\"Weibull Fit (β={beta_est:.2f}, η={eta_est:,.0f}h, R²={r_value**2:.3f})\",\n    fit_points,\n    stroke=True,\n    show_dots=False,\n    stroke_style={\"width\": 8, \"linecap\": \"round\", \"linejoin\": \"round\"},\n)\n\n# Failure data points — blue (Imprint position 3)\nfailure_points = [\n    {\n        \"value\": (float(x), float(y)),\n        \"label\": f\"Failure at {np.exp(x):,.0f}h — F={((1 - np.exp(-np.exp(y))) * 100):.1f}%\",\n    }\n    for x, y in zip(ln_x_failures, weibull_y_failures, strict=True)\n]\nchart.add(f\"Failures (n={n_failures})\", failure_points, stroke=False, dots_size=12)\n\n# Censored data points — lavender (Imprint position 2), visually distinct from failures\ncensored_points = [\n    {\"value\": (float(x), float(y)), \"label\": f\"Censored at {np.exp(x):,.0f}h (suspended test)\"}\n    for x, y in zip(ln_censored_x, censored_y_on_line, strict=True)\n]\nchart.add(f\"Censored (n={n_censored})\", censored_points, stroke=False, dots_size=12)\n\n# 63.2% reference line — theme-adaptive muted (structural reference, not data)\nref_line = [(float(x_min_ln), float(ref_y)), (float(x_max_ln), float(ref_y))]\nchart.add(\n    \"63.2% Characteristic Life\",\n    ref_line,\n    stroke=True,\n    show_dots=False,\n    stroke_style={\"width\": 6, \"dasharray\": \"16, 10\", \"linecap\": \"round\"},\n)\n\n# η marker — ochre (Imprint position 4), characteristic life\neta_marker = [\n    {\"value\": (float(eta_ln_x), float(ref_y)), \"label\": f\"η = {eta_est:,.0f}h (Characteristic Life at 63.2%)\"}\n]\nchart.add(\n    f\"η = {eta_est:,.0f}h\",\n    eta_marker,\n    stroke=False,\n    dots_size=22,\n    print_values=True,\n    formatter=lambda x: f\"η = {eta_est:,.0f}h\",\n)\n\n# B10 marker — matte red (Imprint position 5), 10% reliability threshold\nb10_marker = [{\"value\": (float(b10_ln_x), float(b10_y)), \"label\": f\"B10 = {b10_hours:,.0f}h (10% Failure Life)\"}]\nchart.add(\n    f\"B10 = {b10_hours:,.0f}h\",\n    b10_marker,\n    stroke=False,\n    dots_size=22,\n    print_values=True,\n    formatter=lambda x: f\"B10 = {b10_hours:,.0f}h\",\n)\n\n# Save\nchart.render_to_png(f\"plot-{THEME}.png\")\nchart.render_to_file(f\"plot-{THEME}.html\")\n"}