docs: def poisson_estimate_state(data, clusters, init_means=None, init_weights=None, method='NoLips', max_iters=30, tol=0, disp=False, inner_max_iters=100, normalize=True, initialization='tsvd', parallel=True, threads=4, max_assign_weight=0.75, run_w_first=True, constrain_w=False, regularization=0.0, write_progress_file=None, **kwargs):
      """
      Uses a Poisson Covex Mixture model to estimate cell states and
      cell state mixing weights.
  
      To lower computational costs, use a sparse matrix, set disp to False, and set tol to 0.
  
      Args:
          data (array): genes x cells array or sparse matrix.
          clusters (int): number of mixture components
          init_means (array, optional): initial centers - genes x clusters. Default: from Poisson kmeans
          init_weights (array, optional): initial weights - clusters x cells, or assignments as produced by clustering. Default: from Poisson kmeans
          method (str, optional): optimization method. Current options are 'NoLips' and 'L-BFGS-B'. Default: 'NoLips'.
          max_iters (int, optional): maximum number of iterations. Default: 30
          tol (float, optional): if both M and W change by less than tol (RMSE), then the iteration is stopped. Default: 1e-10
          disp (bool, optional): whether or not to display optimization progress. Default: False
          inner_max_iters (int, optional): Number of iterations to run in the optimization subroutine for M and W. Default: 100
          normalize (bool, optional): True if the resulting W should sum to 1 for each cell. Default: True.
          initialization (str, optional): If initial means and weights are not provided, this describes how they are initialized. Options: 'cluster' (poisson cluster for means and weights), 'kmpp' (kmeans++ for means, random weights), 'km' (regular k-means), 'tsvd' (tsvd(50) + k-means). Default: tsvd.
          parallel (bool, optional): Whether to use parallel updates (sparse NoLips only). Default: True
          threads (int, optional): How many threads to use in the parallel computation. Default: 4
          max_assign_weight (float, optional): If using a clustering-based initialization, how much weight to assign to the max weight cluster. Default: 0.75
          run_w_first (bool, optional): Whether or not to optimize W first (if false, M will be optimized first). Default: True
          constrain_w (bool, optional): If True, then W is normalized after every iteration. Default: False
          regularization (float, optional): Regularization coefficient for M and W. Default: 0 (no regularization).
          write_progress_file (str, optional): filename to write progress updates to.
  
      Returns:
          M (array): genes x clusters - state means
          W (array): clusters x cells - state mixing components for each cell
          ll (float): final log-likelihood
      """
      genes, cells = data.shape
      means, w_init = initialize_means_weights(data, clusters, init_means, init_weights, initialization, max_assign_weight)
      X = data.astype(float)
      XT = X.T
      is_sparse = False
      if sparse.issparse(X):
          is_sparse = True
          update_fn = sparse_nolips_update_w
          # convert to csc
          X = sparse.csc_matrix(X)
          XT = sparse.csc_matrix(XT)
          if parallel:
              update_fn = parallel_sparse_nolips_update_w
          Xsum = np.asarray(X.sum(0)).flatten()
          Xsum_m = np.asarray(X.sum(1)).flatten()
          # L-BFGS-B won't work right now for sparse matrices
          method = 'NoLips'
          objective_fn = _call_sparse_obj
      else:
          objective_fn = objective
          update_fn = nolips_update_w
          Xsum = X.sum(0)
          Xsum_m = X.sum(1)
          # If method is NoLips, converting to a sparse matrix
          # will always improve the performance (?) and never lower accuracy...
          if method == 'NoLips':
              is_sparse = True
              X = sparse.csc_matrix(X)
              XT = sparse.csc_matrix(XT)
              update_fn = sparse_nolips_update_w
              if parallel:
                  update_fn = parallel_sparse_nolips_update_w
              objective_fn = _call_sparse_obj
      w_new = w_init
      for i in range(max_iters):
          if disp:
              print('iter: {0}'.format(i))
          if run_w_first:
              # step 1: given M, estimate W
              w_new = _estimate_w(X, w_new, means, Xsum, update_fn, objective_fn, is_sparse, parallel, threads, method, tol, disp, inner_max_iters, 'W', regularization)
              if constrain_w:
                  w_new = w_new/w_new.sum(0)
              if disp:
                  w_ll = objective_fn(X, means, w_new)
                  print('Finished updating W. Objective value: {0}'.format(w_ll))
              # step 2: given W, update M
              means = _estimate_w(XT, means.T, w_new.T, Xsum_m, update_fn, objective_fn, is_sparse, parallel, threads, method, tol, disp, inner_max_iters, 'M', regularization)
              means = means.T
              if disp:
                  w_ll = objective_fn(X, means, w_new)
                  print('Finished updating M. Objective value: {0}'.format(w_ll))
          else:
              # step 1: given W, update M
              means = _estimate_w(XT, means.T, w_new.T, Xsum_m, update_fn, objective_fn, is_sparse, parallel, threads, method, tol, disp, inner_max_iters, 'M', regularization)
              means = means.T
              if disp:
                  w_ll = objective_fn(X, means, w_new)
                  print('Finished updating M. Objective value: {0}'.format(w_ll))
              # step 2: given M, estimate W
              w_new = _estimate_w(X, w_new, means, Xsum, update_fn, objective_fn, is_sparse, parallel, threads, method, tol, disp, inner_max_iters, 'W', regularization)
              if constrain_w:
                  w_new = w_new/w_new.sum(0)
              if disp:
                  w_ll = objective_fn(X, means, w_new)
                  print('Finished updating W. Objective value: {0}'.format(w_ll))
          # write progress to progress file
          if write_progress_file is not None:
              progress = open(write_progress_file, 'w')
              progress.write(str(i))
              progress.close()
      if normalize:
          w_new = w_new/w_new.sum(0)
      m_ll = objective_fn(X, means, w_new)
      return means, w_new, m_ll