Skip to content

Commit 435553a

Browse files
committed
Pythonized the normal_modes module - the function to generate an xyz string for NM visualization with py3Dmol; also updated the function for extraction of the normal modes from the dynamical matrix computed with QE
1 parent a953878 commit 435553a

File tree

2 files changed

+210
-132
lines changed

2 files changed

+210
-132
lines changed

src/libra_py/normal_modes.py

Lines changed: 58 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -792,62 +792,79 @@ def compute_dynmat(R, D, M, E, params):
792792
return w, w_inv_cm, U_a
793793

794794

795-
def get_xyz(E, R, M, U, mode):
796-
"""
797795

798-
This function returns a string in the xyz format with X, Y, Z and UX, UY, UZ
799-
where X,Y,Z are the coordinates, UX, UY, UZ - vectors coming from those coordinates - e.g. normal modes
796+
def normal2xyz(elements, R, M, U, mode, mass_weighted=True, comment="Normal mode"):
797+
"""
798+
Generate an XYZ-format string for visualizing a normal mode.
800799
801-
Args:
802-
E ( list of ndof/3 strings ): atom names (elements) of all atoms
803-
R ( MATRIX(ndof x nsteps-1) ): coordinates of all DOFs for all mid-timesteps
804-
M ( MATRIX(ndof x 1) ): masses of all DOFs
805-
U ( MATRIX(ndof x ndof) ): a matrix containing normal mode vectors
806-
mode ( int ): index of the normal mode that we want to visualize
800+
Each atom line contains:
801+
X, Y, Z : Cartesian coordinates
802+
UX, UY, UZ : displacement vector components of the selected normal mode
807803
808-
Returns:
809-
string: A string representing an xyz file
804+
Parameters
805+
----------
806+
elements : list of str, length = nat
807+
Atomic symbols (one per atom).
808+
R : np.ndarray, shape (3*nat,) or (nat, 3)
809+
Cartesian coordinates in Angstrom.
810+
M : np.ndarray or None, shape (3*nat,) or (nat, 3)
811+
Masses associated with each Cartesian degree of freedom.
812+
Required only if mass_weighted=True.
813+
U : np.ndarray, shape (3*nat, ndof)
814+
Normal-mode eigenvectors. Each column corresponds to one mode.
815+
mode : int
816+
Index of the normal mode to visualize (column index of U).
817+
mass_weighted : bool, optional
818+
If True, divide displacement vectors by sqrt(mass) for each DOF.
819+
Default is True.
820+
comment : str, optional
821+
Comment line written to the XYZ file.
810822
811-
"""
823+
Returns
824+
-------
825+
xyz_str : str
826+
String containing the XYZ-formatted data.
812827
813-
natoms = len(E)
814-
res = "%3i\nComment\n" % (natoms)
815828
816-
for i in range(0, natoms):
817-
x, y, z = R.get(3 * i, 0), R.get(3 * i + 1, 0), R.get(3 * i + 2, 0)
818-
ux = U.get(3 * i + 0, mode) / math.sqrt(M.get(3 * i + 0))
819-
uy = U.get(3 * i + 1, mode) / math.sqrt(M.get(3 * i + 1))
820-
uz = U.get(3 * i + 2, mode) / math.sqrt(M.get(3 * i + 2))
829+
Examples
830+
--------
831+
>>> normal2xyz(E, R, M, U, mode, mass_weighted=True)
832+
>>> normal2xyz(E, R, None, U, mode, mass_weighted=False)
821833
822-
res = res + "%s %5.3f %5.3f %5.3f %5.3f %5.3f %5.3f\n" % (E[i], x, y, z, ux, uy, uz)
834+
"""
823835

824-
return res
836+
elements = list(elements)
837+
nat = len(elements)
825838

839+
# --- reshape coordinates if needed ---
840+
R = np.asarray(R)
841+
if R.ndim == 1:
842+
R = R.reshape(nat, 3)
826843

827-
def get_xyz2(E, R, U, mode):
828-
"""
844+
# --- extract mode vector ---
845+
mode_vec = U[:, mode].reshape(nat, 3)
829846

830-
This function returns a string in the xyz format with X, Y, Z and UX, UY, UZ
831-
where X,Y,Z are the coordinates, UX, UY, UZ - vectors coming from those coordinates - e.g. normal modes
847+
# --- apply mass weighting if requested ---
848+
if mass_weighted:
849+
if M is None:
850+
raise ValueError("Mass array M must be provided when mass_weighted=True")
832851

833-
Args:
834-
E ( list of ndof/3 string ): atom names (elements) of all atoms
835-
R ( MATRIX(ndof x nsteps-1) ): coordinates of all DOFs for all mid-timesteps
836-
U ( MATRIX(ndof x ndof) ): a matrix containing normal mode vectors
837-
mode ( int ): index of the normal mode that we want to visualize
852+
M = np.asarray(M)
853+
if M.ndim == 1:
854+
M = M.reshape(nat, 3)
838855

839-
Returns:
840-
string: A string representing an xyz file
856+
mode_vec = mode_vec / np.sqrt(M)
841857

842-
"""
858+
# --- build XYZ string ---
859+
lines = [f"{nat}", comment]
843860

844-
natoms = len(E)
845-
res = "%3i\nComment\n" % (natoms)
861+
for elt, (x, y, z), (ux, uy, uz) in zip(elements, R, mode_vec):
862+
lines.append(
863+
f"{elt:2s} "
864+
f"{x:10.5f} {y:10.5f} {z:10.5f} "
865+
f"{ux:10.5f} {uy:10.5f} {uz:10.5f}"
866+
)
846867

847-
for i in range(0, natoms):
848-
x, y, z = R.get(3 * i, 0), R.get(3 * i + 1, 0), R.get(3 * i + 2, 0)
849-
ux, uy, uz = U.get(3 * i + 0, mode), U.get(3 * i + 1, mode), U.get(3 * i + 2, mode)
868+
return "\n".join(lines) + "\n"
850869

851-
res = res + "%s %5.3f %5.3f %5.3f %5.3f %5.3f %5.3f\n" % (E[i], x, y, z, ux, uy, uz)
852870

853-
return res

0 commit comments

Comments
 (0)