docstr: Initialize prototypical counterfactual method.
  
  Parameters
  ----------
  predict
      `tensorflow` model or any other model's prediction function returning class probabilities.
  shape
      Shape of input data starting with batch size.
  kappa
      Confidence parameter for the attack loss term.
  beta
      Regularization constant for L1 loss term.
  feature_range
      Tuple with `min` and `max` ranges to allow for perturbed instances. `Min` and `max` ranges can be `float`
      or `numpy` arrays with dimension (1x nb of features) for feature-wise ranges.
  gamma
      Regularization constant for optional auto-encoder loss term.
  ae_model
      Optional auto-encoder model used for loss regularization.
  enc_model
      Optional encoder model used to guide instance perturbations towards a class prototype.
  theta
      Constant for the prototype search loss term.
  cat_vars
      Dict with as keys the categorical columns and as values the number of categories per categorical variable.
  ohe
      Whether the categorical variables are one-hot encoded (OHE) or not. If not OHE, they are
      assumed to have ordinal encodings.
  use_kdtree
      Whether to use k-d trees for the prototype loss term if no encoder is available.
  learning_rate_init
      Initial learning rate of optimizer.
  max_iterations
      Maximum number of iterations for finding a counterfactual.
  c_init
      Initial value to scale the attack loss term.
  c_steps
      Number of iterations to adjust the constant scaling the attack loss term.
  eps
      If numerical gradients are used to compute `dL/dx = (dL/dp) * (dp/dx)`, then `eps[0]` is used to
      calculate `dL/dp` and `eps[1]` is used for `dp/dx`. `eps[0]` and `eps[1]` can be a combination of `float`
      values and `numpy` arrays. For `eps[0]`, the array dimension should be (1x nb of prediction categories)
      and for `eps[1]` it should be (1x nb of features).
  clip
      Tuple with min and max clip ranges for both the numerical gradients and the gradients
      obtained from the `tensorflow` graph.
  update_num_grad
      If numerical gradients are used, they will be updated every `update_num_grad` iterations.
  write_dir
      Directory to write `tensorboard` files to.
  sess
      Optional `tensorflow` session that will be used if passed instead of creating or inferring one internally.
docstr_tok: ["Initialize","prototypical","counterfactual","method",".","Parameters","--","--","--","--","--","predict","`","tensorflow","`","model","or","any","other","model","'s","prediction","function","returning","class","probabilities",".","shape","Shape","of","input","data","starting","with","batch","size",".","kappa","Confidence","parameter","for","the","attack","loss","term",".","beta","Regularization","constant","for","L1","loss","term",".","feature_range","Tuple","with","`","min","`","and","`","max","`","ranges","to","allow","for","perturbed","instances",".","`","Min","`","and","`","max","`","ranges","can","be","`","float","`","or","`","numpy","`","arrays","with","dimension","(","1x","nb","of","features",")","for","feature-wise","ranges",".","gamma","Regularization","constant","for","optional","auto-encoder","loss","term",".","ae_model","Optional","auto-encoder","model","used","for","loss","regularization",".","enc_model","Optional","encoder","model","used","to","guide","instance","perturbations","towards","a","class","prototype",".","theta","Constant","for","the","prototype","search","loss","term",".","cat_vars","Dict","with","as","keys","the","categorical","columns","and","as","values","the","number","of","categories","per","categorical","variable",".","ohe","Whether","the","categorical","variables","are","one-hot","encoded","(","OHE",")","or","not",".","If","not","OHE",",","they","are","assumed","to","have","ordinal","encodings",".","use_kdtree","Whether","to","use","k-d","trees","for","the","prototype","loss","term","if","no","encoder","is","available",".","learning_rate_init","Initial","learning","rate","of","optimizer",".","max_iterations","Maximum","number","of","iterations","for","finding","a","counterfactual",".","c_init","Initial","value","to","scale","the","attack","loss","term",".","c_steps","Number","of","iterations","to","adjust","the","constant","scaling","the","attack","loss","term",".","eps","If","numerical","gradients","are","used","to","compute","`","dL\/dx","=","(","dL\/dp",")","*","(","dp\/dx",")","`",",","then","`","eps","[","0","]","`","is","used","to","calculate","`","dL\/dp","`","and","`","eps","[","1","]","`","is","used","for","`","dp\/dx","`",".","`","eps","[","0","]","`","and","`","eps","[","1","]","`","can","be","a","combination","of","`","float","`","values","and","`","numpy","`","arrays",".","For","`","eps","[","0","]","`",",","the","array","dimension","should","be","(","1x","nb","of","prediction","categories",")","and","for","`","eps","[","1","]","`","it","should","be","(","1x","nb","of","features",")",".","clip","Tuple","with","min","and","max","clip","ranges","for","both","the","numerical","gradients","and","the","gradients","obtained","from","the","`","tensorflow","`","graph",".","update_num_grad","If","numerical","gradients","are","used",",","they","will","be","updated","every","`","update_num_grad","`","iterations",".","write_dir","Directory","to","write","`","tensorboard","`","files","to",".","sess","Optional","`","tensorflow","`","session","that","will","be","used","if","passed","instead","of","creating","or","inferring","one","internally","."]
code: def __init__(
      self,
      predict: Union[
          Callable[[np.ndarray], np.ndarray], tf.keras.Model
      ],
      shape: tuple,
      kappa: float = 0.0,
      beta: float = 0.1,
      feature_range: Tuple[
          Union[float, np.ndarray], Union[float, np.ndarray]
      ] = (-1e10, 1e10),
      gamma: float = 0.0,
      ae_model: Optional[tf.keras.Model] = None,
      enc_model: Optional[tf.keras.Model] = None,
      theta: float = 0.0,
      cat_vars: Optional[Dict[int, int]] = None,
      ohe: bool = False,
      use_kdtree: bool = False,
      learning_rate_init: float = 1e-2,
      max_iterations: int = 1000,
      c_init: float = 10.0,
      c_steps: int = 10,
      eps: tuple = (1e-3, 1e-3),
      clip: tuple = (-1000.0, 1000.0),
      update_num_grad: int = 1,
      write_dir: Optional[str] = None,
      sess: Optional[tf.Session] = None,
  ) -> None:
      """
      Initialize prototypical counterfactual method.
  
      Parameters
      ----------
      predict
          `tensorflow` model or any other model's prediction function returning class probabilities.
      shape
          Shape of input data starting with batch size.
      kappa
          Confidence parameter for the attack loss term.
      beta
          Regularization constant for L1 loss term.
      feature_range
          Tuple with `min` and `max` ranges to allow for perturbed instances. `Min` and `max` ranges can be `float`
          or `numpy` arrays with dimension (1x nb of features) for feature-wise ranges.
      gamma
          Regularization constant for optional auto-encoder loss term.
      ae_model
          Optional auto-encoder model used for loss regularization.
      enc_model
          Optional encoder model used to guide instance perturbations towards a class prototype.
      theta
          Constant for the prototype search loss term.
      cat_vars
          Dict with as keys the categorical columns and as values the number of categories per categorical variable.
      ohe
          Whether the categorical variables are one-hot encoded (OHE) or not. If not OHE, they are
          assumed to have ordinal encodings.
      use_kdtree
          Whether to use k-d trees for the prototype loss term if no encoder is available.
      learning_rate_init
          Initial learning rate of optimizer.
      max_iterations
          Maximum number of iterations for finding a counterfactual.
      c_init
          Initial value to scale the attack loss term.
      c_steps
          Number of iterations to adjust the constant scaling the attack loss term.
      eps
          If numerical gradients are used to compute `dL/dx = (dL/dp) * (dp/dx)`, then `eps[0]` is used to
          calculate `dL/dp` and `eps[1]` is used for `dp/dx`. `eps[0]` and `eps[1]` can be a combination of `float`
          values and `numpy` arrays. For `eps[0]`, the array dimension should be (1x nb of prediction categories)
          and for `eps[1]` it should be (1x nb of features).
      clip
          Tuple with min and max clip ranges for both the numerical gradients and the gradients
          obtained from the `tensorflow` graph.
      update_num_grad
          If numerical gradients are used, they will be updated every `update_num_grad` iterations.
      write_dir
          Directory to write `tensorboard` files to.
      sess
          Optional `tensorflow` session that will be used if passed instead of creating or inferring one internally.
      """
      super().__init__(meta=copy.deepcopy(DEFAULT_META_CFP))
      params = locals()
      remove = [
          "self",
          "predict",
          "ae_model",
          "enc_model",
          "sess",
          "__class__",
      ]
      for key in remove:
          params.pop(key)
      self.meta["params"].update(params)
  
      self.predict = predict
  
      # check if the passed object is a model and get session
      is_model = isinstance(predict, tf.keras.Model)
      model_sess = tf.compat.v1.keras.backend.get_session()
      is_ae = isinstance(ae_model, tf.keras.Model)
      is_enc = isinstance(enc_model, tf.keras.Model)
      self.meta["params"].update(
          is_model=is_model, is_ae=is_ae, is_enc=is_enc
      )
  
      # if session provided, use it
      if isinstance(sess, tf.Session):
          self.sess = sess
      else:
          self.sess = model_sess
  
      if is_model:  # Keras or TF model
          self.model = True
          self.classes = self.predict.predict(np.zeros(shape)).shape[1]  # type: ignore
      else:  # black-box model
          self.model = False
          self.classes = self.predict(np.zeros(shape)).shape[1]
  
      if is_enc:
          self.enc_model = True
      else:
          self.enc_model = False
  
      if is_ae:
          self.ae_model = True
      else:
          self.ae_model = False
  
      if use_kdtree and self.enc_model:
          logger.warning(
              "Both an encoder and k-d trees enabled. Using the encoder for the prototype loss term."
          )
  
      if use_kdtree or self.enc_model:
          self.enc_or_kdtree = True
      else:
          self.enc_or_kdtree = False
      self.meta["params"].update(enc_or_kdtree=self.enc_or_kdtree)
  
      if cat_vars:
          self.is_cat = True
      else:
          self.is_cat = False
          cat_vars = dict()  # to avoid further None checks
      self.meta["params"].update(is_cat=self.is_cat)
  
      self.shape = shape
      self.kappa = kappa
      self.beta = beta
      self.gamma = gamma
      self.theta = theta
      self.ae = ae_model
      self.enc = enc_model
      self.cat_vars = cat_vars
      self.ohe = ohe
      self.use_kdtree = use_kdtree
      self.batch_size = shape[0]
      self.max_iterations = max_iterations
      self.c_init = c_init
      self.c_steps = c_steps
      self.feature_range = tuple(
          [
              (np.ones(shape[1:]) * feature_range[_])[None, :]
              if isinstance(feature_range[_], float)
              else np.array(feature_range[_])
              for _ in range(2)
          ]
      )
      self.update_num_grad = update_num_grad
      self.eps = eps
      self.clip = clip
      self.write_dir = write_dir
  
      if self.is_cat:
          # compute dimensionality after conversion from OHE to ordinal encoding
          shape = ohe_to_ord_shape(
              shape, cat_vars=cat_vars, is_ohe=self.ohe
          )
  
          # define ragged tensor for mapping from categorical to numerical values
          self.map_cat_to_num = tf.ragged.constant(
              [np.zeros(v) for _, v in cat_vars.items()]
          )
  
          # define placeholder for mapping which can be fed after the fit step
          max_key = max(cat_vars, key=cat_vars.get)  # type: ignore[arg-type] # feature with the most categories
          self.max_cat = cat_vars[max_key]
          cat_keys = list(cat_vars.keys())
          n_cat = len(cat_keys)
          self.assign_map = tf.placeholder(
              tf.float32, (n_cat, self.max_cat), name="assign_map"
          )
          self.map_var = tf.Variable(
              np.zeros((n_cat, self.max_cat)),
              dtype=tf.float32,
              name="map_var",
          )
  
          # update ragged mapping tensor
          lengths = [v for k, v in cat_vars.items()]
          map_cat_to_num_val = tf.RaggedTensor.from_tensor(
              self.map_var, lengths=list(lengths)
          )
          self.map_cat_to_num = tf.ragged.map_flat_values(
              tf.add, self.map_cat_to_num, map_cat_to_num_val
          )
  
          # store categorical columns assuming ordinal encoding
          # used for the mapping between numerical values and categories
          if self.ohe:
              cat_vars_ord = {}
              c, k = 0, 0
              while c < self.shape[-1]:
                  if c in cat_keys:
                      v = cat_vars[c]
                      cat_vars_ord[k] = v
                      k += 1
                      c += v
                      continue
                  k += 1
                  c += 1
              cat_keys_ord = list(cat_vars_ord.keys())
              cat_cols_ord = tf.constant(
                  cat_keys_ord, name="cat_keys_ord"
              )
          else:
              cat_cols_ord = tf.constant(cat_keys, name="cat_keys_ord")
  
          # mapping from numerical values to categories and vice versa
          # supports mapping to and from both ordinal encoding and OHE
          def is_eq(col, cat_cols):
              """
              Check if a column represents a categorical variable.
  
              Parameters
              ----------
              col
                  Column index to check.
              cat_cols
                  Indices of categorical variables.
  
              Returns
              -------
              Boolean whether the column is a categorical variable.
              """
              eq = tf.math.equal(col, cat_cols)
              eq_any = tf.reduce_any(eq)
              return tf.equal(eq_any, tf.constant(True))
  
          def cond_loop(
              icol, iohe, icat, adv_to_map, adv_map, map_cols
          ):
              """
              Condition for while loop, only iterate over columns of instance.
  
              Parameters
              ----------
              icol
                  Iteration over columns of instance.
              adv_to_map
                  Instance that needs to be mapped from categories to numerical values or vice versa.
              map_cols
                  Number of columns in instance to be mapped.
  
              Returns
              -------
              Boolean whether condition is met.
              """
              return tf.less(
                  icol, tf.minimum(map_cols, tf.shape(adv_to_map)[1])
              )
  
          def body_ord_to_num(
              icol, iohe, icat, adv_to_map, adv_map, map_cols
          ):
              """
              Body executed in while loop when mapping ordinal categories to numerical values.
  
              Parameters
              ----------
              icol
                  Iteration over columns of instance.
              icat
                  Iteration over categorical variables.
              adv_to_map
                  Instance that needs to be mapped from categories to numerical values or vice versa.
              adv_map
                  Mapped instance from categories to numerical values.
              """
              # check if icol is a categorical variable
              eq_any_true = is_eq(icol, cat_cols_ord)
  
              # map category to its numerical value
              def true_fn():
                  try:
                      return self.map_cat_to_num[icat][
                          adv_to_map[0, icol]
                      ]
                  except (
                      TypeError
                  ):  # the value of adv_to_map[0, icol] is a float
                      # TODO: check error type
                      idx = round_grad(adv_to_map[0, icol])
                      return self.map_cat_to_num[icat][idx]
  
              def false_fn():
                  return adv_to_map[0, icol]
  
              # write column to array
              adv_map_col = tf.cond(eq_any_true, true_fn, false_fn)
              adv_map = adv_map.write(icol, adv_map_col)
  
              # increment
              icol = tf.add(icol, 1)
              icat = tf.cond(
                  eq_any_true, lambda: tf.add(icat, 1), lambda: icat
              )  # if categorical variable
  
              return [icol, iohe, icat, adv_to_map, adv_map, map_cols]
  
          def body_num_to_ord(
              icol, iohe, icat, adv_to_map, adv_map, map_cols
          ):
              """
              Body executed in while loop when mapping numerical values to ordinal categories.
  
              Parameters
              ----------
              icol
                  Iteration over columns of instance.
              icat
                  Iteration over categorical variables.
              adv_to_map
                  Instance that needs to be mapped from categories to numerical values or vice versa.
              adv_map
                  Mapped instance from numerical values to categories.
              """
              # check if icol is a categorical variable
              eq_any_true = is_eq(icol, cat_cols_ord)
  
              # map numerical value to category
              def true_fn():
                  return argmin_grad(
                      adv_to_map[0, icol], self.map_cat_to_num[icat]
                  )
  
              def false_fn():
                  return adv_to_map[0, icol]
  
              # write column to array
              adv_map_col = tf.cond(eq_any_true, true_fn, false_fn)
              adv_map = adv_map.write(icol, adv_map_col)
  
              # increment
              icol = tf.add(icol, 1)
              icat = tf.cond(
                  eq_any_true, lambda: tf.add(icat, 1), lambda: icat
              )  # if categorical variable
  
              return [icol, iohe, icat, adv_to_map, adv_map, map_cols]
  
          def body_ohe_to_num(
              icol, iohe, icat, adv_to_map, adv_map, map_cols
          ):
              """
              Body executed in while loop when mapping OHE categories to numerical values.
  
              Parameters
              ----------
              icol
                  Iteration over columns of instance.
              iohe
                  Iteration over OHE columns of instance.
              icat
                  Iteration over categorical variables.
              adv_to_map
                  Instance that needs to be mapped from categories to numerical values or vice versa.
              adv_map
                  Mapped instance from categories to numerical values.
              """
              # check if icol is a categorical variable
              eq_any_true = is_eq(icol, cat_cols_ord)
  
              # nb of categories
              v = tf.cond(
                  eq_any_true,
                  lambda: tf.shape(self.map_cat_to_num[icat])[0],
                  lambda: tf.constant(1),
              )
  
              # map category to its numerical value
              def true_fn():
                  adv_ord = argmax_grad(
                      adv_to_map[0, iohe : iohe + v]
                  )  # map to ord
                  return self.map_cat_to_num[icat][
                      adv_ord
                  ]  # map to num
  
              def false_fn():
                  return adv_to_map[0, iohe]
  
              # write column to array
              adv_map_col = tf.cond(eq_any_true, true_fn, false_fn)
              adv_map = adv_map.write(icol, adv_map_col)
  
              # increment
              icol = tf.add(icol, 1)
              iohe = tf.add(iohe, v)
              icat = tf.cond(
                  eq_any_true, lambda: tf.add(icat, 1), lambda: icat
              )  # if categorical variable
  
              return [icol, iohe, icat, adv_to_map, adv_map, map_cols]
  
          def body_num_to_ohe(
              icol, iohe, icat, adv_to_map, adv_map, map_cols
          ):
              """
              Body executed in while loop when mapping numerical values to OHE categories.
  
              Parameters
              ----------
              icol
                  Iteration over columns of instance.
              iohe
                  Iteration over OHE columns of instance.
              icat
                  Iteration over categorical variables.
              adv_to_map
                  Instance that needs to be mapped from categories to numerical values or vice versa.
              adv_map
                  Mapped instance from numerical values to categories.
              """
              # check if icol is a categorical variable
              eq_any_true = is_eq(icol, cat_cols_ord)
  
              def true_fn():
                  cat_ord = argmin_grad(
                      adv_to_map[0, icol], self.map_cat_to_num[icat]
                  )  # map to ord
                  cat_ohe = one_hot_grad(
                      cat_ord, self.map_cat_to_num[icat]
                  )  # map to OHE
                  return cat_ohe
  
              def false_fn():
                  return tf.reshape(adv_to_map[0, icol], (1,))
  
              # get OHE mapped columns
              adv_map_col = tf.cond(eq_any_true, true_fn, false_fn)
  
              def while_ohe(i_ohe, i_ohe_cat, adv_ohe):
                  return tf.less(i_ohe_cat, tf.shape(adv_map_col)[0])
  
              def body_ohe(i_ohe, i_ohe_cat, adv_ohe):
                  i_write = tf.add(i_ohe_cat, i_ohe)
                  adv_ohe = adv_ohe.write(
                      i_write, adv_map_col[i_ohe_cat]
                  )
                  i_ohe_cat = tf.add(i_ohe_cat, 1)
                  return [i_ohe, i_ohe_cat, adv_ohe]
  
              # write OHE columns to array
              iohe, iohecat, adv_map = tf.while_loop(
                  while_ohe, body_ohe, [iohe, tf.constant(0), adv_map]
              )
  
              # increment
              icol = tf.add(icol, 1)
              iohe = tf.add(iohe, iohecat)
              icat = tf.cond(
                  eq_any_true, lambda: tf.add(icat, 1), lambda: icat
              )  # if categorical variable
  
              return [icol, iohe, icat, adv_to_map, adv_map, map_cols]
  
          def apply_map(adv_to_map, to_num):
              """
              Apply mapping from numerical to ordinal or OHE categorical variables
              or vice versa for an instance.
  
              Parameters
              ----------
              adv_to_map
                  Instance to map.
              to_num
                  Map from categorical to numerical values if ``True``, vice versa if ``False``.
  
              Returns
              -------
              Mapped instance.
              """
              icol = tf.constant(0)
              iohe = tf.constant(0)
              icat = tf.constant(0)
  
              if self.ohe:
                  body_to_num, body_to_cat = (
                      body_ohe_to_num,
                      body_num_to_ohe,
                  )
              else:
                  body_to_num, body_to_cat = (
                      body_ord_to_num,
                      body_num_to_ord,
                  )
  
              if self.ohe and not to_num:
                  shape_adv_map = self.shape
              else:
                  shape_adv_map = shape
  
              adv_map = tf.TensorArray(
                  dtype=tf.float32, size=shape_adv_map[1]
              )
              loop_vars = (
                  icol,
                  iohe,
                  icat,
                  adv_to_map,
                  adv_map,
                  shape_adv_map[1],
              )
  
              if to_num:  # map from categorical to numerical values
                  _, _, _, _, adv_map, _ = tf.while_loop(
                      cond_loop,
                      body_to_num,
                      loop_vars,
                      parallel_iterations=1,
                      back_prop=True,
                  )
              else:  # map from numerical to categorical values
                  _, _, _, _, adv_map, _ = tf.while_loop(
                      cond_loop,
                      body_to_cat,
                      loop_vars,
                      parallel_iterations=1,
                      back_prop=True,
                  )
              adv_map_stack = tf.reshape(adv_map.stack(), shape_adv_map)
              return adv_map_stack
  
      # define tf variables for original and perturbed instances, and target labels
      self.orig = tf.Variable(
          np.zeros(shape), dtype=tf.float32, name="orig"
      )
      self.adv = tf.Variable(
          np.zeros(shape), dtype=tf.float32, name="adv"
      )
      self.adv_s = tf.Variable(
          np.zeros(shape), dtype=tf.float32, name="adv_s"
      )
      self.target = tf.Variable(
          np.zeros((self.batch_size, self.classes)),
          dtype=tf.float32,
          name="target",
      )
  
      # variable for target class proto
      if self.enc_model:
          self.shape_enc = self.enc.predict(np.zeros(self.shape)).shape  # type: ignore[union-attr]
      else:
          self.shape_enc = shape
  
      self.target_proto = tf.Variable(
          np.zeros(self.shape_enc),
          dtype=tf.float32,
          name="target_proto",
      )
  
      # define tf variable for constant used in FISTA optimization
      self.const = tf.Variable(
          np.zeros(self.batch_size), dtype=tf.float32, name="const"
      )
      self.global_step = tf.Variable(
          0.0, trainable=False, name="global_step"
      )
  
      # define placeholders that will be assigned to relevant variables
      self.assign_orig = tf.placeholder(
          tf.float32, shape, name="assign_orig"
      )
      self.assign_adv = tf.placeholder(
          tf.float32, shape, name="assign_adv"
      )
      self.assign_adv_s = tf.placeholder(
          tf.float32, shape, name="assign_adv_s"
      )
      self.assign_target = tf.placeholder(
          tf.float32,
          (self.batch_size, self.classes),
          name="assign_target",
      )
      self.assign_const = tf.placeholder(
          tf.float32, [self.batch_size], name="assign_const"
      )
      self.assign_target_proto = tf.placeholder(
          tf.float32, self.shape_enc, name="assign_target_proto"
      )
  
      # define conditions and values for element-wise shrinkage thresholding
      with tf.name_scope("shrinkage_thresholding") as scope:
          cond = [
              tf.cast(
                  tf.greater(
                      tf.subtract(self.adv_s, self.orig), self.beta
                  ),
                  tf.float32,
              ),
              tf.cast(
                  tf.less_equal(
                      tf.abs(tf.subtract(self.adv_s, self.orig)),
                      self.beta,
                  ),
                  tf.float32,
              ),
              tf.cast(
                  tf.less(
                      tf.subtract(self.adv_s, self.orig),
                      tf.negative(self.beta),
                  ),
                  tf.float32,
              ),
          ]
          upper = tf.minimum(
              tf.subtract(self.adv_s, self.beta),
              tf.cast(feature_range[1], tf.float32),
          )
          lower = tf.maximum(
              tf.add(self.adv_s, self.beta),
              tf.cast(feature_range[0], tf.float32),
          )
          self.assign_adv = (
              tf.multiply(cond[0], upper)
              + tf.multiply(cond[1], self.orig)
              + tf.multiply(cond[2], lower)
          )
  
      # perturbation update and vector projection on correct feature range set
      with tf.name_scope("perturbation_y") as scope:
          self.zt = tf.divide(
              self.global_step,
              self.global_step + tf.cast(3, tf.float32),
          )
          self.assign_adv_s = self.assign_adv + tf.multiply(
              self.zt, self.assign_adv - self.adv
          )
          # map to feature space
          self.assign_adv_s = tf.minimum(
              self.assign_adv_s, tf.cast(feature_range[1], tf.float32)
          )
          self.assign_adv_s = tf.maximum(
              self.assign_adv_s, tf.cast(feature_range[0], tf.float32)
          )
  
      # assign counterfactual of step k+1 to k
      with tf.name_scope("update_adv") as scope:
          self.adv_updater = tf.assign(self.adv, self.assign_adv)
          self.adv_updater_s = tf.assign(self.adv_s, self.assign_adv_s)
  
      # from perturbed instance, derive deviation delta
      with tf.name_scope("update_delta") as scope:
          self.delta = self.orig - self.adv
          self.delta_s = self.orig - self.adv_s
  
      # define L1 and L2 loss terms; L1+L2 is later used as an optimization constraint for FISTA
      ax_sum = list(np.arange(1, len(shape)))
      with tf.name_scope("loss_l1_l2") as scope:
          self.l2 = tf.reduce_sum(tf.square(self.delta), axis=ax_sum)
          self.l2_s = tf.reduce_sum(
              tf.square(self.delta_s), axis=ax_sum
          )
          self.l1 = tf.reduce_sum(tf.abs(self.delta), axis=ax_sum)
          self.l1_s = tf.reduce_sum(tf.abs(self.delta_s), axis=ax_sum)
          self.l1_l2 = self.l2 + tf.multiply(self.l1, self.beta)
          self.l1_l2_s = self.l2_s + tf.multiply(self.l1_s, self.beta)
  
          # sum losses
          self.loss_l1 = tf.reduce_sum(self.l1)
          self.loss_l1_s = tf.reduce_sum(self.l1_s)
          self.loss_l2 = tf.reduce_sum(self.l2)
          self.loss_l2_s = tf.reduce_sum(self.l2_s)
  
      if self.is_cat:  # map adv and adv_s to categories
          self.adv_cat = apply_map(self.adv, to_num=False)
          self.adv_cat_s = apply_map(self.adv_s, to_num=False)
      else:
          self.adv_cat = self.adv
          self.adv_cat_s = self.adv_s
  
      with tf.name_scope("loss_ae") as scope:
          # gamma * AE loss
          if self.ae_model:
              # run autoencoder
              self.adv_ae = self.ae(self.adv_cat)  # type: ignore[misc]
              self.adv_ae_s = self.ae(self.adv_cat_s)  # type: ignore[misc]
              if (
                  self.is_cat
              ):  # map output autoencoder back to numerical values
                  self.adv_ae = apply_map(self.adv_ae, to_num=True)
                  self.adv_ae_s = apply_map(self.adv_ae_s, to_num=True)
              # compute loss
              self.loss_ae = self.gamma * tf.square(
                  tf.norm(self.adv_ae - self.adv)
              )
              self.loss_ae_s = self.gamma * tf.square(
                  tf.norm(self.adv_ae_s - self.adv_s)
              )
          else:  # no auto-encoder available
              self.loss_ae = tf.constant(0.0)
              self.loss_ae_s = tf.constant(0.0)
  
      with tf.name_scope("loss_attack") as scope:
          if not self.model:
              self.loss_attack = tf.placeholder(tf.float32)
          elif (
              self.c_init == 0.0 and self.c_steps == 1
          ):  # prediction loss term not used
              # make predictions on perturbed instance
              self.pred_proba = self.predict(self.adv_cat)
              self.pred_proba_s = self.predict(self.adv_cat_s)
  
              self.loss_attack = tf.constant(0.0)
              self.loss_attack_s = tf.constant(0.0)
          else:
              # make predictions on perturbed instance
              self.pred_proba = self.predict(self.adv_cat)
              self.pred_proba_s = self.predict(self.adv_cat_s)
  
              # probability of target label prediction
              self.target_proba = tf.reduce_sum(
                  self.target * self.pred_proba, 1
              )
              target_proba_s = tf.reduce_sum(
                  self.target * self.pred_proba_s, 1
              )
  
              # max probability of non target label prediction
              self.nontarget_proba_max = tf.reduce_max(
                  (1 - self.target) * self.pred_proba
                  - (self.target * 10000),
                  1,
              )
              nontarget_proba_max_s = tf.reduce_max(
                  (1 - self.target) * self.pred_proba_s
                  - (self.target * 10000),
                  1,
              )
  
              # loss term f(x,d)
              loss_attack = tf.maximum(
                  0.0,
                  -self.nontarget_proba_max
                  + self.target_proba
                  + self.kappa,
              )
              loss_attack_s = tf.maximum(
                  0.0,
                  -nontarget_proba_max_s + target_proba_s + self.kappa,
              )
  
              # c * f(x,d)
              self.loss_attack = tf.reduce_sum(self.const * loss_attack)
              self.loss_attack_s = tf.reduce_sum(
                  self.const * loss_attack_s
              )
  
      with tf.name_scope("loss_prototype") as scope:
          if self.enc_model:
              self.loss_proto = self.theta * tf.square(
                  tf.norm(self.enc(self.adv_cat) - self.target_proto)
              )  # type: ignore[misc]
              self.loss_proto_s = self.theta * tf.square(
                  tf.norm(self.enc(self.adv_cat_s) - self.target_proto)
              )  # type: ignore[misc]
          elif self.use_kdtree:
              self.loss_proto = self.theta * tf.square(
                  tf.norm(self.adv - self.target_proto)
              )
              self.loss_proto_s = self.theta * tf.square(
                  tf.norm(self.adv_s - self.target_proto)
              )
          else:  # no encoder available and no k-d trees used
              self.loss_proto = tf.constant(0.0)
              self.loss_proto_s = tf.constant(0.0)
  
      with tf.name_scope("loss_combined") as scope:
          # no need for L1 term in loss to optimize when using FISTA
          if self.model:
              self.loss_opt = (
                  self.loss_attack_s
                  + self.loss_l2_s
                  + self.loss_ae_s
                  + self.loss_proto_s
              )
          else:  # separate numerical computation of loss attack gradient
              self.loss_opt = (
                  self.loss_l2_s + self.loss_ae_s + self.loss_proto_s
              )
  
          # add L1 term to overall loss; this is not the loss that will be directly optimized
          self.loss_total = (
              self.loss_attack
              + self.loss_l2
              + self.loss_ae
              + tf.multiply(self.beta, self.loss_l1)
              + self.loss_proto
          )
  
      with tf.name_scope("training") as scope:
          self.learning_rate = tf.train.polynomial_decay(
              learning_rate_init,
              self.global_step,
              self.max_iterations,
              0,
              power=0.5,
          )
          optimizer = tf.train.GradientDescentOptimizer(
              self.learning_rate
          )
          start_vars = set(x.name for x in tf.global_variables())
  
          # first compute, then apply grads
          self.compute_grads = optimizer.compute_gradients(
              self.loss_opt, var_list=[self.adv_s]
          )
          self.grad_ph = tf.placeholder(tf.float32, name="grad_adv_s")
          var = [
              tvar
              for tvar in tf.trainable_variables()
              if tvar.name.startswith("adv_s")
          ][
              -1
          ]  # get the last in
          # case explainer is re-initialized and a new graph is created
          grad_and_var = [(self.grad_ph, var)]
          self.apply_grads = optimizer.apply_gradients(
              grad_and_var, global_step=self.global_step
          )
          end_vars = tf.global_variables()
          new_vars = [x for x in end_vars if x.name not in start_vars]
  
      # variables to initialize
      self.setup: list = []
      self.setup.append(self.orig.assign(self.assign_orig))
      self.setup.append(self.target.assign(self.assign_target))
      self.setup.append(self.const.assign(self.assign_const))
      self.setup.append(self.adv.assign(self.assign_adv))
      self.setup.append(self.adv_s.assign(self.assign_adv_s))
      self.setup.append(
          self.target_proto.assign(self.assign_target_proto)
      )
      if self.is_cat:
          self.setup.append(self.map_var.assign(self.assign_map))
  
      self.init = tf.variables_initializer(
          var_list=[self.global_step]
          + [self.adv_s]
          + [self.adv]
          + new_vars
      )
  
      if self.write_dir is not None:
          self.writer = tf.summary.FileWriter(
              write_dir, tf.get_default_graph()
          )
          self.writer.add_graph(tf.get_default_graph())
      else:
          self.writer = None
  
imports: import copy
  import logging
  import sys
  from typing import Any, Callable, Dict, Optional, Sequence, Tuple, Union
  import numpy
  import tensorflow.compat.v1
  from alibi.api.defaults import DEFAULT_DATA_CFP, DEFAULT_META_CFP
  from alibi.api.interfaces import Explainer, Explanation, FitMixin
  from alibi.confidence import TrustScore
  from alibi.utils.discretizer import Discretizer
  from alibi.utils.distance import abdm, multidim_scaling, mvdm
  from alibi.utils.gradients import perturb
  from alibi.utils.mapping import num_to_ord, ohe_to_ord, ohe_to_ord_shape, ord_to_num, ord_to_ohe
  from alibi.utils.tf import argmax_grad, argmin_grad, one_hot_grad, round_grad
label_desc: Use image node_id 1 to create a new CounterfactualProto object from inherited base classes: Explainer, FitMixin with example: obj = CounterfactualProto(predict, shape, kappa, beta, feature_range, gamma, ae_model, enc_model, theta, cat_vars, ohe, use_kdtree, learning_rate_init, max_iterations, c_init, c_steps, eps, clip, update_num_grad, write_dir, sess)