class_docstr: A powerful adversarial attack that requires neither gradients
  nor probabilities.
  
  This is the reference implementation for the attack. [#Bren18]_
  
  Notes:
      Differences to the original reference implementation:
      * We do not perform internal operations with float64
      * The samples within a batch can currently influence each other a bit
      * We don't perform the additional convergence confirmation
      * The success rate tracking changed a bit
      * Some other changes due to batching and merged loops
  
  Args:
      init_attack : Attack to use to find a starting points. Defaults to
          LinearSearchBlendedUniformNoiseAttack. Only used if starting_points is None.
      steps : Maximum number of steps to run. Might converge and stop before that.
      spherical_step : Initial step size for the orthogonal (spherical) step.
      source_step : Initial step size for the step towards the target.
      source_step_convergance : Sets the threshold of the stop criterion:
          if source_step becomes smaller than this value during the attack,
          the attack has converged and will stop.
      step_adaptation : Factor by which the step sizes are multiplied or divided.
      tensorboard : The log directory for TensorBoard summaries. If False, TensorBoard
          summaries will be disabled (default). If None, the logdir will be
          runs/CURRENT_DATETIME_HOSTNAME.
      update_stats_every_k :
  
  References:
      .. [#Bren18] Wieland Brendel (*), Jonas Rauber (*), Matthias Bethge,
         "Decision-Based Adversarial Attacks: Reliable Attacks
         Against Black-Box Machine Learning Models",
         https://arxiv.org/abs/1712.04248
code: def run(
      self,
      model: Model,
      inputs: T,
      criterion: Union[Criterion, T],
      *,
      early_stop: Optional[float] = None,
      starting_points: Optional[T] = None,
      **kwargs: Any,
  ) -> T:
      raise_if_kwargs(kwargs)
      originals, restore_type = ep.astensor_(inputs)
      del inputs, kwargs
  
      verify_input_bounds(originals, model)
  
      criterion = get_criterion(criterion)
      is_adversarial = get_is_adversarial(criterion, model)
  
      if starting_points is None:
          init_attack: MinimizationAttack
          if self.init_attack is None:
              init_attack = LinearSearchBlendedUniformNoiseAttack(
                  steps=50
              )
              logging.info(
                  f"Neither starting_points nor init_attack given. Falling"
                  f" back to {init_attack!r} for initialization."
              )
          else:
              init_attack = self.init_attack
          # TODO: use call and support all types of attacks (once early_stop is
          # possible in __call__)
          best_advs = init_attack.run(
              model, originals, criterion, early_stop=early_stop
          )
      else:
          best_advs = ep.astensor(starting_points)
  
      is_adv = is_adversarial(best_advs)
      if not is_adv.all():
          failed = is_adv.logical_not().float32().sum()
          if starting_points is None:
              raise ValueError(
                  f"init_attack failed for {failed} of {len(is_adv)} inputs"
              )
          else:
              raise ValueError(
                  f"{failed} of {len(is_adv)} starting_points are not adversarial"
              )
      del starting_points
  
      tb = TensorBoard(logdir=self.tensorboard)
  
      N = len(originals)
      ndim = originals.ndim
      spherical_steps = ep.ones(originals, N) * self.spherical_step
      source_steps = ep.ones(originals, N) * self.source_step
  
      tb.scalar("batchsize", N, 0)
  
      # create two queues for each sample to track success rates
      # (used to update the hyper parameters)
      stats_spherical_adversarial = ArrayQueue(maxlen=100, N=N)
      stats_step_adversarial = ArrayQueue(maxlen=30, N=N)
  
      bounds = model.bounds
  
      for step in range(1, self.steps + 1):
          converged = source_steps < self.source_step_convergance
          if converged.all():
              break  # pragma: no cover
          converged = atleast_kd(converged, ndim)
  
          # TODO: performance: ignore those that have converged
          # (we could select the non-converged ones, but we currently
          # cannot easily invert this in the end using EagerPy)
  
          unnormalized_source_directions = originals - best_advs
          source_norms = ep.norms.l2(
              flatten(unnormalized_source_directions), axis=-1
          )
          source_directions = (
              unnormalized_source_directions
              / atleast_kd(source_norms, ndim)
          )
  
          # only check spherical candidates every k steps
          check_spherical_and_update_stats = (
              step % self.update_stats_every_k == 0
          )
  
          candidates, spherical_candidates = draw_proposals(
              bounds,
              originals,
              best_advs,
              unnormalized_source_directions,
              source_directions,
              source_norms,
              spherical_steps,
              source_steps,
          )
          candidates.dtype == originals.dtype
          spherical_candidates.dtype == spherical_candidates.dtype
  
          is_adv = is_adversarial(candidates)
  
          spherical_is_adv: Optional[ep.Tensor]
          if check_spherical_and_update_stats:
              spherical_is_adv = is_adversarial(spherical_candidates)
              stats_spherical_adversarial.append(spherical_is_adv)
              # TODO: algorithm: the original implementation ignores those samples
              # for which spherical is not adversarial and continues with the
              # next iteration -> we estimate different probabilities (conditional vs. unconditional)
              # TODO: thoughts: should we always track this because we compute it anyway
              stats_step_adversarial.append(is_adv)
          else:
              spherical_is_adv = None
  
          # in theory, we are closer per construction
          # but limited numerical precision might break this
          distances = ep.norms.l2(
              flatten(originals - candidates), axis=-1
          )
          closer = distances < source_norms
          is_best_adv = ep.logical_and(is_adv, closer)
          is_best_adv = atleast_kd(is_best_adv, ndim)
  
          cond = converged.logical_not().logical_and(is_best_adv)
          best_advs = ep.where(cond, candidates, best_advs)
  
          tb.probability("converged", converged, step)
          tb.scalar(
              "updated_stats", check_spherical_and_update_stats, step
          )
          tb.histogram("norms", source_norms, step)
          tb.probability("is_adv", is_adv, step)
          if spherical_is_adv is not None:
              tb.probability("spherical_is_adv", spherical_is_adv, step)
          tb.histogram("candidates/distances", distances, step)
          tb.probability("candidates/closer", closer, step)
          tb.probability("candidates/is_best_adv", is_best_adv, step)
          tb.probability(
              "new_best_adv_including_converged", is_best_adv, step
          )
          tb.probability("new_best_adv", cond, step)
  
          if check_spherical_and_update_stats:
              full = stats_spherical_adversarial.isfull()
              tb.probability("spherical_stats/full", full, step)
              if full.any():
                  probs = stats_spherical_adversarial.mean()
                  cond1 = ep.logical_and(probs > 0.5, full)
                  spherical_steps = ep.where(
                      cond1,
                      spherical_steps * self.step_adaptation,
                      spherical_steps,
                  )
                  source_steps = ep.where(
                      cond1,
                      source_steps * self.step_adaptation,
                      source_steps,
                  )
                  cond2 = ep.logical_and(probs < 0.2, full)
                  spherical_steps = ep.where(
                      cond2,
                      spherical_steps / self.step_adaptation,
                      spherical_steps,
                  )
                  source_steps = ep.where(
                      cond2,
                      source_steps / self.step_adaptation,
                      source_steps,
                  )
                  stats_spherical_adversarial.clear(
                      ep.logical_or(cond1, cond2)
                  )
                  tb.conditional_mean(
                      "spherical_stats/isfull/success_rate/mean",
                      probs,
                      full,
                      step,
                  )
                  tb.probability_ratio(
                      "spherical_stats/isfull/too_linear",
                      cond1,
                      full,
                      step,
                  )
                  tb.probability_ratio(
                      "spherical_stats/isfull/too_nonlinear",
                      cond2,
                      full,
                      step,
                  )
  
              full = stats_step_adversarial.isfull()
              tb.probability("step_stats/full", full, step)
              if full.any():
                  probs = stats_step_adversarial.mean()
                  # TODO: algorithm: changed the two values because we are currently tracking p(source_step_sucess)
                  # instead of p(source_step_success | spherical_step_sucess) that was tracked before
                  cond1 = ep.logical_and(probs > 0.25, full)
                  source_steps = ep.where(
                      cond1,
                      source_steps * self.step_adaptation,
                      source_steps,
                  )
                  cond2 = ep.logical_and(probs < 0.1, full)
                  source_steps = ep.where(
                      cond2,
                      source_steps / self.step_adaptation,
                      source_steps,
                  )
                  stats_step_adversarial.clear(
                      ep.logical_or(cond1, cond2)
                  )
                  tb.conditional_mean(
                      "step_stats/isfull/success_rate/mean",
                      probs,
                      full,
                      step,
                  )
                  tb.probability_ratio(
                      "step_stats/isfull/success_rate_too_high",
                      cond1,
                      full,
                      step,
                  )
                  tb.probability_ratio(
                      "step_stats/isfull/success_rate_too_low",
                      cond2,
                      full,
                      step,
                  )
  
          tb.histogram("spherical_step", spherical_steps, step)
          tb.histogram("source_step", source_steps, step)
      tb.close()
      return restore_type(best_advs)
  
imports: from typing import Union, Tuple, Optional, Any
  from typing_extensions import Literal
  import numpy
  import eagerpy
  import logging
  from ..devutils import flatten
  from ..devutils import atleast_kd
  from ..types import Bounds
  from ..models import Model
  from ..criteria import Criterion
  from ..distances import l2
  from ..tensorboard import TensorBoard
  from .blended_noise import LinearSearchBlendedUniformNoiseAttack
  from .base import MinimizationAttack
  from .base import T
  from .base import get_criterion
  from .base import get_is_adversarial
  from .base import raise_if_kwargs
  from .base import verify_input_bounds
label_desc: Use image node_id 2 for calling the BoundaryAttack obj's underlying member method code with example usage: obj.run(model, inputs, criterion) and returns: restore_type