import torch
import torch.nn as nn
from ..utils import *



class inv_StepAttack(object):
    """
    Base class for all attacks.
    """
    def __init__(self, attack, model, epsilon, targeted, random_start, norm, loss,device=None, step_type='ln', **kwargs):
        """
        Initialize the hyperparameters
        Arguments:
            attack (str): the name of attack.
            model (torch.nn.Module): the surrogate model for attack.
            epsilon (float): the perturbation budget.
            targeted (bool): targeted/untargeted attack.
            random_start (bool): whether using random initialization for delta.
            norm (str): the norm of perturbation, l2/linfty.
            loss (str): the loss function.
            device (torch.device): the device for data. If it is None, the device would be same as model
        """
        if norm not in ['l2', 'linfty']:
            raise Exception("Unsupported norm {}".format(norm))
        self.attack = attack
        self.model = self.load_model(model)
        self.epsilon = epsilon
        self.targeted = targeted
        self.random_start = random_start
        self.norm = norm
        self.device = 'cuda' #next(model.parameters()).device if device is None else device
        self.loss = self.loss_function(loss)
        self.step_type = step_type
        self.op = None
        if self.step_type == 'ln':
            self.op = self.ln
        elif self.step_type == 'exp':
            self.op = self.exp
        elif self.step_type == self.power:
            self.op = self.power
        elif self.step_type == 'linear':
            self.op = self.linear
        elif self.step_type == 'pvalue':
            self.op = self.pvalue
        elif self.step_type == 'identity':
            self.op = self.identity
        else:
            raise Exception("Unsupported step type {}".format(step_type))
        
    
    def load_model(self, model_name):
        model = wrap_model(model_list[model_name](weights='DEFAULT').eval().cuda())
        return model

    
    def forward(self, data, label, **kwargs):
        """
        The general attack procedure
        Arguments:
            data: (N, C, H, W) tensor for input images
            labels: (N,) tensor for ground-truth labels if untargetd, otherwise targeted labels
        """
        data = data.clone().detach().to(self.device)
        label = label.clone().detach().to(self.device)
        # Initialize adversarial perturbation
        delta = self.init_delta(data)
        # delta_dual = delta.clone().detach().to(self.device)
        # Initialize momentum
        momentum = 0.
        # momentum_dual = 0.
        grad = 0.
        # self.moving_gradient = 0.
        self.ai = self.normalize(self.op, self.epoch)
        
        for _ in range(self.epoch):
            # Obtain the output
            # self.random_start = True
            # delta = self.init_delta(data)
            logits = self.get_logits(self.transform(data+delta, momentum=grad, alpha=self.ai[_]))
            # self.random_start = False
            # Calculate the loss
            loss = self.get_loss(logits, label)
            # Calculate the gradients
            grad = self.get_grad(loss, delta)
            # Calculate the momentum
            momentum = self.get_momentum(grad, momentum,decay=self.decay)
            # momentum_dual = momentum#(_ * momentum_dual + momentum)/(_ + 1)
            delta = self.update_delta(delta, data, grad, self.ai[_])
            # delta_dual = self.update_delta(delta_dual, data, momentum_dual, self.ai[_])
        return delta.detach()
        # return delta.detach()
    
    def get_logits(self, x, **kwargs):
        """
        The inference stage, which should be overridden when the attack need to change the models (e.g., ensemble-model attack, ghost, etc.) or the input (e.g. DIM, SIM, etc.)
        """
        return self.model(x)

    def get_loss(self, logits, label):
        """
        The loss calculation, which should be overrideen when the attack change the loss calculation (e.g., ATA, etc.)
        """
        # Calculate the loss
        return -self.loss(logits, label) if self.targeted else self.loss(logits, label)
        

    def get_grad(self, loss, delta, **kwargs):
        """
        The gradient calculation, which should be overridden when the attack need to tune the gradient (e.g., TIM, variance tuning, enhanced momentum, etc.)
        """
        return torch.autograd.grad(loss, delta, retain_graph=False, create_graph=False)[0]

    def get_momentum(self, grad, momentum, decay=None, **kwargs):
        """
        The momentum calculation
        """
        if decay == None:
            decay = self.decay
        return momentum * decay + grad / (grad.abs().mean(dim=(1,2,3), keepdim=True))

    def init_delta(self, data, **kwargs):
        delta = torch.zeros_like(data).to(self.device)
        if self.random_start:
            if self.norm == 'linfty':
                delta.uniform_(-self.epsilon, self.epsilon)
            else:
                delta.normal_(-self.epsilon, self.epsilon)
                d_flat = delta.view(delta.size(0), -1)
                n = d_flat.norm(p=2, dim=10).view(delta.size(0), 1, 1, 1)
                r = torch.zeros_like(data).uniform_(0,1).to(self.device)
                delta *= r/n*self.epsilon
            delta = clamp(delta, img_min-data, img_max-data)
        delta.requires_grad = True
        return delta

    def update_delta(self, delta, data, grad, alpha, **kwargs):
        if self.norm == 'linfty':
            delta = torch.clamp(delta + alpha * grad.sign(), -self.epsilon, self.epsilon)
        else:
            grad_norm = torch.norm(grad.view(grad.size(0), -1), dim=1).view(-1, 1, 1, 1)
            scaled_grad = grad / (grad_norm + 1e-20)
            delta = (delta + scaled_grad * alpha).view(delta.size(0), -1).renorm(p=2, dim=0, maxnorm=self.epsilon).view_as(delta)
        delta = clamp(delta, img_min-data, img_max-data)
        return delta


    def loss_function(self, loss):
        """
        Get the loss function
        """
        if loss == 'ce':
            return nn.CrossEntropyLoss()
        else:
            raise Exception("Unsupported loss {}".format(loss))
        
    def transform(self, data, **kwargs):
        return data

    def __call__(self, *input, **kwargs):
        self.model.eval()
        return self.forward(*input, **kwargs)
    @staticmethod
    def exp(val):
        return 2**val
    @staticmethod
    def ln(val):
        # return np.log(val)
        # print('change ln')
        return np.log(val)
    @staticmethod
    def power(val, factor=2):
        return val ** factor
    @staticmethod
    def linear(val, factor=1):
        return val * factor
    @staticmethod
    def normalize(fn, len):
        val = np.array([fn(len-i) for i in range(len)])
        summation = np.sum(val)
        return 16/255*(val / summation).astype(np.float32)
    @staticmethod
    def pvalue(val,epsion=8/255, order=0.6):
        return epsion/(val+1)**order
    @staticmethod
    def identity(val):
        return 1.

class InvScIFGSM(inv_StepAttack):
    """
    I-FGSM Attack
    'Adversarial Examples in the Physical World (ICLR 2017)'(https://arxiv.org/abs/1607.02533)

    Arguments:
        model (torch.nn.Module): the surrogate model for attack.
        epsilon (float): the perturbation budget.
        alpha (float): the step size.
        epoch (int): the number of iterations.
        targeted (bool): targeted/untargeted attack
        random_start (bool): whether using random initialization for delta.
        norm (str): the norm of perturbation, l2/linfty.
        loss (str): the loss function.
        device (torch.device): the device for data. If it is None, the device would be same as model

    Official arguments:
        epsilon=16/255, alpha=epsilon/epoch=1.6/255, epoch=10
    """

    def __init__(self, model, epsilon=16/255, alpha=1.6/255, epoch=10, targeted=False, random_start=False, 
                norm='linfty', loss='crossentropy', device=None, **kwargs):
        super().__init__('I-FGSM', model=model, epsilon=epsilon, targeted=targeted, random_start=random_start, norm=norm, loss=loss, device=device, **kwargs)
        self.alpha = alpha
        self.epoch = epoch
        self.decay = 0
        self.alpha = alpha # epsilon/self.epoch


class InvScMIFGSM(inv_StepAttack):
    """
    MI-FGSM Attack
    'Boosting Adversarial Attacks with Momentum (CVPR 2018)'(https://arxiv.org/abs/1710.06081)

    Arguments:
        model (torch.nn.Module): the surrogate model for attack.
        epsilon (float): the perturbation budget.
        alpha (float): the step size.
        epoch (int): the number of iterations.
        decay (float): the decay factor for momentum calculation.
        targeted (bool): targeted/untargeted attack.
        random_start (bool): whether using random initialization for delta.
        norm (str): the norm of perturbation, l2/linfty.
        loss (str): the loss function.
        device (torch.device): the device for data. If it is None, the device would be same as model
        
    Official arguments:
        epsilon=16/255, alpha=epsilon/epoch=1.6/255, epoch=10, decay=1.
    """
    
    def __init__(self, model, epsilon=16/255, alpha=1.6/255, epoch=10, decay=1., targeted=False, random_start=False, 
                norm='linfty', loss='crossentropy', device=None, attack='MI-FGSM', adapted=False,rho=1.0,**kwargs):
        super().__init__(attack, model=model, epsilon=epsilon, targeted=targeted, random_start=random_start, norm=norm, loss=loss, device=device, **kwargs)
        self.alpha = alpha # epsilon/epoch# alpha
        self.epoch = epoch
        self.decay = decay
    def forward(self, data, label, **kwargs):
        """
        The general attack procedure
        Arguments:
            data: (N, C, H, W) tensor for input images
            labels: (N,) tensor for ground-truth labels if untargetd, otherwise targeted labels
        """
        data = data.clone().detach().to(self.device)
        label = label.clone().detach().to(self.device)
        # Initialize adversarial perturbation
        delta = self.init_delta(data)
        # delta_dual = delta.clone().detach().to(self.device)
        # Initialize momentum
        momentum = 0.
        # momentum_dual = 0.
        grad = 0.
        # self.moving_gradient = 0.
        self.ai = self.normalize(self.op, self.epoch)
        
        for _ in range(self.epoch):
            # Obtain the output
            self.random_start = True
            # delta = self.init_delta(data)
            logits = self.get_logits(self.transform(data+delta, momentum=grad, alpha=self.ai[_]))
            self.random_start = False
            # Calculate the loss
            loss = self.get_loss(logits, label)
            # Calculate the gradients
            grad = self.get_grad(loss, delta)
            # Calculate the momentum
            momentum = self.get_momentum(grad, momentum,decay=self.decay)
            # momentum_dual = momentum#(_ * momentum_dual + momentum)/(_ + 1)
            delta = self.update_delta(delta, data, grad, self.ai[_])
            # delta_dual = self.update_delta(delta_dual, data, momentum_dual, self.ai[_])
        return delta.detach()
        
class InvScNIFGSM(InvScMIFGSM):
    """
    NI-FGSM Attack
    'Nesterov Accelerated Gradient and Scale Invariance for Adversarial Attacks (ICLR 2020)'(https://arxiv.org/abs/1908.06281)
    Arguments:
        model (torch.nn.Module): the surrogate model for attack.
        epsilon (float): the perturbation budget.
        alpha (float): the step size.
        epoch (int): the number of iterations.
        decay (float): the decay factor for momentum calculation.
        targeted (bool): targeted/untargeted attack.
        random_start (bool): whether using random initialization for delta.
        norm (str): the norm of perturbation, l2/linfty.
        loss (str): the loss function.
        device (torch.device): the device for data. If it is None, the device would be same as model
        
    Official arguments:
        epsilon=16/255, alpha=epsilon/epoch=1.6/255, epoch=10, decay=1.
    """
    
    def __init__(self, model, epsilon=16/255, alpha=1.6/255, epoch=10, decay=1., targeted=False, random_start=False, 
                norm='linfty', loss='crossentropy', device=None, attack='NI-FGSM', **kwargs):
        super().__init__(model, epsilon, alpha, epoch, decay, targeted, random_start, norm, loss, device, attack, **kwargs)
    
    def transform(self, x, momentum, **kwargs):
        """
        look ahead for NI-FGSM
        """
        if 'alpha' in kwargs:
            alpha = kwargs['alpha']
        else:
            alpha = self.alpha
        return x + alpha*self.decay*momentum
    def forward(self, data, label, **kwargs):
        """
        The general attack procedure
        Arguments:
            data: (N, C, H, W) tensor for input images
            labels: (N,) tensor for ground-truth labels if untargetd, otherwise targeted labels
        """
        data = data.clone().detach().to(self.device)
        label = label.clone().detach().to(self.device)
        # Initialize adversarial perturbation
        delta = self.init_delta(data)
        # delta_dual = delta.clone().detach().to(self.device)
        # Initialize momentum
        momentum = 0.
        # momentum_dual = 0.
        grad = 0.
        # self.moving_gradient = 0.
        self.ai = self.normalize(self.op, self.epoch)
        
        for _ in range(self.epoch):
            # Obtain the output
            # self.random_start = True
            # delta = self.init_delta(data)
            logits = self.get_logits(self.transform(data+delta, momentum=grad, alpha=self.ai[_]))
            # self.random_start = False
            # Calculate the loss
            loss = self.get_loss(logits, label)
            # Calculate the gradients
            grad = self.get_grad(loss, delta)
            # Calculate the momentum
            momentum = self.get_momentum(grad, momentum,decay=self.decay)
            # momentum_dual = momentum#(_ * momentum_dual + momentum)/(_ + 1)
            delta = self.update_delta(delta, data, grad, self.ai[_])
            # delta_dual = self.update_delta(delta_dual, data, momentum_dual, self.ai[_])
        return delta.detach()

import torch.nn.functional as F

class InvScPIFGSM(InvScNIFGSM):
    """
    PI-FGSM Attack
    'Patch-wise Attack for Fooling Deep Neural Network (ECCV 2020)'(https://arxiv.org/abs/2007.06765)
    Arguments:
        model (torch.nn.Module): the surrogate model for attack.
        epsilon (float): the perturbation budget.
        alpha (float): the step size.
        epoch (int): the number of iterations.
        decay (float): the decay factor for momentum calculation.
        targeted (bool): targeted/untargeted attack.
        random_start (bool): whether using random initialization for delta.
        norm (str): the norm of perturbation, l2/linfty.
        loss (str): the loss function.
        device (torch.device): the device for data. If it is None, the device would be same as model
        
    """
    
    def __init__(self, model, epsilon=16/255, alpha=1.6/255, epoch=10, decay=1., targeted=False, random_start=False, 
                norm='linfty', loss='crossentropy', device=None, attack='PI-FGSM', **kwargs):
        super().__init__(model, epsilon, alpha, epoch, decay, targeted, random_start, norm, loss, device, attack, **kwargs)
        self.kern_size = 3
        self.alpha_beta = self.alpha * epoch
        self.gamma = self.alpha_beta
        P_kern = self.project_kern(self.kern_size)
        self.device = "cuda:0"# next(model.parameters()).device if device is None else device
        self.w = torch.from_numpy(P_kern).to(self.device) # project kernel

    
    def project_kern(self, kern_size):
        kern = np.ones((kern_size, kern_size), dtype=np.float32) / (kern_size ** 2 - 1)
        kern[kern_size // 2, kern_size // 2] = 0.0
        kern = kern.astype(np.float32)
        stack_kern = np.stack([kern, kern, kern])
        stack_kern = np.expand_dims(stack_kern, 1)
        return stack_kern
    
    def project_noise(self, x, stack_kern, padding_size):
        # x = tf.pad(x, [[0,0],[kern_size,kern_size],[kern_size,kern_size],[0,0]], "CONSTANT")
        x = F.conv2d(x, stack_kern, padding=(padding_size, padding_size), groups=3)
        return x
    
    
    def update_delta(self, delta, data, grad,  **kwargs):
        if self.norm == 'linfty':
            delta = torch.clamp(delta +  grad, -self.epsilon, self.epsilon)
        else:
            grad_norm = torch.norm(grad.view(grad.size(0), -1), dim=1).view(-1, 1, 1, 1)
            scaled_grad = grad / (grad_norm + 1e-20)
            delta = (delta + scaled_grad ).view(delta.size(0), -1).renorm(p=2, dim=0, maxnorm=self.epsilon).view_as(delta)
        delta = clamp(delta, img_min-data, img_max-data)
        return delta

    def forward(self, data, label, **kwargs):
        """
        Overriden for PI-FGSM
        Arguments:
            data: (N, C, H, W) tensor for input images
            labels: (N,) tensor for ground-truth labels if untargetd, otherwise targeted labels
        """
        data = data.clone().detach().to(self.device)
        label = label.clone().detach().to(self.device)

        # Initialize adversarial perturbation
        delta = self.init_delta(data)
        delta.requires_grad = True
        grad = 0.
        self.ai = self.normalize(self.op, self.epoch)
        self.ai_beta = [self.ai[i] * self.epoch for i in range(self.epoch)]
        self.ai_gamma = [self.ai[i] * self.epoch for i in range(self.epoch)]

        amplification = 0.0 
        amplification_dual = 0.0
        momentum = 0.0
        
        # delta_dual = delta.clone().detach().to(self.device)
        # delta_dual.requires_grad = True
        # Initialize momentum
        momentum = 0.
        # momentum_dual = 0.
        grad=0.
        for _ in range(self.epoch):
            # Obtain the output
            logits = self.get_logits(self.transform(data+delta, momentum=grad, alpha=self.ai_beta[_]))

            # Calculate the loss
            loss = self.get_loss(logits, label)
            

            # Calculate the gradients
            grad = self.get_grad(loss, delta)
            momentum = self.get_momentum(grad, momentum)
            momentum_dual =momentum# (_ * momentum_dual + momentum)/(_ + 1)
            # Calculate the cut noise
            amplification += self.alpha_beta * momentum.sign()
            amplification_dual = amplification# (_ * amplification_dual + amplification)/(_ + 1)
            cut_noise = torch.clamp(abs(amplification) - self.epsilon, 0., 10000.0) * torch.sign(amplification)
            cut_noise_dual = torch.clamp(abs(amplification_dual) - self.epsilon, 0., 10000.0) * torch.sign(amplification_dual)
            projection = self.ai_gamma[_] * torch.sign(self.project_noise(cut_noise, self.w, self.kern_size//2))
            projection_dual = self.ai_gamma[_] * torch.sign(self.project_noise(cut_noise_dual, self.w, self.kern_size//2))
            amplification += projection
            amplification_dual += projection_dual
            # sum_grad = torch.sum(grad)
            # second_grad = torch.autograd.grad(sum_grad, delta, create_graph=True)[0]
            delta = self.update_delta(delta, data, self.ai_beta[_] * grad.sign())
            # delta_dual = self.update_delta(delta_dual, data, self.ai_beta[_] * momentum_dual.sign() + projection_dual)
            # print('iter: ', _,'mean: ', torch.mean(delta),'std: ', torch.std(delta),'grad l2 norm: ', torch.norm(grad,p=2),'2-th order grad: mean:', torch.mean(second_grad),'std: ', torch.std(second_grad))
        # exit()

        return delta.detach()



    
    
class InvScVMIFGSM(inv_StepAttack):
    """
    VMI-FGSM Attack
    'Enhancing the transferability of adversarial attacks through variance tuning (CVPR 2021)'(https://arxiv.org/abs/2103.15571)
    Arguments:
        model (torch.nn.Module): the surrogate model for attack.
        epsilon (float): the perturbation budget.
        alpha (float): the step size.
        beta (float): the relative value for the neighborhood.
        num_neighbor (int): the number of samples for estimating the gradient variance.
        epoch (int): the number of iterations.
        decay (float): the decay factor for momentum calculation.
        targeted (bool): targeted/untargeted attack.
        random_start (bool): whether using random initialization for delta.
        norm (str): the norm of perturbation, l2/linfty.
        loss (str): the loss function.
        device (torch.device): the device for data. If it is None, the device would be same as model
        
    Official arguments:
        epsilon=16/255, alpha=epsilon/epoch=1.6/255, beta=1.5, num_neighbor=20, epoch=10, decay=1.
    """
    
    def __init__(self, model, epsilon=16/255, alpha=1.6/255, beta=1.5, num_neighbor=20, epoch=10, decay=1., targeted=False, 
                random_start=False, norm='linfty', loss='crossentropy', device=None, attack='VMI-FGSM', **kwargs):
        super().__init__(attack, model, epsilon, targeted, random_start, norm, loss, device, **kwargs)
        self.alpha = alpha
        self.radius = beta * epsilon
        self.epoch = epoch
        self.decay = decay
        self.num_neighbor = num_neighbor

    def get_variance(self, data, delta, label, cur_grad, momentum, **kwargs):
        """
        Calculate the gradient variance    
        """
        grad = 0
        for _ in range(self.num_neighbor):
            # Obtain the output
            # This is inconsistent for transform!
            logits = self.get_logits(self.transform(data+delta+torch.zeros_like(delta).uniform_(-self.radius, self.radius).to(self.device), momentum=momentum))

            # Calculate the loss
            loss = self.get_loss(logits, label)

            # Calculate the gradients
            grad += self.get_grad(loss, delta)

        return grad / self.num_neighbor - cur_grad

    def forward(self, data, label, **kwargs):
        """
        The attack procedure for VMI-FGSM
        Arguments:
            data: (N, C, H, W) tensor for input images
            labels: (N,) tensor for ground-truth labels if untargetd, otherwise targeted labels
        """
        data = data.clone().detach().to(self.device)
        label = label.clone().detach().to(self.device)

        # Initialize adversarial perturbation
        delta = self.init_delta(data)
        delta_dual = delta.clone().detach().to(self.device)
        self.ai = self.normalize(self.op, self.epoch)
        momentum, variance = 0, 0
        grad=0.
        momentum_dual, variance_dual = 0, 0
        for _ in range(self.epoch):
            # Obtain the output
            logits = self.get_logits(self.transform(data+delta, momentum=grad, alpha=self.ai[_]))

            # Calculate the loss
            loss = self.get_loss(logits, label)
            

            # Calculate the gradients
            grad = self.get_grad(loss, delta)

            # Calculate the momentum
            momentum = self.get_momentum(grad+variance, momentum)
            momentum_dual = momentum#(_ * momentum_dual + momentum)/(_ + 1)
            # Calculate the variance
            variance = self.get_variance(data, delta, label, grad, momentum)
            variance_dual = variance#(_ * variance_dual + variance)/(_ + 1)
            # sum_grad = torch.sum(grad)
            # second_grad = torch.autograd.grad(sum_grad, delta, create_graph=True)[0]
            # Update adversarial perturbation
            delta = self.update_delta(delta, data, grad, self.ai[_])
            delta_dual = self.update_delta(delta_dual, data, momentum_dual, self.ai[_])
            # print('iter: ', _,'mean: ', torch.mean(delta),'std: ', torch.std(delta),'grad l2 norm: ', torch.norm(grad,p=2),'2-th order grad: mean:', torch.mean(second_grad),'std: ', torch.std(second_grad))
        # exit()

        return delta.detach()

class InvScGIMIFGSM(InvScMIFGSM):
    """
    MI-FGSM Attack
    'Boosting Adversarial Attacks with Momentum (CVPR 2018)'(https://arxiv.org/abs/1710.06081)
    Arguments:
        model (torch.nn.Module): the surrogate model for attack.
        epsilon (float): the perturbation budget.
        alpha (float): the step size.
        epoch (int): the number of iterations.
        decay (float): the decay factor for momentum calculation.
        targeted (bool): targeted/untargeted attack.
        random_start (bool): whether using random initialization for delta.
        norm (str): the norm of perturbation, l2/linfty.
        loss (str): the loss function.
        device (torch.device): the device for data. If it is None, the device would be same as model
        
    Official arguments:
        epsilon=16/255, alpha=epsilon/epoch=1.6/255, epoch=10, decay=1.
    """
    
    def __init__(self, model, epsilon=16/255, alpha=1.6/255, epoch=10, decay=1., targeted=False, random_start=False, 
                norm='linfty', loss='crossentropy', device=None, attack='MI-FGSM', pre_epoch=5, s=10, **kwargs):
        super().__init__(attack=attack, model=model, epsilon=epsilon, targeted=targeted, random_start=random_start, norm=norm, loss=loss, device=device, **kwargs)
        self.alpha = alpha
        self.epoch = epoch
        self.decay = decay
        self.pre_epoch = pre_epoch
        self.s = s

    def forward(self, data, label, **kwargs):
        """
        The general attack procedure
        Arguments:
            data: (N, C, H, W) tensor for input images
            labels: (N,) tensor for ground-truth labels if untargetd, otherwise targeted labels
        """
        data = data.clone().detach().to(self.device)
        label = label.clone().detach().to(self.device)
        momentum = 0.
        self.ai = self.normalize(self.op, self.epoch)
        self.ai_pre = self.normalize(self.op, self.pre_epoch)
        delta = self.init_delta(data).to(self.device)
        delta_dual = delta.clone().detach().to(self.device)
        momentum_dual = 0.
        for _ in range(self.pre_epoch):
            # Obtain the output
            logits = self.get_logits(self.transform(data+delta, momentum=momentum))
            # Calculate the loss
            loss = self.get_loss(logits, label)
            # Calculate the gradients
            grad = self.get_grad(loss, delta)
            # Calculate the momentum
            momentum = self.get_momentum(grad, momentum)
            momentum_dual = (_ * momentum_dual + momentum)/(_ + 1)
            # Update adversarial perturbation
            delta = self.update_delta(delta, data, momentum, self.ai_pre[_]*self.s)
        
        delta = self.init_delta(data).to(self.device)
        grad = 0.
        for _ in range(self.epoch):
            # Obtain the output
            logits = self.get_logits(self.transform(data+delta, momentum=momentum, alpha=self.ai[_]))
            # Calculate the loss
            loss = self.get_loss(logits, label)
            # Calculate the gradients
            grad = self.get_grad(loss, delta)
            # Calculate the momentum
            momentum = self.get_momentum(grad, momentum)
            momentum_dual = momentum#(_ * momentum_dual + momentum)/(_ + 1)
            
            # Update adversarial perturbation
            delta = self.update_delta(delta, data, grad, self.ai[_])
            # delta_dual = self.update_delta(delta_dual, data, momentum_dual, self.ai[_])
            # print('iter: ', _,'mean: ', torch.mean(delta),'std: ', torch.std(delta),'grad l2 norm: ', torch.norm(grad,p=2))
        # exit()
        # exit()
        return delta.detach()