{"spec_id":"nyquist-basic","library":"matplotlib","language":"python","code":"\"\"\" anyplot.ai\nnyquist-basic: Nyquist Plot for Control Systems\nLibrary: matplotlib 3.11.0 | Python 3.13.13\nQuality: 89/100 | Updated: 2026-06-17\n\"\"\"\n\nimport os\nimport sys\n\n\n# Remove script directory from sys.path so it doesn't shadow the installed matplotlib package.\n_script_dir = os.path.dirname(os.path.abspath(__file__))\nsys.path = [p for p in sys.path if os.path.abspath(p) != _script_dir and p != \"\"]\n\nimport matplotlib.pyplot as plt\nimport numpy as np\nfrom matplotlib.patches import Circle\nfrom scipy import signal\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\"\nBRAND = \"#009E73\"  # Imprint palette position 1 — ALWAYS first series\nCRITICAL_COLOR = \"#AE3030\"  # Imprint semantic red for critical/error\n\n# Data — cascaded third-order system: G(s) = 2 / (s+1)(0.5s+1)(0.2s+1)\nnum = [2.0]\nden = np.polymul(np.polymul([1, 1], [0.5, 1]), [0.2, 1])\nsystem = signal.TransferFunction(num, den)\n\nomega = np.logspace(-1.5, 2, 800)\n_, H = signal.freqresp(system, w=omega)\n\nreal = H.real\nimag = H.imag\n\n# Stability margins from frequency response\nmagnitude = np.abs(H)\nphase_deg = np.unwrap(np.angle(H)) * 180 / np.pi\n\n# Phase crossover (phase → -180°) → gain margin\nwpc_idx = np.argmin(np.abs(phase_deg + 180))\ngm_db = 20 * np.log10(1 / magnitude[wpc_idx])\n\n# Gain crossover (|H| → 1) → phase margin\nwgc_idx = np.argmin(np.abs(magnitude - 1))\npm_deg = 180 + phase_deg[wgc_idx]\n\n# Plot\nfig, ax = plt.subplots(figsize=(6, 6), dpi=400, facecolor=PAGE_BG)\nax.set_facecolor(PAGE_BG)\n\nax.plot(real, imag, color=BRAND, linewidth=2.5, label=\"G(jω)\", zorder=3)\nax.plot(real, -imag, color=BRAND, linewidth=2.5, alpha=0.35, linestyle=\"--\", label=\"G(−jω)\", zorder=3)\n\n# Direction arrows along the curve\nfor frac in [0.08, 0.2, 0.4, 0.65]:\n    idx = int(frac * len(omega))\n    ax.annotate(\n        \"\",\n        xy=(real[idx + 1], imag[idx + 1]),\n        xytext=(real[idx], imag[idx]),\n        arrowprops={\"arrowstyle\": \"-|>\", \"color\": BRAND, \"lw\": 2.0, \"mutation_scale\": 18},\n        zorder=4,\n    )\n\n# Unit circle\nunit_circle = Circle((0, 0), 1, fill=False, color=INK_SOFT, linewidth=1.5, linestyle=\":\", zorder=2)\nax.add_patch(unit_circle)\n\n# Critical point (-1, 0)\nax.plot(\n    -1,\n    0,\n    marker=\"x\",\n    color=CRITICAL_COLOR,\n    markersize=14,\n    markeredgewidth=3.0,\n    zorder=5,\n    label=\"Critical point (−1, 0)\",\n)\n\n# Phase margin: gain crossover on unit circle (|H|=1)\nax.plot(real[wgc_idx], imag[wgc_idx], \"D\", color=BRAND, markersize=7, zorder=6)\nax.annotate(\n    r\"$\\phi_m$\" + f\" = {pm_deg:.1f}°\",\n    xy=(real[wgc_idx], imag[wgc_idx]),\n    xytext=(14, 12),\n    textcoords=\"offset points\",\n    fontsize=9,\n    color=BRAND,\n    fontweight=\"bold\",\n    ha=\"left\",\n    va=\"bottom\",\n    zorder=6,\n)\n\n# Gain margin: phase crossover on negative real axis (phase = -180°)\nax.plot(real[wpc_idx], imag[wpc_idx], \"s\", color=CRITICAL_COLOR, markersize=6, zorder=6)\nax.annotate(\n    r\"$G_m$\" + f\" = {gm_db:.1f} dB\",\n    xy=(real[wpc_idx], imag[wpc_idx]),\n    xytext=(-0.55, 0.40),\n    textcoords=\"data\",\n    fontsize=9,\n    color=CRITICAL_COLOR,\n    fontweight=\"bold\",\n    ha=\"center\",\n    va=\"bottom\",\n    zorder=6,\n    arrowprops={\"arrowstyle\": \"->\", \"color\": CRITICAL_COLOR, \"lw\": 1.0, \"shrinkA\": 0, \"shrinkB\": 5},\n)\n\n# Frequency annotations at key points\nfreq_annotations = [(0.3, (15, 12)), (1.0, (15, 12)), (2.0, (-15, -18)), (5.0, (-15, 14)), (10.0, (12, 12))]\nfor freq_val, (ox, oy) in freq_annotations:\n    idx = np.argmin(np.abs(omega - freq_val))\n    ax.plot(real[idx], imag[idx], \"o\", color=BRAND, markersize=6, zorder=5)\n    ha = \"left\" if ox > 0 else \"right\"\n    va = \"bottom\" if oy > 0 else \"top\"\n    ax.annotate(\n        f\"ω={freq_val:g}\",\n        xy=(real[idx], imag[idx]),\n        xytext=(ox, oy),\n        textcoords=\"offset points\",\n        fontsize=9,\n        color=INK_SOFT,\n        fontweight=\"medium\",\n        ha=ha,\n        va=va,\n        zorder=5,\n    )\n\n# Style\nax.set_xlabel(\"Real\", fontsize=10, color=INK)\nax.set_ylabel(\"Imaginary\", fontsize=10, color=INK)\ntitle = \"nyquist-basic · python · matplotlib · anyplot.ai\"\nax.set_title(title, fontsize=12, fontweight=\"medium\", color=INK)\nax.tick_params(axis=\"both\", labelsize=8, colors=INK_SOFT, labelcolor=INK_SOFT)\nax.set_aspect(\"equal\")\nax.axhline(0, color=INK_SOFT, linewidth=0.8, alpha=0.4, zorder=1)\nax.axvline(0, color=INK_SOFT, linewidth=0.8, alpha=0.4, zorder=1)\n\nfor s in (\"left\", \"bottom\"):\n    ax.spines[s].set_color(INK_SOFT)\nax.spines[\"top\"].set_visible(False)\nax.spines[\"right\"].set_visible(False)\n\nax.grid(True, alpha=0.15, linewidth=0.8, color=INK)\n\nleg = ax.legend(fontsize=8, loc=\"lower left\", framealpha=0.9)\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\nfig.subplots_adjust(left=0.10, right=0.96, top=0.93, bottom=0.10)\n\n# Save\nplt.savefig(os.path.join(_script_dir, f\"plot-{THEME}.png\"), dpi=400, facecolor=PAGE_BG)\n"}