def kruskal_align(U, V, permute_U=False, permute_V=False):
    unrm = [f / np.linalg.norm(f, axis=0) for f in U.factors]
    vnrm = [f / np.linalg.norm(f, axis=0) for f in V.factors]
    sim_matrices = [np.dot(u.T, v) for u, v in zip(unrm, vnrm)]
    cost = 1 - np.mean(np.abs(sim_matrices), axis=0)
    indices = Munkres().compute(cost.copy())
    prmU, prmV = zip(*indices)
    similarity = np.mean(1 - cost[prmU, prmV])
    unmatched_U = list(set(range(U.rank)) - set(prmU))
    unmatched_V = list(set(range(V.rank)) - set(prmV))
    if permute_U and permute_V:
        idx = np.argsort(cost[prmU, prmV])
    elif permute_V:
        idx = np.argsort(prmU)
    elif permute_U:
        idx = np.argsort(prmV)
    else:
        return similarity
    prmU = [prmU[i] for i in idx]
    prmV = [prmV[i] for i in idx]
    if permute_U:
        U.permute(prmU)
    if permute_V:
        V.permute(prmV)
    flips = np.sign([F[prmU, prmV] for F in sim_matrices])
    flips[0] *= np.prod(flips, axis=0)
    if permute_U:
        for i, f in enumerate(flips):
            U.factors[i] *= f
    elif permute_V:
        for i, f in enumerate(flips):
            V.factors[i] *= f
    return similarity