//  [python]
def solve_fantope_relaxation_fr(X,Y,d=1,init_beta=None,device="cpu", out_iters=50000, in_iters_adv=1,
                             in_iters_clf = 1, epsilon = 0.001, batch_size = 16, replay = False):

    def get_loss_fn(X,Y,alpha,beta, bce_loss_fn, optimize_beta=False):
        I = torch.eye(X.shape[1]).to(device)
        bce = bce_loss_fn(alpha(X@(I - beta)).squeeze(), Y) + ALPHA * alpha.weight.data.norm()**2 
        if optimize_beta:
            bce = -bce
            
        return bce

    def solve_linear_minimization(grad, iters=15):

         s = torch.randn_like(grad)
         s.requires_grad = True
         optimizer = torch.optim.SGD([s], lr = 1e-2, momentum = 0.8)
         for i in range(iters):
                optimizer.zero_grad()
                loss = (grad*s).sum()
                loss.backward()
                optimizer.step()
           
                # project
            
                D,U = torch.linalg.eigh(symmetric(s))
                D = D.detach().cpu().numpy()
                D_plus_diag = solve_constraint(D,d=d)
                D = torch.tensor(np.diag(D_plus_diag).real).float().to(device)
                s.data = U@D@U.T
         return s
    
    
    # init models
    X_torch = torch.tensor(X).float().to(device)
    Y_torch = torch.tensor(Y).float().to(device)
    
    w = torch.nn.Linear(X.shape[1], 1).to(device) 
    A = torch.randn(d, X.shape[1]).to(device)
    #A = A / A.norm(dim=1, keepdim=True)
    beta = 1e-2*torch.randn(X.shape[1], X.shape[1]).to(device) #(A.T@A).to(device)
    print('inited')
    
    w.requires_grad = True
    beta.requires_grad = True
    
    # optimizers
    
    optimizer_w = ExtraSGD(w.parameters(), lr=0.1, momentum=0.9) #torch.optim.SGD(w.parameters(), lr = 1e-3, momentum = 0.6)
    optimizer_adv = ExtraSGD([beta], lr=0.1, momentum=0.9) #torch.optim.SGD([beta],  lr = 1e-3, momentum = 0.6)

    # loss, recordings
    
    bce_loss_fn = torch.nn.BCEWithLogitsLoss()
    ws = [w.weight.data.detach().cpu().numpy().copy()]
    advs = [beta.detach().cpu().numpy().copy()]
    
    grads_beta = []
    grads_alpha = []
    best_adv, best_score = None, 1
    count_examples=0.
    best_adv = beta.detach().cpu().numpy().copy()
    print("starting...")
    for i in range(out_iters):
        
        gamma = 2/(2+i)
        
        for j in range(in_iters_adv):
            print(j)
            beta = symmetric(beta)
            optimizer_adv.zero_grad()
            
            idx = np.arange(0,X_torch.shape[0])
            np.random.shuffle(idx)
            X_batch, Y_batch = X_torch[idx[:batch_size]], Y_torch[idx[:batch_size]]
            
            loss_adv = get_loss_fn(X_batch, Y_batch, w, symmetric(beta), bce_loss_fn, optimize_beta = True)
            loss_adv.backward()
            optimizer_adv.step()
            grads_beta.append(beta.grad.detach().cpu().norm().numpy())
            
            s = solve_linear_minimization(beta.grad.detach().cpu())
            if np.random.rand() < 1e-2: print((beta.data-s).norm())
            beta.data += 0.1*s
            advs.append(beta.detach().cpu().numpy().copy())
            
        for j in range(in_iters_clf):
            
            optimizer_w.zero_grad()
            idx = np.arange(0,X_torch.shape[0])
            np.random.shuffle(idx)
            X_batch, Y_batch = X_torch[idx[:batch_size]], Y_torch[idx[:batch_size]]
            
            loss_w = get_loss_fn(X_batch, Y_batch, w, symmetric(beta), bce_loss_fn, optimize_beta = False)
            loss_w.backward()
            grads_alpha.append(w.weight.grad.detach().cpu().norm().numpy())
            optimizer_w.step()
            #w.weight.data = (1-gamma) * w.weight.data + gamma*(-w.weight.grad)
            #w.bias.data = (1-gamma) * w.bias.data + gamma*(-w.bias.grad)

        count_examples += batch_size    
            
        if i % 50 == 0:
            diffs_ws = np.mean([np.linalg.norm(ws[i]-ws[i-1]) for i in range(1,len(ws))])
            diffs_advs = np.mean([np.linalg.norm(advs[i]-advs[i-1]) for i in range(1,len(advs))])
            
            ws = ws[-1000:]
            advs = advs[-250:]
            
            print("{}/{}".format(i, out_iters))
  
            score = get_score(X,Y, advs[-1], d)
            print("Accuracy after projection:{}".format(score))
            if score < best_score:
                best_adv, best_score = symmetric(beta).detach().cpu().numpy().copy(), score
            grads_alpha = []
            grads_beta = []
            
            if i > 5 and np.abs(best_score - max(Y.mean(),1-Y.mean())) < epsilon:
                advs.append(symmetric(beta).detach().cpu().numpy().copy())
                return ws, advs, best_adv
    print("=============================")
    print("Best score is: {}".format(best_score))
    advs.append(symmetric(beta).detach().cpu().numpy().copy())
    return ws, advs, best_adv