def METHOD_NAME(self, i):
    """
    There are two standard steps for each iteration: expectation (E) and
    minimization (M). The E-step (assignment) is performed with an exhaustive
    search and the M-step (centroid computation) is performed with
    the exact solution.
    Args:
        - i: step number
    Remarks:
        - The E-step heavily uses PyTorch broadcasting to speed up computations
          and reduce the memory overhead
    """
    # assignments (E-step)
    distances = self.compute_distances()  # (n_centroids x out_features)
    self.assignments = torch.argmin(distances, dim=0)  # (out_features)
    n_empty_clusters = self.resolve_empty_clusters()
    # centroids (M-step)
    for k in range(self.n_centroids):
        W_k = self.W[:, self.assignments == k]  # (in_features x size_of_cluster_k)
        self.centroids[k] = W_k.mean(dim=1)  # (in_features)
    # book-keeping
    obj = (self.centroids[self.assignments].t() - self.W).norm(p=2).item()
    self.objective.append(obj)
    if self.verbose:
        logging.info(
            f"Iteration: {i},\t"
            f"objective: {obj:.6f},\t"
            f"resolved empty clusters: {n_empty_clusters}"
        )