def __init__( self, elements: Union[Iterable[int], Iterable[str]], coordinates: ArrayLike2D, dummy_index: int, attached_index: Union[int, Iterable[int]], radii: Optional[ArrayLike1D] = None, radii_type: str = "crc", n_rot_vectors: int = 3600, excluded_atoms: Optional[Sequence[int]] = None, calculate: bool = True, ) -> None: # Convert elements to atomic numbers if the are symbols elements = convert_elements(elements, output="numbers") coordinates = np.array(coordinates) if excluded_atoms is None: excluded_atoms = [] excluded_atoms = np.array(excluded_atoms) # Get radii if they are not supplied if radii is None: radii = get_radii(elements, radii_type=radii_type) radii = np.array(radii) # Add dummy atom if multiple attached indices are given if isinstance(attached_index, Iterable): attached_dummy_coordinates = np.mean( [coordinates[i - 1] for i in attached_index], axis=0) attached_dummy_coordinates = cast(np.ndarray, attached_dummy_coordinates) coordinates = np.vstack([coordinates, attached_dummy_coordinates]) elements.append(0) radii = np.concatenate([radii, [0.0]]) attached_index = len(elements) # Set up coordinate array all_coordinates = coordinates all_radii = radii # Translate coordinates so origin is at atom 2 origin = all_coordinates[attached_index - 1] all_coordinates -= origin # Get vector pointing from atom 2 to atom 1 vector_2_to_1 = (all_coordinates[attached_index - 1] - all_coordinates[dummy_index - 1]) bond_length = np.linalg.norm(vector_2_to_1) vector_2_to_1 = vector_2_to_1 / np.linalg.norm(vector_2_to_1) # Get rotation quaternion that overlays vector with x-axis x_axis = np.array([[1.0, 0.0, 0.0]]) R = kabsch_rotation_matrix(vector_2_to_1.reshape(1, -1), x_axis, center=False) all_coordinates = (R @ all_coordinates.T).T self._rotation_matrix = R # Get list of atoms as Atom objects atoms = [] for i, (element, radius, coord) in enumerate(zip(elements, all_radii, all_coordinates), start=1): atom = Atom(element, coord, radius, i) atoms.append(atom) if i == dummy_index: dummy_atom = atom if i == attached_index: attached_atom = atom # Set up attributes self._atoms = atoms self._excluded_atoms = set(excluded_atoms) self._origin = origin self._dummy_atom = dummy_atom self._attached_atom = attached_atom self.bond_length = bond_length self._n_rot_vectors = n_rot_vectors if calculate: self.calculate()
def __init__( self, elements: Iterable[int] | Iterable[str], coordinates: ArrayLike2D, metal_index: int, excluded_atoms: Sequence[int] | None = None, radii: ArrayLike1D | None = None, include_hs: bool = False, radius: float = 3.5, radii_type: str = "bondi", radii_scale: float = 1.17, density: float = 0.001, z_axis_atoms: Sequence[int] | None = None, xz_plane_atoms: Sequence[int] | None = None, ) -> None: # Get center and and reortient coordinate system coordinates: Array2DFloat = np.array(coordinates) center = coordinates[metal_index - 1] coordinates -= center if excluded_atoms is None: excluded_atoms = [] excluded_atoms = set(excluded_atoms) if metal_index not in excluded_atoms: excluded_atoms.add(metal_index) if z_axis_atoms is not None and xz_plane_atoms is not None: z_axis_coordinates = coordinates[np.array(z_axis_atoms) - 1] z_point = np.mean(z_axis_coordinates, axis=0) xz_plane_coordinates = coordinates[np.array(xz_plane_atoms) - 1] xz_point = np.mean(xz_plane_coordinates, axis=0) v_1 = z_point - center v_2 = xz_point - center # TODO: Remove type ignores when https://github.com/numpy/numpy/pull/21216 is released v_3: Array1DFloat = np.cross(v_2, v_1) real: Array2DFloat = np.vstack([v_1, v_3]) # type: ignore real /= np.linalg.norm(real, axis=1).reshape(-1, 1) ref_1 = np.array([0.0, 0.0, -1.0]) ref_2 = np.array([0.0, 1.0, 0.0]) ref = np.vstack([ref_1, ref_2]) R = kabsch_rotation_matrix(real, ref, center=False) coordinates = (R @ coordinates.T).T elif z_axis_atoms is not None: z_axis_coordinates = coordinates[np.array(z_axis_atoms) - 1] z_point = np.mean(z_axis_coordinates, axis=0) v_1 = z_point - center v_1 = v_1 / np.linalg.norm(v_1) coordinates = rotate_coordinates(coordinates, v_1, np.array([0, 0, -1])) self._z_axis_atoms = z_axis_atoms self._xz_plane_atoms = xz_plane_atoms # Save density and coordinates for steric map plotting. self._density = density self._all_coordinates = coordinates # Converting element ids to atomic numbers if the are symbols elements = convert_elements(elements, output="numbers") # Getting radii if they are not supplied if radii is None: radii = get_radii(elements, radii_type=radii_type, scale=radii_scale) radii: Array1DFloat = np.array(radii) # Get list of atoms as Atom objects atoms = [] for i, (element, radius_, coord) in enumerate(zip(elements, radii, coordinates), start=1): if i in excluded_atoms: continue elif (not include_hs) and element == 1: continue else: atom = Atom(element, coord, radius_, i) atoms.append(atom) # Set variables for outside access and function access. self._atoms = atoms self._excluded_atoms = set(excluded_atoms) # Compute buried volume self._compute_buried_volume(center=center, radius=radius, density=density)
def _parse_gaussian_log(self, file: Union[str, PathLike]) -> None: # noqa: C901 # Read the log file with open(file) as f: lines = f.readlines() # Set up read flags read_b_atoms = False read_b_vectors = False read_fc_matrix = False read_ifc_matrix = False read_input_orientation = False read_standard_orientation = False read_internal = False read_internal_modes = False read_hp_modes = False read_masses = False # Set up containers for reading data B_atom_map: Dict[int, List[int]] = {} B_vectors: Dict[int, List[float]] = {} normal_modes: List[List[List[float]]] = [] internal_modes: List[List[float]] = [] force_constants: List[float] = [] masses: List[float] = [] fc_matrix = np.array([]) ifc_matrix = np.array([]) input_coordinates: List[float] = [] standard_coordinates: List[float] = [] n_imag: int = 0 n_atoms: int = 0 internal_indices: Dict[FrozenSet[int], int] = {} atomic_numbers: List[int] = [] coordinates: List[List[float]] = [] # Parse through log file content counter = 0 internal_names: List[str] = [] internal_vector: List[float] = [] values: Any value: Any for line in lines: # Read internal coordinate definitions if read_internal: if counter > 1: if (" --------------------------------------------------------------------------------" # noqa: B950 in line): read_internal = False n_internals = len(internal_indices.items()) else: split_line = line.strip().split() name = split_line[1] internal_names.append(name) atoms = split_line[2][2:].replace(")", "").split(",") atoms = [int(atom) for atom in atoms] internal_indices[frozenset(atoms)] = counter - 2 counter += 1 # Read Cartesian force constant matrix elif read_fc_matrix: if " FormGI is forming" in line: read_fc_matrix = False fc_matrix = np.triu(fc_matrix.T, 1) + fc_matrix elif " " in line: column_indices = [ int(value) for value in line.strip().split() ] elif "D" in line: split_line = line.strip().split() row_index = int(split_line[0]) - 1 values = [ float(value.replace("D", "E")) for value in split_line[1:] ] for i, value in enumerate(values): column_index = column_indices[i] - 1 fc_matrix[row_index, column_index] = value # Read internal force constant matrix elif read_ifc_matrix: if "Leave Link 716" in line: read_ifc_matrix = False ifc_matrix = np.triu(ifc_matrix.T, 1) + ifc_matrix elif " " in line: column_indices = [ int(value) for value in line.strip().split() ] elif "D" in line: split_line = line.strip().split() row_index = int(split_line[0]) - 1 values = [ float(value.replace("D", "E")) for value in split_line[1:] ] for i, value in enumerate(values): column_index = column_indices[i] - 1 ifc_matrix[row_index, column_index] = value # Read atoms for creation of B matrix elif read_b_atoms: if " B Matrix in FormBX:" in line: read_b_atoms = False else: if counter == 0: atoms = [int(value) for value in line.strip().split()] for atom in atoms: B_atom_map[atom] = [] if counter > 0 and counter < 5: values = [ int(value) for value in line.strip().split()[1:] ] for atom, value in zip(atoms, values): B_atom_map[atom].append(value) counter += 1 if counter == 5: counter = 0 # Read values of B matrix elif read_b_vectors: if (" IB Matrix in Red2BG:" in line or "Iteration" in line or " G Matrix:" in line): read_b_vectors = False else: if counter == 0: atoms = [int(value) for value in line.strip().split()] for atom in atoms: B_vectors[atom] = [] if counter > 0 and counter < 13: values = [ float(value.replace("D", "E")) for value in line.strip().split()[1:] ] for atom, value in zip(atoms, values): B_vectors[atom].append(value) counter += 1 if counter == 13: counter = 0 # Read atomic coordinates in input orientation elif read_input_orientation: if counter > 3: if ("---------------------------------------------------------------------" # noqa: B950 in line): read_input_orientation = False else: strip_line = line.strip().split() values = [float(value) for value in strip_line[3:]] input_coordinates.append(values) atomic_numbers.append(int(strip_line[1])) counter += 1 # Read atomic coordinates in standard orientation elif read_standard_orientation: if counter > 3: if ("---------------------------------------------------------------------" # noqa: B950 in line): read_standard_orientation = False else: strip_line = line.strip().split() values = [float(value) for value in strip_line[3:]] standard_coordinates.append(values) counter += 1 # Read decomposition of normal modes in internal coordinates elif read_internal_modes: if counter > 3: if ("--------------------------------------------------------------------------------" # noqa: B950 in line): read_internal_modes = False internal_modes.append(internal_vector) else: value = float(line.strip().split()[3]) internal_vector.append(value) counter += 1 # Read high-precision normal modes elif read_hp_modes: if counter < n_atoms * 3: strip_line = line.strip().split() values = [float(value) for value in strip_line[3:]] coordinates.append(values) if counter == n_atoms * 3: normal_modes.append(coordinates) read_hp_modes = False counter += 1 # Read atomic masses elif read_masses: if "Molecular mass: " in line: read_masses = False elif "and mass" in line: masses.append(float(line.strip().split()[8])) # Read number of atoms if " NAtoms=" in line and not n_atoms: n_atoms = int(line.strip().split()[1]) # Read normal mode force constants elif "Frc consts" in line: split_line = line.strip().split() values = [float(value) for value in split_line[3:]] force_constants.extend(values) # Read number of imaginary frequencies elif "imaginary frequencies (negative Signs)" in line: n_imag = int(line.strip().split()[1]) # Detect when to read data elif ("Name Definition Value Derivative Info." in line): read_internal = True counter = 1 internal_names = [] elif "- Thermochemistry -" in line: read_masses = True elif " IB Matrix in FormBX:" in line: read_b_atoms = True counter = 0 elif " B Matrix in FormBX:" in line: read_b_vectors = True counter = 0 elif " Force constants in Cartesian coordinates: " in line: read_fc_matrix = True fc_matrix = np.zeros((3 * n_atoms, 3 * n_atoms)) counter = 0 elif " Force constants in internal coordinates: " in line: read_ifc_matrix = True ifc_matrix = np.zeros((n_internals, n_internals)) counter = 0 elif "Input orientation: " in line: read_input_orientation = True counter = 0 elif "Standard orientation: " in line: read_standard_orientation = True counter = 0 elif "Normal Mode" in line: read_internal_modes = True counter = 1 internal_vector = [] elif " Coord Atom Element:" in line: read_hp_modes = True coordinates = [] counter = 0 # Process internal coordinates if len(internal_indices) > 0: for name, indices in zip(internal_names, internal_indices): if name[0] == "R" and len(indices) == 2: self._internal_coordinates.add_internal_coordinate( list(indices)) if name[0] == "A" and len(indices) == 3: self._internal_coordinates.add_internal_coordinate( list(indices)) if name[0] == "D" and len(indices) == 4: self._internal_coordinates.add_internal_coordinate( list(indices)) # Construct the B matrix from atoms and vectors if B_vectors: n_cartesian = n_atoms * 3 B = np.zeros((n_internals, n_cartesian)) for i in range(n_internals): for j, atom in enumerate(B_atom_map[i + 1]): if atom: B[i][(atom - 1) * 3:(atom - 1) * 3 + 3] = B_vectors[i + 1][j * 3:j * 3 + 3] B_inv = np.linalg.pinv(B) # Detect whether the internal coordinate system is redundant if B.shape[0] == len(force_constants): self._redundant = False else: self._redundant = True else: B = np.array([]) B_inv = np.array([]) # Detect whether the input coordinates have been rotated. If so, rotate # B matrix and its inverse. input_coordinates = np.array(input_coordinates).reshape(-1, 3) standard_coordinates = np.array(standard_coordinates).reshape(-1, 3) if (not np.array_equal(input_coordinates, standard_coordinates) and standard_coordinates.size > 0): rotation_i_to_s = kabsch_rotation_matrix(input_coordinates, standard_coordinates) if len(B) > 0: B = (rotation_i_to_s @ B.reshape(-1, 3).T).T.reshape( n_internals, -1) B_inv = np.linalg.pinv(B) # Set up attributes self._B = B self._B_inv = B_inv self._fc_matrix = fc_matrix if fc_matrix.size == 0 and ifc_matrix.size > 0: self._fc_matrix = B.T @ ifc_matrix @ B self._ifc_matrix = ifc_matrix self._force_constants = np.array(force_constants) if len(normal_modes) > 0: self._normal_modes = np.hstack(normal_modes).T else: self._normal_modes = np.array([]) if len(internal_modes) > 0: self._D = np.vstack(internal_modes).T self.n_imag = n_imag self._masses = np.array(masses) self._input_coordinates = input_coordinates self._standard_coordinates = standard_coordinates self._coordinates = input_coordinates self._elements = convert_elements(atomic_numbers, output="numbers")