# Source code for psi4.driver.qcdb.molecule

    #
    # @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 os
    import hashlib
    import collections

    import numpy as np

    import qcelemental as qcel

    from .util import parse_dertype
    from .libmintsmolecule import *
    from .psiutil import compare_values, compare_integers, compare_molrecs
    from .bfs import BFS

    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

        def to_arrays(self, dummy=False, ghost_as_dummy=False):
            """Exports coordinate info into NumPy arrays.

            Parameters
            ----------
            dummy : bool, optional
                Whether or not to include dummy atoms in returned arrays.
            ghost_as_dummy : bool, optional
                Whether or not to treat ghost atoms as dummies.

            Returns
            -------
            geom, mass, elem, elez, uniq : ndarray, ndarray, ndarray, ndarray, 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)

        def to_string(self, dtype, units='Angstrom', atom_format=None, ghost_format=None, width=17, prec=12):
            """Format a string representation of QM molecule."""

            molrec = self.to_dict(np_out=True)
            smol = qcel.molparse.to_string(
                molrec,
                dtype=dtype,
                units=units,
                atom_format=atom_format,
                ghost_format=ghost_format,
                width=width,
                prec=prec)
            return smol

        def run_dftd3(self, func=None, dashlvl=None, dashparam=None, dertype=None, verbose=1):
            """Compute dispersion correction via Grimme's DFTD3 program.

            Parameters
            ----------
            func : str, optional
                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 : str, optional
                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 : dict, optional
                Values for the same keys as `dashcoeff[dashlvl]['default']`
                used to override any or all values initialized by `func`.
                Extra parameters will error.
            dertype : int or str, optional
                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 : int, optional
                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].

            """
            from . import intf_dftd3

            if dertype is None:
                derint, derdriver = -1, 'gradient'
            else:
                derint, derdriver = parse_dertype(dertype, max_derivative=1)

            jobrec = intf_dftd3.run_dftd3_from_arrays(
                molrec=self.to_dict(np_out=False),
                name_hint=func,
                level_hint=dashlvl,
                param_tweaks=dashparam,
                ptype=derdriver,
                verbose=verbose)

            if isinstance(self, Molecule):
                pass
            else:
                from psi4 import core

                for k, qca in jobrec['qcvars'].items():
                    if not isinstance(qca.data, np.ndarray):
                        core.set_variable(k, float(qca.data))

            if derint == -1:
                return (float(jobrec['qcvars']['DISPERSION CORRECTION ENERGY'].data),
                        jobrec['qcvars']['DISPERSION CORRECTION GRADIENT'].data)
            elif derint == 0:
                return float(jobrec['qcvars']['DISPERSION CORRECTION ENERGY'].data)
            elif derint == 1:
                return jobrec['qcvars']['DISPERSION CORRECTION GRADIENT'].data

        @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)

        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

        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."""
                )
                molrec['fragment_charges'] = [None] * len(fragments)
                molrec['fragment_multiplicities'] = [None] * len(fragments)
                validated_molrec = qcel.molparse.from_arrays(speclabel=False, verbose=0, domain='qm', **molrec)
                forgive.append('fragment_charges')
                forgive.append('fragment_multiplicities')
            compare_molrecs(validated_molrec, molrec, 6, 'to_dict', 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=np.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()

        def BFS(self,
                seed_atoms=None,
                bond_threshold=1.20,
                return_arrays=False,
                return_molecules=False,
                return_molecule=False):
            """Detect fragments among real atoms through a breadth-first search (BFS) algorithm.

            Parameters
            ----------
            self : qcdb.Molecule or psi4.core.Molecule
            seed_atoms : list, optional
                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 : float, optional
                Factor beyond average of covalent radii to determine bond cutoff.
            return_arrays : bool, optional
                If `True`, also return fragments as list of arrays.
            return_molecules : bool, optional
                If True, also return fragments as list of Molecules.
            return_molecule : bool, optional
                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`.

            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.

            Authors
            -------
            Original code from Michael S. Marshall, linear-scaling algorithm from
            Trent M. Parker, revamped by Lori A. Burns

            """
            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:]

        def B787(concern_mol,
                 ref_mol,
                 do_plot=False,
                 verbose=1,
                 atoms_map=False,
                 run_resorting=False,
                 mols_align=False,
                 run_to_completion=False,
                 uno_cutoff=1.e-3,
                 run_mirror=False):
            """Finds shift, rotation, and atom reordering of `concern_mol` that best
            aligns with `ref_mol`.

            Wraps :py:func:`qcdb.align.B787` for :py:class:`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 : qcdb.Molecule or psi4.core.Molecule
                Molecule of concern, to be shifted, rotated, and reordered into
                best coincidence with `ref_mol`.
            ref_mol : qcdb.Molecule or psi4.core.Molecule
                Molecule to match.
            atoms_map : bool, optional
                Whether atom1 of `ref_mol` corresponds to atom1 of `concern_mol`, etc.
                If true, specifying `True` can save much time.
            mols_align : bool, optional
                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 : bool, optional
                Pops up a mpl plot showing before, after, and ref geometries.
            run_to_completion : bool, optional
                Run reorderings to completion (past RMSD = 0) even if unnecessary because
                `mols_align=True`. Used to test worst-case timings.
            run_resorting : bool, optional
                Run the resorting machinery even if unnecessary because `atoms_map=True`.
            uno_cutoff : float, optional
                TODO
            run_mirror : bool, optional
                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.

            """
            from .align import B787

            rgeom, rmass, relem, relez, runiq = ref_mol.to_arrays()
            cgeom, cmass, celem, celez, cuniq = concern_mol.to_arrays()

            rmsd, solution = 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

        def scramble(ref_mol,
                     do_shift=True,
                     do_rotate=True,
                     do_resort=True,
                     deflection=1.0,
                     do_mirror=False,
                     do_plot=False,
                     run_to_completion=False,
                     run_resorting=False,
                     verbose=1):
            """Tester for B787 by shifting, rotating, and atom shuffling `ref_mol` and
            checking that the aligner returns the opposite transformation.

            Parameters
            ----------
            ref_mol : qcdb.Molecule or psi4.core.Molecule
                Molecule to perturb.
            do_shift : bool or array-like, optional
                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 : bool or array-like, optional
                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 : bool or array-like, optional
                Whether to shuffle atoms (`True`) or leave 1st atom 1st, etc. (`False`).
                To specify shuffle, supply a nat-element list of indices.
            deflection : float, optional
                If `do_rotate`, how random a rotation: 0.0 is no change, 0.1 is small
                perturbation, 1.0 is completely random.
            do_mirror : bool, optional
                Whether to construct the mirror image structure by inverting y-axis.
            do_plot : bool, optional
                Pops up a mpl plot showing before, after, and ref geometries.
            run_to_completion : bool, optional
                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 : bool, optional
                Even if atoms not shuffled, test the resorting machinery.
            verbose : int, optional
                Print level.

            Returns
            -------
            None

            """
            from .align import compute_scramble

            rgeom, rmass, relem, relez, runiq = ref_mol.to_arrays()
            nat = rgeom.shape[0]

            perturbation = 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
    from .interface_gcp import run_gcp as _gcp_qcdb_yo
    Molecule.run_gcp = _gcp_qcdb_yo