//  [python]
def get_symmetry(xyz, mask, rms_cut=2.5, nfold_cut=0.1, angle_cut=0.05, trans_cut=2.0):
    nops = xyz.shape[0]
    L = xyz.shape[1]//2

    # PASS 1: find all symm axes
    symmaxes = []
    for i in range(nops):
        # if there are multiple biomt records, this may occur.
        # rather than try to rescue, we will take the 1st (typically author-assigned)
        offset0 = torch.linalg.norm(xyz[i,:L,1]-xyz[0,:L,1], dim=-1)
        if (torch.mean(offset0)>1e-4):
            continue

        # get alignment
        mask_i = mask[i,:L,1]*mask[i,L:,1]
        xyz_i = xyz[i,:L,1][mask_i,:]
        xyz_j = xyz[i,L:,1][mask_i,:]
        rms_ij, Uij, cI, cJ = kabsch(xyz_i, xyz_j)
        if (rms_ij > rms_cut):
            #print (i,'rms',rms_ij)
            continue

        # get axis and point symmetry about axis
        angle, axis = rotation_from_matrix(Uij)
        nfold = 2*np.pi/torch.abs(angle)
        # a) ensure integer # of subunits per rotation
        if (torch.abs( nfold - torch.round(nfold) ) > nfold_cut ):
            #print ('nfold fail',nfold)
            continue
        nfold = torch.round(nfold).long()
        # b) ensure rotation only (no translation)
        delCOM = torch.mean(xyz_i, dim=-2) - torch.mean(xyz_j, dim=-2)
        trans_dot_symaxis = nfold * torch.abs(torch.dot(delCOM, axis))
        if (trans_dot_symaxis > trans_cut ):
            #print ('trans fail',trans_dot_symaxis)
            continue


        # 3) get a point on the symm axis from CoMs and angle 
        cIJ = torch.sign(angle) * (cJ-cI).squeeze(0)
        dIJ = torch.linalg.norm(cIJ)
        p_mid = (cI+cJ).squeeze(0) / 2
        u = cIJ / dIJ            # unit vector in plane of circle
        v = torch.cross(axis, u, dim=-1) # unit vector from sym axis to p_mid
        r = dIJ / (2*torch.sin(angle/2))
        d = torch.sqrt( r*r - dIJ*dIJ/4 ) # distance from mid-chord to center
        point = p_mid - (d)*v

        # check if redundant
        toadd = True
        for j,(nf_j,ax_j,pt_j,err_j) in enumerate(symmaxes):
            if (not intersect(pt_j,ax_j,point,axis)):
                continue
            angle_j = get_angle(ax_j,axis)
            if (angle_j < angle_cut):
                if (nf_j < nfold): # stored is a subsymmetry of complex, overwrite
                    symmaxes[j] = (nfold, axis, point, i)
                toadd = False

        if (toadd):
            symmaxes.append( (nfold, axis, point, i) )

    # PASS 2: combine
    symmgroup = 'C1'
    subsymm = []
    if len(symmaxes)==1:
        symmgroup = 'C%d'%(symmaxes[0][0])
        subsymm = [symmaxes[0][3]]
    elif len(symmaxes)>1:
        symmaxes = sorted(symmaxes, key=lambda x: x[0], reverse=True)
        angle = get_angle(symmaxes[0][1],symmaxes[1][1])
        subsymm = [symmaxes[0][3],symmaxes[1][3]]

        # 2-fold and n-fold intersect at 90 degress => Dn
        if (symmaxes[1][0] == 2 and torch.abs(angle-np.pi/2) < angle_cut):
            symmgroup = 'D%d'%(symmaxes[0][0])
        else:
            # polyhedral rules:
            #   3-Fold + 2-fold intersecting at acos(-1/sqrt(3)) -> T
            angle_tgt = np.arccos(-1/np.sqrt(3))
            if (symmaxes[0][0] == 3 and symmaxes[1][0] == 2 and torch.abs(angle - angle_tgt) < angle_cut):
                symmgroup = 'T'

            #   3-Fold + 2-fold intersecting at asin(1/sqrt(3)) -> O
            angle_tgt = np.arcsin(1/np.sqrt(3))
            if (symmaxes[0][0] == 3 and symmaxes[1][0] == 2 and torch.abs(angle - angle_tgt) < angle_cut):
                symmgroup = 'O'

            #   4-Fold + 3-fold intersecting at acos(1/sqrt(3)) -> O
            angle_tgt = np.arccos(1/np.sqrt(3))
            if (symmaxes[0][0] == 4 and symmaxes[1][0] == 3 and torch.abs(angle - angle_tgt) < angle_cut):
                symmgroup = 'O'

            #   3-Fold + 2-fold intersecting at 0.5*acos(sqrt(5)/3) -> I
            angle_tgt = 0.5*np.arccos(np.sqrt(5)/3)
            if (symmaxes[0][0] == 3 and symmaxes[1][0] == 2 and torch.abs(angle - angle_tgt) < angle_cut):
                symmgroup = 'I'

            #   5-Fold + 2-fold intersecting at 0.5*acos(1/sqrt(5)) -> I
            angle_tgt = 0.5*np.arccos(1/np.sqrt(5))
            if (symmaxes[0][0] == 5 and symmaxes[1][0] == 2 and torch.abs(angle - angle_tgt) < angle_cut):
                symmgroup = 'I'

            #   5-Fold + 3-fold intersecting at 0.5*acos((4*sqrt(5)-5)/15) -> I
            angle_tgt = 0.5*np.arccos((4*np.sqrt(5)-5)/15)
            if (symmaxes[0][0] == 5 and symmaxes[1][0] == 3 and torch.abs(angle - angle_tgt) < angle_cut):
                symmgroup = 'I'
            else:
                pass
                #fd: we could use a single symmetry here instead.  
                #    But these cases mostly are bad BIOUNIT annotations...
                #print ('nomatch',angle, [(x,y) for x,_,_,y in symmaxes])

    return symmgroup, subsymm