def fit(self, X, ranks, replicates=1, verbose=True):
        """
        Fits CP tensor decompositions for different choices of rank.

        Parameters
        ----------
        X : array_like
            Real tensor
        ranks : int, or iterable
            iterable specifying number of components in each model
        replicates: int
            number of models to fit at each rank
        verbose : bool
            If True, prints summaries and optimization progress.
        """

        # Make ranks iterable if necessary.
        if not isinstance(ranks, collections.Iterable):
            ranks = (ranks,)

        # Iterate over model ranks, optimize multiple replicates at each rank.
        for r in ranks:

            # Initialize storage
            if r not in self.results:
                self.results[r] = []

            # Display fitting progress.
            if verbose:
                itr = trange(replicates,
                             desc='Fitting rank-{} models'.format(r),
                             leave=False)
            else:
                itr = range(replicates)

            # Fit replicates.
            for i in itr:
                model_fit = self._fit_method(X, r, **self._fit_options)
                self.results[r].append(model_fit)

            # Print summary of results.
            if verbose:
                min_obj = min([res.obj for res in self.results[r]])
                max_obj = max([res.obj for res in self.results[r]])
                elapsed = sum([res.total_time for res in self.results[r]])
                print('Rank-{} models:  min obj, {:.2f};  '
                      'max obj, {:.2f};  time to fit, '
                      '{:.1f}s'.format(r, min_obj, max_obj, elapsed))

        # Sort results from lowest to largest loss.
        for r in ranks:
            idx = np.argsort([result.obj for result in self.results[r]])
            self.results[r] = [self.results[r][i] for i in idx]

        # Align best model within each rank to best model of next larger rank.
        # Here r0 is the rank of the lower-dimensional model and r1 is the rank
        # of the high-dimensional model.
        for i in reversed(range(1, len(ranks))):
            r0, r1 = ranks[i-1], ranks[i]
            U = self.results[r0][0].factors
            V = self.results[r1][0].factors
            kruskal_align(U, V, permute_U=True)

        # For each rank, align everything to the best model
        for r in ranks:
            # store best factors
            U = self.results[r][0].factors       # best model factors
            self.results[r][0].similarity = 1.0  # similarity to itself

            # align lesser fit models to best models
            for res in self.results[r][1:]:
                res.similarity = kruskal_align(U, res.factors, permute_V=True)
Fits CP tensor decompositions for different choices of rank.

        Parameters
        ----------
        X : array_like
            Real tensor
        ranks : int, or iterable
            iterable specifying number of components in each model
        replicates: int
            number of models to fit at each rank
        verbose : bool
            If True, prints summaries and optimization progress.