docstr: Find a counterfactual (CF) for instance `X` using a fast iterative shrinkage-thresholding algorithm (FISTA).
  
  Parameters
  ----------
  X
      Instance to attack.
  Y
      Labels for `X` as one-hot-encoding.
  target_class
      List with target classes used to find closest prototype. If ``None``, the nearest prototype
      except for the predict class on the instance is used.
  k
      Number of nearest instances used to define the prototype for a class. Defaults to using all
      instances belonging to the class if an encoder is used and to 1 for k-d trees.
  k_type
      Use either the average encoding of the k nearest instances in a class (``k_type='mean'``) or
      the k-nearest encoding in the class (``k_type='point'``) to define the prototype of that class.
      Only relevant if an encoder is used to define the prototypes.
  threshold
      Threshold level for the ratio between the distance of the counterfactual to the prototype of the
      predicted class for the original instance over the distance to the prototype of the predicted class
      for the counterfactual. If the trust score is below the threshold, the proposed counterfactual does
      not meet the requirements.
  verbose
      Print intermediate results of optimization if ``True``.
  print_every
      Print frequency if verbose is ``True``.
  log_every
      `tensorboard` log frequency if write directory is specified.
  
  Returns
  -------
  Overall best attack and gradients for that attack.
docstr_tok: ["Find","a","counterfactual","(","CF",")","for","instance","`","X","`","using","a","fast","iterative","shrinkage-thresholding","algorithm","(","FISTA",")",".","Parameters","--","--","--","--","--","X","Instance","to","attack",".","Y","Labels","for","`","X","`","as","one-hot-encoding",".","target_class","List","with","target","classes","used","to","find","closest","prototype",".","If","``","None","``",",","the","nearest","prototype","except","for","the","predict","class","on","the","instance","is","used",".","k","Number","of","nearest","instances","used","to","define","the","prototype","for","a","class",".","Defaults","to","using","all","instances","belonging","to","the","class","if","an","encoder","is","used","and","to","1","for","k-d","trees",".","k_type","Use","either","the","average","encoding","of","the","k","nearest","instances","in","a","class","(","``","k_type='mean","'","``",")","or","the","k-nearest","encoding","in","the","class","(","``","k_type='point","'","``",")","to","define","the","prototype","of","that","class",".","Only","relevant","if","an","encoder","is","used","to","define","the","prototypes",".","threshold","Threshold","level","for","the","ratio","between","the","distance","of","the","counterfactual","to","the","prototype","of","the","predicted","class","for","the","original","instance","over","the","distance","to","the","prototype","of","the","predicted","class","for","the","counterfactual",".","If","the","trust","score","is","below","the","threshold",",","the","proposed","counterfactual","does","not","meet","the","requirements",".","verbose","Print","intermediate","results","of","optimization","if","``","True","``",".","print_every","Print","frequency","if","verbose","is","``","True","``",".","log_every","`","tensorboard","`","log","frequency","if","write","directory","is","specified",".","Returns","--","--","--","-","Overall","best","attack","and","gradients","for","that","attack","."]
code: def attack(
      self,
      X: np.ndarray,
      Y: np.ndarray,
      target_class: Optional[list] = None,
      k: Optional[int] = None,
      k_type: str = "mean",
      threshold: float = 0.0,
      verbose: bool = False,
      print_every: int = 100,
      log_every: int = 100,
  ) -> Tuple[np.ndarray, Tuple[np.ndarray, np.ndarray]]:
      """
      Find a counterfactual (CF) for instance `X` using a fast iterative shrinkage-thresholding algorithm (FISTA).
  
      Parameters
      ----------
      X
          Instance to attack.
      Y
          Labels for `X` as one-hot-encoding.
      target_class
          List with target classes used to find closest prototype. If ``None``, the nearest prototype
          except for the predict class on the instance is used.
      k
          Number of nearest instances used to define the prototype for a class. Defaults to using all
          instances belonging to the class if an encoder is used and to 1 for k-d trees.
      k_type
          Use either the average encoding of the k nearest instances in a class (``k_type='mean'``) or
          the k-nearest encoding in the class (``k_type='point'``) to define the prototype of that class.
          Only relevant if an encoder is used to define the prototypes.
      threshold
          Threshold level for the ratio between the distance of the counterfactual to the prototype of the
          predicted class for the original instance over the distance to the prototype of the predicted class
          for the counterfactual. If the trust score is below the threshold, the proposed counterfactual does
          not meet the requirements.
      verbose
          Print intermediate results of optimization if ``True``.
      print_every
          Print frequency if verbose is ``True``.
      log_every
          `tensorboard` log frequency if write directory is specified.
  
      Returns
      -------
      Overall best attack and gradients for that attack.
      """
  
      # make sure nb of instances in X equals batch size
      assert self.batch_size == X.shape[0]
  
      def compare(x: Union[float, int, np.ndarray], y: int) -> bool:
          """
          Compare predictions with target labels and return whether counterfactual conditions hold.
  
          Parameters
          ----------
          x
              Predicted class probabilities or labels.
          y
              Target or predicted labels.
  
          Returns
          -------
          Bool whether counterfactual conditions hold.
          """
          if not isinstance(x, (float, int, np.int64)):
              x = np.copy(x)
              x[y] += self.kappa
              x = np.argmax(x)  # type: ignore[assignment]
          return x != y
  
      # define target classes for prototype if not specified yet
      if target_class is None:
          target_class = list(range(self.classes))
          target_class.remove(np.argmax(Y, axis=1))
          if verbose:
              print("Predicted class: {}".format(np.argmax(Y, axis=1)))
              print("Target classes: {}".format(target_class))
  
      if self.is_cat and self.ohe:  # map categorical to numerical data
          X_ord = ohe_to_ord(X, self.cat_vars)[0]
          X_num = ord_to_num(X_ord, self.d_abs)
      elif self.is_cat:
          X_num = ord_to_num(X, self.d_abs)
      else:
          X_num = X
  
      # find closest prototype in the target class list
      dist_proto = {}
      if self.enc_model:
          X_enc = self.enc.predict(X)  # type: ignore[union-attr]
          class_dict = self.class_proto if k is None else self.class_enc
  
          for c, v in class_dict.items():
              if c not in target_class:
                  continue
              if k is None:
                  dist_proto[c] = np.linalg.norm(X_enc - v)
              elif k is not None:
                  dist_k = np.linalg.norm(
                      X_enc.reshape(X_enc.shape[0], -1)
                      - v.reshape(v.shape[0], -1),
                      axis=1,
                  )
                  idx = np.argsort(dist_k)[:k]
                  if k_type == "mean":
                      dist_proto[c] = np.mean(dist_k[idx])
                  else:
                      dist_proto[c] = dist_k[idx[-1]]
                  self.class_proto[c] = np.expand_dims(
                      np.mean(v[idx], axis=0), axis=0
                  )
      elif self.use_kdtree:
          if k is None:
              k = 1
          self.class_proto = {}
          for c in range(self.classes):
              if c not in target_class:
                  continue
              dist_c, idx_c = self.kdtrees[c].query(X_num, k=k)
              dist_proto[c] = dist_c[0][-1]
              self.class_proto[c] = self.X_by_class[c][
                  idx_c[0][-1]
              ].reshape(1, -1)
  
      if self.enc_or_kdtree:
          self.id_proto = min(dist_proto, key=dist_proto.get)  # type: ignore[arg-type]
          proto_val = self.class_proto[self.id_proto]
          if verbose:
              print("Prototype class: {}".format(self.id_proto))
      else:  # no prototype loss term used
          proto_val = np.zeros(self.shape_enc)
  
      # set shape for perturbed instance and gradients
      pert_shape = ohe_to_ord_shape(
          self.shape, cat_vars=self.cat_vars, is_ohe=self.ohe
      )
  
      # set the lower and upper bounds for the constant 'c' to scale the attack loss term
      # these bounds are updated for each c_step iteration
      const_lb = np.zeros(self.batch_size)
      const = np.ones(self.batch_size) * self.c_init
      const_ub = np.ones(self.batch_size) * 1e10
  
      # init values for the best attack instances for each instance in the batch
      overall_best_dist = [1e10] * self.batch_size
      overall_best_attack = [np.zeros(self.shape[1:])] * self.batch_size
      overall_best_grad = (np.zeros(self.shape), np.zeros(self.shape))
  
      # keep track of counterfactual evolution
      self.cf_global: dict = {i: [] for i in range(self.c_steps)}
  
      # iterate over nb of updates for 'c'
      for _ in range(self.c_steps):
          # init variables
          self.sess.run(self.init)
  
          # reset current best distances and scores
          current_best_dist = [1e10] * self.batch_size
          current_best_proba = [-1] * self.batch_size
  
          # assign variables for the current iteration
          feed_dict = {
              self.assign_orig: X_num,
              self.assign_target: Y,
              self.assign_const: const,
              self.assign_adv: X_num,
              self.assign_adv_s: X_num,
              self.assign_target_proto: proto_val,
          }
          if self.is_cat:
              feed_dict[self.assign_map] = self.d_abs_ragged
          self.sess.run(self.setup, feed_dict=feed_dict)
  
          X_der_batch: Any = []
          X_der_batch_s: Any = []
  
          for i in range(self.max_iterations):
              # numerical gradients
              grads_num = np.zeros(pert_shape)
              grads_num_s = np.zeros(pert_shape)
  
              # check if numerical gradient computation is needed
              if not self.model and (
                  self.c_init != 0.0 or self.c_steps > 1
              ):
                  X_der = self.adv.eval(session=self.sess)
                  X_der_s = self.adv_s.eval(session=self.sess)
                  X_der_batch.append(X_der)
                  X_der_batch_s.append(X_der_s)
  
                  if (
                      i % self.update_num_grad == 0 and i > 0
                  ):  # compute numerical gradients
                      c = self.const.eval(session=self.sess)
                      X_der_batch = np.concatenate(X_der_batch)
                      X_der_batch_s = np.concatenate(X_der_batch_s)
                      grads_num = (
                          self.get_gradients(
                              X_der_batch,
                              Y,
                              cat_vars_ord=self.cat_vars_ord,
                              grads_shape=pert_shape[1:],
                          )
                          * c
                      )
                      grads_num_s = (
                          self.get_gradients(
                              X_der_batch_s,
                              Y,
                              cat_vars_ord=self.cat_vars_ord,
                              grads_shape=pert_shape[1:],
                          )
                          * c
                      )
                      # clip gradients
                      grads_num = np.clip(
                          grads_num, self.clip[0], self.clip[1]
                      )
                      grads_num_s = np.clip(
                          grads_num_s, self.clip[0], self.clip[1]
                      )
                      X_der_batch, X_der_batch_s = [], []
  
              # compute and clip gradients defined in graph
              grads_vars_graph = self.sess.run(self.compute_grads)
              grads_graph = [g for g, _ in grads_vars_graph][0]
              grads_graph = np.clip(
                  grads_graph, self.clip[0], self.clip[1]
              )
  
              # apply gradients
              grads = grads_graph + grads_num_s
              self.sess.run(
                  self.apply_grads, feed_dict={self.grad_ph: grads}
              )
  
              # update adv and adv_s with perturbed instances
              self.sess.run(
                  [
                      self.adv_updater,
                      self.adv_updater_s,
                      self.delta,
                      self.delta_s,
                  ]
              )
  
              # compute overall and attack loss, L1+L2 loss, prediction probabilities
              # on perturbed instances and new adv
              # L1+L2 and prediction probabilities used to see if adv is better than the current best adv under FISTA
              if self.model:
                  (
                      loss_tot,
                      loss_attack,
                      loss_l1_l2,
                      pred_proba,
                      adv,
                  ) = self.sess.run(
                      [
                          self.loss_total,
                          self.loss_attack,
                          self.l1_l2,
                          self.pred_proba,
                          self.adv,
                      ]
                  )
              else:
                  X_der = self.adv.eval(
                      session=self.sess
                  )  # get updated perturbed instances
                  if (
                      self.is_cat
                  ):  # map back to categories to make predictions
                      X_der = num_to_ord(X_der, self.d_abs)
                  if self.ohe:
                      X_der = ord_to_ohe(X_der, self.cat_vars_ord)[0]
                  pred_proba = self.predict(X_der)
  
                  # compute attack, total and L1+L2 losses as well as new perturbed instance
                  loss_attack = self.loss_fn(pred_proba, Y)
                  feed_dict = {self.loss_attack: loss_attack}
                  loss_tot, loss_l1_l2, adv = self.sess.run(
                      [self.loss_total, self.l1_l2, self.adv],
                      feed_dict=feed_dict,
                  )
  
              if i % log_every == 0 or i % print_every == 0:
                  loss_l2, loss_l1, loss_ae, loss_proto = self.sess.run(
                      [
                          self.loss_l2,
                          self.loss_l1,
                          self.loss_ae,
                          self.loss_proto,
                      ]
                  )
                  target_proba = np.sum(pred_proba * Y)
                  nontarget_proba_max = np.max((1 - Y) * pred_proba)
                  loss_opt = (
                      loss_l1_l2 + loss_attack + loss_ae + loss_proto
                  )
  
              if i % log_every == 0 and self.writer is not None:
                  lr, zt, gs = self.sess.run(
                      [self.learning_rate, self.zt, self.global_step]
                  )
  
                  # add values and images to tensorboard
                  summary = tf.Summary()
                  summary.value.add(
                      tag="loss/Optimized", simple_value=loss_opt
                  )
                  summary.value.add(
                      tag="loss/Total", simple_value=loss_tot
                  )
                  summary.value.add(tag="loss/L1", simple_value=loss_l1)
                  summary.value.add(tag="loss/L2", simple_value=loss_l2)
                  summary.value.add(
                      tag="loss/AutoEncoder", simple_value=loss_ae
                  )
                  summary.value.add(
                      tag="loss/ClassPrototype", simple_value=loss_proto
                  )
                  summary.value.add(
                      tag="loss/PredScale", simple_value=const[0]
                  )
                  summary.value.add(
                      tag="loss/PredLoss", simple_value=loss_attack
                  )
                  summary.value.add(tag="training/lr", simple_value=lr)
                  summary.value.add(tag="training/z", simple_value=zt)
                  summary.value.add(
                      tag="training/GlobalStep", simple_value=gs
                  )
                  self.writer.add_summary(summary)
                  self.writer.flush()
  
              if verbose and i % print_every == 0:
                  print(
                      "\nIteration: {}; Const: {}".format(i, const[0])
                  )
                  print(
                      "Loss total: {:.3f}, loss attack: {:.3f}".format(
                          loss_tot, loss_attack
                      )
                  )
                  print(
                      "L2: {:.3f}, L1: {:.3f}, loss AE: {:.3f}".format(
                          loss_l2, loss_l1, loss_ae
                      )
                  )
                  print("Loss proto: {:.3f}".format(loss_proto))
                  print(
                      "Target proba: {:.2f}, max non target proba: {:.2f}".format(
                          target_proba, nontarget_proba_max
                      )
                  )
                  print(
                      "Gradient graph min/max: {:.3f}/{:.3f}".format(
                          grads_graph.min(), grads_graph.max()
                      )
                  )
                  print(
                      "Gradient graph mean/abs mean: {:.3f}/{:.3f}".format(
                          np.mean(grads_graph),
                          np.mean(np.abs(grads_graph)),
                      )
                  )
                  if not self.model:
                      print(
                          "Gradient numerical attack min/max: {:.3f}/{:.3f}".format(
                              grads_num.min(), grads_num.max()
                          )
                      )
                      print(
                          "Gradient numerical mean/abs mean: {:.3f}/{:.3f}".format(  # type: ignore[str-format]
                              np.mean(grads_num),
                              np.mean(np.abs(grads_num)),
                          )
                      )
                  sys.stdout.flush()
  
              # update best perturbation (distance) and class probabilities
              # if beta * L1 + L2 < current best and predicted label is different from the initial label:
              # update best current step or global perturbations
              for batch_idx, (dist, proba, adv_idx) in enumerate(
                  zip(loss_l1_l2, pred_proba, adv)
              ):
                  Y_class = np.argmax(Y[batch_idx])
                  adv_class = np.argmax(proba)
                  adv_idx = np.expand_dims(adv_idx, axis=0)
  
                  if self.is_cat:  # map back to categories
                      adv_idx = num_to_ord(adv_idx, self.d_abs)
  
                  if self.ohe:  # map back from ordinal to OHE
                      adv_idx = ord_to_ohe(adv_idx, self.cat_vars_ord)[
                          0
                      ]
  
                  # calculate trust score
                  if threshold > 0.0:
                      score = self.score(adv_idx, np.argmax(pred_proba), Y_class)  # type: ignore
                      above_threshold = score > threshold
                  else:
                      above_threshold = True
  
                  # current step
                  if (
                      dist < current_best_dist[batch_idx]
                      and compare(proba, Y_class)  # type: ignore
                      and above_threshold
                      and adv_class in target_class
                  ):
                      current_best_dist[batch_idx] = dist
                      current_best_proba[batch_idx] = adv_class  # type: ignore
  
                  # global
                  if (
                      dist < overall_best_dist[batch_idx]
                      and compare(proba, Y_class)  # type: ignore
                      and above_threshold
                      and adv_class in target_class
                  ):
                      if verbose:
                          print("\nNew best counterfactual found!")
                      overall_best_dist[batch_idx] = dist
                      overall_best_attack[batch_idx] = adv_idx
                      overall_best_grad = (grads_graph, grads_num)
                      self.best_attack = True
                      self.cf_global[_].append(adv_idx)
  
          # adjust the 'c' constant for the first loss term
          for batch_idx in range(self.batch_size):
              if (
                  compare(
                      current_best_proba[batch_idx],
                      np.argmax(Y[batch_idx]),
                  )
                  and current_best_proba[batch_idx]  # type: ignore
                  != -1
              ):
                  # want to refine the current best solution by putting more emphasis on the regularization terms
                  # of the loss by reducing 'c'; aiming to find a perturbation closer to the original instance
                  const_ub[batch_idx] = min(
                      const_ub[batch_idx], const[batch_idx]
                  )
                  if const_ub[batch_idx] < 1e9:
                      const[batch_idx] = (
                          const_lb[batch_idx] + const_ub[batch_idx]
                      ) / 2
              else:
                  # no valid current solution; put more weight on the first loss term to try and meet the
                  # prediction constraint before finetuning the solution with the regularization terms
                  const_lb[batch_idx] = max(
                      const_lb[batch_idx], const[batch_idx]
                  )  # update lower bound to constant
                  if const_ub[batch_idx] < 1e9:
                      const[batch_idx] = (
                          const_lb[batch_idx] + const_ub[batch_idx]
                      ) / 2
                  else:
                      const[batch_idx] *= 10
  
      # return best overall attack
      best_attack = np.concatenate(overall_best_attack, axis=0)
      if best_attack.shape != self.shape:
          best_attack = np.expand_dims(best_attack, axis=0)
  
      return best_attack, overall_best_grad
  
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 6 for calling the CounterfactualProto obj's underlying member method code with example usage: obj.attack(X, Y, target_class, k, k_type, threshold, verbose, print_every, log_every) and returns: best_attack,  overall_best_grad, unknown