docs: def admm(X, prox_f, step_f, prox_g=None, step_g=None, L=None, e_rel=1e-6, e_abs=0, max_iter=1000, traceback=None):
      """Alternating Direction Method of Multipliers
  
      This method implements the linearized ADMM from Parikh & Boyd (2014).
  
      Args:
          X: initial X will be updated
          prox_f: proxed function f
          step_f: step size for prox_f
          prox_g: proxed function g
          step_g: specific value of step size for prox_g (experts only!)
              By default, set to the maximum value of step_f * ||L||_s^2.
          L: linear operator of the argument of g.
              Matrix can be numpy.array, scipy.sparse, or None (for identity).
          e_rel: relative error threshold for primal and dual residuals
          e_abs: absolute error threshold for primal and dual residuals
          max_iter: maximum iteration number, irrespective of current residuals
          traceback: utils.Traceback to hold variable histories
  
      Returns:
          converged: whether the optimizer has converged within e_rel
          error: X^it - X^it-1
  
      Reference:
          Moolekamp & Melchior, Algorithm 1 (arXiv:1708.09066)
      """
  
      # use matrix adapter for convenient & fast notation
      _L = utils.MatrixAdapter(L)
      # get/check compatible step size for g
      if prox_g is not None and step_g is None:
          step_g = utils.get_step_g(step_f, _L.spectral_norm)
  
      # init
      Z,U = utils.initZU(X, _L)
      it = 0
  
      if traceback is not None:
          traceback.update_history(it, X=X, step_f=step_f, Z=Z, U=U, R=np.zeros(Z.shape, dtype=Z.dtype), S=np.zeros(X.shape, dtype=X.dtype), step_g=step_g)
  
      while it < max_iter:
  
          # Update the variables, return LX and primal/dual residual
          LX, R, S = utils.update_variables(X, Z, U, prox_f, step_f, prox_g, step_g, _L)
  
          # Optionally store the variables in the history
          if traceback is not None:
              traceback.update_history(it+1, X=X, step_f=step_f, Z=Z, U=U, R=R, S=S,  step_g=step_g)
  
          # convergence criteria, adapted from Boyd 2011, Sec 3.3.1
          converged, error = utils.check_constraint_convergence(X, _L, LX, Z, U, R, S,
                                                                  step_f, step_g, e_rel, e_abs)
  
          if converged:
              break
  
          it += 1
  
          # if X and primal residual does not change: decrease step_f and step_g, and restart
          if prox_g is not None:
              if it > 1:
                  if (X == X_).all() and (R == R_).all():
                      step_f /= 2
                      step_g /= 2
                      # re-init
                      it = 0
  
                      Z,U  = utils.initZU(X, _L)
                      logger.info("Restarting with step_f = %.3f" % step_f)
                      if traceback is not None:
                          traceback.reset()
                          traceback.update_history(it, X=X, Z=Z, U=U, R=np.zeros(Z.shape, dtype=Z.dtype), S=np.zeros(X.shape, dtype=X.dtype), step_f=step_f, step_g=step_g)
              X_ = X.copy()
              R_ = R
  
      logger.info("Completed {0} iterations".format(it+1))
      if not converged:
          logger.warning("Solution did not converge")
  
      return converged, error