def fit(self, X, ranks, replicates=1, verbose=True):
        if not isinstance(ranks, collections.Iterable):
            ranks = (ranks,)
        for r in ranks:
            if r not in self.results:
                self.results[r] = []
            if verbose:
                itr = trange(replicates,
                             desc='Fitting rank-{} models'.format(r),
                             leave=False)
            else:
                itr = range(replicates)
            for i in itr:
                model_fit = self._fit_method(X, r, **self._fit_options)
                self.results[r].append(model_fit)
            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))
        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]
        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 r in ranks:
            U = self.results[r][0].factors
            self.results[r][0].similarity = 1.0
            for res in self.results[r][1:]:
                res.similarity = kruskal_align(U, res.factors, permute_V=True)