def poison(self, x: np.ndarray, y: np.ndarray | None = None, **kwargs) -> tuple[np.ndarray, np.ndarray]:
        """
        Calls perturbation function on the dataset x and returns only the perturbed input and their
        indices in the dataset.

        :param x: An array in the shape NxCxWxH with the points to draw source and target samples from.
                  Source indicates the class(es) that the backdoor would be added to cause misclassification into the
                  target label. Target indicates the class that the backdoor should cause misclassification into.
        :param y: The labels of the provided samples. If none, we will use the classifier to label the data.
        :return: A tuple holding the `(poison samples, indices in x that the poison samples should replace)`.
        """
        import torch

        data = np.copy(x)
        if y is None:
            estimated_labels = self.estimator.predict(data)
        else:
            estimated_labels = check_and_transform_label_format(y, nb_classes=self.estimator.nb_classes)

        # Get indices of target class
        if not self.is_index:
            poison_class = self.target
            poison_indices = np.where(np.all(estimated_labels == poison_class, axis=1))[0]
            num_poison = int(np.ceil(self.poison_percent * len(poison_indices)))
            if num_poison == 0:
                raise ValueError("No data points with target label found")

            poison_indices = np.random.choice(poison_indices, num_poison, replace=False)
        else:
            poison_class = estimated_labels[self.target[0]]  # type: ignore
            poison_indices = self.target
            if not np.all(np.all(estimated_labels[poison_indices] == poison_class, axis=1)):
                raise ValueError("The target indices do not share the same label")

            num_poison = len(poison_indices)

        # Get indices of source class
        if not self.is_index:
            trigger_class = self.source
            trigger_indices = np.where(np.all(estimated_labels == trigger_class, axis=1))[0]
            num_trigger = min(len(trigger_indices), num_poison)
            if num_trigger == 0:
                raise ValueError("No data points with source label found")
            if num_trigger < num_poison:
                raise ValueError("There must be at least as many images with the source label as the target.")

            # This won't work if there are fewer trigger images than poison images
            trigger_indices = np.random.choice(trigger_indices, num_poison, replace=False)
            num_trigger = len(trigger_indices)

        # Otherwise, we treat it as an index
        else:
            trigger_indices = self.source
            num_trigger = len(trigger_indices)
            if np.any(np.all(estimated_labels[poison_indices] == poison_class, axis=1)):
                raise ValueError("There is a source class that is labeled as the target indices")
            if num_trigger < num_poison:
                raise ValueError("There must be at least as many images with the source label as the target.")

        logger.info("Number of poison inputs: %d", num_poison)
        logger.info("Number of trigger inputs: %d", num_trigger)

        batches = int(np.ceil(num_poison / float(self.batch_size)))

        losses = LossMeter()
        final_poison = np.copy(data[poison_indices])

        original_images = torch.from_numpy(np.copy(data[poison_indices])).to(self.estimator.device)

        for batch_id in trange(batches, desc="Hidden Trigger", disable=not self.verbose):

            cur_index = self.batch_size * batch_id
            offset = min(self.batch_size, num_poison - cur_index)
            poison_batch_indices = poison_indices[cur_index : cur_index + offset]
            trigger_batch_indices = trigger_indices[cur_index : cur_index + offset]

            poison_samples = torch.from_numpy(data[poison_batch_indices]).to(self.estimator.device)

            # First, we add the backdoor to the source samples and get the feature representation
            trigger_samples, _ = self.backdoor.poison(data[trigger_batch_indices], self.target, broadcast=True)
            trigger_samples = torch.from_numpy(trigger_samples).to(self.estimator.device)  # type: ignore

            feat1 = self.estimator.get_activations(trigger_samples, self.feature_layer, 1, framework=True)
            feat1 = feat1.detach().clone()

            for i in range(self.max_iter):
                poison_samples.requires_grad_()
                if isinstance(self.decay_iter, int):
                    decay_exp = i // self.decay_iter
                else:
                    max_index = [ii for ii, _ in enumerate(self.decay_iter) if self.decay_iter[ii] <= i]
                    if len(max_index) == 0:
                        decay_exp = 0
                    else:
                        decay_exp = max(max_index) + 1
                learning_rate = self.learning_rate * (self.decay_coeff**decay_exp)

                # Compute the feature representation of the current poisons and
                # identify the closest trigger sample for each poison
                feat2 = self.estimator.get_activations(poison_samples, self.feature_layer, 1, framework=True)
                feat11 = feat1.clone()
                dist = torch.cdist(feat1, feat2)

                for _ in range(feat2.size(0)):
                    dist_min_index = (dist == torch.min(dist)).nonzero().squeeze()
                    if dist_min_index.dim() > 1:  # If multiple values in dist equal torch.min(dist), return the first.
                        dist_min_index = dist_min_index[0]
                    feat1[dist_min_index[1]] = feat11[dist_min_index[0]]
                    dist[dist_min_index[0], dist_min_index[1]] = 1e5

                loss = torch.norm(feat1 - feat2) ** 2
                losses.update(loss.item(), trigger_samples.size(0))  # type: ignore
                loss.backward()

                # Update the poison and clip
                if poison_samples.grad is not None:
                    poison_samples = poison_samples - learning_rate * poison_samples.grad
                else:
                    raise ValueError("Gradient term in PyTorch model is `None`.")
                pert = poison_samples - original_images[cur_index : cur_index + offset]
                pert = torch.clamp(pert, -self.eps, self.eps).detach_()
                poison_samples = pert + original_images[cur_index : cur_index + offset]
                poison_samples = poison_samples.clamp(*self.estimator.clip_values)

                if i % self.print_iter == 0:
                    print(
                        f"Batch: {batch_id} | i: {i:5d} | \
                        LR: {learning_rate:2.5f} | \
                        Loss Val: {losses.val:5.3f} | Loss Avg: {losses.avg:5.3f}"
                    )

                if loss.item() < self.stopping_threshold or i == (self.max_iter - 1):
                    print(f"Max_Loss: {loss.item()}")
                    final_poison[cur_index : cur_index + offset] = poison_samples.detach().cpu().numpy()
                    break

        return final_poison, poison_indices

        Calls perturbation function on the dataset x and returns only the perturbed input and their
        indices in the dataset.

        :param x: An array in the shape NxCxWxH with the points to draw source and target samples from.
                  Source indicates the class(es) that the backdoor would be added to cause misclassification into the
                  target label. Target indicates the class that the backdoor should cause misclassification into.
        :param y: The labels of the provided samples. If none, we will use the classifier to label the data.
        :return: A tuple holding the `(poison samples, indices in x that the poison samples should replace)`.
        
art/attacks/poisoning/hidden_trigger_backdoor/hidden_trigger_backdoor_pytorch.py