def generate(
        self,
        x: np.ndarray,
        y: np.ndarray | None = None,
        **kwargs,
    ) -> np.ndarray:
        """
        Applies the Brendel & Bethge attack.

        :param x: The original clean inputs.
        :param y: The labels for inputs `x`.

        :Keyword Arguments:
            * *starting_points* (``np.ndarray``)
                Optional. Adversarial inputs to use as a starting points, in particular for targeted attacks.
        """
        starting_points = kwargs.get("starting_points")
        # early_stop = kwargs.get("early_stop")

        originals = x.copy()

        if y is not None:
            y = check_and_transform_label_format(y, nb_classes=self.estimator.nb_classes)

        if y is None:
            # Throw error if attack is targeted, but no targets are provided
            if self.targeted:
                raise ValueError("Target labels `y` need to be provided for a targeted attack.")

            # Use model predictions as correct outputs
            logger.info("Using model predictions as correct labels for FGM.")
            y = get_labels_np_array(self.estimator.predict(x, batch_size=self.batch_size))  # type: ignore

        if self.estimator.nb_classes == 2 and y.shape[1] == 1:
            raise ValueError(
                "This attack has not yet been tested for binary classification with a single output classifier."
            )

        # Prediction from the initial adversarial examples if not None
        x_adv_init = kwargs.get("x_adv_init")

        if x_adv_init is not None:
            init_preds = np.argmax(self.estimator.predict(x_adv_init, batch_size=self.batch_size), axis=1)
        else:
            init_preds = [None] * len(x)
            x_adv_init = [None] * len(x)

        classes = y

        if starting_points is None:

            starting_points = np.zeros_like(x)
            # First, create an initial adversarial sample
            clip_min, clip_max = self.estimator.clip_values
            # Prediction from the original images
            preds = np.argmax(self.estimator.predict(x, batch_size=self.batch_size), axis=1)
            y_index = np.argmax(y, axis=1)

            for i_x in range(x.shape[0]):
                initial_sample = self._init_sample(
                    x=x[i_x],
                    y=y_index[i_x],
                    y_p=preds[i_x],
                    init_pred=init_preds[i_x],
                    adv_init=x_adv_init[i_x],
                    clip_min=clip_min,
                    clip_max=clip_max,
                )

                if initial_sample is None:
                    starting_points[i_x] = x[i_x]
                else:
                    starting_points[i_x] = initial_sample[0]

        best_advs = starting_points

        if self.targeted:
            assert (np.argmax(self.estimator.predict(x=best_advs), axis=1) == np.argmax(y, axis=1)).all()
        else:
            assert (np.argmax(self.estimator.predict(x=best_advs), axis=1) != np.argmax(y, axis=1)).all()

        # perform binary search to find adversarial boundary
        # TODO: Implement more efficient search with breaking condition
        N = len(originals)
        rows = range(N)

        bounds = self.estimator.clip_values
        min_, max_ = bounds

        x0 = originals
        x0_np_flatten = x0.reshape((N, -1))
        x1 = best_advs

        lower_bound = np.zeros(shape=(N,))
        upper_bound = np.ones(shape=(N,))

        for _ in range(self.binary_search_steps):
            epsilons = (lower_bound + upper_bound) / 2
            mid_points = self.mid_points(x0, x1, epsilons, bounds)
            if self.targeted:
                is_advs = (np.argmax(self.estimator.predict(x=mid_points), axis=1) == np.argmax(y, axis=1)).all()
            else:
                is_advs = (np.argmax(self.estimator.predict(x=mid_points), axis=1) != np.argmax(y, axis=1)).all()
            lower_bound = np.where(is_advs, lower_bound, epsilons)
            upper_bound = np.where(is_advs, epsilons, upper_bound)

        starting_points = self.mid_points(x0, x1, upper_bound, bounds)

        x = starting_points.astype(config.ART_NUMPY_DTYPE)
        lrs = self.lr * np.ones(N)
        lr_reduction_interval = max(1, int(self.steps / self.lr_num_decay))
        converged = np.zeros(N, dtype=bool)
        rate_normalization = np.prod(x.shape) * (max_ - min_)
        original_shape = x.shape
        _best_advs = best_advs.copy()

        from tqdm.auto import trange

        for step in trange(1, self.steps + 1):
            if converged.all():
                break  # pragma: no cover

            # get logits and local boundary geometry
            # TODO: only perform forward pass on non-converged samples

            logits = self.estimator.predict(x=x)

            exclude = classes
            logits_exclude = logits.copy()

            logits_exclude[:, np.argmax(exclude, axis=1)] = -np.inf

            best_other_classes = np.argmax(logits_exclude, axis=1)

            if self.targeted:
                c_minimize = best_other_classes
                c_maximize = np.argmax(classes, axis=1)
            else:
                c_minimize = np.argmax(classes, axis=1)
                c_maximize = best_other_classes

            logits_diffs = logits[rows, c_minimize] - logits[rows, c_maximize]

            _boundary = self.estimator.loss_gradient(x=x, y=y)

            if self.targeted:
                _boundary = -_boundary

            # record optimal adversarials
            distances = self.norms(originals - x)
            source_norms = self.norms(originals - best_advs)

            closer = distances < source_norms
            closer = np.squeeze(closer)
            is_advs = logits_diffs < 0
            closer = np.logical_and(closer, is_advs)

            x_np_flatten = x.reshape((N, -1))

            if closer.any():
                _best_advs = best_advs.copy()
                _closer = closer.flatten()
                for idx in np.arange(N)[_closer]:
                    _best_advs[idx] = x_np_flatten[idx].reshape(original_shape[1:])

            best_advs = _best_advs.copy()

            # denoise estimate of boundary using a short history of the boundary
            if step == 1:
                boundary = _boundary
            else:
                boundary = (1 - self.momentum) * _boundary + self.momentum * boundary

            # learning rate adaptation
            if (step + 1) % lr_reduction_interval == 0:
                lrs *= self.lr_decay

            # compute optimal step within trust region depending on metric
            x = x.reshape((N, -1))
            region = lrs * rate_normalization

            # we aim to slight overshoot over the boundary to stay within the adversarial region
            corr_logits_diffs = np.where(
                -logits_diffs < 0,
                -self.overshoot * logits_diffs,
                -(2 - self.overshoot) * logits_diffs,
            )

            # employ solver to find optimal step within trust region for each sample
            deltas, k = [], 0

            for sample in range(N):
                if converged[sample]:
                    # don't perform optimisation on converged samples
                    deltas.append(np.zeros_like(x0_np_flatten[sample]))  # pragma: no cover
                else:
                    _x0 = x0_np_flatten[sample]
                    _x = x_np_flatten[sample]
                    _b = boundary[k].flatten()
                    _c = corr_logits_diffs[k]
                    r = region[sample]

                    delta = self._optimizer.solve(_x0, _x, _b, bounds[0], bounds[1], _c, r)  # type: ignore
                    deltas.append(delta)

                    k += 1  # idx of masked sample

            deltas_array = np.stack(deltas).astype(np.float32)

            # add step to current perturbation
            x = (x + deltas_array).reshape(original_shape)

        return best_advs.astype(config.ART_NUMPY_DTYPE)

        Applies the Brendel & Bethge attack.

        :param x: The original clean inputs.
        :param y: The labels for inputs `x`.

        :Keyword Arguments:
            * *starting_points* (``np.ndarray``)
                Optional. Adversarial inputs to use as a starting points, in particular for targeted attacks.
        
art/attacks/evasion/brendel_bethge.py