//  [python]
def get_internal_coordinate(
    cv: list, 
    coords: torch.tensor, 
    ndim: int=3,
) -> torch.tensor:
    """Get internal coordinate 

    Args:
        cv: definition of internal coordinate
            Available:
                ["distance",     [idx0, idx1]]
                ["angle",        [idx0, idx1, idx2]]
                ["torsion",     [idx0, idx1, idx2, idx3]]
                ["min_distance", [[idx0, idx1], [idx2, idx3], ...]
                ["coordination_number", [[idx0, idx1], [...], r_0, exp_nom, exp_denom]]]
        coords: Cartesian coordinates
        ndim: Number of dimensions of coords
    
    Returns:
        cv: internal coordinate
    """
    z = coords.view(int(torch.numel(coords)/ndim), ndim)
    
    if cv[0].lower() == "distance":
        xi = torch.linalg.norm(z[cv[1][0]] - z[cv[1][1]])

    elif cv[0].lower() == "angle":
        q12 = z[cv[1][0]] - z[cv[1][1]]
        q23 = z[cv[1][1]] - z[cv[1][2]]

        q12_n = torch.linalg.norm(q12)
        q23_n = torch.linalg.norm(q23)
                
        q12_u = q12 / q12_n
        q23_u = q23 / q23_n

        xi = torch.arccos(torch.dot(-q12_u, q23_u))  
                
    elif cv[0].lower() == "torsion":
        q12 = z[cv[1][1]] - z[cv[1][0]]
        q23 = z[cv[1][2]] - z[cv[1][1]]
        q34 = z[cv[1][3]] - z[cv[1][2]]

        q23_u = q23 / torch.linalg.norm(q23)
        n1 = -q12 - torch.dot(-q12, q23_u) * q23_u
        n2 = q34 - torch.dot(q34, q23_u) * q23_u
        
        xi = torch.atan2(
            torch.dot(torch.cross(q23_u, n1), n2), torch.dot(n1, n2)
        )

    elif cv[0].lower() == "min_distance":
        # returns minimum distance of list of distances
        dists = []
        for x in cv[1]:
            dists.append(torch.linalg.norm(z[x[0]] - z[x[1]]))
        xi = min(dists)
    
    elif cv[0].lower() == "coordination_number":
        
        cv_def    = cv[1]
        exp_denom = int(cv_def[-1])
        exp_nom   = int(cv_def[-2])
        r_0       = float(cv_def[-3]) / BOHR_to_ANGSTROM

        xi = 0.0
        for atoms in cv_def[:-3]:
            r12 = torch.linalg.norm(z[atoms[1]] - z[atoms[0]])

            # for numerical stability
            if abs(r12-r_0) < 1.e-6:
                r = r12 / (r_0 * 1.000001)
            else:
                r = r12 / r_0

            nom   = 1. - torch.pow(r, exp_nom)
            denom = 1. - torch.pow(r, exp_denom)     
            xi += nom / denom

    elif cv[0].lower() == "cec":
        # Modified Center-of-Excess Charge (mCEC) for Proton Transfer (PT)
        from .proton_transfer import PT
        pt_def = cv[1]
        pt_cv = PT(
            r_sw   = pt_def.get("r_sw", 1.4),
            d_sw   = pt_def.get("d_sw", 0.05),
            n_pair = pt_def.get("n_pair", 15), 
            requires_grad = True,
        )
        xi = pt_cv.gmcec(
            coords, 
            pt_def["proton_idx"],
            pt_def["heavy_idx"],
            pt_def["heavy_weights"],
            pt_def["ref_idx"],
            pair_def=pt_def.get("pair_def", []),
            mapping=pt_def.get("mapping", "default"),
        )

    else:
        raise ValueError(" >>> ERROR: wrong definition of internal coordinate!")
        
    return xi