def _compute_perturbation(
        self,
        x: "tf.Tensor",
        y: "tf.Tensor",
        mask: "tf.Tensor" | None,
        decay: float | None = None,
        momentum: "tf.Tensor" | None = None,
    ) -> "tf.Tensor":
        """
        Compute perturbations.

        :param x: Current adversarial examples.
        :param y: Target values (class labels) one-hot-encoded of shape `(nb_samples, nb_classes)` or indices of shape
                  (nb_samples,). Only provide this parameter if you'd like to use true labels when crafting adversarial
                  samples. Otherwise, model predictions are used as labels to avoid the "label leaking" effect
                  (explained in this paper: https://arxiv.org/abs/1611.01236). Default is `None`.
        :param mask: An array with a mask broadcastable to input `x` defining where to apply adversarial perturbations.
                     Shape needs to be broadcastable to the shape of x and can also be of the same shape as `x`. Any
                     features for which the mask is zero will not be adversarially perturbed.
        :param decay: Decay factor for accumulating the velocity vector when using momentum.
        :param momentum: An array accumulating the velocity vector in the gradient direction for MIFGSM.
        :return: Perturbations.
        """
        import tensorflow as tf

        # Get gradient wrt loss; invert it if attack is targeted
        grad: tf.Tensor = self.estimator.loss_gradient(x, y) * tf.constant(
            -1 if self.targeted else 1, dtype=ART_NUMPY_DTYPE
        )

        # Write summary
        if self.summary_writer is not None:  # pragma: no cover
            self.summary_writer.update(
                batch_id=self._batch_id,
                global_step=self._i_max_iter,
                grad=grad.numpy(),
                patch=None,
                estimator=self.estimator,
                x=x.numpy(),
                y=y.numpy(),
                targeted=self.targeted,
            )

        # Check for NaN before normalisation and replace with 0
        if tf.reduce_any(tf.math.is_nan(grad)):  # pragma: no cover
            logger.warning("Elements of the loss gradient are NaN and have been replaced with 0.0.")
            grad = tf.where(tf.math.is_nan(grad), tf.zeros_like(grad), grad)

        # Apply mask
        if mask is not None:
            grad = tf.where(mask == 0.0, 0.0, grad)

        # Compute gradient momentum
        if decay is not None and momentum is not None:
            raise NotImplementedError(  # Upon fixing #2439, remove pylint disable flag below.
                "Momentum Iterative Attack currently disabled for Tensorflow framework. See issue #2439"
            )
            # Update momentum in-place (important).
            # The L1 normalization for accumulation is an arbitrary choice of the paper.
            grad_2d = tf.reshape(grad, (len(grad), -1))  # pylint: disable=unreachable
            norm1 = tf.norm(grad_2d, ord=1, axis=1, keepdims=True)
            normalized_grad = tf.reshape((grad_2d * tf.where(norm1 == 0, 0.0, 1 / norm1)), grad.shape)
            momentum *= self.decay
            momentum += normalized_grad
            # Use the momentum to compute the perturbation, instead of the gradient
            grad = momentum

        # Apply norm bound
        norm: float = np.inf if self.norm == "inf" else float(self.norm)
        grad_2d = tf.reshape(grad, (len(grad), -1))
        if norm == np.inf:
            grad_2d = tf.ones_like(grad_2d, dtype=grad_2d.dtype)
        elif norm == 1:
            grad_2d = tf.abs(grad_2d)
            grad_2d = tf.where(grad_2d == tf.reduce_max(grad_2d, axis=1, keepdims=True), 1.0, 0.0)
            grad_2d /= tf.reduce_sum(grad_2d, axis=1, keepdims=True)
        elif norm > 1:
            conjugate = norm / (norm - 1)
            q_norm = tf.norm(grad_2d, ord=conjugate, axis=1, keepdims=True)
            grad_2d = (tf.abs(grad_2d) * tf.where(q_norm == 0, 0.0, 1 / q_norm)) ** (conjugate - 1)

        grad = tf.reshape(grad_2d, grad.shape) * tf.sign(grad)

        assert x.shape == grad.shape

        return grad

        Compute perturbations.

        :param x: Current adversarial examples.
        :param y: Target values (class labels) one-hot-encoded of shape `(nb_samples, nb_classes)` or indices of shape
                  (nb_samples,). Only provide this parameter if you'd like to use true labels when crafting adversarial
                  samples. Otherwise, model predictions are used as labels to avoid the "label leaking" effect
                  (explained in this paper: https://arxiv.org/abs/1611.01236). Default is `None`.
        :param mask: An array with a mask broadcastable to input `x` defining where to apply adversarial perturbations.
                     Shape needs to be broadcastable to the shape of x and can also be of the same shape as `x`. Any
                     features for which the mask is zero will not be adversarially perturbed.
        :param decay: Decay factor for accumulating the velocity vector when using momentum.
        :param momentum: An array accumulating the velocity vector in the gradient direction for MIFGSM.
        :return: Perturbations.
        
art/attacks/evasion/projected_gradient_descent/projected_gradient_descent_tensorflow_v2.py