{"spec_id":"bode-basic","library":"matplotlib","language":"python","code":"\"\"\" anyplot.ai\nbode-basic: Bode Plot for Frequency Response\nLibrary: matplotlib 3.11.0 | Python 3.13.14\nQuality: 92/100 | Updated: 2026-06-17\n\"\"\"\n\nimport os\n\nimport matplotlib.pyplot as plt\nimport numpy as np\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\n# Imprint palette — 8 hues, hybrid-v3 sort\nIMPRINT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\"]\nBRAND = IMPRINT_PALETTE[0]  # main frequency response line — brand green\nCOLOR_GM = IMPRINT_PALETTE[2]  # gain margin annotation — blue\nCOLOR_PM = IMPRINT_PALETTE[3]  # phase margin annotation — ochre\n\n# Data — third-order open-loop transfer function:\n# H(s) = K / ((s/w1 + 1)(s/w2 + 1)(s/w3 + 1))\n# Three poles at 1, 10, and 80 Hz with gain K=20\nK = 20\npoles_hz = np.array([1.0, 10.0, 80.0])\npoles_rad = 2 * np.pi * poles_hz\n\nfrequency_hz = np.logspace(-1, 3, 500)\nomega = 2 * np.pi * frequency_hz\njw = 1j * omega\n\nH = K / np.prod([(jw / p + 1) for p in poles_rad], axis=0)\nmagnitude_db = 20 * np.log10(np.abs(H))\nphase_deg = np.degrees(np.unwrap(np.angle(H)))\n\n# Gain crossover (0 dB crossing)\nsign_changes_mag = np.diff(np.sign(magnitude_db))\ngain_crossover_idx = np.where(sign_changes_mag != 0)[0]\nif len(gain_crossover_idx) > 0:\n    gc_idx = gain_crossover_idx[0]\n    gain_crossover_freq = frequency_hz[gc_idx]\n    phase_at_gc = phase_deg[gc_idx]\n    phase_margin = 180 + phase_at_gc\nelse:\n    gain_crossover_freq = None\n\n# Phase crossover (-180 deg crossing)\nsign_changes_phase = np.diff(np.sign(phase_deg + 180))\nphase_crossover_idx = np.where(sign_changes_phase != 0)[0]\nif len(phase_crossover_idx) > 0:\n    pc_idx = phase_crossover_idx[0]\n    phase_crossover_freq = frequency_hz[pc_idx]\n    gain_at_pc = magnitude_db[pc_idx]\n    gain_margin = -gain_at_pc\nelse:\n    phase_crossover_freq = None\n\n# Plot\ntitle = \"bode-basic · python · matplotlib · anyplot.ai\"\ntitle_n = len(title)\ntitle_fontsize = max(8, round(12 * 67 / title_n)) if title_n > 67 else 12\n\nfig, (ax_mag, ax_phase) = plt.subplots(\n    2,\n    1,\n    figsize=(8, 4.5),\n    dpi=400,\n    sharex=True,\n    facecolor=PAGE_BG,\n    gridspec_kw={\"height_ratios\": [1, 1], \"hspace\": 0.08},\n)\nax_mag.set_facecolor(PAGE_BG)\nax_phase.set_facecolor(PAGE_BG)\n\n# Magnitude plot\nax_mag.semilogx(frequency_hz, magnitude_db, color=BRAND, linewidth=2.5)\nax_mag.axhline(y=0, color=INK_MUTED, linewidth=1, linestyle=\"--\", alpha=0.7)\n\nif phase_crossover_freq is not None:\n    ax_mag.vlines(phase_crossover_freq, gain_at_pc, 0, colors=COLOR_GM, linewidth=2)\n    ax_mag.plot(phase_crossover_freq, gain_at_pc, \"o\", color=COLOR_GM, markersize=7, zorder=5)\n    ax_mag.annotate(\n        f\"GM = {gain_margin:.1f} dB\",\n        xy=(phase_crossover_freq, (gain_at_pc + 0) / 2),\n        xytext=(phase_crossover_freq * 3, (gain_at_pc + 0) / 2 + 8),\n        fontsize=9,\n        color=COLOR_GM,\n        fontweight=\"bold\",\n        arrowprops={\"arrowstyle\": \"->\", \"color\": COLOR_GM, \"lw\": 1.5},\n    )\n\n# Phase plot\nax_phase.semilogx(frequency_hz, phase_deg, color=BRAND, linewidth=2.5)\nax_phase.axhline(y=-180, color=INK_MUTED, linewidth=1, linestyle=\"--\", alpha=0.7)\n\nif gain_crossover_freq is not None:\n    ax_phase.vlines(gain_crossover_freq, -180, phase_at_gc, colors=COLOR_PM, linewidth=2)\n    ax_phase.plot(gain_crossover_freq, phase_at_gc, \"o\", color=COLOR_PM, markersize=7, zorder=5)\n    ax_phase.annotate(\n        f\"PM = {phase_margin:.1f}°\",\n        xy=(gain_crossover_freq, (-180 + phase_at_gc) / 2),\n        xytext=(gain_crossover_freq * 6, (-180 + phase_at_gc) / 2 + 20),\n        fontsize=9,\n        color=COLOR_PM,\n        fontweight=\"bold\",\n        arrowprops={\"arrowstyle\": \"->\", \"color\": COLOR_PM, \"lw\": 1.5},\n    )\n\n# Style — magnitude panel\nax_mag.set_ylabel(\"Magnitude (dB)\", fontsize=10, color=INK)\nax_mag.set_title(title, fontsize=title_fontsize, fontweight=\"medium\", color=INK)\nax_mag.tick_params(axis=\"both\", labelsize=8, colors=INK_SOFT, labelcolor=INK_SOFT)\nax_mag.spines[\"top\"].set_visible(False)\nax_mag.spines[\"right\"].set_visible(False)\nfor s in (\"left\", \"bottom\"):\n    ax_mag.spines[s].set_color(INK_SOFT)\nax_mag.yaxis.grid(True, alpha=0.2, linewidth=0.8, color=INK)\nax_mag.xaxis.grid(True, alpha=0.15, linewidth=0.8, color=INK)\n\n# Style — phase panel\nax_phase.set_xlabel(\"Frequency (Hz)\", fontsize=10, color=INK)\nax_phase.set_ylabel(\"Phase (°)\", fontsize=10, color=INK)\nax_phase.tick_params(axis=\"both\", labelsize=8, colors=INK_SOFT, labelcolor=INK_SOFT)\nax_phase.spines[\"top\"].set_visible(False)\nax_phase.spines[\"right\"].set_visible(False)\nfor s in (\"left\", \"bottom\"):\n    ax_phase.spines[s].set_color(INK_SOFT)\nax_phase.yaxis.grid(True, alpha=0.2, linewidth=0.8, color=INK)\nax_phase.xaxis.grid(True, alpha=0.15, linewidth=0.8, color=INK)\nax_phase.set_yticks([0, -45, -90, -135, -180, -225, -270])\n\n# Save\nfig.subplots_adjust(left=0.12, right=0.97, top=0.92, bottom=0.12)\nplt.savefig(f\"plot-{THEME}.png\", dpi=400, facecolor=PAGE_BG)\n"}