def __group_coaxial_cylinders(cylinders,
                              tol_ang=0.5,
                              tol_lin=0.1,
                              roundDigit=6):
    """According to cylinders' axis, categorize a list of cylinders into a dictionary by using its axis as a key.

    Arguments:
        cylinders {[TopoDS_Face,...]} -- list of TopoDS_Face
        tol_ang {float} -- [Unit - degree] the angle between these two axis below this value will be recognize as two parallel axis
        tol_lin

    Returns:
        {'string': [TopoDS_Face,...]} -- returns a dictionary, key is string of axis and location vector, value is list of TopoDS_Shape
    """
    logging.debug('Entering group_coaxial')
    tol_rad = radians(tol_ang)
    skipList = []
    cyl_ax_grp = {}
    for i in range(0, len(cylinders)):
        if i in skipList:
            continue

        cyl1 = BRepAdaptor_Surface(cylinders[i], True).Cylinder()
        axis1 = cyl1.Axis()
        location1 = cyl1.Location()
        axisDir1 = (round(axis1.Direction().X(), roundDigit),
                    round(axis1.Direction().Y(),
                          roundDigit), round(axis1.Direction().Z(),
                                             roundDigit))
        cylLoc1 = (round(location1.X(),
                         roundDigit), round(location1.Y(), roundDigit),
                   round(location1.Z(), roundDigit))
        key = (axisDir1, cylLoc1)

        if key not in cyl_ax_grp.keys():
            cyl_ax_grp[key] = [cylinders[i]]
        else:
            logging.warning('Error !!! Please check the logic again !')

        for j in range(i + 1, len(cylinders)):
            # logging.debug('i = %d, j = %d' % (i, j))

            if j in skipList:
                # logging.debug('skip !!')
                continue
            cyl2 = BRepAdaptor_Surface(cylinders[j]).Cylinder()
            axis2 = cyl2.Axis()
            if axis1.IsCoaxial(axis2, tol_rad, tol_lin) or axis1.IsCoaxial(
                    axis2.Reversed(), tol_rad, tol_lin):
                # logging.debug('Coaxial !!')
                cyl_ax_grp[key].append(cylinders[j])
                skipList.append(j)
    return cyl_ax_grp
def group_cyl_byPln(solid, distTol=0.5, angTolDeg=5.0):
    cylinders = RecognizeTopo(solid).cylinders()
    cyl_dirGrp = group_cylinders_byAxisDir(cylinders,
                                           anglTolDeg=angTolDeg,
                                           groupParallelAx=True)
    cyl_dirGrpKeys = list(cyl_dirGrp.keys())

    # group cylinders by plane
    CylinderGrpsFromDiffPln = {}
    j = 0
    for dirKey in cyl_dirGrpKeys:
        CylinderGrpsFromDiffPln[dirKey] = []
        parallel_cyl_grps = cyl_dirGrp[dirKey].copy()
        grp_cylinderOnSamePln = {}
        while len(parallel_cyl_grps) >= 1:
            # take out the first element for creating plane, and search the other cylinders on the same plane
            firstCylinder = parallel_cyl_grps.pop(0)
            # fitting a geomety surface to TopoDS_Surface
            brepCylinder = BRepAdaptor_Surface(firstCylinder).Cylinder()
            dir1 = brepCylinder.Axis().Direction()
            # location of cylinder is usually the location of local coordinate system, which is on the axis of cylinder
            loc1 = brepCylinder.Location()
            # create gp_Pln
            pln = gp_Pln(loc1, dir1)
            grp_cylinderOnSamePln[pln] = [firstCylinder]
            # Search the cylinders on the same pln, if yes, extract from parallel_cyl_grps
            # loop all elements in parallel_cyl_grps
            while j < len(parallel_cyl_grps):
                brepCylinder2 = BRepAdaptor_Surface(
                    parallel_cyl_grps[j]).Cylinder()
                loc2 = brepCylinder2.Location()
                if pln.Distance(gp_Pnt(loc2.X(), loc2.Y(),
                                       loc2.Z())) < distTol:
                    grp_cylinderOnSamePln[pln].append(parallel_cyl_grps[j])
                    parallel_cyl_grps.pop(j)
                else:
                    j += 1
            j = 0
        CylinderGrpsFromDiffPln[dirKey] = grp_cylinderOnSamePln
    return CylinderGrpsFromDiffPln