{"spec_id":"bode-basic","library":"plotly","language":"python","code":"\"\"\" anyplot.ai\nbode-basic: Bode Plot for Frequency Response\nLibrary: plotly 6.8.0 | Python 3.13.14\nQuality: 93/100 | Updated: 2026-06-17\n\"\"\"\n\nimport os\nimport sys\n\n\n# This file is named plotly.py — remove its directory from sys.path so\n# 'import plotly' resolves to the installed package, not this script.\n_here = os.path.dirname(os.path.abspath(__file__))\nsys.path = [p for p in sys.path if os.path.abspath(p or os.getcwd()) != _here]\n\nimport numpy as np\nimport plotly.graph_objects as go\nfrom plotly.subplots import make_subplots\n\n\n# Theme — Imprint palette chrome 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\"\nGRID = \"rgba(26,26,23,0.15)\" if THEME == \"light\" else \"rgba(240,239,232,0.15)\"\nGRID_MINOR = \"rgba(26,26,23,0.07)\" if THEME == \"light\" else \"rgba(240,239,232,0.07)\"\n\n# Imprint palette colors — positions 1→3 in canonical order\nCLR_MAIN = \"#009E73\"  # Imprint green (position 1) — main Bode curve\nCLR_GAIN = \"#C475FD\"  # Imprint lavender (position 2) — gain margin annotation\nCLR_PHASE = \"#4467A3\"  # Imprint blue (position 3) — phase margin annotation\n\n# Data — Third-order transfer function: H(s) = K / [(s/p1+1)(s²/wn²+2ζs/wn+1)]\n# Complex pair at wn=10 Hz with ζ=0.2 produces a clear resonance peak;\n# single real pole at 200 Hz ensures a finite gain margin (phase crosses -180°).\nK = 2\np1 = 2 * np.pi * 200  # real pole at 200 Hz (well above resonance)\nwn = 2 * np.pi * 10  # resonance at 10 Hz\nzeta = 0.2  # underdamped — visible resonance peak\n\nfrequency_hz = np.logspace(-1, 4, 800)\nomega = 2 * np.pi * frequency_hz\ns = 1j * omega\n\nH = K / ((s / p1 + 1) * (s**2 / wn**2 + 2 * zeta * s / wn + 1))\nmagnitude_db = 20 * np.log10(np.abs(H))\nphase_deg = np.degrees(np.unwrap(np.angle(H)))\n\n# Gain crossover: where magnitude crosses 0 dB\ngain_cross_idx = np.where(np.diff(np.sign(magnitude_db)))[0]\ngc_found = len(gain_cross_idx) > 0\nif gc_found:\n    gc_idx = gain_cross_idx[-1]\n    gc_freq = frequency_hz[gc_idx]\n    gc_phase = phase_deg[gc_idx]\n    phase_margin = 180 + gc_phase\n\n# Phase crossover: where phase crosses -180 degrees\nphase_cross_idx = np.where(np.diff(np.sign(phase_deg + 180)))[0]\npc_found = len(phase_cross_idx) > 0\nif pc_found:\n    pc_idx = phase_cross_idx[0]\n    pc_freq = frequency_hz[pc_idx]\n    pc_mag = magnitude_db[pc_idx]\n    gain_margin = -pc_mag\n\n# Figure — dual-panel Bode layout\nfig = make_subplots(rows=2, cols=1, shared_xaxes=True, vertical_spacing=0.08, row_heights=[0.55, 0.45])\n\n# Magnitude trace (Imprint green — first series)\nfig.add_trace(\n    go.Scatter(\n        x=frequency_hz,\n        y=magnitude_db,\n        mode=\"lines\",\n        line={\"color\": CLR_MAIN, \"width\": 2.5},\n        name=\"Magnitude\",\n        showlegend=False,\n    ),\n    row=1,\n    col=1,\n)\n\n# 0 dB reference line (dashed — zeroline suppressed on y-axis to avoid solid overlay)\nfig.add_hline(y=0, row=1, col=1, line={\"color\": INK_SOFT, \"width\": 1, \"dash\": \"dash\"})\n\n# Phase trace (same Imprint green — single-system Bode)\nfig.add_trace(\n    go.Scatter(\n        x=frequency_hz,\n        y=phase_deg,\n        mode=\"lines\",\n        line={\"color\": CLR_MAIN, \"width\": 2.5},\n        name=\"Phase\",\n        showlegend=False,\n    ),\n    row=2,\n    col=1,\n)\n\n# -180 degree reference line\nfig.add_hline(y=-180, row=2, col=1, line={\"color\": INK_SOFT, \"width\": 1, \"dash\": \"dash\"})\n\n# Gain margin annotation (Imprint lavender — position 2)\nif pc_found:\n    fig.add_shape(\n        type=\"rect\",\n        x0=pc_freq * 0.8,\n        x1=pc_freq * 1.25,\n        y0=pc_mag,\n        y1=0,\n        fillcolor=CLR_GAIN,\n        opacity=0.12,\n        line={\"width\": 0},\n        row=1,\n        col=1,\n    )\n    fig.add_trace(\n        go.Scatter(\n            x=[pc_freq, pc_freq],\n            y=[pc_mag, 0],\n            mode=\"lines+markers\",\n            line={\"color\": CLR_GAIN, \"width\": 2},\n            marker={\"size\": 8, \"symbol\": \"diamond\"},\n            showlegend=False,\n        ),\n        row=1,\n        col=1,\n    )\n    fig.add_annotation(\n        x=np.log10(pc_freq),\n        y=(pc_mag + 0) / 2,\n        text=f\"<b>GM = {gain_margin:.1f} dB</b>\",\n        showarrow=True,\n        arrowhead=0,\n        arrowwidth=1.5,\n        arrowcolor=CLR_GAIN,\n        ax=65,\n        ay=0,\n        font={\"size\": 12, \"color\": CLR_GAIN},\n        bgcolor=ELEVATED_BG,\n        bordercolor=CLR_GAIN,\n        borderwidth=1.5,\n        borderpad=4,\n        xref=\"x\",\n        yref=\"y\",\n        row=1,\n        col=1,\n    )\n    # Cross-panel marker on phase plot\n    fig.add_trace(\n        go.Scatter(\n            x=[pc_freq],\n            y=[-180],\n            mode=\"markers\",\n            marker={\"size\": 10, \"color\": CLR_GAIN, \"symbol\": \"diamond\"},\n            showlegend=False,\n        ),\n        row=2,\n        col=1,\n    )\n\n# Phase margin annotation (Imprint blue — position 3)\nif gc_found:\n    fig.add_shape(\n        type=\"rect\",\n        x0=gc_freq * 0.8,\n        x1=gc_freq * 1.25,\n        y0=gc_phase,\n        y1=-180,\n        fillcolor=CLR_PHASE,\n        opacity=0.12,\n        line={\"width\": 0},\n        row=2,\n        col=1,\n    )\n    fig.add_trace(\n        go.Scatter(\n            x=[gc_freq, gc_freq],\n            y=[gc_phase, -180],\n            mode=\"lines+markers\",\n            line={\"color\": CLR_PHASE, \"width\": 2, \"dash\": \"dash\"},\n            marker={\"size\": 8, \"symbol\": \"square\"},\n            showlegend=False,\n        ),\n        row=2,\n        col=1,\n    )\n    fig.add_annotation(\n        x=np.log10(gc_freq),\n        y=(gc_phase + (-180)) / 2,\n        text=f\"<b>PM = {phase_margin:.1f}°</b>\",\n        showarrow=True,\n        arrowhead=0,\n        arrowwidth=1.5,\n        arrowcolor=CLR_PHASE,\n        ax=-65,\n        ay=0,\n        font={\"size\": 12, \"color\": CLR_PHASE},\n        bgcolor=ELEVATED_BG,\n        bordercolor=CLR_PHASE,\n        borderwidth=1.5,\n        borderpad=4,\n        xref=\"x2\",\n        yref=\"y2\",\n        row=2,\n        col=1,\n    )\n    # Cross-panel marker on magnitude plot\n    fig.add_trace(\n        go.Scatter(\n            x=[gc_freq],\n            y=[0],\n            mode=\"markers\",\n            marker={\"size\": 10, \"color\": CLR_PHASE, \"symbol\": \"square\"},\n            showlegend=False,\n        ),\n        row=1,\n        col=1,\n    )\n\n# Layout — theme-adaptive chrome\nfig.update_layout(\n    autosize=False,\n    title={\n        \"text\": \"bode-basic · python · plotly · anyplot.ai\",\n        \"font\": {\"size\": 16, \"color\": INK},\n        \"x\": 0.5,\n        \"y\": 0.98,\n    },\n    paper_bgcolor=PAGE_BG,\n    plot_bgcolor=PAGE_BG,\n    font={\"color\": INK},\n    showlegend=False,\n    margin={\"l\": 80, \"r\": 40, \"t\": 70, \"b\": 60},\n)\n\n# X-axes (log scale — shared between both panels)\nfig.update_xaxes(\n    type=\"log\",\n    row=2,\n    col=1,\n    title={\"text\": \"Frequency (Hz)\", \"font\": {\"size\": 12, \"color\": INK}},\n    tickfont={\"size\": 10, \"color\": INK_SOFT},\n    showgrid=True,\n    gridcolor=GRID,\n    gridwidth=1,\n    showline=True,\n    linecolor=INK_SOFT,\n    mirror=False,\n    zeroline=False,\n    minor={\"showgrid\": True, \"gridcolor\": GRID_MINOR},\n)\nfig.update_xaxes(\n    type=\"log\",\n    row=1,\n    col=1,\n    tickfont={\"size\": 10, \"color\": INK_SOFT},\n    showgrid=True,\n    gridcolor=GRID,\n    gridwidth=1,\n    showline=False,\n    linecolor=INK_SOFT,\n    mirror=False,\n    zeroline=False,\n    minor={\"showgrid\": True, \"gridcolor\": GRID_MINOR},\n)\n\n# Y-axes\nfig.update_yaxes(\n    row=1,\n    col=1,\n    title={\"text\": \"Magnitude (dB)\", \"font\": {\"size\": 12, \"color\": INK}},\n    tickfont={\"size\": 10, \"color\": INK_SOFT},\n    showgrid=True,\n    gridcolor=GRID,\n    gridwidth=1,\n    showline=True,\n    linecolor=INK_SOFT,\n    mirror=False,\n    zeroline=False,  # suppress solid zeroline so only the dashed add_hline renders\n    range=[-80, 25],\n)\nfig.update_yaxes(\n    row=2,\n    col=1,\n    title={\"text\": \"Phase (degrees)\", \"font\": {\"size\": 12, \"color\": INK}},\n    tickfont={\"size\": 10, \"color\": INK_SOFT},\n    showgrid=True,\n    gridcolor=GRID,\n    gridwidth=1,\n    showline=True,\n    linecolor=INK_SOFT,\n    mirror=False,\n    zeroline=False,\n)\n\n# Save — landscape 3200 × 1800 (width=800, height=450, scale=4)\nfig.write_image(f\"plot-{THEME}.png\", width=800, height=450, scale=4)\nfig.write_html(f\"plot-{THEME}.html\", include_plotlyjs=\"cdn\")\n"}