Check for consistency of two geometries and atom symbol lists

    Cartesian coordinates are considered consistent with the input
    coords if each component matches to within `tol`.  If coords or
    atoms vectors are passed that are of mismatched lengths, a
    |False| value is returned.

    Both coords vectors must be three times the length of the atoms vectors
    or a :exc:`~exceptions.ValueError` is raised.

    Parameters
    ----------
    c1
        length-3N |npfloat_| --
        Vector of first set of stacked 'lab-frame' Cartesian coordinates

    a1
        length-N |str| or |int| --
        Vector of first set of atom symbols or atomic numbers

    c2
        length-3N |npfloat_| --
        Vector of second set of stacked 'lab-frame' Cartesian coordinates

    a2
        length-N |str| or |int| --
        Vector of second set of atom symbols or atomic numbers

    tol
        |float|, optional --
        Tolerance for acceptable deviation of each geometry coordinate
        from that in the reference instance to still be considered
        matching. Default value is specified by
        :attr:`opan.const.DEF.XYZ_COORD_MATCH_TOL`)

    Returns
    -------
    match
        |bool| --
        Whether input coords and atoms match (|True|) or
        not (|False|)

    fail_type
        :class:`~opan.const.EnumCheckGeomMismatch` or |None|
        -- Type of check failure

        If `match` == |True|:

            Returns as |None|

        If `match` == |False|:

            An :class:`~opan.const.EnumCheckGeomMismatch` value
            indicating the reason for the failed match:

                :attr:`~opan.const.EnumCheckGeomMismatch.DIMENSION`
                -- Mismatch in geometry size (number of atoms)

                :attr:`~opan.const.EnumCheckGeomMismatch.COORDS`
                -- Mismatch in one or more coordinates

                :attr:`~opan.const.EnumCheckGeomMismatch.ATOMS`
                -- Mismatch in one or more atoms

    fail_loc
        length-3N |bool| or length-N |bool| or |None| --
        Mismatched elements

        If `match` == |True|:

            Returns as |None|

        If `match` == |False|:

            For "array-level" problems such as a dimension mismatch, a
            |None| value is returned.

            For "element-level" problems, a vector is returned
            indicating positions of mismatch in either `coords` or `atoms`,
            depending on the value of `fail_type`.

                |True| elements indicate **MATCHING** values

                |False| elements mark **MISMATCHES**

    Raises
    ------
    ~exceptions.ValueError
        If a pair of coords & atoms array lengths is inconsistent:

        .. code-block:: python

            if len(c1) != 3 * len(a1) or len(c2) != 3 * len(a2):
                raise ValueError(...)

def check_geom(c1, a1, c2, a2, tol=_DEF.XYZ_COORD_MATCH_TOL):
    """ Check for consistency of two geometries and atom symbol lists

    Cartesian coordinates are considered consistent with the input
    coords if each component matches to within `tol`.  If coords or
    atoms vectors are passed that are of mismatched lengths, a
    |False| value is returned.

    Both coords vectors must be three times the length of the atoms vectors
    or a :exc:`~exceptions.ValueError` is raised.

    Parameters
    ----------
    c1
        length-3N |npfloat_| --
        Vector of first set of stacked 'lab-frame' Cartesian coordinates

    a1
        length-N |str| or |int| --
        Vector of first set of atom symbols or atomic numbers

    c2
        length-3N |npfloat_| --
        Vector of second set of stacked 'lab-frame' Cartesian coordinates

    a2
        length-N |str| or |int| --
        Vector of second set of atom symbols or atomic numbers

    tol
        |float|, optional --
        Tolerance for acceptable deviation of each geometry coordinate
        from that in the reference instance to still be considered
        matching. Default value is specified by
        :attr:`opan.const.DEF.XYZ_COORD_MATCH_TOL`)

    Returns
    -------
    match
        |bool| --
        Whether input coords and atoms match (|True|) or
        not (|False|)

    fail_type
        :class:`~opan.const.EnumCheckGeomMismatch` or |None|
        -- Type of check failure

        If `match` == |True|:

            Returns as |None|

        If `match` == |False|:

            An :class:`~opan.const.EnumCheckGeomMismatch` value
            indicating the reason for the failed match:

                :attr:`~opan.const.EnumCheckGeomMismatch.DIMENSION`
                -- Mismatch in geometry size (number of atoms)

                :attr:`~opan.const.EnumCheckGeomMismatch.COORDS`
                -- Mismatch in one or more coordinates

                :attr:`~opan.const.EnumCheckGeomMismatch.ATOMS`
                -- Mismatch in one or more atoms

    fail_loc
        length-3N |bool| or length-N |bool| or |None| --
        Mismatched elements

        If `match` == |True|:

            Returns as |None|

        If `match` == |False|:

            For "array-level" problems such as a dimension mismatch, a
            |None| value is returned.

            For "element-level" problems, a vector is returned
            indicating positions of mismatch in either `coords` or `atoms`,
            depending on the value of `fail_type`.

                |True| elements indicate **MATCHING** values

                |False| elements mark **MISMATCHES**

    Raises
    ------
    ~exceptions.ValueError
        If a pair of coords & atoms array lengths is inconsistent:

        .. code-block:: python

            if len(c1) != 3 * len(a1) or len(c2) != 3 * len(a2):
                raise ValueError(...)

    """

    # Import(s)
    from ..const import atom_num
    import numpy as np
    from ..const import EnumCheckGeomMismatch as ECGM

    # Initialize return value to success condition
    match = True

    #** Check coords for suitable shape. Assume 1-D np.arrays.
    if not len(c1.shape) == 1:
        # Cannot coerce to vector; complain.
        raise ValueError(("'c1' is not a vector."))
    ## end if
    if not len(c2.shape) == 1:
        # Cannot coerce to vector; complain.
        raise ValueError(("'c2' is not a vector."))
    ## end if

    #** Check atoms for suitable shape. Assume lists of strings, so
    # convert to np.array to check.
    if not len(a1.shape) == 1:
        # Not a vector; complain
        raise ValueError(("'a1' is not a simple list."))
    ## end if
    if not len(a2.shape) == 1:
        # Not a vector; complain.
        raise ValueError(("'a2' is not a simple list."))
    ## end if

    #** Confirm proper lengths of coords vs atoms
    if not c1.shape[0] == 3 * a1.shape[0]:
        raise ValueError("len(c1) != 3*len(a1)")
    ## end if
    if not c2.shape[0] == 3 * a2.shape[0]:
        raise ValueError("len(c2) != 3*len(a2)")
    ## end if

    #** Confirm matching lengths of coords and atoms w/corresponding
    #  objects among the two geometries
    if not c1.shape[0] == c2.shape[0]:
        match = False
        fail_type = ECGM.DIMENSION
        return match, fail_type, None
    ## end if

    #** Element-wise check for geometry match to within 'tol'
    fail_loc = np.less_equal(np.abs(np.subtract(c1,c2)), tol)
    if sum(fail_loc) != c2.shape[0]:
        # Count of matching coordinates should equal the number of
        #  coordinates. If not, complain with 'coord_mismatch' fail type.
        match = False
        fail_type = ECGM.COORDS
        return match, fail_type, fail_loc
    ## end if

    #** Element-wise check for atoms match. Quietly convert both input and
    #  instance atom arrays to atom_nums to allow np.equals comparison.
    if np.issubdtype(a1.dtype, np.dtype('str')):
        # Presume atomic symbol data and attempt conversion
        a1 = np.array([atom_num[e] for e in a1])
    ## end if
    if np.issubdtype(a2.dtype, np.dtype('str')):
        # Presume atomic symbol data and attempt conversion
        a2 = np.array([atom_num[e] for e in a2])
    ## end if
    fail_loc = np.equal(a1, a2)

    #** Perform the test to ensure all atoms match.
    if sum(fail_loc) != a2.shape[0]:
        # Count of matching atoms should equal number of atoms. If not,
        #  complain with the 'atom_mismatch' fail type.
        match = False
        fail_type = ECGM.ATOMS
        return match, fail_type, fail_loc

    #** If reached here, all tests passed; return success.
    return match, None, None