Source code for mag4.cli.bond_angles

"""CLI: M – L – M bridging-angle analysis from a CIF."""

from __future__ import annotations

import argparse
import sys

from pymatgen.core import Structure

from mag4.analysis.bond_angles import (
    compress_angles,
    find_bridging_angles,
    get_mm_pairs,
    group_into_shells,
    print_angles_for_shell,
    print_header,
    print_shell_summary,
    write_report,
)
from mag4.constants import DIST_EQUIV_TOL


[docs] def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( prog="mag4-angles", description="Compute M-L-M bond angles for each J coupling from a CIF.", formatter_class=argparse.RawDescriptionHelpFormatter, epilog="""\ Examples: mag4-angles --cif Gd2S3.cif --element Gd --ligand S mag4-angles --cif Gd2S3.cif --element Gd --ligand S,O mag4-angles --cif Gd2S3.cif --element Gd # auto-detect ligands mag4-angles --cif Gd2S3.cif --element Gd --ligand S --bond-cutoff 3.5 """, ) parser.add_argument("--cif", required=True, help="CIF file path") parser.add_argument("--element", required=True, help="Magnetic element symbol (e.g. Gd)") parser.add_argument("--ligand", default=None, help="Bridging ligand element(s), comma-separated " "(e.g. S or S,O). Default: all non-magnetic elements") parser.add_argument("--cutoff", type=float, default=7.0, help="M-M distance cutoff in Å (default: 7.0)") parser.add_argument("--bond-cutoff", type=float, default=3.5, help="M-L bond distance cutoff in Å (default: 3.5)") parser.add_argument("--equiv-tol", type=float, default=DIST_EQUIV_TOL, help=f"Tolerance for grouping M-M distances " f"(default: {DIST_EQUIV_TOL})") parser.add_argument("--output", type=str, default=None, help="Output file path (default: bond_angles_report.dat)") return parser
[docs] def main(argv=None) -> int: parser = build_parser() args = parser.parse_args(argv) structure = Structure.from_file(args.cif) element = args.element all_elements = sorted({s.specie.symbol for s in structure}) if args.ligand: ligand_elements = [x.strip() for x in args.ligand.split(",")] for lig in ligand_elements: if lig not in all_elements: print(f"WARNING: ligand '{lig}' not found in structure. " f"Available: {all_elements}") else: ligand_elements = [el for el in all_elements if el != element] print(f"Auto-detected ligand element(s): {', '.join(ligand_elements)}") print_header(structure, element, ligand_elements, args.cutoff, args.bond_cutoff) results = get_mm_pairs(structure, element, args.cutoff) if not results: print(f"No {element}-{element} pairs found within {args.cutoff} Å.") return 0 groups = group_into_shells(results, args.equiv_tol) print_shell_summary(groups, element) lines_out = [] lines_out.append(f"BOND-ANGLE ANALYSIS: {element} – L – {element}") lines_out.append(f"Bridging ligands: {', '.join(ligand_elements)}") lines_out.append(f"Structure: {structure.composition.reduced_formula}") lines_out.append(f"M-M cutoff: {args.cutoff} Å " f"M-L bond cutoff: {args.bond_cutoff} Å") lines_out.append("") for group in groups: print_angles_for_shell(group, structure, element, ligand_elements, args.bond_cutoff, lines_out) W = 72 summary_header = (f"\n{'=' * W}\n" f" SUMMARY: bridging angles per coupling (sorted)\n" f"{'=' * W}") print(summary_header) lines_out.append(summary_header) for group in groups: all_angles_in_shell = [] for pair in group["pairs"]: angles = find_bridging_angles( structure, pair["site_i"], pair["site_j"], pair["image"], ligand_elements, args.bond_cutoff) all_angles_in_shell.extend([a["angle"] for a in angles]) all_angles_in_shell.sort() n_bridges = len(all_angles_in_shell) line = (f"\n {group['label']} (d = {group['distance']:.4f} Å, " f"{group['multiplicity']} pair(s), {n_bridges} bridge(s)):") print(line) lines_out.append(line) if n_bridges > 0: ang_str = " " + compress_angles(all_angles_in_shell) print(ang_str) lines_out.append(ang_str) else: nobridge = " (no bridging paths found)" print(nobridge) lines_out.append(nobridge) print() print("=" * W) lines_out.append("") lines_out.append("=" * W) print() output_file = args.output or "bond_angles_report.dat" write_report(lines_out, output_file) return 0
if __name__ == "__main__": # pragma: no cover sys.exit(main())