qrotor.rotation

Description

This submodule contains tools to rotate molecular structures. Works with Quantum ESPRESSO input files.

Index

rotate_qe() Rotate specific atoms from a Quantum ESPRESSO input file
rotate_coords() Rotate a specific list of coordinates

  1"""
  2# Description
  3
  4This submodule contains tools to rotate molecular structures.
  5Works with Quantum ESPRESSO input files.
  6
  7
  8# Index
  9
 10| | |
 11| --- | --- |
 12| `rotate_qe()`     | Rotate specific atoms from a Quantum ESPRESSO input file |
 13| `rotate_coords()` | Rotate a specific list of coordinates |
 14
 15---
 16"""
 17
 18
 19import numpy as np
 20import os
 21import shutil
 22from scipy.spatial.transform import Rotation
 23from .constants import *
 24import aton.api as api
 25import aton.txt.extract as extract
 26import aton.txt.edit as edit
 27
 28
 29def rotate_qe(
 30        filepath:str,
 31        positions:list,
 32        angle:float,
 33        repeat:bool=False,
 34        precision:int=3,
 35        use_centroid:bool=True,
 36        show_axis:bool=False,
 37    ) -> list:
 38    """Rotates atoms from a Quantum ESPRESSO pw.x input file.
 39
 40    Takes a `filepath` with a molecular structure, and three or more atomic `positions` (list).
 41    These input positions can be approximate, and are used to identify the target atoms.
 42    The decimal precision in the search for these positions is controlled by `precision`.
 43
 44    It rotates the atoms by a specific `angle` in degrees.
 45    Additionally, if `repeat = True` it repeats the same rotation over the whole circunference.
 46    Finally, it writes the rotated structure(s) to a new structural file(s).
 47    Returns a list with the output filename(s).
 48
 49    By default, the rotation axis is defined by the perpendicular vector
 50    passing through the geometrical center of the first three points.
 51    To override this and instead use the vector between the first two atoms
 52    as the rotation axis, set `use_centroid = False`.
 53
 54    **WARNING: The `positions` list is order-sensitive**.
 55    If you rotate more than one chemical group in a structure,
 56    be sure to follow the same direction for each group (e.g. all clockwise)
 57    to ensure that all axes of rotation point in the same direction.
 58
 59    To debug, `show_axis = True` adds two additional helium atoms as the rotation vector.
 60
 61    The resulting rotational potential can be compiled to a CSV file with `qrotor.potential.from_qe()`.
 62    """
 63    print('Rotating Quantum ESPRESSO input structure with QRotor...')
 64    if len(positions) < 3:
 65        raise ValueError("At least three positions are required to define the rotation axis.")
 66    lines = []
 67    full_positions = []
 68    for position in positions:
 69        line = api.pwx.get_atom(filepath=filepath, position=position, precision=precision, literal=True)
 70        lines.append(line)
 71        pos = extract.coords(line)
 72        if len(pos) > 3:  # Keep only the first three coordinates
 73            pos = pos[:3]
 74        # Convert to cartesian
 75        pos_cartesian = api.pwx.to_cartesian(filepath, pos)
 76        full_positions.append(pos_cartesian)
 77        print(f'Found atom: "{line}"')
 78    # Set the angles to rotate
 79    if not repeat:
 80        angles = [angle]
 81    else:
 82        angles = range(0, 360, angle)
 83    # Rotate and save the structure
 84    outputs = []
 85    path = os.path.dirname(filepath)
 86    basename = os.path.basename(filepath)
 87    name, ext = os.path.splitext(basename)
 88    print('Rotating the structure...')
 89    for angle in angles:
 90        output_name = name + f'_{angle}' + ext
 91        output = os.path.join(path, output_name)
 92        rotated_positions_cartesian = rotate_coords(full_positions, angle, use_centroid, show_axis)
 93        rotated_positions = []
 94        for coord in rotated_positions_cartesian:
 95            pos = api.pwx.from_cartesian(filepath, coord)
 96            rotated_positions.append(pos)
 97        _save_qe(filepath, output, lines, rotated_positions)
 98        outputs.append(output)
 99        print(output)
100    return outputs
101
102
103def _save_qe(
104        filename,
105        output:str,
106        lines:list,
107        positions:list
108    ) -> str:
109    """Copies `filename` to `output`, updating the old `lines` with the new `positions`.
110    
111    The angle will be appended at the end of the input prefix to avoid overlapping calculations.
112    """
113    shutil.copy(filename, output)
114    for i, line in enumerate(lines):
115        strings = line.split()
116        atom = strings[0]
117        new_line = f"  {atom}   {positions[i][0]:.15f}   {positions[i][1]:.15f}   {positions[i][2]:.15f}"
118        #print(f'OLD LINE: {line}')  # DEBUG
119        #print(f'NEW_LINE: {new_line}')  # DEBUG
120        edit.replace_line(output, line, new_line, raise_errors=True)
121    if len(lines) + 2 == len(positions):  # In case show_axis=True
122        additional_positions = positions[-2:]
123        for pos in additional_positions:
124            pos.insert(0, 'He')
125            api.pwx.add_atom(output, pos)
126    elif len(lines) != len(positions):
127        raise ValueError(f"What?!  len(lines)={len(lines)} and len(positions)={len(positions)}")
128    # Add angle to calculation prefix
129    output_name = os.path.basename(output)
130    splits = output_name.split('_')
131    angle_str = splits[-1].replace('.in', '')
132    prefix = ''
133    content = api.pwx.read_in(output)
134    if 'prefix' in content.keys():
135        prefix = content['prefix']
136        prefix = prefix.strip("'")
137    prefix = "'" + prefix + angle_str + "'"
138    api.pwx.set_value(output, 'prefix', prefix)
139    return output
140
141
142def rotate_coords(
143        positions:list,
144        angle:float,
145        use_centroid:bool=True,
146        show_axis:bool=False,
147    ) -> list:
148    """Rotates geometrical coordinates.
149
150    Takes a list of atomic `positions` in cartesian coordinates, as
151    `[[x1,y1,z1], [x2,y2,z2], [x3,y3,z3], [etc]`.
152    Then rotates said coordinates by a given `angle` in degrees.
153    Returns a list with the updated positions.
154
155    By default, the rotation vector is defined by the perpendicular
156    passing through the geometrical center of the first three points.
157    To override this and use the vector between the first two atoms
158    as the rotation axis, set `use_centroid = False`.
159
160    **WARNING: The `positions` list is order-sensitive**.
161    If you rotate more than one chemical group in a structure,
162    be sure to follow the same direction for each group (e.g. all clockwise)
163    to ensure that all rotation vectors point in the same direction.
164
165    If `show_axis = True` it returns two additional coordinates at the end of the list,
166    with the centroid and the rotation vector. Only works with `use_centroid = True`.
167
168    The rotation uses Rodrigues' rotation formula,
169    powered by [`scipy.spatial.transform.Rotation.from_rotvec`](https://docs.scipy.org/doc/scipy/reference/generated/scipy.spatial.transform.Rotation.from_rotvec.html#scipy.spatial.transform.Rotation.from_rotvec).
170    """
171    if len(positions) < 3:
172        raise ValueError("At least three atoms must be rotated.")
173    if not isinstance(positions[0], list):
174        raise ValueError(f"Atomic positions must have the form: [[x1,y1,z1], [x2,y2,z2], [x3,y3,z3], etc]. Yours were:\n{positions}")
175    positions = np.array(positions)
176    #print(f'POSITIONS: {positions}')  # DEBUG
177    # Define the geometrical center
178    center_atoms = positions[:2]
179    if use_centroid:
180        center_atoms = positions[:3]
181    center = np.mean(center_atoms, axis=0)
182    # Ensure the axis passes through the geometrical center
183    centered_positions = positions - center
184    # Define the perpendicular axis (normal to the plane formed by the first three points)
185    v1 = centered_positions[0] - centered_positions[1]
186    v2 = centered_positions[0] - centered_positions[2]
187    axis = v1  # Axis defined by the first two points
188    if use_centroid:  # Axis defined by the cross product of the first three points
189        axis = np.cross(v2, v1)
190    axis_length = np.linalg.norm(axis)
191    axis = axis / axis_length
192    # Create the rotation object using scipy
193    rotation = Rotation.from_rotvec(np.radians(angle) * axis)
194    # Rotate all coordinates around the geometrical center
195    rotated_centered_positions = rotation.apply(centered_positions)
196    rotated_positions = (rotated_centered_positions + center).tolist()
197    #print(f'ROTATED_POSITIONS: {rotated_positions}')  # DEBUG
198    if show_axis and use_centroid:
199        rotated_positions.append(center.tolist())
200        rotated_positions.append((center + axis).tolist())
201    return rotated_positions
def rotate_qe( filepath: str, positions: list, angle: float, repeat: bool = False, precision: int = 3, use_centroid: bool = True, show_axis: bool = False) -> list:
 30def rotate_qe(
 31        filepath:str,
 32        positions:list,
 33        angle:float,
 34        repeat:bool=False,
 35        precision:int=3,
 36        use_centroid:bool=True,
 37        show_axis:bool=False,
 38    ) -> list:
 39    """Rotates atoms from a Quantum ESPRESSO pw.x input file.
 40
 41    Takes a `filepath` with a molecular structure, and three or more atomic `positions` (list).
 42    These input positions can be approximate, and are used to identify the target atoms.
 43    The decimal precision in the search for these positions is controlled by `precision`.
 44
 45    It rotates the atoms by a specific `angle` in degrees.
 46    Additionally, if `repeat = True` it repeats the same rotation over the whole circunference.
 47    Finally, it writes the rotated structure(s) to a new structural file(s).
 48    Returns a list with the output filename(s).
 49
 50    By default, the rotation axis is defined by the perpendicular vector
 51    passing through the geometrical center of the first three points.
 52    To override this and instead use the vector between the first two atoms
 53    as the rotation axis, set `use_centroid = False`.
 54
 55    **WARNING: The `positions` list is order-sensitive**.
 56    If you rotate more than one chemical group in a structure,
 57    be sure to follow the same direction for each group (e.g. all clockwise)
 58    to ensure that all axes of rotation point in the same direction.
 59
 60    To debug, `show_axis = True` adds two additional helium atoms as the rotation vector.
 61
 62    The resulting rotational potential can be compiled to a CSV file with `qrotor.potential.from_qe()`.
 63    """
 64    print('Rotating Quantum ESPRESSO input structure with QRotor...')
 65    if len(positions) < 3:
 66        raise ValueError("At least three positions are required to define the rotation axis.")
 67    lines = []
 68    full_positions = []
 69    for position in positions:
 70        line = api.pwx.get_atom(filepath=filepath, position=position, precision=precision, literal=True)
 71        lines.append(line)
 72        pos = extract.coords(line)
 73        if len(pos) > 3:  # Keep only the first three coordinates
 74            pos = pos[:3]
 75        # Convert to cartesian
 76        pos_cartesian = api.pwx.to_cartesian(filepath, pos)
 77        full_positions.append(pos_cartesian)
 78        print(f'Found atom: "{line}"')
 79    # Set the angles to rotate
 80    if not repeat:
 81        angles = [angle]
 82    else:
 83        angles = range(0, 360, angle)
 84    # Rotate and save the structure
 85    outputs = []
 86    path = os.path.dirname(filepath)
 87    basename = os.path.basename(filepath)
 88    name, ext = os.path.splitext(basename)
 89    print('Rotating the structure...')
 90    for angle in angles:
 91        output_name = name + f'_{angle}' + ext
 92        output = os.path.join(path, output_name)
 93        rotated_positions_cartesian = rotate_coords(full_positions, angle, use_centroid, show_axis)
 94        rotated_positions = []
 95        for coord in rotated_positions_cartesian:
 96            pos = api.pwx.from_cartesian(filepath, coord)
 97            rotated_positions.append(pos)
 98        _save_qe(filepath, output, lines, rotated_positions)
 99        outputs.append(output)
100        print(output)
101    return outputs

Rotates atoms from a Quantum ESPRESSO pw.x input file.

Takes a filepath with a molecular structure, and three or more atomic positions (list). These input positions can be approximate, and are used to identify the target atoms. The decimal precision in the search for these positions is controlled by precision.

It rotates the atoms by a specific angle in degrees. Additionally, if repeat = True it repeats the same rotation over the whole circunference. Finally, it writes the rotated structure(s) to a new structural file(s). Returns a list with the output filename(s).

By default, the rotation axis is defined by the perpendicular vector passing through the geometrical center of the first three points. To override this and instead use the vector between the first two atoms as the rotation axis, set use_centroid = False.

WARNING: The positions list is order-sensitive. If you rotate more than one chemical group in a structure, be sure to follow the same direction for each group (e.g. all clockwise) to ensure that all axes of rotation point in the same direction.

To debug, show_axis = True adds two additional helium atoms as the rotation vector.

The resulting rotational potential can be compiled to a CSV file with qrotor.potential.from_qe().

def rotate_coords( positions: list, angle: float, use_centroid: bool = True, show_axis: bool = False) -> list:
143def rotate_coords(
144        positions:list,
145        angle:float,
146        use_centroid:bool=True,
147        show_axis:bool=False,
148    ) -> list:
149    """Rotates geometrical coordinates.
150
151    Takes a list of atomic `positions` in cartesian coordinates, as
152    `[[x1,y1,z1], [x2,y2,z2], [x3,y3,z3], [etc]`.
153    Then rotates said coordinates by a given `angle` in degrees.
154    Returns a list with the updated positions.
155
156    By default, the rotation vector is defined by the perpendicular
157    passing through the geometrical center of the first three points.
158    To override this and use the vector between the first two atoms
159    as the rotation axis, set `use_centroid = False`.
160
161    **WARNING: The `positions` list is order-sensitive**.
162    If you rotate more than one chemical group in a structure,
163    be sure to follow the same direction for each group (e.g. all clockwise)
164    to ensure that all rotation vectors point in the same direction.
165
166    If `show_axis = True` it returns two additional coordinates at the end of the list,
167    with the centroid and the rotation vector. Only works with `use_centroid = True`.
168
169    The rotation uses Rodrigues' rotation formula,
170    powered by [`scipy.spatial.transform.Rotation.from_rotvec`](https://docs.scipy.org/doc/scipy/reference/generated/scipy.spatial.transform.Rotation.from_rotvec.html#scipy.spatial.transform.Rotation.from_rotvec).
171    """
172    if len(positions) < 3:
173        raise ValueError("At least three atoms must be rotated.")
174    if not isinstance(positions[0], list):
175        raise ValueError(f"Atomic positions must have the form: [[x1,y1,z1], [x2,y2,z2], [x3,y3,z3], etc]. Yours were:\n{positions}")
176    positions = np.array(positions)
177    #print(f'POSITIONS: {positions}')  # DEBUG
178    # Define the geometrical center
179    center_atoms = positions[:2]
180    if use_centroid:
181        center_atoms = positions[:3]
182    center = np.mean(center_atoms, axis=0)
183    # Ensure the axis passes through the geometrical center
184    centered_positions = positions - center
185    # Define the perpendicular axis (normal to the plane formed by the first three points)
186    v1 = centered_positions[0] - centered_positions[1]
187    v2 = centered_positions[0] - centered_positions[2]
188    axis = v1  # Axis defined by the first two points
189    if use_centroid:  # Axis defined by the cross product of the first three points
190        axis = np.cross(v2, v1)
191    axis_length = np.linalg.norm(axis)
192    axis = axis / axis_length
193    # Create the rotation object using scipy
194    rotation = Rotation.from_rotvec(np.radians(angle) * axis)
195    # Rotate all coordinates around the geometrical center
196    rotated_centered_positions = rotation.apply(centered_positions)
197    rotated_positions = (rotated_centered_positions + center).tolist()
198    #print(f'ROTATED_POSITIONS: {rotated_positions}')  # DEBUG
199    if show_axis and use_centroid:
200        rotated_positions.append(center.tolist())
201        rotated_positions.append((center + axis).tolist())
202    return rotated_positions

Rotates geometrical coordinates.

Takes a list of atomic positions in cartesian coordinates, as [[x1,y1,z1], [x2,y2,z2], [x3,y3,z3], [etc]. Then rotates said coordinates by a given angle in degrees. Returns a list with the updated positions.

By default, the rotation vector is defined by the perpendicular passing through the geometrical center of the first three points. To override this and use the vector between the first two atoms as the rotation axis, set use_centroid = False.

WARNING: The positions list is order-sensitive. If you rotate more than one chemical group in a structure, be sure to follow the same direction for each group (e.g. all clockwise) to ensure that all rotation vectors point in the same direction.

If show_axis = True it returns two additional coordinates at the end of the list, with the centroid and the rotation vector. Only works with use_centroid = True.

The rotation uses Rodrigues' rotation formula, powered by scipy.spatial.transform.Rotation.from_rotvec.