#
# @BEGIN LICENSE
#
# Psi4: an open-source quantum chemistry software package
#
# Copyright (c) 2007-2021 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 os
import hashlib
import collections
from typing import Dict, List, Tuple, Union
import numpy as np
import qcelemental as qcel
import psi4
from .util import parse_dertype
from .libmintsmolecule import *
from .testing import compare_values, compare_integers, compare_molrecs
from .bfs import BFS
qcdbmol = "psi4.driver.qcdb.molecule.Molecule"
[docs]class Molecule(LibmintsMolecule):
"""Class to store the elements, coordinates, fragmentation pattern,
charge, multiplicity of a molecule. Largely replicates psi4's libmints
Molecule class, developed by Justin M. Turney and Andy M. Simmonett
with incremental improvements by other psi4 developers. Major
This class extends `qcdb.LibmintsMolecule` and occasionally
`psi4.core.Molecule` itself.
"""
def __init__(self,
molinit=None,
dtype=None,
geom=None,
elea=None,
elez=None,
elem=None,
mass=None,
real=None,
elbl=None,
name=None,
units='Angstrom',
input_units_to_au=None,
fix_com=None,
fix_orientation=None,
fix_symmetry=None,
fragment_separators=None,
fragment_charges=None,
fragment_multiplicities=None,
molecular_charge=None,
molecular_multiplicity=None,
comment=None,
provenance=None,
connectivity=None,
enable_qm=True,
enable_efp=True,
missing_enabled_return_qm='none',
missing_enabled_return_efp='none',
missing_enabled_return='error',
tooclose=0.1,
zero_ghost_fragments=False,
nonphysical=False,
mtol=1.e-3,
verbose=1):
"""Initialize Molecule object from LibmintsMolecule"""
super(Molecule, self).__init__()
if molinit is not None or geom is not None:
if isinstance(molinit, dict):
molrec = molinit
elif isinstance(molinit, str):
compound_molrec = qcel.molparse.from_string(
molstr=molinit,
dtype=dtype,
name=name,
fix_com=fix_com,
fix_orientation=fix_orientation,
fix_symmetry=fix_symmetry,
return_processed=False,
enable_qm=enable_qm,
enable_efp=enable_efp,
missing_enabled_return_qm=missing_enabled_return_qm,
missing_enabled_return_efp=missing_enabled_return_efp,
verbose=verbose)
molrec = compound_molrec['qm']
elif molinit is None and geom is not None:
molrec = qcel.molparse.from_arrays(
geom=geom,
elea=elea,
elez=elez,
elem=elem,
mass=mass,
real=real,
elbl=elbl,
name=name,
units=units,
input_units_to_au=input_units_to_au,
fix_com=fix_com,
fix_orientation=fix_orientation,
fix_symmetry=fix_symmetry,
fragment_separators=fragment_separators,
fragment_charges=fragment_charges,
fragment_multiplicities=fragment_multiplicities,
molecular_charge=molecular_charge,
molecular_multiplicity=molecular_multiplicity,
comment=comment,
provenance=provenance,
connectivity=connectivity,
domain='qm',
missing_enabled_return=missing_enabled_return,
tooclose=tooclose,
zero_ghost_fragments=zero_ghost_fragments,
nonphysical=nonphysical,
mtol=mtol,
verbose=verbose)
# ok, got the molrec dictionary; now build the thing
self._internal_from_dict(molrec, verbose=verbose)
# The comment line
self.tagline = ""
def __str__(self):
text = """ ==> qcdb Molecule %s <==\n\n""" % (self.name())
text += """ => %s <=\n\n""" % (self.tagline)
text += self.create_psi4_string_from_molecule()
return text
def __setattr__(self, name, value):
"""Function to overload setting attributes to allow geometry
variable assigment as if member data.
"""
if 'all_variables' in self.__dict__:
if name.upper() in self.__dict__['all_variables']:
self.set_variable(name, value)
super(Molecule, self).__setattr__(name, value)
def __getattr__(self, name):
"""Function to overload accessing attribute contents to allow
retrival of geometry variable values as if member data.
"""
if 'all_variables' in self.__dict__ and name.upper() in self.__dict__['all_variables']:
return self.get_variable(name)
else:
raise AttributeError
@classmethod
def init_with_xyz(cls, xyzfilename, no_com=False, no_reorient=False, contentsNotFilename=False):
"""Pull information from an XYZ file. No fragment info detected.
Bohr/Angstrom pulled from first line if available. Charge,
multiplicity, tagline pulled from second line if available. Body
accepts atom symbol or atom charge in first column. Arguments
*no_com* and *no_reorient* can be used to turn off shift and
rotation. If *xyzfilename* is a string of the contents of an XYZ
file, rather than the name of a file, set *contentsNotFilename*
to ``True``.
>>> H2O = qcdb.Molecule.init_with_xyz('h2o.xyz')
"""
raise FeatureDeprecated(
"""qcdb.Molecule.init_with_xyz. Replace with: qcdb.Molecule.from_string(..., dtype='xyz+')""")
@classmethod
def init_with_mol2(cls, xyzfilename, no_com=False, no_reorient=False, contentsNotFilename=False):
"""Pull information from a MOl2 file. No fragment info detected.
Bohr/Angstrom pulled from first line if available. Charge,
multiplicity, tagline pulled from second line if available. Body
accepts atom symbol or atom charge in first column. Arguments
*no_com* and *no_reorient* can be used to turn off shift and
rotation. If *xyzfilename* is a string of the contents of an XYZ
file, rather than the name of a file, set *contentsNotFilename*
to ``True``.
NOTE: chg/mult NYI
>>> H2O = qcdb.Molecule.init_with_mol2('h2o.mol2')
"""
instance = cls()
instance.lock_frame = False
instance.PYmove_to_com = not no_com
instance.PYfix_orientation = no_reorient
if contentsNotFilename:
text = xyzfilename.splitlines()
else:
try:
infile = open(xyzfilename, 'r')
except IOError:
raise ValidationError(
"""Molecule::init_with_mol2: given filename '%s' does not exist.""" % (xyzfilename))
if os.stat(xyzfilename).st_size == 0:
raise ValidationError("""Molecule::init_with_mol2: given filename '%s' is blank.""" % (xyzfilename))
text = infile.readlines()
# fixed-width regex ((?=[ ]*-?\d+)[ -\d]{5})
v2000 = re.compile(r'^((?=[ ]*\d+)[ \d]{3})((?=[ ]*\d+)[ \d]{3})(.*)V2000\s*$')
vend = re.compile(r'^\s*M\s+END\s*$')
NUMBER = "((?:[-+]?\\d*\\.\\d+(?:[DdEe][-+]?\\d+)?)|(?:[-+]?\\d+\\.\\d*(?:[DdEe][-+]?\\d+)?))"
xyzM = re.compile(
r'^(?:\s*)' + NUMBER + r'(?:\s+)' + NUMBER + r'(?:\s+)' + NUMBER + r'(?:\s+)([A-Z](?:[a-z])?)(?:\s+)(.*)',
re.IGNORECASE)
## now charge and multiplicity
# $chargem = 0 ; $multm = 1 ;
#while (<MOL>) {
#if (/CHARGE/) { $chargem = <MOL> ; chop($chargem) ;}
#if (/MULTIPLICITY/) { $multm = <MOL> ; chop($multm) }
# } # end while charge and multiplicity
if not text:
raise ValidationError("Molecule::init_with_mol2: file blank")
# Try to match header/footer
if vend.match(text[-1]):
pass
else:
raise ValidationError("Molecule::init_with_mol2: Malformed file termination\n%s" % (text[-1]))
sysname = '_'.join(text[0].strip().split())
comment = text[2].strip()
if comment:
instance.tagline = sysname + ' ' + comment
else:
instance.tagline = sysname
#instance.tagline = text[0].strip() + ' ' + text[2].strip()
fileUnits = 'Angstrom' # defined for MOL
#instance.set_molecular_charge(int(xyz2.match(text[1]).group(1)))
#instance.set_multiplicity(int(xyz2.match(text[1]).group(2)))
if v2000.match(text[3]):
fileNatom = int(v2000.match(text[3]).group(1))
fileNbond = int(v2000.match(text[3]).group(2))
else:
raise ValidationError("Molecule::init_with_mol2: Malformed fourth line\n%s" % (text[3]))
if fileNatom < 1:
raise ValidationError("Molecule::init_with_mol2: Malformed Natom\n%s" % (str(fileNatom)))
# Next line begins the useful information.
for i in range(fileNatom):
try:
if xyzM.match(text[4 + i]):
fileX = float(xyzM.match(text[4 + i]).group(1))
fileY = float(xyzM.match(text[4 + i]).group(2))
fileZ = float(xyzM.match(text[4 + i]).group(3))
fileAtom = xyzM.match(text[4 + i]).group(4).upper()
# Check that the atom symbol is valid
z = qcel.periodictable.to_Z(fileAtom)
# Add it to the molecule.
instance.add_atom(z, fileX, fileY, fileZ, fileAtom, qcel.periodictable.to_mass(fileAtom), z)
else:
raise ValidationError("Molecule::init_with_mol2: Malformed atom information line %d." % (i + 5))
except IndexError:
raise ValidationError(
"Molecule::init_with_mol2: Expected atom in file at line %d.\n%s" % (i + 5, text[i + 4]))
# We need to make 1 fragment with all atoms
instance.fragments.append([0, fileNatom - 1])
instance.fragment_types.append('Real')
instance.fragment_charges.append(instance.molecular_charge())
instance.fragment_multiplicities.append(instance.multiplicity())
# Set the units properly
instance.PYunits = fileUnits
if fileUnits == 'Bohr':
instance.PYinput_units_to_au = 1.0
elif fileUnits == 'Angstrom':
instance.PYinput_units_to_au = 1.0 / qcel.constants.bohr2angstroms
instance.update_geometry()
return instance
def save_string_xyz(self, save_ghosts=True, save_natom=False):
"""Save a string for a XYZ-style file.
>>> H2OH2O.save_string_xyz()
6
-2 3 water_dimer
O -1.551007000000 -0.114520000000 0.000000000000
H -1.934259000000 0.762503000000 0.000000000000
H -0.599677000000 0.040712000000 0.000000000000
O 1.350625000000 0.111469000000 0.000000000000
H 1.680398000000 -0.373741000000 -0.758561000000
H 1.680398000000 -0.373741000000 0.758561000000
"""
factor = 1.0 if self.PYunits == 'Angstrom' else qcel.constants.bohr2angstroms
N = self.natom()
if not save_ghosts:
N = 0
for i in range(self.natom()):
if self.Z(i):
N += 1
text = ''
if save_natom:
text += "%d\n" % (N)
text += '%d %d %s\n' % (self.molecular_charge(), self.multiplicity(), self.tagline)
for i in range(self.natom()):
[x, y, z] = self.atoms[i].compute()
if save_ghosts or self.Z(i):
text += '%2s %17.12f %17.12f %17.12f\n' % ((self.symbol(i) if self.Z(i) else "Gh"), \
x * factor, y * factor, z * factor)
return text
def save_xyz(self, filename, save_ghosts=True, save_natom=True):
"""Save an XYZ file.
>>> H2OH2O.save_xyz('h2o.xyz')
"""
outfile = open(filename, 'w')
outfile.write(self.save_string_xyz(save_ghosts, save_natom))
outfile.close()
def format_molecule_for_numpy(self, npobj=True):
"""Returns a NumPy array of the non-dummy atoms of the geometry
in Cartesian coordinates in Angstroms with element encoded as
atomic number. If *npobj* is False, returns representation of
NumPy array.
"""
factor = 1.0 if self.PYunits == 'Angstrom' else qcel.constants.bohr2angstroms
self.update_geometry()
# TODO fn title is format_mol... but return args not compatible
geo = []
for i in range(self.natom()):
[x, y, z] = self.atoms[i].compute()
geo.append([self.Z(i), x * factor, y * factor, z * factor])
nparr = np.array(geo)
return nparr if npobj else np.array_repr(nparr)
def format_molecule_for_psi4(self):
"""Returns string of molecule definition block."""
text = 'molecule mol {\n'
for line in self.create_psi4_string_from_molecule().splitlines():
text += ' ' + line + '\n'
text += '}\n'
return text
def format_molecule_for_qchem_old(self, mixedbas=True):
"""Returns geometry section of input file formatted for Q-Chem.
For ghost atoms, prints **Gh** as elemental symbol, with expectation
that element identity will be established in mixed basis section.
For ghost atoms when *mixedbas* is False, prints @ plus element symbol.
prints whole dimer for unCP mono when called dir (as opposed to passing thru str
no frag markers
"""
factor = 1.0 if self.PYunits == 'Angstrom' else qcel.constants.bohr2angstroms
text = ""
text += '$molecule\n'
text += '%d %d\n' % (self.molecular_charge(), self.multiplicity())
for i in range(self.natom()):
[x, y, z] = self.atoms[i].compute()
if mixedbas:
text += '%2s ' % (self.symbol(i) if self.Z(i) else "Gh")
else:
text += '%-3s ' % (('' if self.Z(i) else '@') + self.symbol(i))
text += '%17.12f %17.12f %17.12f\n' % (x * factor, y * factor, z * factor)
text += '$end\n\n'
# prepare molecule keywords to be set as c-side keywords
options = collections.defaultdict(lambda: collections.defaultdict(dict))
#options['QCHEM'['QCHEM_CHARGE']['value'] = self.molecular_charge()
#options['QCHEM'['QCHEM_MULTIPLICITY']['value'] = self.multiplicity()
options['QCHEM']['QCHEM_INPUT_BOHR']['value'] = False
#options['QCHEM']['QCHEM_COORDINATES']['value'] = 'CARTESIAN'
#SYM_IGNORE equiv to no_reorient, no_com, symmetry c1
options['QCHEM']['QCHEM_INPUT_BOHR']['clobber'] = True
return text, options
def format_molecule_for_psi4_xyz(self):
"""not much examined
"""
text = ""
if self.nallatom():
factor = 1.0 if self.PYunits == 'Angstrom' else qcel.constants.bohr2angstroms
# append units and any other non-default molecule keywords
text += "units Angstrom\n"
#text += " units %-s\n" % ("Angstrom" if self.units() == 'Angstrom' else "Bohr")
if not self.PYmove_to_com:
text += "no_com\n"
if self.PYfix_orientation:
text += "no_reorient\n"
# append atoms and coordentries and fragment separators with charge and multiplicity
Pfr = 0
for fr in range(self.nfragments()):
if self.fragment_types[fr] == 'Absent' and not self.has_zmatrix():
continue
text += "%s%s%d %d\n" % ("" if Pfr == 0 else "--\n", "#" if self.fragment_types[fr] == 'Ghost'
or self.fragment_types[fr] == 'Absent' else "", self.fragment_charges[fr],
self.fragment_multiplicities[fr])
Pfr += 1
for at in range(self.fragments[fr][0], self.fragments[fr][1] + 1):
if self.fragment_types[fr] == 'Absent' or self.fsymbol(at) == "X":
pass
else:
if self.fZ(at):
text += "%-8s" % (self.flabel(at))
else:
text += "%-8s" % ("Gh(" + self.flabel(at) + ")")
[x, y, z] = self.full_atoms[at].compute()
text += '%17.12f %17.12f %17.12f\n' % \
(x * factor, y * factor, z * factor)
text += "\n"
wtext = 'molecule mol {\n'
for line in text.splitlines():
wtext += ' ' + line + '\n'
wtext += '}\n'
return wtext
def format_molecule_for_molpro(self):
"""
"""
factor = 1.0 if self.PYunits == 'Angstrom' else qcel.constants.bohr2angstroms
# TODO keep fix_or? # Jan 2015 turning off fix_or
#self.fix_orientation(True)
#self.PYmove_to_com = False
self.update_geometry()
text = ""
text += 'angstrom\n'
text += 'geometry={\n'
dummy = []
for i in range(self.natom()):
[x, y, z] = self.atoms[i].compute()
text += '%-2s %17.12f %17.12f %17.12f\n' % (self.symbol(i), \
x * factor, y * factor, z * factor)
if not self.Z(i):
dummy.append(str(i + 1)) # Molpro atom number is 1-indexed
text += '}\n\n'
text += 'SET,CHARGE=%d\n' % (self.molecular_charge())
text += 'SET,SPIN=%d\n' % (self.multiplicity() - 1) # Molpro wants (mult-1)
if len(dummy) > 0:
text += 'dummy,' + ','.join(dummy) + '\n'
return text
def format_molecule_for_cfour(self):
"""Function to print Molecule in a form readable by Cfour.
"""
self.update_geometry()
factor = 1.0 if self.PYunits == 'Angstrom' else qcel.constants.bohr2angstroms
#factor = 1.0 if self.PYunits == 'Bohr' else 1.0/psi_bohr2angstroms
text = 'auto-generated by qcdb from molecule %s\n' % (self.tagline)
# append atoms and coordentries
for i in range(self.natom()):
[x, y, z] = self.atoms[i].compute()
text += '%-2s %17.12f %17.12f %17.12f\n' % ((self.symbol(i) if self.Z(i) else "GH"), \
x * factor, y * factor, z * factor)
#for fr in range(self.nfragments()):
# if self.fragment_types[fr] == 'Absent':
# pass
# else:
# for at in range(self.fragments[fr][0], self.fragments[fr][1] + 1):
# [x, y, z] = self.atoms[at].compute()
# text += '%-2s %17.12f %17.12f %17.12f\n' % ((self.symbol(at) if self.Z(at) else "GH"), \
# x * factor, y * factor, z * factor)
text += '\n'
# prepare molecule keywords to be set as c-side keywords
options = collections.defaultdict(lambda: collections.defaultdict(dict))
options['CFOUR']['CFOUR_CHARGE']['value'] = self.molecular_charge()
options['CFOUR']['CFOUR_MULTIPLICITY']['value'] = self.multiplicity()
options['CFOUR']['CFOUR_UNITS']['value'] = 'ANGSTROM'
#options['CFOUR']['CFOUR_UNITS']['value'] = 'BOHR'
options['CFOUR']['CFOUR_COORDINATES']['value'] = 'CARTESIAN'
#options['CFOUR']['CFOUR_SUBGROUP']['value'] = self.symmetry_from_input().upper()
#print self.inertia_tensor()
#print self.inertial_system()
options['CFOUR']['CFOUR_CHARGE']['clobber'] = True
options['CFOUR']['CFOUR_MULTIPLICITY']['clobber'] = True
options['CFOUR']['CFOUR_UNITS']['clobber'] = True
options['CFOUR']['CFOUR_COORDINATES']['clobber'] = True
return text, options
def format_basis_for_cfour(self, puream):
"""Function to print the BASIS=SPECIAL block for Cfour according
to the active atoms in Molecule. Special short basis names
are used by Psi4 libmints GENBAS-writer in accordance with
Cfour constraints.
"""
text = ''
cr = 1
for fr in range(self.nfragments()):
if self.fragment_types[fr] == 'Absent':
pass
else:
for at in range(self.fragments[fr][0], self.fragments[fr][1] + 1):
text += """%s:P4_%d\n""" % (self.symbol(at).upper(), cr)
cr += 1
text += '\n'
options = collections.defaultdict(lambda: collections.defaultdict(dict))
options['CFOUR']['CFOUR_BASIS']['value'] = 'SPECIAL'
options['CFOUR']['CFOUR_SPHERICAL']['value'] = puream
options['CFOUR']['CFOUR_BASIS']['clobber'] = True
options['CFOUR']['CFOUR_SPHERICAL']['clobber'] = True
options['CFOUR']['CFOUR_BASIS']['superclobber'] = True
options['CFOUR']['CFOUR_SPHERICAL']['superclobber'] = True
return text, options
def format_molecule_for_orca(self):
"""
Format the molecule into an orca xyz format
"""
options = collections.defaultdict(lambda: collections.defaultdict(dict))
self.update_geometry()
factor = 1.0 if self.PYunits == 'Angstrom' else qcel.constants.bohr2angstroms
text = ""
text += '* xyz {} {}\n'.format(self.molecular_charge(), self.multiplicity())
n_frags = self.nfragments()
for fr in range(n_frags):
if self.fragment_types[fr] == 'Absent':
pass
else:
for at in range(self.fragments[fr][0], self.fragments[fr][1] + 1):
if self.fragment_types[fr] == 'Ghost':
# TODO: add support for ghost atoms
# atom += ':'
continue
x, y, z = self.atoms[at].compute()
atom = self.symbol(at)
if n_frags > 1:
text += ' {:2s}({:d}) {:> 17.12f} {:> 17.12f} {:> 17.12f}\n'.format(\
atom, fr + 1, x * factor, y * factor, z * factor)
else:
text += ' {:2s} {:> 17.12f} {:> 17.12f} {:> 17.12f}\n'.format(\
atom, x * factor, y * factor, z * factor)
text += '*'
return text, options
def format_molecule_for_qchem(self, mixedbas=True):
"""Returns geometry section of input file formatted for Q-Chem.
For ghost atoms, prints **Gh** as elemental symbol, with expectation
that element identity will be established in mixed basis section.
For ghost atoms when *mixedbas* is False, prints @ plus element symbol.
candidate modeled after psi4_xyz so that absent fragments observed force xyz
"""
text = ""
if self.nallatom():
factor = 1.0 if self.PYunits == 'Angstrom' else qcel.constants.bohr2angstroms
Pfr = 0
# any general starting notation here <<<
text += '$molecule\n'
text += '%d %d\n' % (self.molecular_charge(), self.multiplicity())
# >>>
for fr in range(self.nfragments()):
if self.fragment_types[fr] == 'Absent' and not self.has_zmatrix():
continue
# any fragment marker here <<<
if self.nactive_fragments() > 1:
# this only distinguishes Real frags so Real/Ghost don't get
# fragmentation. may need to change
text += """--\n"""
# >>>
# any fragment chgmult here <<<
if self.nactive_fragments() > 1:
text += """{}{} {}\n""".format('!' if self.fragment_types[fr] in ['Ghost', 'Absent'] else '',
self.fragment_charges[fr], self.fragment_multiplicities[fr])
# >>>
Pfr += 1
for at in range(self.fragments[fr][0], self.fragments[fr][1] + 1):
if self.fragment_types[fr] == 'Absent' or self.fsymbol(at) == "X":
pass
else:
if self.fZ(at):
# label for real live atom <<<
text += """{:>3s} """.format(self.fsymbol(at))
# >>>
else:
# label for ghost atom <<<
text += """{:>3s} """.format('Gh' if mixedbas else ('@' + self.fsymbol(at)))
# >>>
[x, y, z] = self.full_atoms[at].compute()
# Cartesian coordinates <<<
text += """{:>17.12f} {:>17.12f} {:>17.12f}\n""".format(x * factor, y * factor, z * factor)
# >>>
# any general finishing notation here <<<
text += '$end\n\n'
# >>>
# prepare molecule keywords to be set as c-side keywords
options = collections.defaultdict(lambda: collections.defaultdict(dict))
#options['QCHEM'['QCHEM_CHARGE']['value'] = self.molecular_charge()
#options['QCHEM'['QCHEM_MULTIPLICITY']['value'] = self.multiplicity()
options['QCHEM']['QCHEM_INPUT_BOHR']['value'] = False
#options['QCHEM']['QCHEM_COORDINATES']['value'] = 'CARTESIAN'
if (not self.PYmove_to_com) or self.PYfix_orientation:
options['QCHEM']['QCHEM_SYM_IGNORE']['value'] = True
#SYM_IGNORE equiv to no_reorient, no_com, symmetry c1
options['QCHEM']['QCHEM_INPUT_BOHR']['clobber'] = True
options['QCHEM']['QCHEM_SYM_IGNORE']['clobber'] = True
return text, options
def format_molecule_for_cfour_old(self):
"""Function to print Molecule in a form readable by Cfour. This
version works as long as zmat is composed entirely of variables,
not internal values, while cartesian is all internal values,
no variables. Cutting off this line of development because,
with getting molecules after passing through libmints Molecule,
all zmats with dummies (Cfour's favorite kind) have already been
converted into cartesian. Next step, if this line was pursued
would be to shift any zmat internal values to external and any
cartesian external values to internal.
"""
text = ''
text += 'auto-generated by qcdb from molecule %s\n' % (self.tagline)
## append units and any other non-default molecule keywords
#text += " units %-s\n" % ("Angstrom" if self.units() == 'Angstrom' else "Bohr")
#if not self.PYmove_to_com:
# text += " no_com\n"
#if self.PYfix_orientation:
# text += " no_reorient\n"
# append atoms and coordentries and fragment separators with charge and multiplicity
Pfr = 0
isZMat = False
isCart = False
for fr in range(self.nfragments()):
if self.fragment_types[fr] == 'Absent' and not self.has_zmatrix():
continue
# text += "%s %s%d %d\n" % (
# "" if Pfr == 0 else " --\n",
# "#" if self.fragment_types[fr] == 'Ghost' or self.fragment_types[fr] == 'Absent' else "",
# self.fragment_charges[fr], self.fragment_multiplicities[fr])
Pfr += 1
for at in range(self.fragments[fr][0], self.fragments[fr][1] + 1):
if type(self.full_atoms[at]) == ZMatrixEntry:
isZMat = True
elif type(self.full_atoms[at]) == CartesianEntry:
isCart = True
if self.fragment_types[fr] == 'Absent':
text += "%s" % ("X")
elif self.fZ(at) or self.fsymbol(at) == "X":
text += "%s" % (self.fsymbol(at))
else:
text += "%s" % ("GH") # atom info is lost + self.fsymbol(at) + ")")
text += "%s" % (self.full_atoms[at].print_in_input_format_cfour())
text += "\n"
# append any coordinate variables
if len(self.geometry_variables):
for vb, val in self.geometry_variables.items():
text += """%s=%.10f\n""" % (vb, val)
text += "\n"
# prepare molecule keywords to be set as c-side keywords
options = collections.defaultdict(lambda: collections.defaultdict(dict))
options['CFOUR']['CFOUR_CHARGE']['value'] = self.molecular_charge()
options['CFOUR']['CFOUR_MULTIPLICITY']['value'] = self.multiplicity()
options['CFOUR']['CFOUR_UNITS']['value'] = self.units()
if isZMat and not isCart:
options['CFOUR']['CFOUR_COORDINATES']['value'] = 'INTERNAL'
elif isCart and not isZMat:
options['CFOUR']['CFOUR_COORDINATES']['value'] = 'CARTESIAN'
else:
raise ValidationError("""Strange mix of Cartesian and ZMatrixEntries in molecule unsuitable for Cfour.""")
return text, options
def format_molecule_for_nwchem(self):
"""
"""
factor = 1.0 if self.PYunits == 'Angstrom' else qcel.constants.bohr2angstroms
text = ""
text += '%d %d %s\n' % (self.molecular_charge(), self.multiplicity(), self.tagline)
for i in range(self.natom()):
[x, y, z] = self.atoms[i].compute()
text += '%4s %17.12f %17.12f %17.12f\n' % (("" if self.Z(i) else 'Bq') + self.symbol(i), \
x * factor, y * factor, z * factor)
return text
pass
# if symm print M2OUT "nosym\nnoorient\n";
# print DIOUT "angstrom\ngeometry={\n";
def inertia_tensor(self, masswt=True, zero=ZERO):
"""Compute inertia tensor.
>>> print H2OH2O.inertia_tensor()
[[8.704574864178731, -8.828375721817082, 0.0], [-8.828375721817082, 280.82861714077666, 0.0], [0.0, 0.0, 281.249500988553]]
"""
return self.inertia_tensor_partial(range(self.natom()), masswt, zero)
def inertia_tensor_partial(self, part, masswt=True, zero=ZERO):
"""Compute inertia tensor based on atoms in *part*.
"""
tensor = [[0, 0, 0], [0, 0, 0], [0, 0, 0]]
for i in part:
if masswt:
# I(alpha, alpha)
tensor[0][0] += self.mass(i) * (self.y(i) * self.y(i) + self.z(i) * self.z(i))
tensor[1][1] += self.mass(i) * (self.x(i) * self.x(i) + self.z(i) * self.z(i))
tensor[2][2] += self.mass(i) * (self.x(i) * self.x(i) + self.y(i) * self.y(i))
# I(alpha, beta)
tensor[0][1] -= self.mass(i) * self.x(i) * self.y(i)
tensor[0][2] -= self.mass(i) * self.x(i) * self.z(i)
tensor[1][2] -= self.mass(i) * self.y(i) * self.z(i)
else:
# I(alpha, alpha)
tensor[0][0] += self.y(i) * self.y(i) + self.z(i) * self.z(i)
tensor[1][1] += self.x(i) * self.x(i) + self.z(i) * self.z(i)
tensor[2][2] += self.x(i) * self.x(i) + self.y(i) * self.y(i)
# I(alpha, beta)
tensor[0][1] -= self.x(i) * self.y(i)
tensor[0][2] -= self.x(i) * self.z(i)
tensor[1][2] -= self.y(i) * self.z(i)
# mirror
tensor[1][0] = tensor[0][1]
tensor[2][0] = tensor[0][2]
tensor[2][1] = tensor[1][2]
# Check the elements for zero and make them a hard zero.
for i in range(3):
for j in range(3):
if math.fabs(tensor[i][j]) < zero:
tensor[i][j] = 0.0
return tensor
def inertial_system_partial(self, part, masswt=True, zero=ZERO):
"""Solve inertial system based on atoms in *part*"""
return diagonalize3x3symmat(self.inertia_tensor_partial(part, masswt, zero))
def inertial_system(self, masswt=True, zero=ZERO):
"""Solve inertial system"""
return diagonalize3x3symmat(self.inertia_tensor(masswt, zero))
def print_ring_planes(self, entity1, entity2, entity3=None, entity4=None):
"""(reals only, 1-indexed)
"""
pass
# TODO allow handle lines
text = ""
summ = []
#for entity in [entity1, entity2, entity3, entity4]:
for item in [entity1, entity2]:
text += """\n ==> Entity %s <==\n\n""" % (item)
# convert plain atoms into list and move from 1-indexed to 0-indexed
entity = []
try:
for idx in item:
entity.append(idx - 1)
except TypeError:
entity = [item - 1]
if len(entity) == 1:
dim = 'point'
elif len(entity) == 2:
dim = 'line'
else:
dim = 'plane'
# compute centroid
cent = [0.0, 0.0, 0.0]
for at in entity:
cent = add(cent, self.xyz(at))
cent = scale(cent, 1.0 / len(entity))
text += ' Centroid: %14.8f %14.8f %14.8f [Angstrom]\n' % \
(cent[0] * qcel.constants.bohr2angstroms, \
cent[1] * qcel.constants.bohr2angstroms, \
cent[2] * qcel.constants.bohr2angstroms)
text += ' Centroid: %14.8f %14.8f %14.8f [Bohr]\n' % \
(cent[0], cent[1], cent[2])
if dim == 'point':
summ.append({'dim': dim, 'geo': cent, 'cent': cent})
# TODO: figure out if should be using mass-weighted
self.translate(scale(cent, -1))
evals, evecs = self.inertial_system_partial(entity, masswt=False)
midx = evals.index(max(evals))
text += ' Normal Vector: %14.8f %14.8f %14.8f [unit]\n' % \
(evecs[0][midx], evecs[1][midx], evecs[2][midx])
text += ' Normal Vector: %14.8f %14.8f %14.8f [unit]\n' % \
(evecs[0][midx] + cent[0], evecs[1][midx] + cent[1], evecs[2][midx] + cent[2])
xplane = [evecs[0][midx], evecs[1][midx], evecs[2][midx], \
-1.0 * (evecs[0][midx] * cent[0] + evecs[1][midx] * cent[1] + evecs[2][midx] * cent[2])]
text += ' Eqn. of Plane: %14.8f %14.8f %14.8f %14.8f [Ai + Bj + Ck + D = 0]\n' % \
(xplane[0], xplane[1], xplane[2], xplane[3])
dtemp = math.sqrt(evecs[0][midx] * evecs[0][midx] + evecs[1][midx] * evecs[1][midx] +
evecs[2][midx] * evecs[2][midx])
hessplane = [evecs[0][midx] / dtemp, evecs[1][midx] / dtemp, evecs[2][midx] / dtemp, xplane[3] / dtemp]
hessplane2 = [xplane[0] / dtemp, xplane[1] / dtemp, xplane[2] / dtemp, xplane[3] / dtemp]
text += ' Eqn. of Plane: %14.8f %14.8f %14.8f %14.8f [Ai + Bj + Ck + D = 0] H\n' % \
(hessplane[0], hessplane[1], hessplane[2], hessplane[3])
text += ' Eqn. of Plane: %14.8f %14.8f %14.8f %14.8f [Ai + Bj + Ck + D = 0] H2\n' % \
(hessplane2[0], hessplane2[1], hessplane2[2], hessplane2[3])
self.translate(cent)
if dim == 'plane':
summ.append({'dim': dim, 'geo': xplane, 'cent': cent})
#print summ
text += """\n ==> 1 (%s) vs. 2 (%s) <==\n\n""" % (summ[0]['dim'], summ[1]['dim'])
#if summ[0]['dim'] == 'plane' and summ[1]['dim'] == 'point':
# cent = summ[1]['geo']
# plane = summ[0]['geo']
# print cent, plane
#
# D = math.fabs(plane[0] * cent[0] + plane[1] * cent[1] + plane[2] * cent[2] + plane[3]) / \
# math.sqrt(plane[0] * plane[0] + plane[1] * plane[1] + plane[2] * plane[2])
# text += ' Pt to Plane: %14.8f [Angstrom]\n' % (D * psi_bohr2angstroms)
#if summ[0]['dim'] == 'plane' and summ[1]['dim'] == 'plane':
if summ[0]['dim'] == 'plane' and (summ[1]['dim'] == 'plane' or summ[1]['dim'] == 'point'):
cent1 = summ[0]['cent']
cent2 = summ[1]['cent']
plane1 = summ[0]['geo']
#plane2 = summ[1]['geo']
distCC = distance(cent1, cent2)
text += ' Distance from Center of %s to Center of %s: %14.8f [Angstrom]\n' % \
('2', '1', distCC * qcel.constants.bohr2angstroms)
distCP = math.fabs(plane1[0] * cent2[0] + plane1[1] * cent2[1] + plane1[2] * cent2[2] + plane1[3])
# distCP expression has a denominator that's one since plane constructed from unit vector
text += ' Distance from Center of %s to Plane of %s: %14.8f [Angstrom]\n' % \
('2', '1', distCP * qcel.constants.bohr2angstroms)
distCPC = math.sqrt(distCC * distCC - distCP * distCP)
text += ' Distance from Center of %s to Center of %s along Plane of %s: %14.8f [Angstrom]\n' % \
('2', '1', '1', distCPC * qcel.constants.bohr2angstroms)
print(text)
# text = " Interatomic Distances (Angstroms)\n\n"
# for i in range(self.natom()):
# for j in range(i + 1, self.natom()):
# eij = sub(self.xyz(j), self.xyz(i))
# dist = norm(eij) * psi_bohr2angstroms
# text += " Distance %d to %d %-8.3lf\n" % (i + 1, j + 1, dist)
# text += "\n\n"
# return text
def rotor_type(self, tol=FULL_PG_TOL):
"""Returns the rotor type.
>>> H2OH2O.rotor_type()
RT_ASYMMETRIC_TOP
"""
evals, evecs = diagonalize3x3symmat(self.inertia_tensor())
evals = sorted(evals)
rot_const = [
1.0 / evals[0] if evals[0] > 1.0e-6 else 0.0, 1.0 / evals[1] if evals[1] > 1.0e-6 else 0.0,
1.0 / evals[2] if evals[2] > 1.0e-6 else 0.0
]
# Determine degeneracy of rotational constants.
degen = 0
for i in range(2):
for j in range(i + 1, 3):
if degen >= 2:
continue
rabs = math.fabs(rot_const[i] - rot_const[j])
tmp = rot_const[i] if rot_const[i] > rot_const[j] else rot_const[j]
if rabs > ZERO:
rel = rabs / tmp
else:
rel = 0.0
if rel < tol:
degen += 1
#print "\tDegeneracy is %d\n" % (degen)
# Determine rotor type
if self.natom() == 1:
rotor_type = 'RT_ATOM'
elif rot_const[0] == 0.0:
rotor_type = 'RT_LINEAR' # 0 < IB == IC inf > B == C
elif degen == 2:
rotor_type = 'RT_SPHERICAL_TOP' # IA == IB == IC A == B == C
elif degen == 1:
if (rot_const[1] - rot_const[2]) < 1.0e-6:
rotor_type = 'RT_PROLATE_SYMMETRIC_TOP' # IA < IB == IC A > B == C
elif (rot_const[0] - rot_const[1]) < 1.0e-6:
rotor_type = 'RT_OBLATE_SYMMETRIC_TOP' # IA == IB < IC A == B > C
else:
rotor_type = 'RT_ASYMMETRIC_TOP' # IA < IB < IC A > B > C
return rotor_type
def center_of_charge(self):
"""Computes center of charge of molecule (does not translate molecule).
>>> H2OH2O.center_of_charge()
[-0.073339893272065401, 0.002959783555632145, 0.0]
"""
ret = [0.0, 0.0, 0.0]
total_c = 0.0
for at in range(self.natom()):
c = self.charge(at)
ret = add(ret, scale(self.xyz(at), c))
total_c += c
ret = scale(ret, 1.0 / total_c)
return ret
def move_to_coc(self):
"""Moves molecule to center of charge
"""
coc = scale(self.center_of_charge(), -1.0)
self.translate(coc)
def rotational_symmetry_number(self):
"""Number of unique orientations of the rigid molecule that only interchange identical atoms.
Notes
-----
Source http://cccbdb.nist.gov/thermo.asp (search "symmetry number")
"""
pg = self.get_full_point_group()
pg = self.full_point_group_with_n()
if pg in ['ATOM', 'C1', 'Ci', 'Cs', 'C_inf_v']:
sigma = 1
elif pg == 'D_inf_h':
sigma = 2
elif pg in ['T', 'Td']:
sigma = 12
elif pg == 'Oh':
sigma = 24
elif pg == 'Ih':
sigma = 60
elif pg in ['Cn', 'Cnv', 'Cnh']:
sigma = self.full_pg_n()
elif pg in ['Dn', 'Dnd', 'Dnh']:
sigma = 2 * self.full_pg_n()
elif pg == 'Sn':
sigma = self.full_pg_n() / 2
else:
raise ValidationError("Can't ID full symmetry group: " + pg)
return sigma
def axis_representation(self, zero=1e-8):
"""Molecule vs. laboratory frame representation (e.g., IR or IIIL).
Parameters
----------
zero : float, optional
Screen for inertial tensor elements
Returns
-------
str
Representation code IR, IIR, IIIR, IL, IIL, IIIL. When
molecule not in inertial frame, string is prefixed by "~".
Notes
-----
Not carefully handling degenerate inertial elements.
"""
it = self.inertia_tensor(zero=zero)
Iidx = np.argsort(np.diagonal(it))
if np.array_equal(Iidx, np.asarray([1, 2, 0])):
ar = 'IR'
elif np.array_equal(Iidx, np.asarray([2, 0, 1])):
ar = 'IIR'
elif np.array_equal(Iidx, np.asarray([0, 1, 2])):
ar = 'IIIR'
elif np.array_equal(Iidx, np.asarray([2, 1, 0])):
ar = 'IL'
elif np.array_equal(Iidx, np.asarray([0, 2, 1])):
ar = 'IIL'
elif np.array_equal(Iidx, np.asarray([1, 0, 2])):
ar = 'IIIL'
# if inertial tensor has non-zero off-diagonals, this whole classification is iffy
if np.count_nonzero(it - np.diag(np.diagonal(it))):
ar = '~' + ar
return ar
[docs] def to_arrays(self, dummy: bool = False, ghost_as_dummy: bool = False) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
"""Exports coordinate info into NumPy arrays.
Parameters
----------
dummy
Whether or not to include dummy atoms in returned arrays.
ghost_as_dummy
Whether or not to treat ghost atoms as dummies.
Returns
-------
geom, mass, elem, elez, uniq : numpy.ndarray, numpy.ndarray, numpy.ndarray, numpy.ndarray, numpy.ndarray
(nat, 3) geometry [a0].
(nat,) mass [u].
(nat,) element symbol.
(nat,) atomic number.
(nat,) hash of element symbol and mass.
Note that coordinate, orientation, and element information is
preserved but fragmentation, chgmult, and dummy/ghost is lost.
Usage
-----
geom, mass, elem, elez, uniq = molinstance.to_arrays()
"""
self.update_geometry()
if dummy:
if isinstance(self, Molecule):
# normal qcdb.Molecule
geom = self.full_geometry(np_out=True)
else:
# psi4.core.Molecule
geom = np.array(self.full_geometry())
mass = np.asarray(
[(0. if (ghost_as_dummy and self.fZ(at) == 0) else self.fmass(at)) for at in range(self.nallatom())])
elem = np.asarray(
['X' if (ghost_as_dummy and self.fZ(at) == 0) else self.fsymbol(at) for at in range(self.nallatom())])
elez = np.asarray(
[0 if (ghost_as_dummy and self.fZ(at) == 0) else self.fZ(at) for at in range(self.nallatom())])
uniq = np.asarray([
hashlib.sha1((str(elem[at]) + str(mass[at])).encode('utf-8')).hexdigest()
for at in range(self.nallatom())
])
else:
if isinstance(self, Molecule):
# normal qcdb.Molecule
geom = self.geometry(np_out=True)
else:
# psi4.core.Molecule
geom = np.array(self.geometry())
mass = np.asarray([self.mass(at) for at in range(self.natom())])
elem = np.asarray([self.symbol(at) for at in range(self.natom())])
elez = np.asarray([self.Z(at) for at in range(self.natom())])
uniq = np.asarray([
hashlib.sha1((str(elem[at]) + str(mass[at])).encode('utf-8')).hexdigest() for at in range(self.natom())
])
return geom, mass, elem, elez, uniq
@staticmethod
def from_string(molstr,
dtype=None,
name=None,
fix_com=None,
fix_orientation=None,
fix_symmetry=None,
return_dict=False,
enable_qm=True,
enable_efp=True,
missing_enabled_return_qm='none',
missing_enabled_return_efp='none',
verbose=1):
molrec = qcel.molparse.from_string(
molstr=molstr,
dtype=dtype,
name=name,
fix_com=fix_com,
fix_orientation=fix_orientation,
fix_symmetry=fix_symmetry,
return_processed=False,
enable_qm=enable_qm,
enable_efp=enable_efp,
missing_enabled_return_qm=missing_enabled_return_qm,
missing_enabled_return_efp=missing_enabled_return_efp,
verbose=verbose)
if return_dict:
return Molecule.from_dict(molrec['qm']), molrec
else:
return Molecule.from_dict(molrec['qm'])
@staticmethod
def from_arrays(geom=None,
elea=None,
elez=None,
elem=None,
mass=None,
real=None,
elbl=None,
name=None,
units='Angstrom',
input_units_to_au=None,
fix_com=False,
fix_orientation=False,
fix_symmetry=None,
fragment_separators=None,
fragment_charges=None,
fragment_multiplicities=None,
molecular_charge=None,
molecular_multiplicity=None,
comment=None,
provenance=None,
connectivity=None,
missing_enabled_return='error',
tooclose=0.1,
zero_ghost_fragments=False,
nonphysical=False,
mtol=1.e-3,
verbose=1,
return_dict=False):
"""Construct Molecule from unvalidated arrays and variables.
Light wrapper around :py:func:`~qcelemental.molparse.from_arrays`
that is a full-featured constructor to dictionary representa-
tion of Molecule. This follows one step further to return
Molecule instance.
Parameters
----------
See :py:func:`~qcelemental.molparse.from_arrays`.
return_dict : bool, optional
Additionally return Molecule dictionary intermediate.
Returns
-------
mol : :py:class:`~qcdb.Molecule`
molrec : dict, optional
Dictionary representation of instance.
Only provided if `return_dict` is True.
"""
molrec = qcel.molparse.from_arrays(
geom=geom,
elea=elea,
elez=elez,
elem=elem,
mass=mass,
real=real,
elbl=elbl,
name=name,
units=units,
input_units_to_au=input_units_to_au,
fix_com=fix_com,
fix_orientation=fix_orientation,
fix_symmetry=fix_symmetry,
fragment_separators=fragment_separators,
fragment_charges=fragment_charges,
fragment_multiplicities=fragment_multiplicities,
molecular_charge=molecular_charge,
molecular_multiplicity=molecular_multiplicity,
comment=comment,
provenance=provenance,
connectivity=connectivity,
domain='qm',
missing_enabled_return=missing_enabled_return,
tooclose=tooclose,
zero_ghost_fragments=zero_ghost_fragments,
nonphysical=nonphysical,
mtol=mtol,
verbose=verbose)
if return_dict:
return Molecule.from_dict(molrec), molrec
else:
return Molecule.from_dict(molrec)
[docs] def to_string(self, dtype, units=None, atom_format=None, ghost_format=None, width=17, prec=12):
"""Format a string representation of QM molecule."""
molrec = self.to_dict(np_out=True)
# flip zeros
molrec['geom'][np.abs(molrec['geom']) < 5**(-(prec))] = 0
smol = qcel.molparse.to_string(
molrec,
dtype=dtype,
units=units,
atom_format=atom_format,
ghost_format=ghost_format,
width=width,
prec=prec)
return smol
[docs] def run_dftd3(self, func: str = None, dashlvl: str = None, dashparam: Dict = None, dertype: Union[int, str] = None, verbose: int = 1):
"""Compute dispersion correction via Grimme's DFTD3 program.
Parameters
----------
func
Name of functional (func only, func & disp, or disp only) for
which to compute dispersion (e.g., blyp, BLYP-D2, blyp-d3bj,
blyp-d3(bj), hf+d). Any or all parameters initialized
from `dashcoeff[dashlvl][func]` can be overwritten via
`dashparam`.
dashlvl
Name of dispersion correction to be applied (e.g., d, D2,
d3(bj), das2010). Must be key in `dashcoeff` or "alias" or
"formal" to one.
dashparam
Values for the same keys as `dashcoeff[dashlvl]['default']`
used to override any or all values initialized by `func`.
Extra parameters will error.
dertype
Maximum derivative level at which to run DFTD3. For large
molecules, energy-only calculations can be significantly more
efficient. Influences return values, see below.
verbose
Amount of printing.
Returns
-------
energy : float
When `dertype=0`, energy [Eh].
gradient : ndarray
When `dertype=1`, (nat, 3) gradient [Eh/a0].
(energy, gradient) : tuple of float and ndarray
When `dertype=None`, both energy [Eh] and (nat, 3) gradient [Eh/a0].
"""
import qcengine as qcng
if dertype is None:
derint, derdriver = -1, 'gradient'
else:
derint, derdriver = parse_dertype(dertype, max_derivative=1)
resinp = {
'molecule': self.to_schema(dtype=2),
'driver': derdriver,
'model': {
'method': func,
'basis': '(auto)',
},
'keywords': {
'verbose': verbose,
},
}
if dashlvl:
resinp['keywords']['level_hint'] = dashlvl
if dashparam:
resinp['keywords']['params_tweaks'] = dashparam
jobrec = qcng.compute(resinp, 'dftd3', raise_error=True)
jobrec = jobrec.dict()
# hack as not checking type GRAD
for k, qca in jobrec['extras']['qcvars'].items():
if isinstance(qca, (list, np.ndarray)):
jobrec['extras']['qcvars'][k] = np.array(qca).reshape(-1, 3)
if isinstance(self, Molecule):
pass
else:
from psi4 import core
for k, qca in jobrec['extras']['qcvars'].items():
if not isinstance(qca, (list, np.ndarray)):
core.set_variable(k, float(qca))
if derint == -1:
return (float(jobrec['extras']['qcvars']['DISPERSION CORRECTION ENERGY']),
jobrec['extras']['qcvars']['DISPERSION CORRECTION GRADIENT'])
elif derint == 0:
return float(jobrec['extras']['qcvars']['DISPERSION CORRECTION ENERGY'])
elif derint == 1:
return jobrec['extras']['qcvars']['DISPERSION CORRECTION GRADIENT']
[docs] def run_gcp(self, func: str = None, dertype: Union[int, str] = None, verbose: int = 1):
"""Compute geometrical BSSE correction via Grimme's GCP program.
Function to call Grimme's GCP program
https://www.chemie.uni-bonn.de/pctc/mulliken-center/software/gcp/gcp
to compute an a posteriori geometrical BSSE correction to *self* for
several HF, generic DFT, and specific HF-3c and PBEh-3c method/basis
combinations, *func*. Returns energy if *dertype* is 0, gradient
if *dertype* is 1, else tuple of energy and gradient if *dertype*
unspecified. The gcp executable must be independently compiled and
found in :envvar:`PATH` or :envvar:`PSIPATH`. *self* may be either a
qcdb.Molecule (sensibly) or a psi4.Molecule (works b/c psi4.Molecule
has been extended by this method py-side and only public interface
fns used) or a string that can be instantiated into a qcdb.Molecule.
Parameters
----------
func : str, optional
Name of method/basis combination or composite method for which to compute the correction
(e.g., HF/cc-pVDZ, DFT/def2-SVP, HF3c, PBEh3c).
dertype : int or str, optional
Maximum derivative level at which to run GCP. For large
molecules, energy-only calculations can be significantly more
efficient. Influences return values, see below.
verbose : int, optional
Amount of printing. Unused at present.
Returns
-------
energy : float
When `dertype=0`, energy [Eh].
gradient : ndarray
When `dertype=1`, (nat, 3) gradient [Eh/a0].
(energy, gradient) : tuple of float and ndarray
When `dertype=None`, both energy [Eh] and (nat, 3) gradient [Eh/a0].
"""
import qcengine as qcng
if dertype is None:
derint, derdriver = -1, 'gradient'
else:
derint, derdriver = parse_dertype(dertype, max_derivative=1)
resinp = {
'molecule': self.to_schema(dtype=2),
'driver': derdriver,
'model': {
'method': func,
'basis': '(auto)',
},
'keywords': {
'verbose': verbose,
},
}
jobrec = qcng.compute(resinp, 'gcp', raise_error=True)
jobrec = jobrec.dict()
# hack (instead of checking dertype GRAD) to collect `(nat, 3)` ndarray of gradient if present
for variable_name, qcv in jobrec['extras']['qcvars'].items():
if isinstance(qcv, (list, np.ndarray)):
jobrec['extras']['qcvars'][variable_name] = np.array(qcv).reshape(-1, 3)
if isinstance(self, Molecule):
pass
else:
from psi4 import core
for variable_name, qcv in jobrec['extras']['qcvars'].items():
if not isinstance(qcv, (list, np.ndarray)):
core.set_variable(variable_name, float(qcv))
if derint == -1:
return (float(jobrec['extras']['qcvars']['GCP CORRECTION ENERGY']),
jobrec['extras']['qcvars']['GCP CORRECTION GRADIENT'])
elif derint == 0:
return float(jobrec['extras']['qcvars']['GCP CORRECTION ENERGY'])
elif derint == 1:
return jobrec['extras']['qcvars']['GCP CORRECTION GRADIENT']
@staticmethod
def from_schema(molschema, return_dict=False, verbose=1):
"""Construct Molecule from non-Psi4 schema.
Light wrapper around :py:func:`~qcdb.Molecule.from_arrays`.
Parameters
----------
molschema : dict
Dictionary form of Molecule following known schema.
return_dict : bool, optional
Additionally return Molecule dictionary intermediate.
verbose : int, optional
Amount of printing.
Returns
-------
mol : :py:class:`~qcdb.Molecule`
molrec : dict, optional
Dictionary representation of instance.
Only provided if `return_dict` is True.
"""
molrec = qcel.molparse.from_schema(molschema, verbose=verbose)
if return_dict:
return Molecule.from_dict(molrec), molrec
else:
return Molecule.from_dict(molrec)
[docs] def to_schema(self, dtype, units='Bohr'):
"""Serializes instance into dictionary according to schema `dtype`."""
molrec = self.to_dict(np_out=True)
schmol = qcel.molparse.to_schema(molrec, dtype=dtype, units=units)
return schmol
[docs] def to_dict(self, force_c1=False, force_units=False, np_out=True):
"""Serializes instance into Molecule dictionary."""
self.update_geometry()
molrec = {}
if self.name() not in ['', 'default']:
molrec['name'] = self.name()
if self.comment() not in ['', 'default']:
molrec['comment'] = self.comment()
# qcdb does not add prov, so rely upon all qcdb.Mol creation happening in molparse for this to return valid value (not [])
molrec['provenance'] = copy.deepcopy(self.provenance())
if self.connectivity() != []:
molrec['connectivity'] = copy.deepcopy(self.connectivity())
if force_units == 'Bohr':
molrec['units'] = 'Bohr'
elif force_units == 'Angstrom':
molrec['units'] = 'Angstrom'
else:
units = self.units()
molrec['units'] = units
if units == 'Angstrom' and abs(self.input_units_to_au() * qcel.constants.bohr2angstroms - 1.) > 1.e-6:
molrec['input_units_to_au'] = self.input_units_to_au()
molrec['fix_com'] = self.com_fixed()
molrec['fix_orientation'] = self.orientation_fixed()
if force_c1:
molrec['fix_symmetry'] = 'c1'
elif self.symmetry_from_input():
molrec['fix_symmetry'] = self.symmetry_from_input()
# if self.has_zmatrix:
# moldict['zmat'] = self.zmat
# TODO zmat, geometry_variables
nat = self.natom()
geom = np.array(self.geometry()) # [a0]
if molrec['units'] == 'Angstrom':
geom *= qcel.constants.bohr2angstroms #self.input_units_to_au()
molrec['geom'] = geom.reshape((-1))
molrec['elea'] = np.array([self.mass_number(at) for at in range(nat)])
molrec['elez'] = np.array([qcel.periodictable.to_Z(self.symbol(at)) for at in range(nat)])
molrec['elem'] = np.array([self.symbol(at).capitalize() for at in range(nat)])
molrec['mass'] = np.array([self.mass(at) for at in range(nat)])
molrec['real'] = np.array([bool(self.Z(at)) for at in range(nat)])
molrec['elbl'] = np.array([self.label(at)[len(self.symbol(at)):].lower() for at in range(nat)])
fragments = [x[:] for x in self.get_fragments()]
fragment_charges = [float(f) for f in self.get_fragment_charges()]
fragment_multiplicities = [m for m in self.get_fragment_multiplicities()]
# do trimming not performed in Molecule class b/c fragment_* member data never directly exposed
for ifr, fr in reversed(list(enumerate(self.get_fragment_types()))):
if fr == 'Ghost':
fragment_charges[ifr] = 0.
fragment_multiplicities[ifr] = 1
elif fr == 'Absent':
del fragment_charges[ifr]
del fragment_multiplicities[ifr]
# readjust atom indices for subsequent fragments
renum = fragments[ifr][0]
for iffr, ffr in enumerate(fragments):
if iffr <= ifr:
continue
lenfr = ffr[1] - ffr[0]
fragments[iffr] = [renum, renum + lenfr]
renum += lenfr
del fragments[ifr]
molrec['fragment_separators'] = [int(f[0]) for f in fragments[1:]] # np.int --> int
molrec['fragment_charges'] = fragment_charges
molrec['fragment_multiplicities'] = fragment_multiplicities
molrec['molecular_charge'] = float(self.molecular_charge())
molrec['molecular_multiplicity'] = self.multiplicity()
# * mass number (elea) untouched by qcdb.Molecule/psi4.core.Molecule and
# likely to be array of -1s, so let from_arrays fill in the values and
# (1) don't complain about the difference and
# (2) return the from_arrays filled-in values
# * from.arrays is expecting speclabel "Co_userlbl" for elbl, but we're
# sending "_userlbl", hence speclabel=False
# * from.arrays sets difference provenance than input mol
forgive = ['elea', 'provenance']
# * from_arrays and comparison lines below are quite unnecessary to
# to_dict, but is included as a check. in practice, only fills in mass
# numbers and heals user chgmult.
try:
validated_molrec = qcel.molparse.from_arrays(speclabel=False, verbose=0, domain='qm', **molrec)
except qcel.ValidationError as err:
# * this can legitimately happen if total chg or mult has been set
# independently b/c fragment chg/mult not reset. so try again.
print(
"""Following warning is harmless if you've altered chgmult through `set_molecular_change` or `set_multiplicity`. Such alterations are an expert feature. Specifying in the original molecule string is preferred. Nonphysical masses may also trigger the warning."""
)
molrec['fragment_charges'] = [None] * len(fragments)
molrec['fragment_multiplicities'] = [None] * len(fragments)
validated_molrec = qcel.molparse.from_arrays(speclabel=False, nonphysical=True, verbose=0, domain='qm', **molrec)
forgive.append('fragment_charges')
forgive.append('fragment_multiplicities')
compare_molrecs(validated_molrec, molrec, 'to_dict', atol=1.e-6, forgive=forgive, verbose=0)
# from_arrays overwrites provenance
validated_molrec['provenance'] = copy.deepcopy(molrec['provenance'])
if not np_out:
validated_molrec = qcel.util.unnp(validated_molrec)
return validated_molrec
@classmethod
def from_dict(cls, molrec, verbose=1):
mol = cls()
mol._internal_from_dict(molrec=molrec, verbose=verbose)
return mol
def _internal_from_dict(self, molrec, verbose=1):
"""Constructs instance from fully validated and defaulted dictionary `molrec`."""
# Compromises for qcdb.Molecule
# * molecular_charge is int, not float
# * fragment_charges are int, not float
self.lock_frame = False
if 'name' in molrec:
self.set_name(molrec['name'])
if 'comment' in molrec:
self.set_comment(molrec['comment'])
self.set_provenance(copy.deepcopy(molrec['provenance']))
if 'connectivity' in molrec:
self.set_connectivity(copy.deepcopy(molrec['connectivity']))
self.set_units(molrec['units'])
if 'input_units_to_au' in molrec:
self.set_input_units_to_au(molrec['input_units_to_au'])
if 'geom_unsettled' in molrec:
nat = len(molrec['geom_unsettled'])
unsettled = True
for iat in range(nat):
entry = molrec['geom_unsettled'][iat]
label = molrec['elem'][iat] + molrec['elbl'][iat]
Z = molrec['elez'][iat] * int(molrec['real'][iat])
self.add_unsettled_atom(Z, entry, molrec['elem'][iat], molrec['mass'][iat], Z, label,
molrec['elea'][iat])
for var in molrec['variables']:
self.set_geometry_variable(var[0], var[1])
else:
geom = np.array(molrec['geom']).reshape((-1, 3))
nat = geom.shape[0]
unsettled = False
for iat in range(nat):
x, y, z = geom[iat]
label = molrec['elem'][iat] + molrec['elbl'][iat]
Z = molrec['elez'][iat] * int(molrec['real'][iat])
self.add_atom(Z, x, y, z, molrec['elem'][iat], molrec['mass'][iat], Z, label, molrec['elea'][iat])
# TODO charge and 2nd elez site
# TODO real back to type Ghost?
# apparently py- and c- sides settled on a diff convention of 2nd of pair in fragments_
fragment_separators = np.array(molrec['fragment_separators'], dtype=int)
fragment_separators = np.insert(fragment_separators, 0, 0)
fragment_separators = np.append(fragment_separators, nat)
fragments = [[fragment_separators[ifr], fr - 1] for ifr, fr in enumerate(fragment_separators[1:])]
self.set_fragment_pattern(fragments, ['Real'] * len(fragments), [int(f) for f in molrec['fragment_charges']],
molrec['fragment_multiplicities'])
self.set_molecular_charge(int(molrec['molecular_charge']))
self.set_multiplicity(molrec['molecular_multiplicity'])
self.fix_com(molrec['fix_com'])
self.fix_orientation(molrec['fix_orientation'])
if 'fix_symmetry' in molrec:
# Save the user-specified symmetry, but don't set it as the point group
# That step occurs in update_geometry, after the atoms are added
self.PYsymmetry_from_input = molrec['fix_symmetry'].lower()
## hack to prevent update_geometry termination upon no atoms
#if nat == 0:
# self.set_lock_frame(True)
if not unsettled:
self.update_geometry()
[docs] def BFS(self,
seed_atoms: List = None,
bond_threshold: float = 1.20,
return_arrays: bool = False,
return_molecules: bool = False,
return_molecule: bool = False):
"""Detect fragments among real atoms through a breadth-first search (BFS) algorithm.
Parameters
----------
self : qcdb.Molecule or psi4.core.Molecule
seed_atoms
List of lists of atoms (0-indexed) belonging to independent fragments.
Useful to prompt algorithm or to define intramolecular fragments through
border atoms. Example: `[[1, 0], [2]]`
bond_threshold
Factor beyond average of covalent radii to determine bond cutoff.
return_arrays
If `True`, also return fragments as list of arrays.
return_molecules
If True, also return fragments as list of Molecules.
return_molecule
If True, also return one big Molecule with fragmentation encoded.
Returns
-------
bfs_map : list of lists
Array of atom indices (0-indexed) of detected fragments.
bfs_arrays : tuple of lists of ndarray, optional
geom, mass, elem info per-fragment.
Only provided if `return_arrays` is True.
bfs_molecules : list of qcdb.Molecule or psi4.core.Molecule, optional
List of molecules, each built from one fragment. Center and
orientation of fragments is fixed so orientation info from `self` is
not lost. Loses chgmult and ghost/dummy info from `self` and contains
default chgmult.
Only provided if `return_molecules` is True.
Returned are of same type as `self`.
bfs_molecule : qcdb.Molecule or psi4.core.Molecule, optional
Single molecule with same number of real atoms as `self` with atoms
reordered into adjacent fragments and fragment markers inserted.
Loses ghost/dummy info from `self`; keeps total charge but not total mult.
Only provided if `return_molecule` is True.
Returned is of same type as `self`.
Authors
-------
Original code from Michael S. Marshall, linear-scaling algorithm from
Trent M. Parker, revamped by Lori A. Burns
Notes
-----
Relies upon van der Waals radii and so faulty for close (especially hydrogen-bonded) fragments. See` `seed_atoms``.
Any existing fragmentation info/chgmult encoded in ``self`` is lost.
"""
self.update_geometry()
if self.natom() != self.nallatom():
raise ValidationError("""BFS not adapted for dummy atoms""")
cgeom, cmass, celem, celez, cuniq = self.to_arrays()
frag_pattern = BFS(cgeom, celez, seed_atoms=seed_atoms, bond_threshold=bond_threshold)
outputs = [frag_pattern]
if return_arrays:
fgeoms = [cgeom[fr] for fr in frag_pattern]
fmasss = [cmass[fr] for fr in frag_pattern]
felems = [celem[fr] for fr in frag_pattern]
outputs.append((fgeoms, fmasss, felems))
if return_molecules:
molrecs = [
qcel.molparse.from_arrays(
geom=cgeom[fr],
mass=cmass[fr],
elem=celem[fr],
elez=celez[fr],
units='Bohr',
fix_com=True,
fix_orientation=True) for fr in frag_pattern
]
if isinstance(self, Molecule):
ret_mols = [Molecule.from_dict(molrec) for molrec in molrecs]
else:
from psi4 import core
ret_mols = [core.Molecule.from_dict(molrec) for molrec in molrecs]
outputs.append(ret_mols)
if return_molecule:
dcontig = qcel.molparse.contiguize_from_fragment_pattern(
frag_pattern, geom=cgeom, elez=celez, elem=celem, mass=cmass)
molrec = qcel.molparse.from_arrays(
geom=dcontig['geom'],
mass=dcontig['mass'],
elem=dcontig['elem'],
elez=dcontig['elez'],
units='Bohr',
molecular_charge=self.molecular_charge(),
# molecular_multiplicity may not be conservable upon fragmentation
# potentially could do two passes and try to preserve it
fix_com=self.com_fixed(),
fix_orientation=self.orientation_fixed(),
fix_symmetry=(None if self.symmetry_from_input() == '' else self.symmetry_from_input()),
fragment_separators=dcontig['fragment_separators'])
if isinstance(self, Molecule):
ret_mol = Molecule.from_dict(molrec)
else:
from psi4 import core
ret_mol = core.Molecule.from_dict(molrec)
outputs.append(ret_mol)
outputs = tuple(outputs)
return (frag_pattern, ) + outputs[1:]
[docs] def B787(concern_mol: Union[qcdbmol, psi4.core.Molecule],
ref_mol: Union[qcdbmol, psi4.core.Molecule],
do_plot: bool = False,
verbose: int = 1,
atoms_map: bool = False,
run_resorting: bool = False,
mols_align: bool = False,
run_to_completion: bool = False,
uno_cutoff: float = 1.e-3,
run_mirror: bool = False):
"""Finds shift, rotation, and atom reordering of `concern_mol` that best
aligns with `ref_mol`.
Wraps :py:func:`qcelemental.molutil.B787` for :py:class:`psi4.driver.qcdb.Molecule` or
:py:class:`psi4.core.Molecule`. Employs the Kabsch, Hungarian, and
Uno algorithms to exhaustively locate the best alignment for
non-oriented, non-ordered structures.
Parameters
----------
concern_mol
Molecule of concern, to be shifted, rotated, and reordered into
best coincidence with `ref_mol`.
ref_mol
Molecule to match.
atoms_map
Whether atom1 of `ref_mol` corresponds to atom1 of `concern_mol`, etc.
If true, specifying `True` can save much time.
mols_align
Whether `ref_mol` and `concern_mol` have identical geometries by eye
(barring orientation or atom mapping) and expected final RMSD = 0.
If `True`, procedure is truncated when RMSD condition met, saving time.
do_plot
Pops up a mpl plot showing before, after, and ref geometries.
run_to_completion
Run reorderings to completion (past RMSD = 0) even if unnecessary because
`mols_align=True`. Used to test worst-case timings.
run_resorting
Run the resorting machinery even if unnecessary because `atoms_map=True`.
uno_cutoff
TODO
run_mirror
Run alternate geometries potentially allowing best match to `ref_mol`
from mirror image of `concern_mol`. Only run if system confirmed to
be nonsuperimposable upon mirror reflection.
Returns
-------
float, tuple, qcdb.Molecule or psi4.core.Molecule
First item is RMSD [A] between `ref_mol` and the optimally aligned
geometry computed.
Second item is a AlignmentMill namedtuple with fields
(shift, rotation, atommap, mirror) that prescribe the transformation
from `concern_mol` and the optimally aligned geometry.
Third item is a crude charge-, multiplicity-, fragment-less Molecule
at optimally aligned (and atom-ordered) geometry. Return type
determined by `concern_mol` type.
"""
rgeom, rmass, relem, relez, runiq = ref_mol.to_arrays()
cgeom, cmass, celem, celez, cuniq = concern_mol.to_arrays()
rmsd, solution = qcel.molutil.B787(
cgeom=cgeom,
rgeom=rgeom,
cuniq=cuniq,
runiq=runiq,
do_plot=do_plot,
verbose=verbose,
atoms_map=atoms_map,
run_resorting=run_resorting,
mols_align=mols_align,
run_to_completion=run_to_completion,
run_mirror=run_mirror,
uno_cutoff=uno_cutoff)
ageom, amass, aelem, aelez, auniq = solution.align_system(cgeom, cmass, celem, celez, cuniq, reverse=False)
adict = qcel.molparse.from_arrays(
geom=ageom,
mass=amass,
elem=aelem,
elez=aelez,
units='Bohr',
molecular_charge=concern_mol.molecular_charge(),
molecular_multiplicity=concern_mol.multiplicity(),
fix_com=True,
fix_orientation=True)
if isinstance(concern_mol, Molecule):
amol = Molecule.from_dict(adict)
else:
from psi4 import core
amol = core.Molecule.from_dict(adict)
compare_values(
concern_mol.nuclear_repulsion_energy(),
amol.nuclear_repulsion_energy(),
4,
'Q: concern_mol-->returned_mol NRE uncorrupted',
verbose=verbose - 1)
if mols_align:
compare_values(
ref_mol.nuclear_repulsion_energy(),
amol.nuclear_repulsion_energy(),
4,
'Q: concern_mol-->returned_mol NRE matches ref_mol',
verbose=verbose - 1)
compare_integers(
True,
np.allclose(ref_mol.geometry(), amol.geometry(), atol=4),
'Q: concern_mol-->returned_mol geometry matches ref_mol',
verbose=verbose - 1)
return rmsd, solution, amol
[docs] def scramble(ref_mol: "Molecule",
do_shift: Union[bool, np.ndarray, List] = True,
do_rotate: Union[bool, np.ndarray, List[List]] = True,
do_resort: Union[bool, List] = True,
deflection: float = 1.0,
do_mirror: bool = False,
do_plot: bool = False,
run_to_completion: bool = False,
run_resorting: bool = False,
verbose: int = 1):
"""Tester for B787 by shifting, rotating, and atom shuffling `ref_mol` and
checking that the aligner returns the opposite transformation.
Parameters
----------
ref_mol
Molecule to perturb.
do_shift
Whether to generate a random atom shift on interval [-3, 3) in each
dimension (`True`) or leave at current origin. To shift by a specified
vector, supply a 3-element list.
do_rotate
Whether to generate a random 3D rotation according to algorithm of Arvo.
To rotate by a specified matrix, supply a 9-element list of lists.
do_resort
Whether to shuffle atoms (`True`) or leave 1st atom 1st, etc. (`False`).
To specify shuffle, supply a nat-element list of indices.
deflection
If `do_rotate`, how random a rotation: 0.0 is no change, 0.1 is small
perturbation, 1.0 is completely random.
do_mirror
Whether to construct the mirror image structure by inverting y-axis.
do_plot
Pops up a mpl plot showing before, after, and ref geometries.
run_to_completion
By construction, scrambled systems are fully alignable (final RMSD=0).
Even so, `True` turns off the mechanism to stop when RMSD reaches zero
and instead proceed to worst possible time.
run_resorting
Even if atoms not shuffled, test the resorting machinery.
verbose
Print level.
Returns
-------
None
"""
rgeom, rmass, relem, relez, runiq = ref_mol.to_arrays()
nat = rgeom.shape[0]
perturbation = qcel.molutil.compute_scramble(
rgeom.shape[0],
do_shift=do_shift,
do_rotate=do_rotate,
deflection=deflection,
do_resort=do_resort,
do_mirror=do_mirror)
cgeom, cmass, celem, celez, cuniq = perturbation.align_system(rgeom, rmass, relem, relez, runiq, reverse=True)
cmol = Molecule.from_arrays(
geom=cgeom,
mass=cmass,
elem=celem,
elez=celez,
units='Bohr',
molecular_charge=ref_mol.molecular_charge(),
molecular_multiplicity=ref_mol.multiplicity(),
fix_com=True,
fix_orientation=True)
rmsd = np.linalg.norm(cgeom - rgeom) * qcel.constants.bohr2angstroms / np.sqrt(nat)
if verbose >= 1:
print('Start RMSD = {:8.4f} [A]'.format(rmsd))
rmsd, solution, amol = cmol.B787(
ref_mol,
do_plot=do_plot,
atoms_map=(not do_resort),
run_resorting=run_resorting,
mols_align=True,
run_to_completion=run_to_completion,
run_mirror=do_mirror,
verbose=verbose)
compare_integers(
True, np.allclose(solution.shift, perturbation.shift, atol=6), 'shifts equiv', verbose=verbose - 1)
if not do_resort:
compare_integers(
True,
np.allclose(solution.rotation.T, perturbation.rotation),
'rotations transpose',
verbose=verbose - 1)
if solution.mirror:
compare_integers(True, do_mirror, 'mirror allowed', verbose=verbose - 1)
def set_fragment_pattern(self, frl, frt, frc, frm):
"""Set fragment member data through public method analogous to psi4.core.Molecule"""
if not (len(frl) == len(frt) == len(frc) == len(frm)):
raise ValidationError("""Molecule::set_fragment_pattern: fragment arguments not of same length.""")
self.fragments = frl
self.fragment_types = frt
self.fragment_charges = frc
self.fragment_multiplicities = frm
# Attach methods to qcdb.Molecule class
from .parker import xyz2mol as _parker_xyz2mol_yo
Molecule.format_molecule_for_mol = _parker_xyz2mol_yo