{"spec_id":"campbell-basic","library":"letsplot","language":"python","code":"\"\"\" anyplot.ai\ncampbell-basic: Campbell Diagram\nLibrary: letsplot 4.10.1 | Python 3.13.13\nQuality: 88/100 | Updated: 2026-05-28\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nfrom lets_plot import *\n\n\nLetsPlot.setup_html()\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\"\nGRID_COLOR = \"#D8D7D0\" if THEME == \"light\" else \"#3A3A36\"\n\nIMPRINT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\"]\n# Modes: positions 1,2,3,4,6 — skip #AE3030 (pos 5), reserved for critical markers\nMODE_COLORS = [IMPRINT_PALETTE[0], IMPRINT_PALETTE[1], IMPRINT_PALETTE[2], IMPRINT_PALETTE[3], IMPRINT_PALETTE[5]]\nCRIT_COLOR = IMPRINT_PALETTE[4]  # #AE3030 — semantic: danger/resonance risk\n\n# Data\nnp.random.seed(42)\nspeed_rpm = np.linspace(0, 6000, 80)\nmax_freq = 120\n\n# Natural frequencies with pronounced speed-dependent variation (gyroscopic effects)\nmodes = {\n    \"1st Bending\": 25 + 0.0035 * speed_rpm + np.random.normal(0, 0.15, 80),\n    \"1st Torsional\": 48 - 0.0030 * speed_rpm + np.random.normal(0, 0.12, 80),\n    \"2nd Bending\": 72 + 0.0045 * speed_rpm + np.random.normal(0, 0.18, 80),\n    \"2nd Torsional\": 88 + 0.0012 * speed_rpm + np.random.normal(0, 0.14, 80),\n    \"Axial\": 105 - 0.0025 * speed_rpm + np.random.normal(0, 0.10, 80),\n}\nmode_order = list(modes.keys())\n\nmodes_df = pd.concat(\n    [pd.DataFrame({\"Speed\": speed_rpm, \"Frequency\": freq, \"Mode\": name}) for name, freq in modes.items()],\n    ignore_index=True,\n)\n\n# Engine order excitation lines (freq = order × RPM / 60)\norders = [1, 2, 3]\norder_labels = [\"1×\", \"2×\", \"3×\"]\n\neo_df = pd.concat(\n    [\n        pd.DataFrame(\n            {\n                \"Speed\": np.linspace(0, min(6000, max_freq * 60 / o), 80),\n                \"Frequency\": o * np.linspace(0, min(6000, max_freq * 60 / o), 80) / 60,\n                \"Order\": lbl,\n            }\n        )\n        for o, lbl in zip(orders, order_labels)\n    ],\n    ignore_index=True,\n)\n\n# Critical speeds: intersections of EO lines with mode curves\ncritical_rows = []\nfor order, olabel in zip(orders, order_labels):\n    eo_at = order * speed_rpm / 60\n    for mname, mfreq in modes.items():\n        diff = eo_at - mfreq\n        for idx in np.where(np.diff(np.sign(diff)))[0]:\n            t = abs(diff[idx]) / (abs(diff[idx]) + abs(diff[idx + 1]))\n            cs = speed_rpm[idx] + t * (speed_rpm[idx + 1] - speed_rpm[idx])\n            cf = order * cs / 60\n            if cf <= max_freq:\n                critical_rows.append({\"Speed\": cs, \"Frequency\": cf, \"Intersection\": f\"{mname} × {olabel}\"})\ncrit_df = pd.DataFrame(critical_rows)\n\n# Resonance risk zones around critical speeds\nzone_df = pd.DataFrame(\n    [\n        {\"xmin\": r[\"Speed\"] - 150, \"xmax\": r[\"Speed\"] + 150, \"ymin\": r[\"Frequency\"] - 3, \"ymax\": r[\"Frequency\"] + 3}\n        for _, r in crit_df.iterrows()\n    ]\n)\n\n# EO direct labels: positioned below mode curves (< 25 Hz) in low-RPM region\neo_label_df = pd.DataFrame(\n    [\n        {\"Speed\": 1200, \"Frequency\": 1 * 1200 / 60 + 2, \"Label\": \"1×\"},\n        {\"Speed\": 500, \"Frequency\": 2 * 500 / 60 + 2, \"Label\": \"2×\"},\n        {\"Speed\": 250, \"Frequency\": 3 * 250 / 60 + 2, \"Label\": \"3×\"},\n    ]\n)\n\n# Annotation for the highest-frequency critical speed (resonance risk highlight)\ndanger_idx = crit_df[\"Frequency\"].idxmax()\ndanger_row = crit_df.loc[danger_idx]\nannot_df = pd.DataFrame(\n    [\n        {\n            \"Speed\": danger_row[\"Speed\"],\n            \"Frequency\": danger_row[\"Frequency\"] + 10,\n            \"Label\": f\"{danger_row['Intersection']}\\n({int(danger_row['Speed'])} RPM)\",\n        }\n    ]\n)\n\n# Plot\nplot = (\n    ggplot()\n    + geom_rect(\n        data=zone_df,\n        mapping=aes(xmin=\"xmin\", xmax=\"xmax\", ymin=\"ymin\", ymax=\"ymax\"),\n        fill=CRIT_COLOR,\n        alpha=0.08,\n        color=\"transparent\",\n    )\n    + geom_line(data=eo_df, mapping=aes(x=\"Speed\", y=\"Frequency\", linetype=\"Order\"), color=INK_MUTED, size=0.9)\n    + geom_line(\n        data=modes_df,\n        mapping=aes(x=\"Speed\", y=\"Frequency\", color=\"Mode\"),\n        size=2.5,\n        tooltips=layer_tooltips()\n        .line(\"@{Mode}\")\n        .line(\"Speed|@{Speed} RPM\")\n        .line(\"Freq|@{Frequency} Hz\")\n        .format(\"Speed\", \",.0f\")\n        .format(\"Frequency\", \".1f\"),\n    )\n    + geom_point(\n        data=crit_df,\n        mapping=aes(x=\"Speed\", y=\"Frequency\"),\n        color=CRIT_COLOR,\n        fill=CRIT_COLOR,\n        size=8,\n        shape=18,\n        tooltips=layer_tooltips()\n        .title(\"Critical Speed\")\n        .line(\"@{Intersection}\")\n        .line(\"Speed|@{Speed} RPM\")\n        .line(\"Freq|@{Frequency} Hz\")\n        .format(\"Speed\", \",.0f\")\n        .format(\"Frequency\", \".1f\"),\n    )\n    + geom_text(\n        data=eo_label_df, mapping=aes(x=\"Speed\", y=\"Frequency\", label=\"Label\"), color=INK_MUTED, size=4, fontface=\"bold\"\n    )\n    + geom_text(\n        data=annot_df,\n        mapping=aes(x=\"Speed\", y=\"Frequency\", label=\"Label\"),\n        color=CRIT_COLOR,\n        size=4,\n        fontface=\"italic\",\n        hjust=0.5,\n    )\n    + scale_color_manual(name=\"Natural Frequency\", values=MODE_COLORS, limits=mode_order)\n    + scale_linetype_manual(name=\"Engine Order\", values=[\"dashed\", \"longdash\", \"dotdash\"], limits=order_labels)\n    + scale_y_continuous(limits=[0, max_freq], expand=[0.01, 0])\n    + scale_x_continuous(limits=[0, 6000], expand=[0.01, 0], format=\",d\")\n    + guides(linetype=guide_legend(override_aes={\"color\": INK_MUTED}))\n    + labs(\n        title=\"campbell-basic · python · letsplot · anyplot.ai\",\n        subtitle=\"Red diamonds mark critical speeds where engine order lines cross natural frequency modes\",\n        x=\"Rotational Speed (RPM)\",\n        y=\"Frequency (Hz)\",\n        caption=\"Data: synthetic rotordynamic model | Shaded zones indicate resonance risk regions\",\n    )\n    + theme(\n        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        panel_background=element_rect(fill=PAGE_BG),\n        panel_grid_major=element_line(color=GRID_COLOR, size=0.4),\n        panel_grid_minor=element_blank(),\n        panel_border=element_blank(),\n        axis_line=element_line(color=INK_SOFT, size=0.5),\n        axis_title=element_text(color=INK, size=12),\n        axis_text=element_text(color=INK_SOFT, size=10),\n        plot_title=element_text(color=INK, size=16, face=\"bold\", hjust=0.5),\n        plot_subtitle=element_text(color=INK_MUTED, size=11, hjust=0.5),\n        plot_caption=element_text(color=INK_MUTED, size=10, face=\"italic\"),\n        legend_position=\"right\",\n        legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),\n        legend_text=element_text(color=INK_SOFT, size=10),\n        legend_title=element_text(color=INK, size=11, face=\"bold\"),\n        plot_margin=[20, 20, 20, 20],\n    )\n    + ggsize(800, 450)\n)\n\n# Save\nggsave(plot, f\"plot-{THEME}.png\", path=\".\", scale=4)\nggsave(plot, f\"plot-{THEME}.html\", path=\".\")\n"}