docs: def assign(self, spike_ids, spike_clusters_rel=0):
          """Make new spike cluster assignments.
  
          Parameters
          ----------
  
          spike_ids : array-like
              List of spike ids.
          spike_clusters_rel : array-like
              Relative cluster ids of the spikes in `spike_ids`. This
              must have the same size as `spike_ids`.
  
          Returns
          -------
  
          up : UpdateInfo instance
  
          Note
          ----
  
          `spike_clusters_rel` contain *relative* cluster indices. Their values
          don't matter: what matters is whether two give spikes
          should end up in the same cluster or not. Adding a constant number
          to all elements in `spike_clusters_rel` results in exactly the same
          operation.
  
          The final cluster ids are automatically generated by the `Clustering`
          class. This is because we must ensure that all modified clusters
          get brand new ids. The whole library is based on the assumption that
          cluster ids are unique and "disposable". Changing a cluster always
          results in a new cluster id being assigned.
  
          If a spike is assigned to a new cluster, then all other spikes
          belonging to the same cluster are assigned to a brand new cluster,
          even if they were not changed explicitely by the `assign()` method.
  
          In other words, the list of spikes affected by an `assign()` is almost
          always a strict superset of the `spike_ids` parameter. The only case
          where this is not true is when whole clusters change: this is called
          a merge. It is implemented in a separate `merge()` method because it
          is logically much simpler, and faster to execute.
  
          """
  
          assert not isinstance(spike_ids, slice)
  
          # Ensure `spike_clusters_rel` is an array-like.
          if not hasattr(spike_clusters_rel, '__len__'):
              spike_clusters_rel = spike_clusters_rel * np.ones(len(spike_ids),
                                                                dtype=np.int64)
  
          spike_ids = _as_array(spike_ids)
          if len(spike_ids) == 0:
              return UpdateInfo()
          assert len(spike_ids) == len(spike_clusters_rel)
          assert spike_ids.min() >= 0
          assert spike_ids.max() < self._n_spikes, "Some spikes don't exist."
  
          # Normalize the spike-cluster assignment such that
          # there are only new or dead clusters, not modified clusters.
          # This implies that spikes not explicitly selected, but that
          # belong to clusters affected by the operation, will be assigned
          # to brand new clusters.
          spike_ids, cluster_ids = _extend_assignment(spike_ids,
                                                      self._spike_clusters,
                                                      spike_clusters_rel,
                                                      self.new_cluster_id(),
                                                      )
  
          up = self._do_assign(spike_ids, cluster_ids)
          undo_state = self.emit('request_undo_state', up)
  
          # Add the assignment to the undo stack.
          self._undo_stack.add((spike_ids, cluster_ids, undo_state))
  
          self.emit('cluster', up)
          return up