Source code for psi4.driver.qcdb.parker
#
# @BEGIN LICENSE
#
# Psi4: an open-source quantum chemistry software package
#
# Copyright (c) 2007-2019 The Psi4 Developers.
#
# The copyrights for code used from other parties are included in
# the corresponding files.
#
# This file is part of Psi4.
#
# Psi4 is free software; you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, version 3.
#
# Psi4 is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License along
# with Psi4; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# @END LICENSE
#
import math
import numpy as np
import qcelemental as qcel
BOND_FACTOR = 1.2 # fudge factor for bond length threshold
_expected_bonds = {
'H': 1,
'C': 4,
'N': 3,
'O': 2,
'F': 1,
'P': 3,
'S': 2,
}
def xyz2mol(self):
"""Returns a string of Molecule formatted for mol2.
Written by Trent M. Parker 9 Jun 2014
"""
bonds = _bond_profile(self)
N = 0
for i in range(self.natom()):
if self.Z(i):
N += 1
# header
text = '%s\n' % (self.name())
text += ' Generated by xyz2mol\n\n'
text += '%3i%3i 0 0 0 0 0 0 0 0999 V2000\n' % (N, len(bonds))
# coordinates
for i in range(self.natom()):
x = self.x(i) * qcel.constants.bohr2angstroms
y = self.y(i) * qcel.constants.bohr2angstroms
z = self.z(i) * qcel.constants.bohr2angstroms
if self.Z(i):
text += ' %9.4f %9.4f %9.4f %-2s 0 0 0 0 0\n' % (x, y, z, self.symbol(i))
# bonds
for p in range(len(bonds)):
text += '%3i%3i%3i' % (bonds[p][0] + 1, bonds[p][1] + 1, bonds[p][2])
text += ' 0 0 0\n'
text += 'M END\n'
return text
def missing_bonds(bonds, bond_tree, at_types):
"""Determine number of bonds missing for each atom"""
n_missing = []
for i in range(len(at_types)):
n_bonds_i = 0
for p in range(len(bonds)):
at1 = bonds[p][0]
at2 = bonds[p][1]
if (at1 == i or at2 == i):
bond_order = bonds[p][2]
n_bonds_i += bond_order
n_expect_i = _expected_bonds[at_types[i]]
n_missing.append(n_expect_i - n_bonds_i)
return n_missing
def missing_neighbors(bond_tree, n_missing):
"""Determine number of neighboring atoms missing bonds for each atom"""
missing_neighbors = []
for i in range(len(bond_tree)):
N_neighbors = len(bond_tree[i])
missing = 0
for a in range(N_neighbors):
j = bond_tree[i][a]
if n_missing[j] > 0:
missing += 1
missing_neighbors.append(missing)
return missing_neighbors
def _bond_profile(self):
"""Obtain bonding topology of molecule"""
# determine bond topology from covalent radii
bonds = []
b2a = qcel.constants.bohr2angstroms
for i in range(self.natom()):
for j in range(i + 1, self.natom()):
try:
# qcdb.Molecule
dist = np.linalg.norm(self.xyz(j, np_out=True) - self.xyz(i, np_out=True))
except TypeError:
# psi4.core.Molecule
dist = self.xyz(j).distance(self.xyz(i))
# TOOD check bohr/ang progress
bonded_dist = BOND_FACTOR * (qcel.covalentradii.get(self.symbol(i)) + qcel.covalentradii.get(self.symbol(j)))
if bonded_dist > dist:
bonds.append([i, j, 1])
# determine bond order from number of bonds
N_atoms = self.natom()
N_bonds = len(bonds)
at_types = [self.symbol(i) for i in range(self.natom())]
bond_tree = [[] for i in range(N_atoms)]
for i in range(N_bonds):
at1 = bonds[i][0]
at2 = bonds[i][1]
bond_tree[at1].append(at2)
bond_tree[at2].append(at1)
# determine bond order for all bonds from bond tree and element types
n_missing = missing_bonds(bonds, bond_tree, at_types)
n_neighbors_missing = missing_neighbors(bond_tree, n_missing)
# add double / triple bonds if only one neighbor missing bonds
N_left = math.floor(sum(n_missing) / 2)
N_left_previous = N_left + 1
N_iter = 0
while N_left > 0:
N_iter += 1
if N_left == N_left_previous:
neighbor_min += 1
else:
neighbor_min = 1
N_left_previous = N_left
# add a multiple bond to a deficient atom with the fewest number of deficient neighbors
BREAK_LOOP = False
for i in range(N_atoms):
if n_missing[i] > 0 and n_neighbors_missing[i] == neighbor_min:
N_neighbors = len(bond_tree[i])
for a in range(N_neighbors):
j = bond_tree[i][a]
if n_missing[j] > 0:
for p in range(N_bonds):
at1 = bonds[p][0]
at2 = bonds[p][1]
if (at1 == i and at2 == j) or (at1 == j and at2 == i):
bonds[p][2] += 1
n_missing[i] += -1
n_missing[j] += -1
n_neighbors_missing[i] += -1
n_neighbors_missing[j] += -1
N_left = math.floor(sum(n_missing) / 2)
BREAK_LOOP = True
if BREAK_LOOP:
break
if BREAK_LOOP:
break
# recalculate incomplete bond topology
n_missing = missing_bonds(bonds, bond_tree, at_types)
n_neighbors_missing = missing_neighbors(bond_tree, n_missing)
# break cycle if takes more than given number of iterations
max_iter = 100
if N_iter > max_iter:
print("""Error: multiple bond determination not complete""")
print(""" %i bonds unaccounted for""" % (N_left))
break
# bond order is number of bonds between each bonded atom pair
bond_order = []
for p in range(N_bonds):
bond_order.append(bonds[p][2])
for p in range(len(bond_order)):
bonds[p][2] = bond_order[p]
return bonds