"""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())