Source code for crystod.ligand_field

"""
Ligand-field splitting workflow for crystod.

Decomposes an atomic orbital (s, p, d, f, ...) into the irreps of a point
group, i.e. the crystal-field / ligand-field splitting of the orbital in the
given point-symmetric environment. Based on script/ligand_field_spliting.py
by Hiroki Koiso (2023).
"""

from __future__ import annotations

import re
from argparse import (
    ArgumentDefaultsHelpFormatter,
    ArgumentParser,
    RawDescriptionHelpFormatter,
    RawTextHelpFormatter,
)

import numpy as np

from .decompose_irrep import decompose, get_character_table


class MyHelpFormatter(
    RawTextHelpFormatter,
    RawDescriptionHelpFormatter,
    ArgumentDefaultsHelpFormatter,
):
    pass


desc = """
Calculate the ligand-field splitting of an atomic orbital in a point-symmetric
field: the (2l+1)-dimensional orbital representation is decomposed into the
irreps of the selected point group.

# Command Examples:
crystod-group --ligand-field d --point-group 4/mmm
crystod-group --ligand-field f --point-group m-3m
"""

#: Azimuthal quantum number ``l`` of every orbital letter accepted by
#: ``--ligand-field`` and ``--multiplet --orbital``.
ORBITAL_AZIMUTHAL_NUMBER = {"s": 0, "p": 1, "d": 2, "f": 3, "g": 4, "h": 5, "i": 6}


def build_parser() -> ArgumentParser:
    parser = ArgumentParser(description=desc, formatter_class=MyHelpFormatter)
    parser.add_argument(
        "--point-group",
        "-pg",
        dest="point_group",
        required=True,
        type=str,
        help="Point group, e.g. 4/mmm.",
    )
    parser.add_argument(
        "--orbital",
        required=True,
        type=str,
        help=f"Orbital: one of {', '.join(ORBITAL_AZIMUTHAL_NUMBER)}.",
    )
    return parser


def _rotation_order(label: str) -> float:
    """Rotation order n from a class label such as C4, S6, C2', C2''."""
    digits = [float(value) for value in re.findall(r"\d", label)]
    order = digits[0]
    for value in digits[1:]:
        order /= value
    return order


[docs] def get_orbital_characters(orbital: str, character_table: dict) -> dict[str, int]: """Characters of the (2l+1)-dimensional representation of an atomic orbital. The reducible representation that ``crystod-group --ligand-field ORB --pg PG`` decomposes into irreps (the crystal-field / ligand-field splitting of the orbital). Uses the angular-momentum character formulas ``chi(C(a)) = sin((l+1/2)a)/sin(a/2)``, ``chi(S(a)) = cos((l+1/2)a)/cos(a/2)``, ``chi(E) = 2l+1``, ``chi(i) = (-1)^l (2l+1)`` and ``chi(sigma) = 1``. Args: orbital: Orbital letter, one of ``s``, ``p``, ``d``, ``f``, ``g``, ``h``, ``i`` (``ORBITAL_AZIMUTHAL_NUMBER`` maps it to ``l``). character_table: Table from ``crystod.group.get_character_table``. Returns: ``{class label: character}`` in the order of ``character_table["rotation_list"]``; pass ``list(result.values())`` to ``crystod.group.decompose`` for the splitting. Raises: KeyError: Unknown orbital letter. ValueError: A class label the formulas do not recognize. Example: >>> from crystod import group >>> ct = group.get_character_table("m-3m") >>> chi = group.get_orbital_characters("d", ct) >>> sizes = [len(ops) for ops in ct["mapping_table"].values()] >>> counts = group.decompose(list(chi.values()), ct, sizes) >>> {name: n for name, n in counts.items() if n} {'Eg': 1, 'T2g': 1} """ l = ORBITAL_AZIMUTHAL_NUMBER[orbital] characters: dict[str, int] = {} for rotation in character_table["rotation_list"]: if rotation == "E": character = 2 * l + 1 elif rotation == "i": character = (-1) ** l * (2 * l + 1) elif "sg" in rotation: # mirror plane: chi = 1 for every l character = (-1) ** l * np.sin((l + 0.5) * np.pi) / np.sin(np.pi / 2) else: alpha = 2 * np.pi / _rotation_order(rotation) if "C" in rotation: character = np.sin((l + 0.5) * alpha) / np.sin(alpha / 2) elif "S" in rotation: character = np.cos((l + 0.5) * alpha) / np.cos(alpha / 2) else: raise ValueError(f"Unrecognized symmetry-operation label '{rotation}'.") characters[rotation] = round(float(character)) return characters
def main(argv: list[str] | None = None) -> None: args = build_parser().parse_args(argv) orbital = args.orbital.strip().lower() if orbital not in ORBITAL_AZIMUTHAL_NUMBER: raise SystemExit( f"ERROR: orbital '{args.orbital}' is not supported. " f"Choose from: {', '.join(ORBITAL_AZIMUTHAL_NUMBER)}" ) character_table = get_character_table(args.point_group) orbital_characters = get_orbital_characters(orbital, character_table) multiplicities = [np.array(ops).shape[0] for ops in character_table["mapping_table"].values()] print(f"\n* Point group *\n{args.point_group}\n") print(f"* Orbital *\n{orbital}\n") print( f"* Reducible representation of the {orbital} orbital " f"in the {args.point_group} field *\n" ) for (rotation, character), multiplicity in zip(orbital_characters.items(), multiplicities): print(f"{multiplicity}{rotation}: {character}") results = decompose(list(orbital_characters.values()), character_table, multiplicities) result = " + ".join(f"{count}({irrep})" for irrep, count in results.items() if count > 0) print("\n* Result *") print(result) print() if __name__ == "__main__": main()