Initialize OpanXYZ geometry object from OpenBabel file

        Import of an arbitrary number of multiple geometries from an OpenBabel
        file. All geometries must have the same number and type of atoms,
        and the ordering of atom types must be retained throughout.

        Parameters
        ----------
        XYZ_path
            |str| --
            Complete path to the .xyz / .trj file to be read

        bohrs
            |bool|, optional --
            Flag for whether coordinates are interpreted as in Bohrs or
            Angstroms. Default is |False| (Angstroms).

        Raises
        ------
        ~opan.error.XYZError
            (typecode :attr:`~opan.error.XYZError.XYZFILE`)
            If indicated geometry file is malformed in some fashion

        ~exceptions.IOError
            If the indicated file does not exist or cannot be read

def _load_file(self, XYZ_path, bohrs=False):
        """ Initialize OpanXYZ geometry object from OpenBabel file

        Import of an arbitrary number of multiple geometries from an OpenBabel
        file. All geometries must have the same number and type of atoms,
        and the ordering of atom types must be retained throughout.

        Parameters
        ----------
        XYZ_path
            |str| --
            Complete path to the .xyz / .trj file to be read

        bohrs
            |bool|, optional --
            Flag for whether coordinates are interpreted as in Bohrs or
            Angstroms. Default is |False| (Angstroms).

        Raises
        ------
        ~opan.error.XYZError
            (typecode :attr:`~opan.error.XYZError.XYZFILE`)
            If indicated geometry file is malformed in some fashion

        ~exceptions.IOError
            If the indicated file does not exist or cannot be read

        """

        # Imports
        import numpy as np
        from .const import CIC, PHYS, atom_num, atom_sym
        from .error import XYZError
        from .utils import safe_cast as scast

        # Complain if already initialized
        if 'geoms' in dir(self):
            raise XYZError(XYZError.OVERWRITE,
                    "Cannot overwrite contents of existing OpanXYZ", "")
        ## end if

        # Open file, read contents, close stream
        # No particular exception handling; that will be the responsibility
        #  of the calling routine.
        # May want to add a check for text file format; but, really, a check
        #  for no-found-geometry-information will likely cover such a case.
        with open(XYZ_path,'rU') as in_fl:
            self.XYZ_path = XYZ_path #: Path of file
            self.in_str = in_fl.read()

        # Check to ensure at least one geom match; else raise some sort of
        #  error
        if not OpanXYZ.p_geom.search(self.in_str):
            raise XYZError(XYZError.XYZFILE,
                    "No valid geometry found",
                    "XYZ file: " + XYZ_path)
        ## end if

        # Store the number of atoms. XYZ files with multiple geometries will
        #  be required to have the same number of atoms in all geometries.
        #  This is fine for Open Anharmonic, but may be highly undesirable
        #  in other applications.  Beware.
        # If the .match() call doesn't have a group attribute, catch the
        #  resulting AttributeError and throw an XYZError. Can't think of any
        #  other situation that would throw an AttributeError here, so probably
        #  I don't have to check for a subtype of AttributeError...
        # Can't think of a need for an else or a finally here
        try:
            self.num_atoms = scast(OpanXYZ.p_geom.match(self.in_str)
                                            .group("num"), np.int_)
        except AttributeError:
            raise XYZError(XYZError.XYZFILE,
                    "No geometry block found at start of file",
                    "XYZ file: " + XYZ_path)
        ## end try

        # Initialize the description vector and geometry array
        # CANNOT use matrix type for the geometry since it's 3D.
        # Retriever function for geometry should convert the given slice
        #  to a vector as an np.matrix, though.
        # Geometry is constructed as a length-3N vector of
        #  coordinates, where atom 1's x/y/z coordinates are sequential,
        #  then atom 2's x/y/z coordinates are sequential, etc.
        # Multiple geometries in a single XYZ file are stacked in a simple
        #  array.
        self.descs = []
        self.geoms = []

        # Initialize the atom symbols vector
        self.atom_syms = []

        # Define a counter for the number of geometries. Store as a persistent
        #  instance variable because it will be useful later.
        self.num_geoms = 0

        # Loop over the geometry blocks found in the input file
        for mch in OpanXYZ.p_geom.finditer(self.in_str):
            # Check that the number of atoms is consistent with the spec
            #  found in the first geometry block
            if not scast(mch.group("num"), np.int_) == self.num_atoms:
                raise XYZError(XYZError.XYZFILE,
                        "Non-constant number of atoms in multiple geometry",
                        "XYZ file: " + XYZ_path)
            ## end if

            # Store the description for the current geometry
            self.descs.append(mch.group("desc"))

            # Assemble the coordinates vector and assemble/check the element
            #  ID vector.
            # Reset the atom counter and the coordinates vector
            atom_count = 0
            coord_vec = np.empty((0,),dtype=np.float_)
            for line_mch in OpanXYZ.p_coords.finditer(mch.group("coord")):
                # Check for whether element list has been fully populated
                if len(self.atom_syms) < self.num_atoms:
                    # If not, continue populating it; have to check for
                    #  whether it's an atomic number or an element symbol
                    if line_mch.group("el").isdigit():
                        # Atomic number
                        # Check for valid number
                        at_num = scast(line_mch.group("el"), np.int_)
                        if not (CIC.MIN_ATOMIC_NUM <= at_num
                                                    <= CIC.MAX_ATOMIC_NUM):
                            raise XYZError(XYZError.XYZFILE,
                                    "Geometry #{0}, atom #{1} is an \
                                        unsupported element"
                                        .format(self.num_geoms, atom_count),
                                     "XYZ file: {0}".format(XYZ_path))
                        ##end if

                        # Tag on the new symbol
                        self.atom_syms.append(atom_sym[at_num])
                    else:
                        # Element symbol; store as all caps
                        # Check for valid element, first by catching if the
                        #  specified element string is even valid
                        try:
                            at_num = atom_num[line_mch.group("el").upper()]
                        except KeyError:
                            raise XYZError(XYZError.XYZFILE,
                                    "Geometry #{0}, atom #{1} is an \
                                        unrecognized element"
                                        .format(self.num_geoms, atom_count),
                                     "XYZ file: {0}".format(XYZ_path))
                        ## end try

                        # Tag on the new symbol
                        self.atom_syms.append(line_mch.group("el").upper())
                    ## end if
                else: # List is fully populated
                    # If so, confirm that the element specified at the current
                    #  line matches that of the first geometry of the file
                    # Have to check whether it's an atomic number or symbol.
                    if line_mch.group("el").isdigit():
                        # Atomic number; don't need safe cast; must trap for
                        #  a bad atomic number
                        at_num = scast(line_mch.group("el"), np.int_)
                        if not (CIC.MIN_ATOMIC_NUM <= at_num
                                                    <= CIC.MAX_ATOMIC_NUM):
                            raise XYZError(XYZError.XYZFILE,
                                    "Geometry #{0}, atom #{1} is an \
                                        unsupported element"
                                        .format(self.num_geoms, atom_count),
                                     "XYZ file: {0}".format(XYZ_path))
                        ## end if
                        if not atom_sym[at_num] == self.atom_syms[atom_count]:
                            raise XYZError(XYZError.XYZFILE,
                                    "Geometry #{0}, atom #{1} is inconsistent \
                                        with geometry #0"
                                        .format(self.num_geoms, atom_count),
                                     "XYZ file: {0}".format(XYZ_path))
                        ## end if

                    else:
                        # Element symbol
                        # Check for valid element, first by catching if the
                        #  specified element string is even valid
                        try:
                            at_num = atom_num[line_mch.group("el").upper()]
                        except KeyError:
                            raise XYZError(XYZError.XYZFILE,
                                    "Geometry #{0}, atom #{1} is an \
                                        unrecognized element"
                                        .format(self.num_geoms, atom_count),
                                     "XYZ file: {0}".format(XYZ_path))
                        ## end try
                        # Confirm symbol matches the initial geometry
                        if not line_mch.group("el").upper() == \
                                                self.atom_syms[atom_count]:
                            raise XYZError(XYZError.XYZFILE,
                                    "Geometry #{0}, atom #{1} is inconsistent \
                                        with geometry #0"
                                        .format(self.num_geoms, atom_count),
                                    "XYZ file: {0}".format(XYZ_path))
                        ## end if
                    ## end if
                ## end if

                # Append the three coordinates of the current atom to the
                #  temp coordinates vector, converting to Bohrs if indicated.
                # Working in Bohrs is desired because they are atomic units
                #  and thus are part of the internal unit definition of the
                #  Hartree.
                for c_str in range(1,4):
                    coord_vec = np.concatenate(
                            (coord_vec, [
                        scast(line_mch.group("c{0}".format(c_str)), np.float_) /
                        (1.0 if bohrs else PHYS.ANG_PER_BOHR)
                                        ]), axis=0)
                ## next c_str

                # Increment the atom_count for the atom number
                atom_count += 1

            ## next line_mch

            # Confirm that number of imported coordinates matches the
            #  number expected from self.num_atoms
            if not coord_vec.shape[0] == 3*self.num_atoms:
                raise XYZError(XYZError.XYZFILE,
                        "Geometry #{0} atom count is inconsistent"
                            .format(self.num_geoms),
                        "XYZ file: {0}".format(XYZ_path))
            ## end if

            # Assemble the coordinates vector into the actual coordinates
            #  stack for the XYZ
            self.geoms.append(coord_vec)

            # Increment the count of the number of geometries. Once the
            #  'mch' iteration is completed, this will accurately reflect
            #  the number of geometries read from the file.
            self.num_geoms += 1