{"spec_id":"curve-dose-response","library":"matplotlib","language":"python","code":"\"\"\" anyplot.ai\ncurve-dose-response: Pharmacological Dose-Response Curve\nLibrary: matplotlib 3.11.0 | Python 3.13.14\nQuality: 90/100 | Updated: 2026-06-24\n\"\"\"\n\nimport os\n\nimport matplotlib.pyplot as plt\nimport matplotlib.ticker as ticker\nimport numpy as np\nfrom scipy.optimize import curve_fit\nfrom scipy.stats import t as t_dist\n\n\n# Theme tokens — Imprint palette chrome\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\n# Imprint palette — positions 1 and 2\nCOLORS = [\"#009E73\", \"#C475FD\"]\n\n# Data\nnp.random.seed(42)\nconcentrations = np.logspace(-9, -4, 8)\n\ndrug_names = [\"Erlotinib\", \"Lapatinib\"]\nbottom_a, top_a, ec50_a, hill_a = 5.0, 95.0, 3e-7, 1.2\nbottom_b, top_b, ec50_b, hill_b = 8.0, 80.0, 5e-6, 0.9\n\n\ndef logistic4pl(conc, bottom, top, ec50, hill):\n    return bottom + (top - bottom) / (1 + (ec50 / conc) ** hill)\n\n\nresponse_a_true = logistic4pl(concentrations, bottom_a, top_a, ec50_a, hill_a)\nresponse_b_true = logistic4pl(concentrations, bottom_b, top_b, ec50_b, hill_b)\n\nsem_a = np.array([2.5, 3.0, 4.5, 5.0, 4.0, 3.5, 2.8, 2.0])\nsem_b = np.array([3.0, 3.5, 5.0, 4.5, 5.5, 4.0, 3.0, 2.5])\n\nresponse_a = response_a_true + np.random.normal(0, 2, len(concentrations))\nresponse_b = response_b_true + np.random.normal(0, 2, len(concentrations))\n\n# Fit 4PL curves\npopt_a, pcov_a = curve_fit(logistic4pl, concentrations, response_a, p0=[5, 95, 1e-7, 1.0], maxfev=10000)\npopt_b, pcov_b = curve_fit(logistic4pl, concentrations, response_b, p0=[8, 80, 1e-6, 1.0], maxfev=10000)\n\nconc_smooth = np.logspace(-9.5, -3.5, 300)\nfit_a = logistic4pl(conc_smooth, *popt_a)\nfit_b = logistic4pl(conc_smooth, *popt_b)\n\n# 95% CI for Erlotinib via delta method with covariance propagation\nn_params = len(popt_a)\nn_data = len(concentrations)\ndof = max(n_data - n_params, 1)\nt_val = t_dist.ppf(0.975, dof)\n\ndelta = 1e-8 * np.abs(popt_a) + 1e-15\njacobian_a = np.zeros((len(conc_smooth), n_params))\nfor i in range(n_params):\n    params_up = popt_a.copy()\n    params_up[i] += delta[i]\n    params_dn = popt_a.copy()\n    params_dn[i] -= delta[i]\n    jacobian_a[:, i] = (logistic4pl(conc_smooth, *params_up) - logistic4pl(conc_smooth, *params_dn)) / (2 * delta[i])\n\npred_var_a = np.sum(jacobian_a @ pcov_a * jacobian_a, axis=1)\npred_se_a = np.sqrt(np.maximum(pred_var_a, 0))\nci_lower_a = fit_a - t_val * pred_se_a\nci_upper_a = fit_a + t_val * pred_se_a\n\n# Fitted EC50 and half-maximal response values\nec50_fit_a = popt_a[2]\nec50_fit_b = popt_b[2]\nhalf_response_a = popt_a[0] + (popt_a[1] - popt_a[0]) / 2\nhalf_response_b = popt_b[0] + (popt_b[1] - popt_b[0]) / 2\n\n# EC50 label strings (inlined — no separate function)\nec50_label_a = f\"EC₅₀ = {ec50_fit_a * 1e9:.0f} nM\" if ec50_fit_a < 1e-6 else f\"EC₅₀ = {ec50_fit_a * 1e6:.1f} µM\"\nec50_label_b = f\"EC₅₀ = {ec50_fit_b * 1e9:.0f} nM\" if ec50_fit_b < 1e-6 else f\"EC₅₀ = {ec50_fit_b * 1e6:.1f} µM\"\n\n# Plot\ntitle = \"curve-dose-response · python · matplotlib · anyplot.ai\"\ntitle_fontsize = max(8, round(12 * 67 / len(title))) if len(title) > 67 else 12\n\nfig, ax = plt.subplots(figsize=(8, 4.5), dpi=400, facecolor=PAGE_BG)\nax.set_facecolor(PAGE_BG)\nax.set_ylim(-5, 105)\n\nax.fill_between(conc_smooth, ci_lower_a, ci_upper_a, alpha=0.15, color=COLORS[0], label=f\"95% CI ({drug_names[0]})\")\nax.plot(conc_smooth, fit_a, linewidth=2.5, color=COLORS[0], label=f\"{drug_names[0]} (fit)\")\nax.plot(conc_smooth, fit_b, linewidth=2.5, color=COLORS[1], label=f\"{drug_names[1]} (fit)\")\n\nax.errorbar(\n    concentrations,\n    response_a,\n    yerr=sem_a,\n    fmt=\"o\",\n    markersize=8,\n    color=COLORS[0],\n    markeredgecolor=PAGE_BG,\n    markeredgewidth=1.0,\n    elinewidth=1.5,\n    capsize=4,\n    capthick=1.5,\n    zorder=5,\n    label=f\"{drug_names[0]} (data)\",\n)\nax.errorbar(\n    concentrations,\n    response_b,\n    yerr=sem_b,\n    fmt=\"s\",\n    markersize=8,\n    color=COLORS[1],\n    markeredgecolor=PAGE_BG,\n    markeredgewidth=1.0,\n    elinewidth=1.5,\n    capsize=4,\n    capthick=1.5,\n    zorder=5,\n    label=f\"{drug_names[1]} (data)\",\n)\n\n# EC50 reference lines\nax.hlines(half_response_a, conc_smooth[0], ec50_fit_a, linestyles=\"dashed\", colors=COLORS[0], linewidth=1.2, alpha=0.6)\nax.vlines(ec50_fit_a, -5, half_response_a, linestyles=\"dashed\", colors=COLORS[0], linewidth=1.2, alpha=0.6)\nax.hlines(half_response_b, conc_smooth[0], ec50_fit_b, linestyles=\"dashed\", colors=COLORS[1], linewidth=1.2, alpha=0.6)\nax.vlines(ec50_fit_b, -5, half_response_b, linestyles=\"dashed\", colors=COLORS[1], linewidth=1.2, alpha=0.6)\n\n# Top and bottom asymptote markers\nax.axhline(y=popt_a[1], linestyle=\":\", color=COLORS[0], alpha=0.3, linewidth=1.0)\nax.axhline(y=popt_b[1], linestyle=\":\", color=COLORS[1], alpha=0.3, linewidth=1.0)\nax.axhline(y=popt_a[0], linestyle=\":\", color=COLORS[0], alpha=0.3, linewidth=0.8)\nax.axhline(y=popt_b[0], linestyle=\":\", color=COLORS[1], alpha=0.3, linewidth=0.8)\n\n# EC50 callout annotations with theme-adaptive boxes\nax.annotate(\n    ec50_label_a,\n    xy=(ec50_fit_a, half_response_a),\n    xytext=(ec50_fit_a * 30, half_response_a + 13),\n    fontsize=8,\n    fontweight=\"bold\",\n    color=COLORS[0],\n    arrowprops={\"arrowstyle\": \"->\", \"color\": COLORS[0], \"lw\": 1.2, \"connectionstyle\": \"arc3,rad=-0.2\"},\n    bbox={\"boxstyle\": \"round,pad=0.3\", \"facecolor\": ELEVATED_BG, \"edgecolor\": COLORS[0], \"alpha\": 0.9},\n    zorder=10,\n)\nax.annotate(\n    ec50_label_b,\n    xy=(ec50_fit_b, half_response_b),\n    xytext=(ec50_fit_b / 5, half_response_b + 14),\n    fontsize=8,\n    fontweight=\"bold\",\n    color=COLORS[1],\n    arrowprops={\"arrowstyle\": \"->\", \"color\": COLORS[1], \"lw\": 1.2, \"connectionstyle\": \"arc3,rad=-0.25\"},\n    bbox={\"boxstyle\": \"round,pad=0.3\", \"facecolor\": ELEVATED_BG, \"edgecolor\": COLORS[1], \"alpha\": 0.9},\n    zorder=10,\n)\n\n# Hill slope footer\nax.text(\n    0.98,\n    0.02,\n    f\"Hill slopes:  {drug_names[0]} = {popt_a[3]:.2f}  |  {drug_names[1]} = {popt_b[3]:.2f}\",\n    transform=ax.transAxes,\n    fontsize=8,\n    color=INK_MUTED,\n    ha=\"right\",\n    va=\"bottom\",\n    style=\"italic\",\n)\n\n# Style\nax.set_xscale(\"log\")\nax.set_xlabel(\"Concentration\", fontsize=10, color=INK)\nax.set_ylabel(\"Response (%)\", fontsize=10, color=INK)\nax.set_title(title, fontsize=title_fontsize, fontweight=\"medium\", color=INK)\nax.tick_params(axis=\"both\", labelsize=8, colors=INK_SOFT, labelcolor=INK_SOFT)\nax.spines[\"top\"].set_visible(False)\nax.spines[\"right\"].set_visible(False)\nfor s in (\"left\", \"bottom\"):\n    ax.spines[s].set_color(INK_SOFT)\nax.yaxis.grid(True, alpha=0.15, linewidth=0.8, color=INK)\n\n# Custom concentration formatter — shows nM below 1 µM, µM above\nax.xaxis.set_major_formatter(\n    ticker.FuncFormatter(lambda x, _: f\"{x * 1e9:.0f} nM\" if x < 1e-6 else f\"{x * 1e6:.0f} µM\")\n)\nax.xaxis.set_minor_locator(ticker.LogLocator(base=10.0, subs=np.arange(2, 10) * 0.1, numticks=50))\nax.xaxis.set_minor_formatter(ticker.NullFormatter())\nax.tick_params(axis=\"x\", which=\"minor\", length=3, width=0.6, colors=INK_SOFT)\n\n# Legend — reordered: A data, A fit, B data, B fit, CI band\nhandles, labels_list = ax.get_legend_handles_labels()\norder = [3, 1, 4, 2, 0]\nleg = ax.legend(\n    [handles[i] for i in order], [labels_list[i] for i in order], fontsize=8, loc=\"upper left\", framealpha=0.9\n)\nleg.get_frame().set_facecolor(ELEVATED_BG)\nleg.get_frame().set_edgecolor(INK_SOFT)\nplt.setp(leg.get_texts(), color=INK_SOFT)\n\nfig.subplots_adjust(left=0.10, right=0.97, top=0.92, bottom=0.13)\n\n# Save\nplt.savefig(f\"plot-{THEME}.png\", dpi=400, facecolor=PAGE_BG)\n"}