def update(
        self,
        batch_id: int,
        global_step: int,
        grad: np.ndarray | None = None,
        patch: np.ndarray | None = None,
        estimator=None,
        x: np.ndarray | None = None,
        y: np.ndarray | None = None,
        targeted: bool = False,
        **kwargs,
    ):
        """
        Update the summary writer.

        :param batch_id: ID of the current mini-batch.
        :param global_step: Global iteration step.
        :param grad: Loss gradients.
        :param patch: Adversarial patch.
        :param estimator: The estimator to evaluate or calculate gradients of `grad` is None to obtain new metrics.
        :param x: Input data.
        :param y: True or target labels.
        :param targeted: Indicates whether the attack is targeted (True) or untargeted (False).
        """

        # Gradients
        if grad is not None:

            l_1 = np.linalg.norm(grad.reshape(grad.shape[0], -1), axis=1, ord=1)
            self.summary_writer.add_scalars(
                f"gradients/norm-L1/batch-{batch_id}",
                {str(i): v for i, v in enumerate(l_1)},
                global_step=global_step,
            )

            l_2 = np.linalg.norm(grad.reshape(grad.shape[0], -1), axis=1, ord=2)
            self.summary_writer.add_scalars(
                f"gradients/norm-L2/batch-{batch_id}",
                {str(i): v for i, v in enumerate(l_2)},
                global_step=global_step,
            )

            l_inf = np.linalg.norm(grad.reshape(grad.shape[0], -1), axis=1, ord=np.inf)
            self.summary_writer.add_scalars(
                f"gradients/norm-Linf/batch-{batch_id}",
                {str(i): v for i, v in enumerate(l_inf)},
                global_step=global_step,
            )

        # Patch
        if patch is not None:
            if patch.shape[2] in [1, 3, 4]:
                patch = np.transpose(patch, (2, 0, 1))
            self.summary_writer.add_image(
                "patch",
                patch,
                global_step=global_step,
            )

        # Losses
        if estimator is not None and x is not None and y is not None:
            if hasattr(estimator, "compute_losses"):
                losses = estimator.compute_losses(x=x, y=y)

                for key, value in losses.items():
                    if np.ndim(value) == 0:
                        self.summary_writer.add_scalar(
                            f"loss/{key}/batch-{batch_id}",
                            value,
                            global_step=global_step,
                        )
                    else:
                        self.summary_writer.add_scalars(
                            f"loss/{key}/batch-{batch_id}",
                            {str(i): v for i, v in enumerate(value)},
                            global_step=global_step,
                        )

            elif hasattr(estimator, "compute_loss"):
                loss = estimator.compute_loss(x=x, y=y)

                if np.ndim(loss) == 0:
                    self.summary_writer.add_scalar(
                        f"loss/batch-{batch_id}",
                        loss,
                        global_step=global_step,
                    )
                else:
                    self.summary_writer.add_scalars(
                        f"loss/batch-{batch_id}",
                        {str(i): v for i, v in enumerate(loss)},
                        global_step=global_step,
                    )

        # Indicators of Attack Failure by Pintor et al. (2021)
        # Paper link: https://arxiv.org/abs/2106.09947
        if self.ind_1:  # Silent Success
            from art.estimators.classification.classifier import ClassifierMixin

            if isinstance(estimator, ClassifierMixin):
                if y is not None:
                    y_pred = estimator.predict(x)  # type: ignore
                    self.i_1 = np.argmax(y_pred, axis=1) == np.argmax(y, axis=1)
                    self.summary_writer.add_scalars(
                        f"Attack Failure Indicator 1 - Silent Success/batch-{batch_id}",
                        {str(i): v for i, v in enumerate(self.i_1)},
                        global_step=global_step,
                    )
                else:
                    raise ValueError("Attack Failure Indicator 1 requires `y`.")
            else:
                raise ValueError(
                    "Attack Failure Indicator 1 is only supported for classification, for the current "
                    "`estimator` set `ind_1=False`."
                )

        if self.ind_2:  # Break-point Angle
            losses = estimator.compute_loss(x=x, y=y)

            if str(batch_id) not in self.losses:
                self.losses[str(batch_id)] = []

            self.losses[str(batch_id)].append(losses)

            self.i_2 = np.ones_like(losses)

            if len(self.losses[str(batch_id)]) >= 3:

                delta_loss = self.losses[str(batch_id)][0] - self.losses[str(batch_id)][-1]
                delta_step = global_step

                side_b = sqrt(2.0)

                for i_step in range(1, len(self.losses[str(batch_id)]) - 1):

                    side_a = np.sqrt(
                        np.square((self.losses[str(batch_id)][0] - self.losses[str(batch_id)][i_step]) / delta_loss)
                        + (i_step / delta_step) ** 2
                    )
                    side_c = np.sqrt(
                        np.square((self.losses[str(batch_id)][i_step] - self.losses[str(batch_id)][-1]) / delta_loss)
                        + ((delta_step - i_step) / delta_step) ** 2
                    )
                    cos_beta = -(side_b**2 - (side_a**2 + side_c**2)) / (2 * side_a * side_c)

                    i_2_step = 1 - np.abs(cos_beta)
                    self.i_2 = np.minimum(self.i_2, i_2_step)

                if np.ndim(self.i_2) == 0:
                    self.summary_writer.add_scalar(
                        f"Attack Failure Indicator 2 - Break-point Angle/batch-{batch_id}",
                        self.i_2,
                        global_step=global_step,
                    )
                else:
                    self.summary_writer.add_scalars(
                        f"Attack Failure Indicator 2 - Break-point Angle/batch-{batch_id}",
                        {str(i): v for i, v in enumerate(self.i_2)},
                        global_step=global_step,
                    )

        if self.ind_3:  # Diverging (Increasing) Loss
            loss = estimator.compute_loss(x=x, y=y)

            if str(batch_id) in self.i_3:
                if targeted:
                    if isinstance(loss, float):
                        loss_add = loss
                    else:
                        loss_add = loss[loss > self.loss_prev[str(batch_id)]]
                    self.i_3[str(batch_id)][loss > self.loss_prev[str(batch_id)]] += loss_add
                else:
                    if isinstance(loss, float):
                        loss_add = loss
                    else:
                        loss_add = loss[loss < self.loss_prev[str(batch_id)]]
                    self.i_3[str(batch_id)][loss < self.loss_prev[str(batch_id)]] += loss_add
            else:
                self.i_3[str(batch_id)] = np.zeros_like(loss)

            if np.ndim(self.i_3[str(batch_id)]) == 0:
                self.summary_writer.add_scalar(
                    f"Attack Failure Indicator 3 - Diverging Loss/batch-{batch_id}",
                    self.i_3[str(batch_id)],
                    global_step=global_step,
                )
            else:
                self.summary_writer.add_scalars(
                    f"Attack Failure Indicator 3 - Diverging Loss/batch-{batch_id}",
                    {str(i): v for i, v in enumerate(self.i_3[str(batch_id)])},
                    global_step=global_step,
                )

            self.loss_prev[str(batch_id)] = loss

        if self.ind_4:  # Zero Gradients

            threshold = 0.0

            if grad is not None:

                if str(batch_id) not in self.i_4:
                    self.i_4[str(batch_id)] = np.zeros(grad.shape[0])

                self.i_4[str(batch_id)][
                    np.linalg.norm(grad.reshape(grad.shape[0], -1), axis=1, ord=2) <= threshold
                ] += 1

                self.summary_writer.add_scalars(
                    f"Attack Failure Indicator 4 - Zero Gradients/batch-{batch_id}",
                    {str(i): v for i, v in enumerate(self.i_4[str(batch_id)] / global_step)},
                    global_step=global_step,
                )
            else:
                raise ValueError("Attack Failure Indicator 4 requires `grad`.")

        Update the summary writer.

        :param batch_id: ID of the current mini-batch.
        :param global_step: Global iteration step.
        :param grad: Loss gradients.
        :param patch: Adversarial patch.
        :param estimator: The estimator to evaluate or calculate gradients of `grad` is None to obtain new metrics.
        :param x: Input data.
        :param y: True or target labels.
        :param targeted: Indicates whether the attack is targeted (True) or untargeted (False).
        
art/summary_writer.py