def poison(self, x: np.ndarray, y: np.ndarray | None = None, **kwargs) -> tuple[np.ndarray, np.ndarray]:
        """
        Iteratively finds optimal attack points starting at values at x

        :param x: The base images to begin the poison process.
        :param y: Target label
        :return: An tuple holding the (poisoning examples, poisoning labels).
        """
        import torch

        class PoisonBatch(torch.nn.Module):
            """
            Implementing this to work with PyTorch optimizers.
            """

            def __init__(self, base_list):
                super().__init__()
                base_batch = torch.stack(base_list, 0)
                self.poison = torch.nn.Parameter(base_batch.clone())

            def forward(self):
                """Forward method."""
                return self.poison

        base_tensor_list = [torch.from_numpy(sample).to(self.estimator.device) for sample in x]
        poison_batch = PoisonBatch([torch.from_numpy(np.copy(sample)).to(self.estimator.device) for sample in x])
        opt_method = self.opt.lower()

        if opt_method == "sgd":  # pragma: no cover
            logger.info("Using SGD to craft poison samples")
            optimizer = torch.optim.SGD(poison_batch.parameters(), lr=self.learning_rate, momentum=self.momentum)
        elif opt_method == "adam":
            logger.info("Using Adam to craft poison samples")
            optimizer = torch.optim.Adam(  # type: ignore
                poison_batch.parameters(), lr=self.learning_rate, betas=(self.momentum, 0.999)
            )

        base_tensor_batch = torch.stack(base_tensor_list, 0)
        base_range01_batch = base_tensor_batch

        # Because we have turned on DP for the substitute networks,
        # the target image's feature becomes random.
        # We can try enforcing the convex polytope in one of the multiple realizations of the feature,
        # but empirically one realization is enough.
        target_feat_list = []
        # Coefficients for the convex combination.
        # Initializing from the coefficients of last step gives faster convergence.
        s_init_coeff_list = []
        n_poisons = len(x)
        s_coeff: "torch.Tensor" | list["torch.Tensor"]
        for _, net in enumerate(self.subsistute_networks):
            # End to end training
            if self.endtoend:
                if isinstance(self.feature_layer, list):
                    block_feats = []
                    for layer in self.feature_layer:
                        activations = net.get_activations(x, layer=layer, batch_size=self.batch_size, framework=True)
                        if activations is not None:
                            block_feats += [torch.stack([feat.detach() for feat in activations], 0)]
                        else:  # pragma: no cover
                            raise ValueError("Activations are None.")
                else:
                    layer_2: int | str = self.feature_layer
                    activations = net.get_activations(x, layer=layer_2, batch_size=self.batch_size, framework=True)
                    if activations is not None:
                        block_feats = [feat.detach() for feat in activations]
                    else:  # pragma: no cover
                        raise ValueError("Activations are None.")
                target_feat_list.append(block_feats)
                s_coeff = [
                    torch.ones(n_poisons, 1).to(self.estimator.device) / n_poisons for _ in range(len(block_feats))
                ]
            else:  # pragma: no cover
                if isinstance(self.feature_layer, list):
                    raise NotImplementedError
                layer_3: int | str = self.feature_layer
                activations = net.get_activations(x, layer=layer_3, batch_size=self.batch_size, framework=True)
                if activations is not None:
                    target_feat_list.append(activations.detach())  # type: ignore
                else:
                    raise ValueError("Activations are None.")
                s_coeff = torch.ones(n_poisons, 1).to(self.estimator.device) / n_poisons

            s_init_coeff_list.append(s_coeff)

        for ite in trange(self.max_iter):
            if ite % self.decay_iter == 0 and ite != 0:
                for param_group in optimizer.param_groups:
                    param_group["lr"] *= self.decay_coeff
                print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} Iteration {ite}, Adjusted lr to {self.learning_rate:.2e}")

            poison_batch.zero_grad()
            total_loss = loss_from_center(
                self.subsistute_networks,
                target_feat_list,
                poison_batch,
                self.net_repeat,
                self.endtoend,
                self.feature_layer,
            )
            total_loss.backward()
            optimizer.step()

            # clip the perturbations into the range
            perturb_range01 = torch.clamp((poison_batch.poison.data - base_tensor_batch), -self.epsilon, self.epsilon)
            perturbed_range01 = torch.clamp(
                base_range01_batch.data + perturb_range01.data,
                self.estimator.clip_values[0],
                self.estimator.clip_values[1],
            )
            poison_batch.poison.data = perturbed_range01

        if y is None:  # pragma: no cover
            raise ValueError("You must pass in the target label as y")

        return get_poison_tuples(poison_batch, y)

        Iteratively finds optimal attack points starting at values at x

        :param x: The base images to begin the poison process.
        :param y: Target label
        :return: An tuple holding the (poisoning examples, poisoning labels).
        
art/attacks/poisoning/bullseye_polytope_attack.py