code: def semi_dual_obj_grad(alpha, a, b, C, regul):
      
      obj = np.dot(alpha, a)
      grad = a.copy()
  
      # X[:, j] = alpha - C[:, j]
      X = alpha[:, np.newaxis] - C
  
      # val.shape = len(b)
      # G.shape = len(a) x len(b)
      val, G = regul.max_Omega(X, b)
  
      obj -= np.dot(b, val)
      grad -= np.dot(G, b)
  
      return obj, grad
docs: Compute objective value and gradient of semi-dual objective.
  
      Parameters
      ----------
      alpha: array, shape = len(a)
          Current iterate of semi-dual potentials.
      a: array, shape = len(a)
      b: array, shape = len(b)
          Input histograms (should be non-negative and sum to 1).
      C: array, shape = len(a) x len(b)
          Ground cost matrix.
      regul: Regularization object
          Should implement a max_Omega(X) method.
  
      Returns
      -------
      obj: float
          Objective value (higher is better).
      grad: array, shape = len(a)
          Gradient w.r.t. alpha.