scripts/structure_analyzer.py

#!/usr/bin/env python3
"""
Structure analysis tool using pymatgen.

Analyzes crystal structures and provides comprehensive information including:
- Composition and formula
- Space group and symmetry
- Lattice parameters
- Density
- Coordination environment
- Bond lengths and angles

Usage:
    python structure_analyzer.py structure_file [options]

Examples:
    python structure_analyzer.py POSCAR
    python structure_analyzer.py structure.cif --symmetry --neighbors
    python structure_analyzer.py POSCAR --export json
"""

import argparse
import json
import sys
from pathlib import Path

try:
    from pymatgen.core import Structure
    from pymatgen.symmetry.analyzer import SpacegroupAnalyzer
    from pymatgen.analysis.local_env import CrystalNN
except ImportError:
    print("Error: pymatgen is not installed. Install with: pip install pymatgen")
    sys.exit(1)


def analyze_structure(struct: Structure, args) -> dict:
    """
    Perform comprehensive structure analysis.

    Args:
        struct: Pymatgen Structure object
        args: Command line arguments

    Returns:
        Dictionary containing analysis results
    """
    results = {}

    # Basic information
    print("\n" + "="*60)
    print("STRUCTURE ANALYSIS")
    print("="*60)

    print("\n--- COMPOSITION ---")
    print(f"Formula (reduced):    {struct.composition.reduced_formula}")
    print(f"Formula (full):       {struct.composition.formula}")
    print(f"Formula (Hill):       {struct.composition.hill_formula}")
    print(f"Chemical system:      {struct.composition.chemical_system}")
    print(f"Number of sites:      {len(struct)}")
    print(f"Number of species:    {len(struct.composition.elements)}")
    print(f"Molecular weight:     {struct.composition.weight:.2f} amu")

    results['composition'] = {
        'reduced_formula': struct.composition.reduced_formula,
        'formula': struct.composition.formula,
        'hill_formula': struct.composition.hill_formula,
        'chemical_system': struct.composition.chemical_system,
        'num_sites': len(struct),
        'molecular_weight': struct.composition.weight,
    }

    # Lattice information
    print("\n--- LATTICE ---")
    print(f"a = {struct.lattice.a:.4f} Å")
    print(f"b = {struct.lattice.b:.4f} Å")
    print(f"c = {struct.lattice.c:.4f} Å")
    print(f"α = {struct.lattice.alpha:.2f}°")
    print(f"β = {struct.lattice.beta:.2f}°")
    print(f"γ = {struct.lattice.gamma:.2f}°")
    print(f"Volume:               {struct.volume:.2f} ų")
    print(f"Density:              {struct.density:.3f} g/cm³")

    results['lattice'] = {
        'a': struct.lattice.a,
        'b': struct.lattice.b,
        'c': struct.lattice.c,
        'alpha': struct.lattice.alpha,
        'beta': struct.lattice.beta,
        'gamma': struct.lattice.gamma,
        'volume': struct.volume,
        'density': struct.density,
    }

    # Symmetry analysis
    if args.symmetry:
        print("\n--- SYMMETRY ---")
        try:
            sga = SpacegroupAnalyzer(struct)

            spacegroup_symbol = sga.get_space_group_symbol()
            spacegroup_number = sga.get_space_group_number()
            crystal_system = sga.get_crystal_system()
            point_group = sga.get_point_group_symbol()

            print(f"Space group:          {spacegroup_symbol} (#{spacegroup_number})")
            print(f"Crystal system:       {crystal_system}")
            print(f"Point group:          {point_group}")

            # Get symmetry operations
            symm_ops = sga.get_symmetry_operations()
            print(f"Symmetry operations:  {len(symm_ops)}")

            results['symmetry'] = {
                'spacegroup_symbol': spacegroup_symbol,
                'spacegroup_number': spacegroup_number,
                'crystal_system': crystal_system,
                'point_group': point_group,
                'num_symmetry_ops': len(symm_ops),
            }

            # Show equivalent sites
            sym_struct = sga.get_symmetrized_structure()
            print(f"Symmetry-equivalent site groups: {len(sym_struct.equivalent_sites)}")

        except Exception as e:
            print(f"Could not determine symmetry: {e}")

    # Site information
    print("\n--- SITES ---")
    print(f"{'Index':<6} {'Species':<10} {'Wyckoff':<10} {'Frac Coords':<30}")
    print("-" * 60)

    for i, site in enumerate(struct):
        coords_str = f"[{site.frac_coords[0]:.4f}, {site.frac_coords[1]:.4f}, {site.frac_coords[2]:.4f}]"
        wyckoff = "N/A"

        if args.symmetry:
            try:
                sga = SpacegroupAnalyzer(struct)
                sym_struct = sga.get_symmetrized_structure()
                wyckoff = sym_struct.equivalent_sites[0][0].species_string  # Simplified
            except:
                pass

        print(f"{i:<6} {site.species_string:<10} {wyckoff:<10} {coords_str:<30}")

    # Neighbor analysis
    if args.neighbors:
        print("\n--- COORDINATION ENVIRONMENT ---")
        try:
            cnn = CrystalNN()

            for i, site in enumerate(struct):
                neighbors = cnn.get_nn_info(struct, i)
                print(f"\nSite {i} ({site.species_string}):")
                print(f"  Coordination number: {len(neighbors)}")

                if len(neighbors) > 0 and len(neighbors) <= 12:
                    print(f"  Neighbors:")
                    for j, neighbor in enumerate(neighbors):
                        neighbor_site = struct[neighbor['site_index']]
                        distance = site.distance(neighbor_site)
                        print(f"    {neighbor_site.species_string} at {distance:.3f} Å")

        except Exception as e:
            print(f"Could not analyze coordination: {e}")

    # Distance matrix (for small structures)
    if args.distances and len(struct) <= 20:
        print("\n--- DISTANCE MATRIX (Å) ---")
        distance_matrix = struct.distance_matrix

        # Print header
        print(f"{'':>4}", end="")
        for i in range(len(struct)):
            print(f"{i:>8}", end="")
        print()

        # Print matrix
        for i in range(len(struct)):
            print(f"{i:>4}", end="")
            for j in range(len(struct)):
                if i == j:
                    print(f"{'---':>8}", end="")
                else:
                    print(f"{distance_matrix[i][j]:>8.3f}", end="")
            print()

    print("\n" + "="*60)

    return results


def main():
    parser = argparse.ArgumentParser(
        description="Analyze crystal structures using pymatgen",
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )

    parser.add_argument(
        "structure_file",
        help="Structure file to analyze (CIF, POSCAR, etc.)"
    )

    parser.add_argument(
        "--symmetry", "-s",
        action="store_true",
        help="Perform symmetry analysis"
    )

    parser.add_argument(
        "--neighbors", "-n",
        action="store_true",
        help="Analyze coordination environment"
    )

    parser.add_argument(
        "--distances", "-d",
        action="store_true",
        help="Show distance matrix (for structures with ≤20 atoms)"
    )

    parser.add_argument(
        "--export", "-e",
        choices=["json", "yaml"],
        help="Export analysis results to file"
    )

    parser.add_argument(
        "--output", "-o",
        help="Output file for exported results"
    )

    args = parser.parse_args()

    # Read structure
    try:
        struct = Structure.from_file(args.structure_file)
    except Exception as e:
        print(f"Error reading structure file: {e}")
        sys.exit(1)

    # Analyze structure
    results = analyze_structure(struct, args)

    # Export results
    if args.export:
        output_file = args.output or f"analysis.{args.export}"

        if args.export == "json":
            with open(output_file, "w") as f:
                json.dump(results, f, indent=2)
            print(f"\n✓ Analysis exported to {output_file}")

        elif args.export == "yaml":
            try:
                import yaml
                with open(output_file, "w") as f:
                    yaml.dump(results, f, default_flow_style=False)
                print(f"\n✓ Analysis exported to {output_file}")
            except ImportError:
                print("Error: PyYAML is not installed. Install with: pip install pyyaml")


if __name__ == "__main__":
    main()
← Back to pymatgen