Project: s41467-024-54738-5 | Function: dW (python)

system: You are an expert Python programmer implementing algorithms from scientific papers.
Complete the masked function based on the algorithm description.
Follow Python best practices and maintain consistent code style.
Return **only** the complete Python function code.

user: Complete this Python function:

**DESCRIPTION:** Implements the biologically plausible plasticity rule for trial-by-trial motor adaptation, updating recurrent weights based on feedback error signals

**KEY TERMS:** {
  "lr": "learning rate",
  "fb": "feedback error signal",
  "presum": "presynaptic activity sum",
  "mask2.weight": "connectivity mask",
  "feedback.weight": "feedback weights"
}

**CONTEXT CODE:**     def __init__(self, n_inputs, n_outputs, n_neurons, alpha, dtype, dt,
                 fwd_delay, fb_delay=0, biolearning=False, noiseout=0, noisein=0,
                 nonlin='relu',fb_sparsity=1,noise_kernel_size=1, rec_sparsity=1):
        super(RNN, self).__init__()
        self.n_neurons = n_neurons
        self.n_inputs = n_inputs
        self.n_outputs = n_outputs
        self.alpha = alpha
        self.dt = dt
        
        self.rnn = nn.RNN(n_inputs, n_neurons, num_layers=2, bias=False) 
        self.output = nn.Linear(n_neurons, n_outputs)
        self.feedback = nn.Linear(n_outputs, n_neurons)
        self.dtype = dtype
        
        self.fwd_delay = fwd_delay
        self.fb_delay = fb_delay
        self.delay = fwd_delay+fb_delay
        
        self.biolearning = biolearning
        if biolearning:
            self.rnn.weight_hh_l0.requires_grad = False
        self.noisein = noisein
        self.noiseout = noiseout
        self.noise_kernel_lenk = noise_kernel_size
        
        if nonlin=='relu':
            self.nonlin = torch.nn.ReLU()
        elif nonlin=='tanh':
            self.nonlin = torch.nn.Tanh()
        elif nonlin=='sigmoid':
            self.nonlin = torch.nn.Sigmoid()

        self.mask = nn.Linear(n_outputs, n_neurons, bias=False) 
        self.mask.weight = nn.Parameter((torch.rand(n_neurons, n_outputs) < fb_sparsity).float()).type(dtype)
        self.mask.weight.requires_grad = False

        self.mask2 = nn.Linear(n_neurons, n_neurons, bias=False) 
        self.mask2.weight = nn.Parameter((torch.rand(n_neurons, n_neurons) < rec_sparsity).float()).type(dtype)
        self.mask2.weight.requires_grad = False

    def init_hidden(self):
        return ((torch.rand(self.batch_size, self.n_neurons)-0.5)*0.2).type(self.dtype)
    
    # ONE SIMULATION STEP
    def f_step(self,xin,x1,r1,v1fb,v1,pin,popto):
        """Masked implementation"""
        raise NotImplementedError("Function implementation has been masked")
    
    # BIOLOGICALLY PLAUSIBLE LEARNING RULE
    def dW(self,pre,post,lr,inp,fb,presum):
        """Masked implementation"""
        raise NotImplementedError("Function implementation has been masked")

                    
    # GET VELOCITY OUTPUT (NOT ERROR)
    def get_output(self,testl1):
        if self.noiseout>0:
            return self.output(testl1)+self.output(testl1)*self.create_noise(testl1)
        else:
            return self.output(testl1)
        
    # SIMULATE MOTOR NOISE (scales with velocity output)
    def create_noise(self,testl1):
        # time varying noise
        tmp = self.noiseout*torch.randn(testl1.shape[1],testl1.shape[0],2)
        tmp = tmp.permute(0,2,1)
        lenk = self.noise_kernel_lenk
        kernel = torch.ones(2,tmp.shape[1],lenk)/lenk
        padding = lenk // 2 + lenk % 2
        noise = torch.nn.functional.conv1d(tmp, kernel, padding=padding)[:,:,:testl1.shape[0]]
        noise = noise.permute(2,0,1)*np.sqrt(lenk)  
        return noise
    
    # RUN MODEL
    def forward(self, X, Xpert, lr=1e-3, popto=None):
        self.batch_size = X.size(1)
        hidden0 = self.init_hidden()
        x1 = hidden0
        r1 = self.nonlin(x1)
        v1 = self.output(r1)
        hidden1 = []
        poserr = []
        presum = r1
        dw_acc = torch.zeros(self.rnn.weight_hh_l0.shape)

        if popto is None:
            popto = torch.zeros((X.shape[0],X.shape[1],r1.shape[1]))
        # prerun simulation (until 'delay' is reached)
        for j in range(self.fwd_delay+1):
            x1,r1,v1 = self.f_step(X[0],x1,r1,
                                         v1*0,v1,Xpert[0],
                                         popto[0]) 
            hidden1.append(r1)
            poserr.append(v1)
        # generate noise (input and output)
        X = X+ self.noisein*torch.randn(X.shape[1],X.shape[2])[None,:,:]
        if self.noiseout>0:
            noise = self.create_noise(Xpert)
        else:
            noise = torch.zeros(Xpert.shape)
        # now real time simulation
        for j in range(X.size(0)):
            if (self.fb_delay<0) | (j<=self.fb_delay):
                x1,r1,v1 = self.f_step(X[j],x1,r1,
                                       v1*0,v1,
                                       Xpert[j]+noise[j]*self.output(r1),
                                       popto[j])
            else:
                x1,r1,v1 = self.f_step(X[j],x1,r1,
                                             poserr[j-self.delay],
                                             v1,Xpert[j]+noise[j]*self.output(r1),
                                             popto[j])
                if self.biolearning and j%5:
                    dw_acc += self.dW(x1[0],r1[0],lr,X[j,0],
                                      poserr[j-self.delay][0],presum[0]).detach()
            if self.biolearning:
                presum += r1.detach()
            hidden1.append(r1)
            poserr.append(v1)
        hidden1 = torch.stack(hidden1[1:])
        poserr = torch.stack(poserr[1:])
        if self.biolearning:
            with torch.no_grad():
                self.rnn.weight_hh_l0 += dw_acc
        if self.fwd_delay==0:
            return poserr, hidden1
        elif self.fwd_delay>0:
            return poserr[(self.fwd_delay):], hidden1[:(-self.fwd_delay)]


def run_model(model,params,data,fb=True,dopert=0):
    if not fb:
        state_dict = model.state_dict()
        save = state_dict['feedback.weight'].detach().clone()
        state_dict['feedback.weight'] *= 0
        model.load_state_dict(state_dict)

    from dataset import perturb
    # SETUP ############
    testdata = data['test_set']
    tout = testdata['target'][:,:,2:]
    tstim = testdata['stimulus']
    stim = torch.Tensor(tstim.transpose(1,0,2)).type(model.dtype)
    pert = torch.zeros(tout.transpose(1,0,2).shape).type(model.dtype)
    if dopert==1: # if perturbation type is random pushes
        pert = perturb(pert,stim.shape[1],params['p1'])
    # RUN ############
    testout,testl1 = model(stim,pert)
    error = testout.cpu().detach().numpy().transpose(1,0,2)


Complete the function implementation replacing "raise NotImplementedError".