docstr: Learn full metric using MMC.
  
  Parameters
  ----------
  X : (n x d) data matrix
    Each row corresponds to a single instance.
  constraints : 4-tuple of arrays
    (a,b,c,d) indices into X, with (a,b) specifying similar and (c,d)
    dissimilar pairs.
docstr_tok: ["Learn","full","metric","using","MMC",".","Parameters","--","--","--","--","--","X",":","(","n","x","d",")","data","matrix","Each","row","corresponds","to","a","single","instance",".","constraints",":","4-tuple","of","arrays","(","a",",","b",",","c",",","d",")","indices","into","X",",","with","(","a",",","b",")","specifying","similar","and","(","c",",","d",")","dissimilar","pairs","."]
code: def _fit_full(self, pairs, y):
      """Learn full metric using MMC.
  
      Parameters
      ----------
      X : (n x d) data matrix
        Each row corresponds to a single instance.
      constraints : 4-tuple of arrays
        (a,b,c,d) indices into X, with (a,b) specifying similar and (c,d)
        dissimilar pairs.
      """
      num_dim = pairs.shape[2]
  
      error2 = 1e10
      eps = 0.01  # error-bound of iterative projection on C1 and C2
      A = self.A_
  
      pos_pairs, neg_pairs = pairs[y == 1], pairs[y == -1]
  
      # Create weight vector from similar samples
      pos_diff = pos_pairs[:, 0, :] - pos_pairs[:, 1, :]
      w = np.einsum("ij,ik->jk", pos_diff, pos_diff).ravel()
      # `w` is the sum of all outer products of the rows in `pos_diff`.
      # The above `einsum` is equivalent to the much more inefficient:
      # w = np.apply_along_axis(
      #         lambda x: np.outer(x,x).ravel(),
      #         1,
      #         X[a] - X[b]
      #     ).sum(axis = 0)
      t = w.dot(A.ravel()) / 100.0
  
      w_norm = np.linalg.norm(w)
      w1 = w / w_norm  # make `w` a unit vector
      t1 = t / w_norm  # distance from origin to `w^T*x=t` plane
  
      cycle = 1
      alpha = 0.1  # initial step size along gradient
      grad1 = self._fS1(pos_pairs, A)  # gradient of similarity
      # constraint function
      grad2 = self._fD1(neg_pairs, A)  # gradient of dissimilarity
      # constraint function
      # gradient of fD1 orthogonal to fS1:
      M = self._grad_projection(grad1, grad2)
  
      A_old = A.copy()
  
      for cycle in range(self.max_iter):
          # projection of constraints C1 and C2
          satisfy = False
  
          for it in range(self.max_proj):
              # First constraint:
              # f(A) = \sum_{i,j \in S} d_ij' A d_ij <= t              (1)
              # (1) can be rewritten as a linear constraint: w^T x = t,
              # where x is the unrolled matrix of A,
              # w is also an unrolled matrix of W where
              # W_{kl}= \sum_{i,j \in S}d_ij^k * d_ij^l
              x0 = A.ravel()
              if w.dot(x0) <= t:
                  x = x0
              else:
                  x = x0 + (t1 - w1.dot(x0)) * w1
                  A[:] = x.reshape(num_dim, num_dim)
  
              # Second constraint:
              # PSD constraint A >= 0
              # project A onto domain A>0
              l, V = np.linalg.eigh((A + A.T) / 2)
              A[:] = np.dot(V * np.maximum(0, l[None, :]), V.T)
  
              fDC2 = w.dot(A.ravel())
              error2 = (fDC2 - t) / t
              if error2 < eps:
                  satisfy = True
                  break
  
          # third constraint: gradient ascent
          # max: g(A) >= 1
          # here we suppose g(A) = fD(A) = \sum_{I,J \in D} sqrt(d_ij' A d_ij)
  
          obj_previous = self._fD(neg_pairs, A_old)  # g(A_old)
          obj = self._fD(neg_pairs, A)  # g(A)
  
          if satisfy and (obj > obj_previous or cycle == 0):
              # If projection of 1 and 2 is successful, and such projection
              # improves objective function, slightly increase learning rate
              # and update from the current A.
              alpha *= 1.05
              A_old[:] = A
              grad2 = self._fS1(pos_pairs, A)
              grad1 = self._fD1(neg_pairs, A)
              M = self._grad_projection(grad1, grad2)
              A += alpha * M
  
          else:
              # If projection of 1 and 2 failed, or obj <= obj_previous due
              # to projection of 1 and 2, shrink learning rate and re-update
              # from the previous A.
              alpha /= 2
              A[:] = A_old + alpha * M
  
          delta = np.linalg.norm(alpha * M) / np.linalg.norm(A_old)
          if delta < self.tol:
              break
          if self.verbose:
              print(
                  "mmc iter: %d, conv = %f, projections = %d"
                  % (cycle, delta, it + 1)
              )
  
      if delta > self.tol:
          self.converged_ = False
          if self.verbose:
              print("mmc did not converge, conv = %f" % (delta,))
      else:
          self.converged_ = True
          if self.verbose:
              print(
                  "mmc converged at iter %d, conv = %f" % (cycle, delta)
              )
      self.A_[:] = A_old
      self.n_iter_ = cycle
  
      self.components_ = components_from_metric(self.A_)
      return self
  
imports: import numpy
  from sklearn.base import TransformerMixin
  from sklearn.utils.validation import assert_all_finite
  from .base_metric import _PairsClassifierMixin, MahalanobisMixin
  from .constraints import Constraints, wrap_pairs
  from ._util import components_from_metric, _initialize_metric_mahalanobis
  import warnings
label_desc: Use image node_id 3 for calling the _BaseMMC obj's underlying member method code with example usage: obj._fit_full(pairs, y) and returns: self