--
--  Copyright (c) 2017, Xavier Gastaldi.
--  All rights reserved.
--
--  This source code is licensed under the BSD-style license found in the
--  LICENSE file in the root directory of this source tree. An additional grant
--  of patent rights can be found in the PATENTS file in the same directory.
--

require 'nn'
require 'cudnn'
require 'cunn'
require 'models/mulconstantslices'

local Convolution = cudnn.SpatialConvolution
local Avg = cudnn.SpatialAveragePooling
local ReLU = cudnn.ReLU
local Max = nn.SpatialMaxPooling
local SBatchNorm = nn.SpatialBatchNormalization

local ShakeShakeBlock, parent = torch.class('nn.ShakeShakeBlock', 'nn.Container')

function ShakeShakeBlock:__init(nInputPlane, nOutputPlane, stride, forwardShake, backwardShake, shakeImage, batchSize)

   parent.__init(self)
   self.gradInput = torch.Tensor()
   self.train = true
   self.forwardShake = forwardShake
   self.backwardShake = backwardShake
   self.shakeImage = shakeImage
   self.batchSize = batchSize

   -- Residual branch #1
   self.net1 = nn.Sequential()
   self.net1:add(ReLU(true))
   self.net1:add(Convolution(nInputPlane, nOutputPlane,3,3,stride,stride,1,1))
   self.net1:add(SBatchNorm(nOutputPlane))
   self.net1:add(ReLU(true))
   self.net1:add(Convolution(nOutputPlane, nOutputPlane,3,3,1,1,1,1))
   self.net1:add(SBatchNorm(nOutputPlane))
   if self.shakeImage == 'true' then -- If we want to work at the image level
     self.net1:add(nn.MulConstantSlices(torch.ones(self.batchSize)))
   else -- If we want to work at the batch level
     self.net1:add(nn.MulConstant(0.5))
   end
    
   -- Residual branch #2
   self.net2 = nn.Sequential()
   self.net2:add(ReLU(true))
   self.net2:add(Convolution(nInputPlane, nOutputPlane,3,3,stride,stride,1,1))
   self.net2:add(SBatchNorm(nOutputPlane))
   self.net2:add(ReLU(true))
   self.net2:add(Convolution(nOutputPlane, nOutputPlane,3,3,1,1,1,1))
   self.net2:add(SBatchNorm(nOutputPlane))
   if self.shakeImage == 'true' then -- If we want to work at the image level
     self.net2:add(nn.MulConstantSlices(torch.ones(self.batchSize)))
   else -- If we want to work at the batch level
     self.net2:add(nn.MulConstant(0.5))
   end

   -- Skip connection
   self.skip = nn.Sequential()

   if nInputPlane == nOutputPlane then
     self.skip:add(nn.Identity())
   else
     self.skip:add(ReLU(true))

     -- Skip path #1
     s1 = nn.Sequential()
     s1:add(nn.SpatialAveragePooling(1, 1, stride, stride))
     s1:add(Convolution(nInputPlane, nOutputPlane/2, 1,1, 1,1, 0,0))

     -- Skip path #2
     s2 = nn.Sequential()
     -- Shift the tensor by one pixel right and one pixel down (to make the 2nd path "see" different pixels)
     s2:add(nn.SpatialZeroPadding(1, -1, 1, -1))
     s2:add(nn.SpatialAveragePooling(1, 1, stride, stride))
     s2:add(Convolution(nInputPlane, nOutputPlane/2, 1,1, 1,1, 0,0))

     -- Concatenate the 2 paths along the width dimension
     self.skip:add(nn.ConcatTable()
          :add(s1)
          :add(s2))
     :add(nn.JoinTable(2))

     self.skip:add(SBatchNorm(nOutputPlane))
   end
    
   self.modules = {self.net1, self.net2, self.skip}

end

function ShakeShakeBlock:updateOutput(input)

   --binary = 'true'
   binary = 'false'

   local skip_forward = self.skip:forward(input)
   self.output:resizeAs(skip_forward):copy(skip_forward)
   if self.shakeImage == 'true' then -- If we want to work at the image level
     if self.train and self.forwardShake == 'true' then -- If we are training and want to randomize the forward pass
       if binary == 'true' then
         --self.alpha = 0.5 + 0.5*torch.sign(torch.randn(self.batchSize))  
         self.alpha = 0.5 + 0.5*torch.sign(-0.5*torch.ones(self.batchSize) + 1.0*torch.rand(self.batchSize))  
       else
         self.alpha = torch.rand(self.batchSize) -- then create a vector of random numbers
       end
       --self.alpha = 0.25 + 0.5*torch.rand(self.batchSize)
       --self.alpha = torch.range(0.0, 1.0, 1.0/(self.batchSize-1))
       --self.alpha = 0.5 + 0.1*torch.randn(self.batchSize) 
       --self.alpha = 0.5*torch.ones(self.batchSize) 
       -- Access the vector where constants are stored in self.net1 nn.MulConstantSlices and overwrite it with self.alpha
       self.net1.modules[7].constant_tensor = self.alpha  
       -- Access the vector where constants are stored in self.net2 nn.MulConstantSlices and overwrite it with 1 - self.alpha
       self.net2.modules[7].constant_tensor = torch.ones(self.batchSize) - self.alpha
     else -- If we are testing or do not want to randomize the forward pass
       -- Access the vector where constants are stored in in self.net1 nn.MulConstantSlices and overwrite it with 0.5s
       self.net1.modules[7].constant_tensor = torch.ones(self.batchSize)*0.5
       -- Access the vector where constants are stored in self.net2 nn.MulConstantSlices and overwrite it with 0.5s
       self.net2.modules[7].constant_tensor = torch.ones(self.batchSize)*0.5
     end
   else -- If we want to work at the batch level
     if self.train and self.forwardShake == 'true' then -- If we are training and want to randomize the forward pass
       self.alpha = torch.rand(1)[1] -- then create a random number
       -- Access the constant in self.net1 nn.MulConstant and overwrite it with self.alpha
       self.net1.modules[7].constant_scalar = self.alpha
       -- Access the constant in self.net2 nn.MulConstant and overwrite it with 1 - self.alpha
       self.net2.modules[7].constant_scalar = 1 - self.alpha
     else -- If we are testing or do not want to randomize the forward pass
       -- Access the constant in self.net1 nn.MulConstant and overwrite it with 0.5
       self.net1.modules[7].constant_scalar = 0.5
       -- Access the constant in self.net2 nn.MulConstant and overwrite it with 0.5
       self.net2.modules[7].constant_scalar = 0.5
     end
   end

   -- Now that the constants have been updated, forward self.net1 and self.net2 and add the results to self.output
   self.output:add(self.net1:forward(input))
   self.output:add(self.net2:forward(input))

   return self.output
end

function ShakeShakeBlock:updateGradInput(input, gradOutput)

   self.gradInput = self.gradInput or input.new()
   self.gradInput:resizeAs(input):copy(self.skip:updateGradInput(input, gradOutput))
   --binary = 'true'
   binary = 'false'
   --probkeep = 0.4

   if self.shakeImage == 'true' then -- If we want to work at the image level
     if self.backwardShake == 'true' then -- If we want to randomize the backward pass
       --self.beta = torch.rand(self.batchSize) -- then create a vector of random numbers
       if binary == 'true' then
         --self.beta = 0.5 + 0.5*torch.sign(torch.randn(self.batchSize))
         --self.beta = torch.ones(self.batchSize) - self.net1.modules[7].constant_tensor	-- the opposite branch
         s1 = 0.5*torch.ones(self.batchSize) - self.net1.modules[7].constant_tensor
         s2 = torch.rand(self.batchSize) - probkeep*torch.ones(self.batchSize)
         self.beta = 0.5 + 0.5*torch.sign( torch.cmul(s1,s2) )
       else
         self.beta = torch.rand(self.batchSize)
       end
       --self.beta = 0.5 + 0.1*torch.randn(self.batchSize) 
       --self.beta = 0.25 + 0.5*torch.rand(self.batchSize)
       --self.beta = torch.range(0.0, 1.0, 1.0/(self.batchSize-1))
       --self.beta = 0.5*torch.ones(self.batchSize) 
       -- Access the vector where constants are stored in self.net1 nn.MulConstantSlices and overwrite it with self.beta
       self.net1.modules[7].constant_tensor = self.beta
       -- Access the vector where constants are stored in self.net2 nn.MulConstantSlices and overwrite it with 1 - self.beta
       self.net2.modules[7].constant_tensor = torch.ones(self.batchSize) - self.beta
     else -- If we do not want to randomize the backward pass
       if binary == 'false' then
         -- Access the vector where constants are stored in in self.net1 nn.MulConstantSlices and overwrite it with 0.5s
         self.net1.modules[7].constant_tensor = torch.ones(self.batchSize)*0.5
         -- Access the vector where constants are stored in in self.net2 nn.MulConstantSlices and overwrite it with 0.5s
         self.net2.modules[7].constant_tensor = torch.ones(self.batchSize)*0.5
       end
     end
   else -- If we want to work at the batch level
     if self.backwardShake == 'true' then -- If we want to randomize the backward pass
       self.beta = torch.rand(1)[1] -- then create a random number
       -- Access the constant in self.net1 nn.MulConstant and overwrite it with self.beta
       self.net1.modules[7].constant_scalar = self.beta
       -- Access the constant in self.net2 nn.MulConstant and overwrite it with 1 - self.beta
       self.net2.modules[7].constant_scalar = 1 - self.beta
     else -- If we do not want to randomize the backward pass
       -- Access the constant in self.net1 nn.MulConstant and overwrite it with 0.5
       self.net1.modules[7].constant_scalar = 0.5
       -- Access the constant in self.net2 nn.MulConstant and overwrite it with 0.5
       self.net2.modules[7].constant_scalar = 0.5
     end
   end

   -- Now that the constants have been updated, "backward" self.net1 and self.net2 and add the results to self.gradInput
   self.gradInput:add(self.net1:updateGradInput(input, gradOutput))
   self.gradInput:add(self.net2:updateGradInput(input, gradOutput))

   return self.gradInput
end

function ShakeShakeBlock:accGradParameters(input, gradOutput, scale)

   scale = scale or 1
   self.skip:accGradParameters(input, gradOutput, scale)
   self.net1:accGradParameters(input, gradOutput, scale)
   self.net2:accGradParameters(input, gradOutput, scale)

end

