Mod:Hunt Research Group/dipole moment atom coordinate

From wiki
Jump to navigation Jump to search

Code to Calculate the Dipole Moment from the Center of Mass (CoM), any Atom, or a given set of coordinates and produce xyz file with the dipole vectors

What the program does?
1. The script needs numpy and contextlib packages to run
2. The script can read a Gausslain log file, extract its coordinates, and calculate the center of mass (CoM) from those coordinates
3. Then it extracts ESP and NBO charges from the log file, determines the center of mass, and calculates dipole w.r.t the center of CoM
4. It saves multiple xyz files to the current directory with a dummy atom (X) with CoM coordinates, and dummy atoms X and Bq to show the dipole vector (from X to Bq)
5. It also prints some information on the screen, including the dipole moment calculated by Gaussian (same info is also exported as a .txt file)

Running the python Script

  • Running the script is very simple. Simply save the script as calculate_dipole_wrt_atom_or_coordinates_g16.py and run it as follows (considering your molecule file is test.log)
python3 calculate_dipole_wrt_atom_or_coordinates_g16.py test.log [-atom N] [-origin X Y Z]
  • -atom can be used to specify the atom number N
  • -origin can be used to specify the X Y Z coordinates as the origin of the dipole vector
  • if no argument is provided, it will use CoM by default as the dipole origin
  • It will produce the following files with their explanation in parentheses.
    • test_ESP_dipole.xyz (molecule with dummy atom (X) as center of vector tail and Bq to show dipole vector arrow w.r.t ESP)
    • test_NBO_dipole.xyz (molecule with dummy atom (X) as center of vector tail and Bq to show dipole vector arrow w.r.t NBO)

Below is the Script


#!/usr/bin/env python3
"""
Script to calculate the center of mass (CoM), center of charge (CoP for ESP and NBO charges) of a molecule from a log file,
then calculate the dipole moment from different charges, and write a new XYZ file with the CoM added as a pseudo-atom 'X'.
Script by Dr. Muhammad Ali Hashmi with the help of ChatGPT (29/10/2025)
Features:
1. Orientation fallback: Standard → Z-Matrix → Input
2. Option to define dipole origin using specific atom (-atom N, 1-based)
3. Option to define manual origin (-origin x y z)
4. Generates .xyz files with dummy atoms (X, Bq) representing dipole direction
   - X = origin
   - Bq = 6 Å along dipole vector
   - Appended to original atoms
   - Separate files for ESP and NBO dipoles
"""

import numpy as np
import os
import sys
from contextlib import redirect_stdout

# === Atomic weights dictionary ===
atomic_masses = {
"H" : 1.00794, "He": 4.002602, "Li": 6.941, "Be": 9.012182, "B": 10.811,
"C": 12.0107, "N": 14.0067, "O": 15.9994, "F": 18.9984032, "Ne": 20.1797,
"Na": 22.98976928, "Mg": 24.3050, "Al": 26.9815386, "Si": 28.0855,
"P": 30.973762, "S": 32.065, "Cl": 35.453, "Ar": 39.948, "K": 39.0983,
"Ca": 40.078, "Sc": 44.955912, "Ti": 47.867, "V": 50.9415, "Cr": 51.9961,
"Mn": 54.938045, "Fe": 55.845, "Co": 58.933195, "Ni": 58.6934, "Cu": 63.546,
"Zn": 65.38, "Ga": 69.723, "Ge": 72.64, "As": 74.92160, "Se": 78.96,
"Br": 79.904, "Kr": 83.798, "Rb": 85.4678, "Sr": 87.62, "Y": 88.90585,
"Zr": 91.224, "Nb": 92.90638, "Mo": 95.96, "Tc": 98.0, "Ru": 101.07,
"Rh": 102.90550, "Pd": 106.42, "Ag": 107.8682, "Cd": 112.411, "In": 114.818,
"Sn": 118.710, "Sb": 121.760, "Te": 127.60, "I": 126.90447, "Xe": 131.293,
"Cs": 132.9054519, "Ba": 137.327, "La": 138.90547, "Ce": 140.116,
"Pr": 140.90765, "Nd": 144.242, "Pm": 145.0, "Sm": 150.36, "Eu": 151.964,
"Gd": 157.25, "Tb": 158.92535, "Dy": 162.500, "Ho": 164.93032, "Er": 167.259,
"Tm": 168.93421, "Yb": 173.054, "Lu": 174.9668, "Hf": 178.49, "Ta": 180.94788,
"W": 183.84, "Re": 186.207, "Os": 190.23, "Ir": 192.217, "Pt": 195.084,
"Au": 196.966569, "Hg": 200.59, "Tl": 204.3833, "Pb": 207.2, "Bi": 208.98040,
"Po": 209.0, "At": 210.0, "Rn": 222.0, "Fr": 223.0, "Ra": 226.0, "Ac": 227.0,
"Th": 232.03806, "Pa": 231.03588, "U": 238.02891
}

atomic_symbols = {i: sym for i, sym in enumerate(atomic_masses.keys(), start=1)}

def is_float(s):
    try:
        float(s)
        return True
    except:
        return False


def extract_data_from_log(filename):
    with open(filename, 'r') as f:
        lines = f.readlines()

    atoms = []
    mol_charge = None
    esp_charges = []
    nbo_charges = []
    gaussian_dipole = []

    # --- Extract total charge ---
    for line in lines:
        if 'Charge =' in line and 'Multiplicity =' in line:
            mol_charge = int(float(line.split()[2]))

    # --- Orientation fallback ---
    orientations = ["Standard orientation:", "Z-Matrix orientation:", "Input orientation:"]
    found = None
    block = []

    for orient in orientations:
        blocks = []
        for i, line in enumerate(lines):
            if orient in line:
                tmp = []
                for j in range(i+5, len(lines)):
                    if '-----' in lines[j]:
                        break
                    tmp.append(lines[j].split())
                blocks.append(tmp)
        if blocks:
            block = blocks[-1]
            found = orient
            break

    if found:
        print(f'✅ Using "{found}" for coordinates.')
    else:
        print("❌ No orientation block found. Exiting.")
        sys.exit(1)

    atoms = [(atomic_symbols[int(a[1])], float(a[3]), float(a[4]), float(a[5])) for a in block]

    # --- ESP charges ---
    esp_section = False
    for line in lines:
        if 'ESP charges:' in line:
            esp_section = True
            continue
        if esp_section:
            if not line.strip() or 'Sum of ESP charges' in line:
                break
            parts = line.split()
            if len(parts) >= 3 and is_float(parts[2]):
                esp_charges.append(float(parts[2]))

    # --- NBO charges ---
    nbo_section = False
    for line in lines:
        if 'Summary of Natural Population Analysis:' in line:
            nbo_section = True
        elif nbo_section and '===' in line:
            break
        elif nbo_section and '----' not in line:
            parts = line.split()
            if len(parts) >= 3 and is_float(parts[2]):
                nbo_charges.append(float(parts[2]))

    # --- Gaussian dipole ---
    for i, line in enumerate(lines):
        if 'Dipole moment (field-independent basis, Debye):' in line:
            for j in range(i+1, i+5):
                if 'X=' in lines[j]:
                    parts = lines[j].split()
                    gaussian_dipole = [float(parts[1]), float(parts[3]), float(parts[5])]
                    break

    return atoms, mol_charge, gaussian_dipole, esp_charges, nbo_charges


def compute_center_of_mass(atoms):
    total_mass = sum(atomic_masses[a[0]] for a in atoms)
    weighted_sum = sum(atomic_masses[a[0]] * np.array(a[1:]) for a in atoms)
    return weighted_sum / total_mass


def compute_dipole(atoms, charges, origin):
    dipole = np.zeros(3)
    for i, (_, x, y, z) in enumerate(atoms):
        dipole += charges[i] * (np.array([x, y, z]) - origin)
    return dipole * 4.80320427


def write_xyz_with_dipole(filename, atoms, origin, dipole_vec, label):
    """Appends dummy atoms X (origin) and Bq (dipole head, 6Å away) and saves an XYZ."""
    dipole_dir = dipole_vec / np.linalg.norm(dipole_vec)
    Bq = origin + 6.0 * dipole_dir  # 6Å arrow length. You can change this value as per need
    X = origin

    xyz_lines = [f"{len(atoms) + 2}\n{label} dipole visualization (X,Bq added)\n"]
    for sym, x, y, z in atoms:
        xyz_lines.append(f"{sym:2s} {x:12.6f} {y:12.6f} {z:12.6f}\n")
    xyz_lines.append(f"X {X[0]:12.6f} {X[1]:12.6f} {X[2]:12.6f}\n")
    xyz_lines.append(f"Bq {Bq[0]:12.6f} {Bq[1]:12.6f} {Bq[2]:12.6f}\n")

    outname = os.path.splitext(filename)[0] + f"_{label}_dipole.xyz"
    with open(outname, 'w') as f:
        f.writelines(xyz_lines)
    print(f"💾 Saved dipole visualization: {outname}")


# === MAIN ===
if __name__ == "__main__":
    if len(sys.argv) < 2:
        print("Usage: python3 script.py file.log [-atom N] [-origin X Y Z]")
        sys.exit(1)

    filename = sys.argv[1]
    atom_origin = None
    manual_origin = None

    if "-atom" in sys.argv:
        try:
            atom_origin = int(sys.argv[sys.argv.index("-atom") + 1])
        except:
            print("⚠️ Invalid atom number after -atom.")
            sys.exit(1)

    if "-origin" in sys.argv:
        try:
            i = sys.argv.index("-origin")
            manual_origin = np.array([float(sys.argv[i+1]), float(sys.argv[i+2]), float(sys.argv[i+3])])
        except:
            print("⚠️ Invalid coordinates after -origin (need X Y Z).")
            sys.exit(1)

    atoms, mol_charge, g_dipole, esp, nbo = extract_data_from_log(filename)

    # Determine origin
    if manual_origin is not None:
        origin = manual_origin
        print(f"✅ Using manual origin: {origin}")
    elif atom_origin is not None:
        if 1 <= atom_origin <= len(atoms):
            atom = atoms[atom_origin - 1]
            origin = np.array(atom[1:])
            print(f"✅ Using Atom #{atom_origin} ({atom[0]}) as origin: {origin}")
        else:
            print("⚠️ Atom index out of range.")
            sys.exit(1)
    else:
        origin = compute_center_of_mass(atoms)
        print(f"✅ Using center of mass as origin: {origin}")

    # Compute and save dipoles
    if esp:
        mu_esp = compute_dipole(atoms, esp, origin)
        print(f"ESP Dipole (wrt origin): {mu_esp}  |μ|={np.linalg.norm(mu_esp):.4f}")
        write_xyz_with_dipole(filename, atoms, origin, mu_esp, "ESP")
    else:
        print("⚠️ ESP charges not found.")

    if nbo:
        mu_nbo = compute_dipole(atoms, nbo, origin)
        print(f"NBO Dipole (wrt origin): {mu_nbo}  |μ|={np.linalg.norm(mu_nbo):.4f}")
        write_xyz_with_dipole(filename, atoms, origin, mu_nbo, "NBO")
    else:
        print("⚠️ NBO charges not found.")