text: # Credits: This script is taken from https://github.com/kundajelab/deeplift/blob/master/deeplift/dinuc_shuffle.py
  
  
  import numpy as np
  
  
  def dinuc_shuffle(seq, num_shufs=None, rng=None):
      """
      Creates shuffles of the given sequence, in which dinucleotide frequencies
      are preserved.
      Arguments:
          `seq`: either a string of length L, or an L x D NumPy array of one-hot
              encodings
          `num_shufs`: the number of shuffles to create, N; if unspecified, only
              one shuffle will be created
          `rng`: a NumPy RandomState object, to use for performing shuffles
      If `seq` is a string, returns a list of N strings of length L, each one
      being a shuffled version of `seq`. If `seq` is a 2D NumPy array, then the
      result is an N x L x D NumPy array of shuffled versions of `seq`, also
      one-hot encoded. If `num_shufs` is not specified, then the first dimension
      of N will not be present (i.e. a single string will be returned, or an L x D
      array).
      """
      if type(seq) is str:
          arr = string_to_char_array(seq)
      elif type(seq) is np.ndarray and len(seq.shape) == 2:
          seq_len, one_hot_dim = seq.shape
          arr = one_hot_to_tokens(seq)
      else:
          raise ValueError("Expected string or one-hot encoded array")
  
      if not rng:
          rng = np.random.RandomState()
  
      # Get the set of all characters, and a mapping of which positions have which
      # characters; use `tokens`, which are integer representations of the
      # original characters
      chars, tokens = np.unique(arr, return_inverse=True)
  
      # For each token, get a list of indices of all the tokens that come after it
      shuf_next_inds = []
      for t in range(len(chars)):
          mask = tokens[:-1] == t  # Excluding last char
          inds = np.where(mask)[0]
          shuf_next_inds.append(inds + 1)  # Add 1 for next token
  
      if type(seq) is str:
          all_results = []
      else:
          all_results = np.empty(
              (num_shufs if num_shufs else 1, seq_len, one_hot_dim),
              dtype=seq.dtype
          )
  
      for i in range(num_shufs if num_shufs else 1):
          # Shuffle the next indices
          for t in range(len(chars)):
              inds = np.arange(len(shuf_next_inds[t]))
              inds[:-1] = rng.permutation(len(inds) - 1)  # Keep last index same
              shuf_next_inds[t] = shuf_next_inds[t][inds]
  
          counters = [0] * len(chars)
  
          # Build the resulting array
          ind = 0
          result = np.empty_like(tokens)
          result[0] = tokens[ind]
          for j in range(1, len(tokens)):
              t = tokens[ind]
              ind = shuf_next_inds[t][counters[t]]
              counters[t] += 1
              result[j] = tokens[ind]
  
          if type(seq) is str:
              all_results.append(char_array_to_string(chars[result]))
          else:
              all_results[i] = tokens_to_one_hot(chars[result], one_hot_dim)
      return all_results if num_shufs else all_results[0]
  
  
  
  def string_to_char_array(seq):
      """
      Converts an ASCII string to a NumPy array of byte-long ASCII codes.
      e.g. "ACGT" becomes [65, 67, 71, 84].
      """
      return np.frombuffer(bytearray(seq, "utf8"), dtype=np.int8)
  
  
  def char_array_to_string(arr):
      """
      Converts a NumPy array of byte-long ASCII codes into an ASCII string.
      e.g. [65, 67, 71, 84] becomes "ACGT".
      """
      return arr.tostring().decode("ascii")
  
  
  def one_hot_to_tokens(one_hot):
      """
      Converts an L x D one-hot encoding into an L-vector of integers in the range
      [0, D], where the token D is used when the one-hot encoding is all 0. This
      assumes that the one-hot encoding is well-formed, with at most one 1 in each
      column (and 0s elsewhere).
      """
      tokens = np.tile(one_hot.shape[1], one_hot.shape[0])  # Vector of all D
      seq_inds, dim_inds = np.where(one_hot)
      tokens[seq_inds] = dim_inds
      return tokens
  
  
  def tokens_to_one_hot(tokens, one_hot_dim):
      """
      Converts an L-vector of integers in the range [0, D] to an L x D one-hot
      encoding. The value `D` must be provided as `one_hot_dim`. A token of D
      means the one-hot encoding is all 0s.
      """
      identity = np.identity(one_hot_dim + 1)[:, :-1]  # Last row is all 0s
      return identity[tokens]
  