{"spec_id":"line-tanabe-sugano","library":"letsplot","language":"python","code":"\"\"\" anyplot.ai\nline-tanabe-sugano: Tanabe-Sugano Diagram for Crystal Field Theory\nLibrary: letsplot 4.11.0 | Python 3.13.15\nQuality: 90/100 | Created: 2026-10-01\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nfrom lets_plot import (\n    LetsPlot,\n    aes,\n    element_blank,\n    element_line,\n    element_rect,\n    element_text,\n    geom_line,\n    geom_segment,\n    geom_text,\n    ggplot,\n    ggsave,\n    ggsize,\n    labs,\n    scale_color_manual,\n    scale_linetype_manual,\n    scale_x_continuous,\n    scale_y_continuous,\n    theme,\n    theme_classic,\n)\n\n\nLetsPlot.setup_html()\n\n# Theme tokens (see prompts/default-style-guide.md \"Theme-adaptive Chrome\")\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 = \"#D8D7D0\" if THEME == \"light\" else \"#3A3A36\"  # 15% ink blended into the page background\nSPIN_ALLOWED = \"#009E73\"  # Imprint palette position 1 — always the first series\nSPIN_FORBIDDEN = \"#C475FD\"  # Imprint palette position 2\n\n# Data — Tanabe-Sugano energy matrices for an octahedral d8 ion (Ni2+), in units of the\n# Racah parameter B with C = 4.5 B and the Racah A dropped. Each octahedral symmetry label\n# gets one matrix over the free-ion terms that produce it; d8 reuses the d2 matrices with\n# the sign of Dq reversed, the electron-hole relation between the two configurations.\nC_OVER_B = 4.5\nDELTA_MAX = 40.0\nENERGY_MAX = 100.0  # drops the runaway 1A1g(S) term, which leaves the frame above E/B = 120\nLABEL_GAP = 4.2  # minimum vertical spacing between two term labels in the gutter\n\ndelta_over_b = np.linspace(0.0, DELTA_MAX, 301)\ndq = -delta_over_b / 10.0\nones = np.ones_like(dq)\n\nfree_3f = -8.0 * ones  # ground term of the free d8 ion\nfree_3p = 7.0 * ones\nfree_1d = (-3.0 + 2 * C_OVER_B) * ones\nfree_1g = (4.0 + 2 * C_OVER_B) * ones\nfree_1s = (14.0 + 7 * C_OVER_B) * ones\nroot3 = np.sqrt(3.0)\nroot6 = np.sqrt(6.0)\n\nblocks = [\n    ([\"3A2g\"], [[free_3f + 12 * dq]]),\n    ([\"3T2g\"], [[free_3f + 2 * dq]]),\n    ([\"3T1g(F)\", \"3T1g(P)\"], [[free_3f - 6 * dq, 4 * dq], [4 * dq, free_3p]]),\n    ([\"1T1g(G)\"], [[free_1g + 2 * dq]]),\n    ([\"1A1g(G)\", \"1A1g(S)\"], [[free_1g + 4 * dq, 4 * root6 * dq], [4 * root6 * dq, free_1s]]),\n    ([\"1Eg(D)\", \"1Eg(G)\"], [[free_1d + 24 / 7 * dq, 40 * root3 / 7 * dq], [40 * root3 / 7 * dq, free_1g + 4 / 7 * dq]]),\n    (\n        [\"1T2g(D)\", \"1T2g(G)\"],\n        [[free_1d - 16 / 7 * dq, 20 * root3 / 7 * dq], [20 * root3 / 7 * dq, free_1g - 26 / 7 * dq]],\n    ),\n]\n\n# Diagonalizing each block in ascending order keeps terms of the same symmetry and\n# multiplicity from swapping identity, so their avoided crossings stay near-touches\nlevels = {}\nfor terms, block in blocks:\n    eigenvalues = np.linalg.eigvalsh(np.moveaxis(np.array(block), -1, 0))\n    for position, term in enumerate(terms):\n        levels[term] = eigenvalues[:, position]\n\n# Energies are measured from the ground term, which therefore runs flat along E/B = 0\nground = np.min(np.stack(list(levels.values())), axis=0)\ncurves = pd.concat(\n    [\n        pd.DataFrame(\n            {\n                \"delta_over_b\": delta_over_b,\n                \"energy_over_b\": energy - ground,\n                \"term\": term,\n                \"multiplicity\": \"Spin-allowed (ΔS = 0)\" if term.startswith(\"3\") else \"Spin-forbidden (ΔS ≠ 0)\",\n            }\n        )\n        for term, energy in levels.items()\n        if (energy - ground).max() <= ENERGY_MAX\n    ],\n    ignore_index=True,\n)\nspin_allowed = curves[curves[\"term\"].str.startswith(\"3\")]\nspin_forbidden = curves[curves[\"term\"].str.startswith(\"1\")]\n\n# Term labels sit in a gutter at the right edge, nudged apart where two curves converge\ntips = curves[curves[\"delta_over_b\"] == DELTA_MAX].sort_values(\"energy_over_b\").reset_index(drop=True)\nlabel_y = tips[\"energy_over_b\"].to_numpy(copy=True)\nfor position in range(1, len(label_y)):\n    label_y[position] = max(label_y[position], label_y[position - 1] + LABEL_GAP)\ntips[\"label_y\"] = label_y\ntips[\"label_x\"] = DELTA_MAX + 1.2\ntips[\"symbol\"] = tips[\"term\"].str.replace(r\"^(\\d)([A-Z])(\\d?)g\", r\"\\\\(^{\\1}\\2_{\\3g}\\\\)\", regex=True)\nclasses = [\"Spin-allowed (ΔS = 0)\", \"Spin-forbidden (ΔS ≠ 0)\"]\n\n# Horizontal grid drawn as segments that stop at the last sample, so no rule reaches into\n# the label gutter; the vertical grid and the y axis line stay with the theme\ny_breaks = [0, 20, 40, 60, 80, 100]\ngrid_y = pd.DataFrame({\"y\": y_breaks, \"x\": 0.0, \"xend\": DELTA_MAX})\npanel_top = label_y[-1] + 2.0\n\n# Plot\nplot = (\n    ggplot(mapping=aes(\"delta_over_b\", \"energy_over_b\"))\n    + geom_segment(aes(x=\"x\", y=\"y\", xend=\"xend\", yend=\"y\"), data=grid_y, color=GRID, size=0.5)\n    + geom_line(aes(color=\"multiplicity\", linetype=\"multiplicity\", group=\"term\"), data=spin_forbidden, size=0.9)\n    + geom_line(aes(color=\"multiplicity\", linetype=\"multiplicity\", group=\"term\"), data=spin_allowed, size=1.9)\n    + geom_segment(aes(xend=\"label_x\", yend=\"label_y\", color=\"multiplicity\"), data=tips, size=0.4, show_legend=False)\n    + geom_text(\n        aes(\"label_x\", \"label_y\", label=\"symbol\", color=\"multiplicity\"),\n        data=tips,\n        size=5.4,\n        hjust=0,\n        nudge_x=0.4,\n        show_legend=False,\n    )\n    + scale_color_manual(values=[SPIN_ALLOWED, SPIN_FORBIDDEN], breaks=classes)\n    + scale_linetype_manual(values=[\"solid\", \"dashed\"], breaks=classes)\n    + scale_x_continuous(limits=(0.0, 48.5), breaks=[0, 10, 20, 30, 40], expand=[0, 0.5])\n    + scale_y_continuous(limits=(0.0, panel_top), breaks=y_breaks, expand=[0, 2.6])\n    + labs(\n        title=\"line-tanabe-sugano · python · letsplot · anyplot.ai\",\n        subtitle=r\"\\(d^{8}\\) ion (\\(Ni^{2+}\\)) in an octahedral ligand field · \\(C/B\\) = 4.5\",\n        x=r\"Reduced ligand-field strength \\(\\Delta_{o}/B\\)\",\n        y=r\"Reduced term energy \\(E/B\\)\",\n    )\n    + ggsize(600, 600)\n    + theme_classic()\n    + theme(\n        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        panel_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        panel_grid_major_x=element_line(color=GRID, size=0.5),\n        panel_grid_major_y=element_blank(),\n        panel_grid_minor=element_blank(),\n        axis_line_x=element_blank(),\n        axis_line_y=element_line(color=INK_SOFT, size=1.0),\n        axis_ticks=element_blank(),\n        axis_title=element_text(size=12, color=INK),\n        axis_text=element_text(size=10, color=INK_SOFT),\n        plot_title=element_text(size=16, color=INK),\n        plot_subtitle=element_text(size=12, color=INK_SOFT),\n        legend_background=element_rect(fill=ELEVATED_BG, color=GRID),\n        legend_text=element_text(size=10, color=INK_SOFT),\n        legend_title=element_blank(),\n        legend_position=\"bottom\",\n    )\n)\n\n# Save\nggsave(plot, f\"plot-{THEME}.png\", scale=4, path=\".\")\nggsave(plot, f\"plot-{THEME}.html\", path=\".\")\n"}