{"spec_id":"nyquist-basic","library":"makie","language":"julia","code":"# anyplot.ai\n# nyquist-basic: Nyquist Plot for Control Systems\n# Library: makie 0.22.10 | Julia 1.11.9\n# Quality: 89/100 | Created: 2026-06-17\n\nusing CairoMakie\nusing Colors\nusing Random\n\nRandom.seed!(42)\n\n# Theme tokens\nconst THEME       = get(ENV, \"ANYPLOT_THEME\", \"light\")\nconst PAGE_BG     = THEME == \"light\" ? colorant\"#FAF8F1\" : colorant\"#1A1A17\"\nconst ELEVATED_BG = THEME == \"light\" ? colorant\"#FFFDF6\" : colorant\"#242420\"\nconst INK         = THEME == \"light\" ? colorant\"#1A1A17\" : colorant\"#F0EFE8\"\nconst INK_SOFT    = THEME == \"light\" ? colorant\"#4A4A44\" : colorant\"#B8B7B0\"\n\nconst IMPRINT_PALETTE = [\n    colorant\"#009E73\",\n    colorant\"#C475FD\",\n    colorant\"#4467A3\",\n    colorant\"#BD8233\",\n    colorant\"#AE3030\",\n    colorant\"#2ABCCD\",\n    colorant\"#954477\",\n    colorant\"#99B314\",\n]\n\n# Imprint sequential colormap: brand green → blue (for continuous frequency encoding)\nconst ANYPLOT_SEQ = cgrad([colorant\"#009E73\", colorant\"#4467A3\"])\n\n# Frequency response: G(s) = 1 / (s(s+1)(0.5s+1))\n# Phase crossover at ω_pc = √2 rad/s → G(jω_pc) = -1/3 → gain margin = 3\nn_points = 800\nω = 10 .^ range(log10(0.04), log10(100.0), length=n_points)\nG_jω = [1.0 / (im * w * (1 + im * w) * (1 + 0.5 * im * w)) for w in ω]\n\nre_vals = real.(G_jω)\nim_vals = imag.(G_jω)\n\n# Gain crossover (|G| = 1) and phase crossover (phase = -180°)\nmag = abs.(G_jω)\ngc_idx = argmin(abs.(mag .- 1.0))\nω_gc_val = ω[gc_idx]\nG_gc = G_jω[gc_idx]\nphase_margin_deg = 180.0 + angle(G_gc) * (180.0 / π)\n\nph_deg = angle.(G_jω) .* (180.0 / π)\npc_idx = argmin(abs.(ph_deg .+ 180.0))\nG_pc   = G_jω[pc_idx]\ngain_margin = round(1.0 / abs(G_pc); digits=1)\n\n# Log-frequency for continuous color encoding along the Nyquist curve\nlog_ω = log10.(ω)\nlog_ω_min, log_ω_max = extrema(log_ω)\n\n# Unit circle\nθ_circle = range(0.0, 2π, length=300)\n\n# Figure — square canvas for 1:1 aspect (2400×2400 output at px_per_unit=2)\nfig = Figure(\n    size            = (1200, 1200),\n    fontsize        = 14,\n    backgroundcolor = PAGE_BG,\n)\n\nink_r = Float32(INK.r)\nink_g = Float32(INK.g)\nink_b = Float32(INK.b)\n\ntitle_str  = \"nyquist-basic · julia · makie · anyplot.ai\"\nn_title    = length(title_str)\ntitle_size = n_title > 67 ? round(Int, 20 * 67 / n_title) : 20\n\nax = Axis(\n    fig[1, 1];\n    title              = title_str,\n    titlesize          = title_size,\n    titlecolor         = INK,\n    xlabel             = \"Real\",\n    ylabel             = \"Imaginary\",\n    xlabelsize         = 14,\n    ylabelsize         = 14,\n    xlabelcolor        = INK,\n    ylabelcolor        = INK,\n    xticklabelcolor    = INK_SOFT,\n    yticklabelcolor    = INK_SOFT,\n    xticklabelsize     = 12,\n    yticklabelsize     = 12,\n    xtickcolor         = INK_SOFT,\n    ytickcolor         = INK_SOFT,\n    backgroundcolor    = PAGE_BG,\n    topspinevisible    = false,\n    rightspinevisible  = false,\n    leftspinecolor     = INK_SOFT,\n    bottomspinecolor   = INK_SOFT,\n    xgridcolor         = RGBAf(ink_r, ink_g, ink_b, 0.12f0),\n    ygridcolor         = RGBAf(ink_r, ink_g, ink_b, 0.12f0),\n    aspect             = DataAspect(),\n)\n\n# Reference lines through origin\nhlines!(ax, [0.0]; color = (INK_SOFT, 0.25), linewidth = 1.0)\nvlines!(ax, [0.0]; color = (INK_SOFT, 0.25), linewidth = 1.0)\n\n# Unit circle reference\nlines!(ax, cos.(θ_circle), sin.(θ_circle);\n    color     = (IMPRINT_PALETTE[3], 0.45),\n    linewidth = 1.8,\n    linestyle = :dash,\n    label     = \"Unit circle\",\n)\n\n# Nyquist curve — colored by log₁₀(ω) using Imprint sequential colormap\n# Low frequency (ω → 0) renders in brand green; high frequency in blue\nlines!(ax, re_vals, im_vals;\n    color      = log_ω,\n    colormap   = ANYPLOT_SEQ,\n    colorrange = (log_ω_min, log_ω_max),\n    linewidth  = 2.8,\n    label      = \"G(jω) = 1 / [s(s+1)(0.5s+1)]\",\n)\n\n# Direction arrows showing increasing-frequency direction\nfor idx in [80, 260, 460]\n    if idx + 4 <= length(re_vals)\n        dx = re_vals[idx + 4] - re_vals[idx]\n        dy = im_vals[idx + 4] - im_vals[idx]\n        nd = sqrt(dx^2 + dy^2)\n        if nd > 1e-10\n            u = dx / nd * 0.07\n            v = dy / nd * 0.07\n            arrows!(ax,\n                [re_vals[idx]], [im_vals[idx]],\n                [u], [v];\n                arrowsize  = 10,\n                arrowcolor = INK_SOFT,\n                linecolor  = INK_SOFT,\n                linewidth  = 1.5,\n            )\n        end\n    end\nend\n\n# Phase crossover marker\nscatter!(ax, [real(G_pc)], [imag(G_pc)];\n    color       = IMPRINT_PALETTE[4],\n    markersize  = 16,\n    marker      = :circle,\n    strokewidth = 2,\n    strokecolor = ELEVATED_BG,\n    label       = \"Phase crossover (ω = √2 rad/s)\",\n)\n\n# Gain crossover marker\nscatter!(ax, [real(G_gc)], [imag(G_gc)];\n    color       = IMPRINT_PALETTE[6],\n    markersize  = 16,\n    marker      = :diamond,\n    strokewidth = 2,\n    strokecolor = ELEVATED_BG,\n    label       = \"Gain crossover (ω ≈ $(round(ω_gc_val; digits=2)) rad/s)\",\n)\n\n# Critical point (-1, 0)\nscatter!(ax, [-1.0], [0.0];\n    color       = IMPRINT_PALETTE[5],\n    markersize  = 22,\n    marker      = :xcross,\n    strokewidth = 3,\n    label       = \"Critical point (-1, 0)\",\n)\n\n# Gain margin: dotted line from phase crossover point to critical point\nlines!(ax, [real(G_pc), -1.0], [0.0, 0.0];\n    color     = (INK_SOFT, 0.45),\n    linewidth = 1.8,\n    linestyle = :dot,\n)\ntext!(ax, (real(G_pc) + (-1.0)) / 2, 0.13;\n    text     = \"GM = $(gain_margin)\",\n    color    = INK_SOFT,\n    fontsize = 11,\n    align    = (:center, :bottom),\n)\n\n# Frequency annotations\ntext!(ax, real(G_pc) + 0.07, imag(G_pc) + 0.16;\n    text     = \"ω = √2 rad/s\",\n    color    = IMPRINT_PALETTE[4],\n    fontsize = 11,\n)\ntext!(ax, -1.0 + 0.07, 0.22;\n    text     = \"(-1, 0)\",\n    color    = IMPRINT_PALETTE[5],\n    fontsize = 11,\n)\ntext!(ax, real(G_gc) - 0.10, imag(G_gc) - 0.28;\n    text     = \"PM ≈ $(round(Int, phase_margin_deg))°\",\n    color    = IMPRINT_PALETTE[6],\n    fontsize = 11,\n    align    = (:right, :top),\n)\n\nxlims!(ax, -2.2, 1.5)\nylims!(ax, -4.0, 2.0)\n\naxislegend(ax;\n    position        = :rt,\n    backgroundcolor = ELEVATED_BG,\n    framecolor      = (INK_SOFT, 0.3),\n    labelsize       = 11,\n    labelcolor      = INK,\n    rowgap          = 4,\n)\n\n# Colorbar: maps log₁₀(ω) to the Imprint sequential gradient — Makie layout element\nColorbar(fig[1, 2];\n    colormap       = ANYPLOT_SEQ,\n    limits         = (log_ω_min, log_ω_max),\n    label          = \"log₁₀(ω)  [rad/s]\",\n    labelsize      = 13,\n    labelcolor     = INK,\n    ticklabelsize  = 11,\n    ticklabelcolor = INK_SOFT,\n    tickcolor      = INK_SOFT,\n    width          = 18,\n)\n\n# Stability summary — Makie Label layout element below the plot\nstability_str = \"System: STABLE  ·  Gain Margin = $(gain_margin)  ·  Phase Margin ≈ $(round(Int, phase_margin_deg))°\"\nLabel(fig[2, 1:2], stability_str;\n    fontsize  = 12,\n    color     = INK_SOFT,\n    halign    = :left,\n    tellwidth = false,\n    padding   = (8, 0, 4, 4),\n)\n\nsave(\"plot-$(THEME).png\", fig; px_per_unit = 2)\n"}