def generate(
        self,
        x: np.ndarray,
        y: np.ndarray | None = None,
        sample_sizes: np.ndarray | None = None,
        automatically_append: bool = True,
        verify_input_data: bool = True,
        perturb_sizes: list[list[int]] | None = None,
        perturb_starts: list[list[int]] | None = None,
        **kwargs,
    ) -> np.ndarray:
        """
        Generates the adversarial examples. x needs to be composed of valid files by default which can support the
        adversarial perturbation and so are malicious and can support the assigned L0 budget. They can be obtained by
        using `pull_out_valid_samples` on the data.

        This check on the input data can be over-ridden by toggling the flag verify_input_data
        This will result in only the data which can be made adversarial being perturbed and so the resulting batch will
        be a mixture of adversarial and unperturbed data.

        To assign the L0 budget we go through each list in perturb_sizes and perturb_starts in order, and
        assign the budget based on the sizes given until the l0 budget is exhausted.

        After all the regions marked in perturb_sizes and perturb_starts have been assigned and automatically_append is
        set to true and remaining l0 perturbation the extra perturbation is added at the end in an append style attack.

        :param x: An array with input data.
        :param y: (N, 1) binary labels to make sure the benign files are zero masked.
        :param sample_sizes: The size of the original file, before it was padded to the input size required by MalConv
        :param automatically_append: Whether to automatically append extra spare perturbation at the end of the file.
        :param verify_input_data: If to check that all the data supplied is valid for adversarial perturbations.
        :param perturb_sizes: A list of length batch size, each element is in itself a list containing
                              the size of the allowable perturbation region
        :param perturb_starts: A list of length batch size, each element is in itself a list containing
                               the start of perturbation region.
        :return x: our adversarial examples.
        """
        import tensorflow as tf

        # make copy so original data is not modified.
        adv_x = x.copy()
        if sample_sizes is None:  # pragma: no cover
            raise ValueError("The size of the original files needs to be supplied")
        if y is None:  # pragma: no cover
            raise ValueError("Labels need to be provided so we only modify the malware")

        # check that the dimensions all match
        assert len(adv_x) == len(y)
        assert len(y) == len(sample_sizes)
        if perturb_sizes is not None:
            assert len(y) == len(perturb_sizes)
        if perturb_starts is not None:
            assert len(y) == len(perturb_starts)

        # check that if perturb_starts is provided perturb_sizes is also provided and vise versa
        if perturb_starts is not None:
            assert perturb_sizes is not None
        if perturb_sizes is not None:
            assert perturb_starts is not None

        # if we do not automatically append then make sure that we have supplied
        # start and end positions for the perturbation.
        if not automatically_append:
            assert perturb_sizes is not None
            assert perturb_starts is not None

        perturbation_size = np.zeros(len(sample_sizes), dtype=int)
        for i, sample_size in enumerate(sample_sizes):
            if self.l_0 < 1:  # l0 is a fraction of the filesize
                perturbation_size[i] = int(sample_size * self.l_0)
            else:  # or l0 is interpreted as total perturbation size
                perturbation_size[i] = int(self.l_0)
        self.total_perturbation = np.copy(perturbation_size)

        if perturb_sizes is not None and perturb_starts is not None:
            perturbation_size, perturb_sizes = self.compute_perturbation_regions(
                perturbation_size, perturb_sizes, automatically_append
            )

        y = self.check_valid_size(y, sample_sizes, perturbation_size)

        if verify_input_data:
            if np.sum(y) != len(y):
                raise ValueError(  # pragma: no cover
                    f"{len(y) - np.sum(y)} invalid samples found in batch which cannot support the assigned "
                    f"perturbation or are benign To filter for samples that can be processed use "
                    f"pull_out_valid_samples on the samples. Checking can be disabled by using verify_input_data"
                )

        adv_x = self.initialise_sample(
            adv_x, y, sample_sizes, perturbation_size, perturb_sizes=perturb_sizes, perturb_starts=perturb_starts
        )

        mask = self.generate_mask(
            adv_x, y, sample_sizes, perturbation_size, perturb_sizes=perturb_sizes, perturb_starts=perturb_starts
        )

        embeddings = tf.nn.embedding_lookup(params=self.embedding_weights, ids=adv_x.astype("int32"))

        for _ in trange(self.num_of_iterations, desc="PE Adv. Malware", disable=not self.verbose):
            gradients = self.estimator.class_gradient(embeddings, label=0)
            # go from (bsize x 1 x features x embedding size) -> (bsize x features x embedding size) in a
            # framework-agnostic manner.
            gradients = gradients[:, 0, :, :]
            gradients = -1 * gradients
            embeddings = self.update_embeddings(embeddings, gradients, mask)

        adv_x = self.get_adv_malware(
            embeddings=embeddings,
            data=adv_x,
            labels=y,
            fsize=sample_sizes,
            perturbation_size=perturbation_size,
            perturb_sizes=perturb_sizes,
            perturb_starts=perturb_starts,
        )

        return adv_x

        Generates the adversarial examples. x needs to be composed of valid files by default which can support the
        adversarial perturbation and so are malicious and can support the assigned L0 budget. They can be obtained by
        using `pull_out_valid_samples` on the data.

        This check on the input data can be over-ridden by toggling the flag verify_input_data
        This will result in only the data which can be made adversarial being perturbed and so the resulting batch will
        be a mixture of adversarial and unperturbed data.

        To assign the L0 budget we go through each list in perturb_sizes and perturb_starts in order, and
        assign the budget based on the sizes given until the l0 budget is exhausted.

        After all the regions marked in perturb_sizes and perturb_starts have been assigned and automatically_append is
        set to true and remaining l0 perturbation the extra perturbation is added at the end in an append style attack.

        :param x: An array with input data.
        :param y: (N, 1) binary labels to make sure the benign files are zero masked.
        :param sample_sizes: The size of the original file, before it was padded to the input size required by MalConv
        :param automatically_append: Whether to automatically append extra spare perturbation at the end of the file.
        :param verify_input_data: If to check that all the data supplied is valid for adversarial perturbations.
        :param perturb_sizes: A list of length batch size, each element is in itself a list containing
                              the size of the allowable perturbation region
        :param perturb_starts: A list of length batch size, each element is in itself a list containing
                               the start of perturbation region.
        :return x: our adversarial examples.
        
art/attacks/evasion/pe_malware_attack.py