//  [python]
def ot_pca_bcd(
    X, k, reg=1e-3, thresh=1e-4, max_iter=10000,
    P0=None, max_iter_sink=100, max_iter_MM=10,
    verbose=False, log=True, method_sink='sinkhorn',
    method='MM', svd_fct_cpu='numpy', device='cpu',
    warn=True
):
    if verbose is False:
        verbose = 0
    elif verbose is True:
        verbose = 1

    if verbose > 0:
        print()

    assert device in ['cpu', 'cuda']

    if device == 'cuda':
        X = torch.tensor(X, dtype=torch.float64)
        X = X.to(device)

    n, d = X.shape
    X = X - X.mean(0)  # center data
    assert k <= d

    t0 = time.time()
    if P0 is None:
        if device == 'cpu':
            pca_fitted = PCA(n_components=k).fit(X)
            P = pca_fitted.components_.T
            if method == 'MM':
                lambda_scm = pca_fitted.explained_variance_[0]
        elif device == 'cuda':
            P, s, _ = torch.linalg.svd(X.T, full_matrices=False)
            P = P[:, :k]
            if method == 'MM':
                lambda_scm = (1/n)*(s[0]**2)
    else:
        P = P0
    if verbose > 1:
        print('init time:', time.time() - t0)

    # marginals
    if device == 'cpu':
        u0 = (1./n)*np.ones(n)
    elif device == 'cuda':
        u0 = (1./n)*torch.ones(n, dtype=torch.float64).to(device)

    # log
    if log:
        log_ = {}
        log_['P'] = []
        log_['G'] = []
        if device == 'cpu':
            log_['P0'] = P
        elif device == 'gpu':
            log_['P0'] = P.detach().clone().cpu().numpy()
        log_['loss'] = []

    # print iterations
    if verbose > 0:
        if method == 'BCD':
            print('OTPCA BCD')
        elif method == 'MM':
            print('OTPCA MM')
        else:
            raise ValueError('Unknown method...')
        print('{:4s}|{:13s}|{:12s}|{:12s}|{:8s}'.format(
            'It.', 'Loss', 'Crit.', 'Thres.', 'Time.'
        ) + '\n' + '-' * 48)

    # loop
    go = True
    it = 0
    warmstart_sinkhorn = None

    while go:
        st = time.time()
        P_old = P

        # Solve transport
        t0 = time.time()
        M = ot.dist(X, (X@P)@P.T)
        G, log_sinkhorn = ot.sinkhorn(
            u0, u0, M, reg,
            numItermax=max_iter_sink,
            method=method_sink, warmstart=warmstart_sinkhorn,
            warn=warn, log=True
        )
        key_warmstart = 'warmstart'
        if key_warmstart in log_sinkhorn:
            alpha, beta = log_sinkhorn[key_warmstart]
            if type(alpha) == type(beta) == torch.Tensor:
                alpha, beta = alpha.detach(), beta.detach()
            warmstart_sinkhorn = (alpha, beta)
        if (G >= 1e-300).all():
            loss = compute_loss(M, G, reg)
        else:
            if device == 'cpu':
                loss = np.inf
            elif device == 'cuda':
                loss = torch.inf
        if verbose > 1:
            print('OT time:', time.time() - t0)

        # log
        if log:
            if device == 'cpu':
                log_['P'].append(P)
                log_['G'].append(G)
                log_['loss'].append(loss)
            elif device == 'cuda':
                log_['P'].append(P.detach().clone().cpu().numpy())
                log_['G'].append(G.detach().clone().cpu().numpy())
                log_['loss'].append(loss.item())

        # Solve PCA
        t0 = time.time()
        G_sym = (G+G.T)/2

        if method == 'BCD':
            # block coordinate descent
            t0_bcd = time.time()

            if device == 'cpu':
                eye = np.eye(n)
            elif device == 'cuda':
                eye = torch.eye(n).to(device)
            S = X.T @ (2*G_sym-(1./n)*eye) @ X

            if verbose > 1:
                print('BCD S computation time:', time.time() - t0_bcd)
            t1_bcd = time.time()

            if device == 'cpu':
                if svd_fct_cpu == 'scipy':
                    _, U = scipy.sparse.linalg.eigsh(S, k, which='LA')
                    P = U[:, ::-1]
                elif svd_fct_cpu == 'numpy':
                    _, U = np.linalg.eigh(S)
                    P = U[:, ::-1][:, :k]
                else:
                    raise ValueError('Wrong value for svd_fct_cpu...')
            elif device == 'cuda':
                _, U = torch.linalg.eigh(S)
                idx = np.array(np.arange(d)[::-1][:k])
                P = U[:, idx]

            if verbose > 1:
                print('BCD svd computation time:', time.time() - t1_bcd)

        elif method == 'MM':
            # majorization-minimization
            t0_mm = time.time()

            if device == 'cpu':
                if svd_fct_cpu == 'scipy':
                    eig, _ = scipy.sparse.linalg.eigsh(G_sym, k=1, which='SA')
                elif svd_fct_cpu == 'numpy':
                    eig, _ = np.linalg.eigh(G_sym)
                else:
                    raise ValueError('Wrong value for svd_fct_cpu...')
                lambda_G = eig[0]
            elif device == 'cuda':
                raise NotImplementedError

            if verbose > 1:
                print('MM lambda min computation time:', time.time() - t0_mm)

            t_R, t_proj = 0, 0
            for _ in range(max_iter_MM):
                t1_mm = time.time()

                X_proj = X@P
                X_T_X_proj = X.T@X_proj

                R = (1/n) * X_T_X_proj
                alpha = 1 - 2*n*lambda_G
                if alpha > 0:
                    R = alpha*(R - lambda_scm*P)
                else:
                    R = alpha*R

                R = R - (2*X.T@(G_sym@X_proj)) + (2*lambda_G*X_T_X_proj)

                t_R += time.time() - t1_mm
                t2_mm = time.time()

                if device == 'cpu':
                    P, _ = np.linalg.qr(R)
                    # U, s, Vt = np.linalg.svd(R, full_matrices=False)
                    # P = U@Vt
                elif device == 'cuda':
                    P, _ = torch.linalg.qr(R)

                t_proj += time.time() - t2_mm

            if verbose > 1:
                print('MM R computation time:', t_R)
                print('MM proj computation time:', t_proj)

        else:
            raise ValueError('Unknown method...')

        if verbose > 1:
            print('Update U time:', time.time() - t0)

        # stop or not
        crit = Grassmann_distance(P_old, P)
        if crit <= thresh:
            go = False
        it += 1
        if it >= max_iter:
            go = False
            if warn:
                warnings.warn('ot_pca_bcd: max iter reached')

        # print
        ed = time.time()
        if verbose > 0:
            print('{:4d}|{:8e}|{:8e}|{:8e}|{:8e}'.format(
                it, loss, crit, thresh, ed-st))

    if log:
        log_['it'] = it

    if device == 'cuda':
        G = G.detach().clone().cpu().numpy()
        P = P.detach().clone().cpu().numpy()

    if log:
        return G, P, log_
    else:
        return G, P