{"spec_id":"line-tanabe-sugano","library":"pygal","language":"python","code":"\"\"\" anyplot.ai\nline-tanabe-sugano: Tanabe-Sugano Diagram for Crystal Field Theory\nLibrary: pygal 3.1.3 | Python 3.13.15\nQuality: 88/100 | Created: 2026-10-01\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pygal\nfrom pygal.style import Style\n\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\"\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)\"\n\n# Imprint palette — position 1 (#009E73) is always the first series\nIMPRINT_PALETTE = (\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\")\n\n# Data — d³ (Cr³⁺) in an octahedral field, every energy in units of the Racah\n# parameter B, at the textbook ratio C/B = 4.5. The field strength enters as\n# Dq/B, with the octahedral splitting Δ_o = 10 Dq.\nracah_c = 4.5\ndelta_over_b = np.linspace(0.0, 40.0, 241)\ndq = delta_over_b / 10.0\nsqrt2 = np.sqrt(2.0)\nsqrt3 = np.sqrt(3.0)\n\n# The ⁴A₂g(t₂g³) ground term; every curve is referenced to it, so it is flat at 0\nground_energy = -12 * dq - 15\n\n# Tanabe-Sugano matrices for d³ (Sugano, Tanabe & Kamimura), one symmetry block\n# per entry as its diagonal elements plus the upper-triangle couplings\nts_blocks = {\n    \"quartet_t1\": ([-2 * dq - 3, 8 * dq - 12], {(0, 1): 6}),\n    \"doublet_e\": (\n        [-12 * dq - 6 + 3 * racah_c, -2 * dq + 8 + 6 * racah_c, -2 * dq - 1 + 3 * racah_c, 18 * dq - 8 + 4 * racah_c],\n        {(0, 1): -6 * sqrt2, (0, 2): -3 * sqrt2, (1, 2): 10, (1, 3): sqrt3 * (2 + racah_c), (2, 3): 2 * sqrt3},\n    ),\n    \"doublet_t1\": (\n        [\n            -12 * dq - 6 + 3 * racah_c,\n            -2 * dq + 3 * racah_c,\n            -2 * dq - 6 + 3 * racah_c,\n            8 * dq - 6 + 3 * racah_c,\n            8 * dq - 2 + 3 * racah_c,\n        ],\n        {\n            (0, 1): -3,\n            (0, 2): 3,\n            (0, 4): -2 * sqrt3,\n            (1, 2): -3,\n            (1, 3): 3,\n            (1, 4): 3 * sqrt3,\n            (2, 3): -3,\n            (2, 4): -sqrt3,\n            (3, 4): 2 * sqrt3,\n        },\n    ),\n    \"doublet_t2\": (\n        [\n            -12 * dq + 5 * racah_c,\n            -2 * dq - 6 + 3 * racah_c,\n            -2 * dq + 4 + 3 * racah_c,\n            8 * dq + 6 + 5 * racah_c,\n            8 * dq - 2 + 3 * racah_c,\n        ],\n        {\n            (0, 1): -3 * sqrt3,\n            (0, 2): -5 * sqrt3,\n            (0, 3): 4 + 2 * racah_c,\n            (0, 4): 2,\n            (1, 2): 3,\n            (1, 3): -3 * sqrt3,\n            (1, 4): -3 * sqrt3,\n            (2, 3): -sqrt3,\n            (2, 4): sqrt3,\n            (3, 4): 10,\n        },\n    ),\n}\n\n# Diagonalizing each block at every field strength keeps terms of the same symmetry\n# and multiplicity as avoided crossings instead of letting them cross\nterm_energies = {}\nfor block_name, (diagonal, couplings) in ts_blocks.items():\n    block = np.zeros((len(dq), len(diagonal), len(diagonal)))\n    for i, element in enumerate(diagonal):\n        block[:, i, i] = element\n    for (i, j), element in couplings.items():\n        block[:, i, j] = block[:, j, i] = element\n    term_energies[block_name] = np.linalg.eigvalsh(block) - ground_energy[:, None]\n\n# Term symbol, curve, spin-allowed flag (same multiplicity as ⁴A₂g) and the field\n# strength the term label is anchored to. Unicode stands in for sub/superscripts:\n# pygal renders plain SVG text with no markup, so the g stays on the baseline.\nterm_curves = [\n    (\"⁴A₂g\", np.zeros_like(dq), True, 40.0),\n    (\"⁴T₂g\", -2 * dq - 15 - ground_energy, True, 40.0),\n    (\"⁴T₁g(F)\", term_energies[\"quartet_t1\"][:, 0], True, 40.0),\n    (\"⁴T₁g(P)\", term_energies[\"quartet_t1\"][:, 1], True, 40.0),\n    (\"²Eg\", term_energies[\"doublet_e\"][:, 0], False, 30.0),\n    (\"²T₁g\", term_energies[\"doublet_t1\"][:, 0], False, 40.0),\n    (\"²T₂g\", term_energies[\"doublet_t2\"][:, 0], False, 40.0),\n    (\"²A₁g\", -2 * dq - 11 + 3 * racah_c - ground_energy, False, 40.0),\n]\n\n# pygal pins stroke-width on every reactive path, hard-codes black label text and drops\n# every curve label below its anchor point, so the thin spin-forbidden lines, the\n# theme-adaptive label color and the flat ground term's label need one more sheet: at\n# E/B = 0 that drop would land ⁴A₂g inside the x-tick row, hence the lift\nspin_forbidden_lines = \",\".join(\n    f\".serie-{i} .line\" for i, (_, _, spin_allowed, _) in enumerate(term_curves) if not spin_allowed\n)\nground_label_lift = \".text-overlay .serie-0 text.label{transform:translate(0px,-100px)}\"\nextra_css = (\n    f\"inline:{spin_forbidden_lines}{{stroke-width:5 !important}}\"\n    f\".text-overlay text{{fill:{INK} !important}}{ground_label_lift}\"\n)\n\n# Plot — square canvas, since the diagram reads best on a near-square plot area\ntitle = \"Cr³⁺ d³, C/B = 4.5 · line-tanabe-sugano · python · pygal · anyplot.ai\"\ncustom_style = Style(\n    background=PAGE_BG,\n    plot_background=PAGE_BG,\n    foreground=INK,\n    foreground_strong=INK,\n    foreground_subtle=INK_SOFT,\n    guide_stroke_color=GRID,\n    major_guide_stroke_color=GRID,\n    guide_stroke_dasharray=\"none\",\n    major_guide_stroke_dasharray=\"none\",\n    colors=IMPRINT_PALETTE,\n    font_family=\"DejaVu Sans, sans-serif\",\n    title_font_size=50,\n    label_font_size=44,\n    major_label_font_size=44,\n    value_font_size=44,\n    value_label_font_size=46,\n    stroke_opacity=\"1\",\n    stroke_width=11,\n)\n\nchart = pygal.XY(\n    # pygal pins the title at title_font_size + spacing from the canvas top, so spacing\n    # — not margin_top — is what buys air above it; the margins then centre the plot area\n    spacing=45,\n    width=2400,\n    height=2400,\n    style=custom_style,\n    css=[\"file://style.css\", \"file://graph.css\", extra_css],\n    title=title,\n    x_title=\"Δₒ / B  (reduced ligand-field strength)\",\n    y_title=\"E / B  (reduced term energy)\",\n    xrange=(0, 40),\n    range=(0, 90),\n    x_labels=[0, 5, 10, 15, 20, 25, 30, 35, 40],\n    y_labels=[0, 10, 20, 30, 40, 50, 60, 70, 80, 90],\n    show_x_guides=True,\n    show_y_guides=True,\n    show_legend=False,\n    dots_size=0,\n    print_labels=True,\n    print_values=False,\n    margin_top=130,\n    margin_bottom=70,\n    margin_left=180,\n    margin_right=300,\n)\n\n# Style — spin-allowed terms thick and solid, the weak spin-forbidden ones thin and\n# dashed; each term symbol is printed next to the sample its own curve passes through\nfor term, energy, spin_allowed, label_at in term_curves:\n    points = list(zip(delta_over_b.tolist(), energy.tolist(), strict=True))\n    anchor = int(np.argmin(np.abs(delta_over_b - label_at)))\n    points[anchor] = {\"value\": points[anchor], \"label\": term}\n    chart.add(term, points, stroke_style=None if spin_allowed else {\"dasharray\": \"26, 20\"})\n\n# Save\nchart.render_to_png(f\"plot-{THEME}.png\")\nchart.render_to_file(f\"plot-{THEME}.html\")\n"}