using CUDA
using Printf
using StaticArrays
using GPUifyLoops: @unroll

# include("deriv_ops.jl")

###
# y_in: input GPU array
# y_out: output GPU array
###

function D2x_GPU_naive(y_in, y_out, Nx, Ny, h, ::Val{TILE_DIM}) where {TILE_DIM}
	tidx = (blockIdx().x - 1) * TILE_DIM + threadIdx().x
	N = Nx*Ny
	# y_out = zeros(N)
	if tidx <= Ny
		y_out[tidx] = (y_in[tidx] - 2 * y_in[Ny + tidx] + y_in[2*Ny + tidx]) / h^2
	end
	sync_threads()

	if Ny+1 <= tidx <= N-Ny
		y_out[tidx] = (y_in[tidx - Ny] - 2 .* y_in[tidx] + y_in[tidx + Ny]) / h^2
	end

	sync_threads()

	if N-Ny+1 <= tidx <= N
		y_out[tidx] = (y_in[tidx - 2*Ny] -2 * y_in[tidx - Ny] + y_in[tidx]) / h^2
	end
	sync_threads()

	nothing
end

function D2x_GPU(y_in::CuArray,y_out::CuArray,Nx,Ny)
	y_out = similar(y_in)
	TILE_DIM_1 = 4
	TILE_DIM_2 = 16
	h = 1/Nx
	griddim = (div(Nx,TILE_DIM_1) + 1, div(Ny,TILE_DIM_2) + 1)
	blockdim = (TILE_DIM_1,TILE_DIM_2)
	@cuda threads=blockdim blocks=griddim D2x_GPU_shared(y_in, y_out, Nx, Ny, h, Val(TILE_DIM_1), Val(TILE_DIM_2))
	synchronize();
	return y_out
end

function D2x_GPU_shared(y_in, y_out, Nx, Ny, h, ::Val{TILE_DIM1}, ::Val{TILE_DIM2}) where {TILE_DIM1, TILE_DIM2}
    tidx = threadIdx().x
    tidy = threadIdx().y

    # for global memory indexing
    i = (blockIdx().x - 1) * TILE_DIM1 + tidx
    j = (blockIdx().y - 1) * TILE_DIM2 + tidy

    global_index = i + (j - 1) * Ny

    HALO_WIDTH = 2 # For second order derivative

    tile = @cuStaticSharedMem(eltype(y_in), (TILE_DIM1, TILE_DIM2 + 2 * HALO_WIDTH))

    # for tile indexing
    k = tidx
    l = tidy

	# Writing pencil-shaped shared memory

	# for tile itself
	# if k <= TILE_DIM1 && l <= TILE_DIM2 && global_index <= Nx*Ny
	if k <= TILE_DIM1 && l <= TILE_DIM2 && i <= Ny && j <= Nx
		# @inbounds tile[k,l+HALO_WIDTH] = y_in[global_index]
		 tile[k,l+HALO_WIDTH] = y_in[global_index]
    end

    sync_threads()

	# for left halo
	# if k <= TILE_DIM1 && l <= HALO_WIDTH && HALO_WIDTH*Ny+1 <= global_index <= (Nx+HALO_WIDTH)*Ny
	if k <= TILE_DIM1 && l <= HALO_WIDTH && i <= Ny && HALO_WIDTH+1 <= j <= HALO_WIDTH + Nx
		# @inbounds tile[k,l] = y_in[global_index - HALO_WIDTH*Ny]
		tile[k,l] = y_in[global_index - HALO_WIDTH*Ny]
	end

	sync_threads()

	# for right halo
	# if k <= TILE_DIM1 && l >= TILE_DIM2 - HALO_WIDTH && HALO_WIDTH*Ny+1 <= global_index <= (Nx-HALO_WIDTH)*Ny
	if k <= TILE_DIM1 && TILE_DIM2 - HALO_WIDTH + 1 <= l <= TILE_DIM2 && i <= Ny && j <= Nx - HALO_WIDTH
		# @inbounds tile[k,l+2*HALO_WIDTH] = y_in[global_index + HALO_WIDTH*Ny]
		tile[k,l+2*HALO_WIDTH] = y_in[global_index + HALO_WIDTH*Ny]
	end

    sync_threads()

    # Finite difference operation starts here

	# Left Boundary
	if k <= TILE_DIM1 && l + HALO_WIDTH <= TILE_DIM2 + 2*HALO_WIDTH -2 && i <= Ny && j == 1
		# @inbounds y_out[global_index] = (tile[k,l + HALO_WIDTH] - 2*tile[k,l + HALO_WIDTH+1] + tile[k,l + HALO_WIDTH+2]) / h^2
		y_out[global_index] = (tile[k,l + HALO_WIDTH] - 2*tile[k,l + HALO_WIDTH+1] + tile[k,l + HALO_WIDTH+2]) / h^2
	end

	# Center
	if k <= TILE_DIM1 && l + HALO_WIDTH <= TILE_DIM2 + 2*HALO_WIDTH - 1 && i <= Ny && 2 <= j <= Nx-1
		# @inbounds y_out[global_index] = (tile[k,l + HALO_WIDTH-1] - 2*tile[k, l + HALO_WIDTH] + tile[k,l + HALO_WIDTH + 1]) / h^2
		y_out[global_index] = (tile[k,l + HALO_WIDTH-1] - 2*tile[k, l + HALO_WIDTH] + tile[k,l + HALO_WIDTH + 1]) / h^2
	end

	# Right Boundary
	if k <= TILE_DIM1 && 3 <= l + HALO_WIDTH <= TILE_DIM2 + 2*HALO_WIDTH && i <= Ny && j == Nx
		@inbounds y_out[global_index] = (tile[k,l + HALO_WIDTH-2] - 2*tile[k,l + HALO_WIDTH - 1] + tile[k,l + HALO_WIDTH]) / h^2
		# y_out[global_index] = (tile[k,l + HALO_WIDTH-2] - 2*tile[k,l + HALO_WIDTH - 1] + tile[k,l + HALO_WIDTH]) / h^2
		# y_out[global_index] = (tile[k,l+HALO_WIDTH-2])
		# y_out[global_index] = 0
	end

    sync_threads()

    nothing
end

function D2x_GPU_p4_naive(y_in, y_out, Nx, Ny, h, ::Val{TILE_DIM}) where {TILE_DIM}
	d = [1/12 -2/3 0 2/3 -1/12]
	bd = [-24/17  59/34  -4/17  -3/34  0     0;
	-1/2    0      1/2    0     0     0;
	 4/43 -59/86   0     59/86 -4/43  0;
	 3/98   0    -59/98   0    32/49 -4/49]
	tidx = (blockIdx().x - 1) * TILE_DIM + threadIdx().x
	N = Nx*Ny
	# y_out = zeros(N)
	if tidx <= Ny
		y_out[tidx] = (y_in[tidx] - 2 * y_in[Ny + tidx] + y_in[2*Ny + tidx]) / h^2
	end
	sync_threads()

	if Ny+1 <= tidx <= N-Ny
		y_out[tidx] = (y_in[tidx - Ny] - 2 .* y_in[tidx] + y_in[tidx + Ny]) / h^2
	end

	sync_threads()

	if N-Ny+1 <= tidx <= N
		y_out[tidx] = (y_in[tidx - 2*Ny] -2 * y_in[tidx - Ny] + y_in[tidx]) / h^2
	end
	sync_threads()

	nothing
end

function Dx_GPU(y_in::CuArray,y_out::CuArray,Nx,Ny)
	y_out = similar(y_in)
	TILE_DIM_1 = 4
	TILE_DIM_2 = 16
	h = 1/Nx
	griddim = (div(Nx,TILE_DIM_1) + 1, div(Ny,TILE_DIM_2) + 1)
	blockdim = (TILE_DIM_1,TILE_DIM_2)
	@cuda threads=blockdim blocks=griddim Dx_GPU_shared(y_in, y_out, Nx, Ny, h, Val(TILE_DIM_1), Val(TILE_DIM_2))
	synchronize();
	return y_out
end

function Dx_GPU_shared(y_in, y_out, Nx, Ny, h, ::Val{TILE_DIM1}, ::Val{TILE_DIM2}) where {TILE_DIM1, TILE_DIM2}
	tidx = threadIdx().x
    tidy = threadIdx().y

    # for global memory indexing
    i = (blockIdx().x - 1) * TILE_DIM1 + tidx
    j = (blockIdx().y - 1) * TILE_DIM2 + tidy

    global_index = i + (j - 1) * Ny

    HALO_WIDTH = 2 # For second order derivative

    tile = @cuStaticSharedMem(eltype(y_in), (TILE_DIM1, TILE_DIM2 + 2 * HALO_WIDTH))

    # for tile indexing
    k = tidx
    l = tidy

	# Writing pencil-shaped shared memory

	# for tile itself
	# if k <= TILE_DIM1 && l <= TILE_DIM2 && global_index <= Nx*Ny
	if k <= TILE_DIM1 && l <= TILE_DIM2 && i <= Ny && j <= Nx
		# @inbounds tile[k,l+HALO_WIDTH] = y_in[global_index]
		 tile[k,l+HALO_WIDTH] = y_in[global_index]
    end

    sync_threads()

	# for left halo
	# if k <= TILE_DIM1 && l <= HALO_WIDTH && HALO_WIDTH*Ny+1 <= global_index <= (Nx+HALO_WIDTH)*Ny
	if k <= TILE_DIM1 && l <= HALO_WIDTH && i <= Ny && HALO_WIDTH+1 <= j <= HALO_WIDTH + Nx
		# @inbounds tile[k,l] = y_in[global_index - HALO_WIDTH*Ny]
		tile[k,l] = y_in[global_index - HALO_WIDTH*Ny]
	end

	sync_threads()

	# for right halo
	# if k <= TILE_DIM1 && l >= TILE_DIM2 - HALO_WIDTH && HALO_WIDTH*Ny+1 <= global_index <= (Nx-HALO_WIDTH)*Ny
	if k <= TILE_DIM1 && TILE_DIM2 - HALO_WIDTH + 1<= l <= TILE_DIM2 && i <= Ny && j <= Nx - HALO_WIDTH
		# @inbounds tile[k,l+2*HALO_WIDTH] = y_in[global_index + HALO_WIDTH*Ny]
		tile[k,l+2*HALO_WIDTH] = y_in[global_index + HALO_WIDTH*Ny]
	end

    sync_threads()

    # Finite difference operation starts here

	# Left Boundary
	if k <= TILE_DIM1 && l + HALO_WIDTH <= TILE_DIM2 + 2*HALO_WIDTH -2 && i <= Ny && j == 1
		# @inbounds y_out[global_index] = (tile[k,l + HALO_WIDTH] - 2*tile[k,l + HALO_WIDTH+1] + tile[k,l + HALO_WIDTH+2]) / h^2
		y_out[global_index] = (tile[k,l + HALO_WIDTH+1] - tile[k,l + HALO_WIDTH]) / h
	end

	# Center
	if k <= TILE_DIM1 && l + HALO_WIDTH <= TILE_DIM2 + 2*HALO_WIDTH - 1 && i <= Ny && 2 <= j <= Nx-1
		# @inbounds y_out[global_index] = (tile[k,l + HALO_WIDTH-1] - 2*tile[k, l + HALO_WIDTH] + tile[k,l + HALO_WIDTH + 1]) / h^2
		y_out[global_index] = (tile[k,l + HALO_WIDTH + 1] - tile[k,l + HALO_WIDTH - 1]) / (2*h)
	end

	# Right Boundary
	if k <= TILE_DIM1 && 3 <= l + HALO_WIDTH <= TILE_DIM2 + 2*HALO_WIDTH && i <= Ny && j == Nx
		# @inbounds y_out[global_index] = (tile[k,l + HALO_WIDTH-2] - 2*tile[k,l + HALO_WIDTH - 1] + tile[k,l + HALO_WIDTH]) / h^2
		y_out[global_index] = (tile[k,l + HALO_WIDTH ] - tile[k,l + HALO_WIDTH - 1]) / h
	end

    sync_threads()

    nothing
end

function Hxinv_GPU_shared(y_in, y_out, Nx, Ny, h, ::Val{TILE_DIM1}, ::Val{TILE_DIM2}) where {TILE_DIM1, TILE_DIM2}
	tidx = threadIdx().x
    tidy = threadIdx().y

    # for global memory indexing
    i = (blockIdx().x - 1) * TILE_DIM1 + tidx
    j = (blockIdx().y - 1) * TILE_DIM2 + tidy

    global_index = i + (j - 1) * Ny

    HALO_WIDTH = 2 # For second order derivative

    tile = @cuStaticSharedMem(eltype(y_in), (TILE_DIM1, TILE_DIM2 + 2 * HALO_WIDTH))

    # for tile indexing
    k = tidx
    l = tidy

	# Writing pencil-shaped shared memory

	# for tile itself
	# if k <= TILE_DIM1 && l <= TILE_DIM2 && global_index <= Nx*Ny
	if k <= TILE_DIM1 && l <= TILE_DIM2 && i <= Ny && j <= Nx
		# @inbounds tile[k,l+HALO_WIDTH] = y_in[global_index]
		 tile[k,l+HALO_WIDTH] = y_in[global_index]
    end

    sync_threads()

	# for left halo
	# if k <= TILE_DIM1 && l <= HALO_WIDTH && HALO_WIDTH*Ny+1 <= global_index <= (Nx+HALO_WIDTH)*Ny
	if k <= TILE_DIM1 && l <= HALO_WIDTH && i <= Ny && HALO_WIDTH+1 <= j <= HALO_WIDTH + Nx
		# @inbounds tile[k,l] = y_in[global_index - HALO_WIDTH*Ny]
		tile[k,l] = y_in[global_index - HALO_WIDTH*Ny]
	end

	sync_threads()

	# for right halo
	# if k <= TILE_DIM1 && l >= TILE_DIM2 - HALO_WIDTH && HALO_WIDTH*Ny+1 <= global_index <= (Nx-HALO_WIDTH)*Ny
	if k <= TILE_DIM1 && TILE_DIM2 - HALO_WIDTH + 1<= l <= TILE_DIM2 && i <= Ny && j <= Nx - HALO_WIDTH
		# @inbounds tile[k,l+2*HALO_WIDTH] = y_in[global_index + HALO_WIDTH*Ny]
		tile[k,l+2*HALO_WIDTH] = y_in[global_index + HALO_WIDTH*Ny]
	end

    sync_threads()

    # Finite difference operation starts here

	# Left Boundary
	if k <= TILE_DIM1 && l + HALO_WIDTH <= TILE_DIM2 + 2*HALO_WIDTH -2 && i <= Ny && j == 1
		# @inbounds y_out[global_index] = (tile[k,l + HALO_WIDTH] - 2*tile[k,l + HALO_WIDTH+1] + tile[k,l + HALO_WIDTH+2]) / h^2
		y_out[global_index] = (2*tile[k,l + HALO_WIDTH]) / h
	end

	# Center
	if k <= TILE_DIM1 && l + HALO_WIDTH <= TILE_DIM2 + 2*HALO_WIDTH - 1 && i <= Ny && 2 <= j <= Nx-1
		# @inbounds y_out[global_index] = (tile[k,l + HALO_WIDTH-1] - 2*tile[k, l + HALO_WIDTH] + tile[k,l + HALO_WIDTH + 1]) / h^2
		y_out[global_index] = (tile[k,l + HALO_WIDTH] ) / (h)
	end

	# Right Boundary
	if k <= TILE_DIM1 && 3 <= l + HALO_WIDTH <= TILE_DIM2 + 2*HALO_WIDTH && i <= Ny && j == Nx
		# @inbounds y_out[global_index] = (tile[k,l + HALO_WIDTH-2] - 2*tile[k,l + HALO_WIDTH - 1] + tile[k,l + HALO_WIDTH]) / h^2
		y_out[global_index] = (2*tile[k,l + HALO_WIDTH ]) / h
	end

    sync_threads()

    nothing
end

function Hx_GPU_shared(y_in, y_out, Nx, Ny, h, ::Val{TILE_DIM1}, ::Val{TILE_DIM2}) where {TILE_DIM1, TILE_DIM2}
	tidx = threadIdx().x
    tidy = threadIdx().y

    # for global memory indexing
    i = (blockIdx().x - 1) * TILE_DIM1 + tidx
    j = (blockIdx().y - 1) * TILE_DIM2 + tidy

    global_index = i + (j - 1) * Ny

    HALO_WIDTH = 2 # For second order derivative

    tile = @cuStaticSharedMem(eltype(y_in), (TILE_DIM1, TILE_DIM2 + 2 * HALO_WIDTH))

    # for tile indexing
    k = tidx
    l = tidy

	# Writing pencil-shaped shared memory

	# for tile itself
	# if k <= TILE_DIM1 && l <= TILE_DIM2 && global_index <= Nx*Ny
	if k <= TILE_DIM1 && l <= TILE_DIM2 && i <= Ny && j <= Nx
		# @inbounds tile[k,l+HALO_WIDTH] = y_in[global_index]
		 tile[k,l+HALO_WIDTH] = y_in[global_index]
    end

    sync_threads()

	# for left halo
	# if k <= TILE_DIM1 && l <= HALO_WIDTH && HALO_WIDTH*Ny+1 <= global_index <= (Nx+HALO_WIDTH)*Ny
	if k <= TILE_DIM1 && l <= HALO_WIDTH && i <= Ny && HALO_WIDTH+1 <= j <= HALO_WIDTH + Nx
		# @inbounds tile[k,l] = y_in[global_index - HALO_WIDTH*Ny]
		tile[k,l] = y_in[global_index - HALO_WIDTH*Ny]
	end

	sync_threads()

	# for right halo
	# if k <= TILE_DIM1 && l >= TILE_DIM2 - HALO_WIDTH && HALO_WIDTH*Ny+1 <= global_index <= (Nx-HALO_WIDTH)*Ny
	if k <= TILE_DIM1 && TILE_DIM2 - HALO_WIDTH + 1 <= l <= TILE_DIM2 && i <= Ny && j <= Nx - HALO_WIDTH
		# @inbounds tile[k,l+2*HALO_WIDTH] = y_in[global_index + HALO_WIDTH*Ny]
		tile[k,l+2*HALO_WIDTH] = y_in[global_index + HALO_WIDTH*Ny]
	end

    sync_threads()

    # Finite difference operation starts here

	# Left Boundary
	if k <= TILE_DIM1 && l + HALO_WIDTH <= TILE_DIM2 + 2*HALO_WIDTH -2 && i <= Ny && j == 1
		# @inbounds y_out[global_index] = (tile[k,l + HALO_WIDTH] - 2*tile[k,l + HALO_WIDTH+1] + tile[k,l + HALO_WIDTH+2]) / h^2
		y_out[global_index] = (h*tile[k,l + HALO_WIDTH]) / 2
	end

	# Center
	if k <= TILE_DIM1 && l + HALO_WIDTH <= TILE_DIM2 + 2*HALO_WIDTH - 1 && i <= Ny && 2 <= j <= Nx-1
		# @inbounds y_out[global_index] = (tile[k,l + HALO_WIDTH-1] - 2*tile[k, l + HALO_WIDTH] + tile[k,l + HALO_WIDTH + 1]) / h^2
		y_out[global_index] = h*(tile[k,l + HALO_WIDTH] )
	end

	# Right Boundary
	if k <= TILE_DIM1 && 3 <= l + HALO_WIDTH <= TILE_DIM2 + 2*HALO_WIDTH && i <= Ny && j == Nx
		# @inbounds y_out[global_index] = (tile[k,l + HALO_WIDTH-2] - 2*tile[k,l + HALO_WIDTH - 1] + tile[k,l + HALO_WIDTH]) / h^2
		y_out[global_index] = (h*tile[k,l + HALO_WIDTH ]) / 2
	end

    sync_threads()

    nothing
end

function D2y_GPU_naive(y_in, y_out, Nx, Ny, h, ::Val{TILE_DIM}) where {TILE_DIM}
	tidx = (blockIdx().x - 1) * TILE_DIM + threadIdx().x
	N = Nx*Ny
	if 2 <= tidx <= N-1
		@inbounds y_out[tidx] = (y_in[tidx-1] - 2y_in[tidx] + y_in[tidx + 1]) / h^2
	end

	if 1 <= tidx <= N && mod(tidx,Ny) == 0
		@inbounds y_out[tidx] = (y_in[tidx] - 2y_in[tidx - 1] + y_in[tidx - 2]) / h^2
		@inbounds y_out[tidx-Ny+1] = (y_in[tidx-Ny+1] - 2y_in[tidx - Ny + 2] + y_in[tidx - Ny + 3]) / h^2
	end

	sync_threads()
	nothing
end

function D2y_GPU_shared(y_in, y_out, Nx, Ny, h, ::Val{TILE_DIM1}, ::Val{TILE_DIM2}) where {TILE_DIM1, TILE_DIM2}
    tidx = threadIdx().x
    tidy = threadIdx().y

	i = (blockIdx().x - 1) * TILE_DIM1 + tidx
	j = (blockIdx().y - 1) * TILE_DIM2 + tidy

	global_index = i + (j-1)*Nx

	HALO_WIDTH = 2
	tile = @cuStaticSharedMem(eltype(y_in),(TILE_DIM1+2*HALO_WIDTH,TILE_DIM2))

	k = tidx
	l = tidy

    # Writing pencil-shaped shared memory

    # for tile itself
	# if k <= TILE_DIM1 && l <= TILE_DIM2 && global_index <= Nx*Ny
	if k <= TILE_DIM1 && l <= TILE_DIM2 && i <= Ny && j <= Nx
		# @inbounds tile[k+HALO_WIDTH,l] = y_in[global_index]
		tile[k+HALO_WIDTH,l] = y_in[global_index]
	end

	sync_threads()

	# For upper halo
	# if k <= HALO_WIDTH && l <= TILE_DIM2 && HALO_WIDTH + 1 <= global_index <= Nx*Ny + HALO_WIDTH
	if k <= HALO_WIDTH && l <= TILE_DIM2 && HALO_WIDTH + 1 <= i <= Ny + HALO_WIDTH && j <= Nx
		# @inbounds tile[k,l] = y_in[global_index - HALO_WIDTH]
		tile[k,l] = y_in[global_index - HALO_WIDTH]
	end

	sync_threads()

	# For lower halo
	# if k >= TILE_DIM1 - HALO_WIDTH && l <= TILE_DIM2 && HALO_WIDTH + 1 <= global_index <= Nx*Ny - HALO_WIDTH
	if  TILE_DIM1 - HALO_WIDTH + 1 <= k <= TILE_DIM1 && l <= TILE_DIM2 && i <= Ny - HALO_WIDTH && j <= Nx
		# @inbounds tile[k+2*HALO_WIDTH,l] = y_in[global_index + HALO_WIDTH]
		tile[k+2*HALO_WIDTH,l] = y_in[global_index + HALO_WIDTH]
	end

    sync_threads()

    # Finite Difference Operations starts

    #Upper Boundary
	if k + HALO_WIDTH <= TILE_DIM1 + 2*HALO_WIDTH -2 && l <= TILE_DIM2 && i == 1 && j <= Ny
		# @inbounds y_out[global_index] = (tile[k+HALO_WIDTH,l] - 2*tile[k+HALO_WIDTH+1,l] + tile[k+HALO_WIDTH+2,l]) / h^2
		y_out[global_index] = (tile[k+HALO_WIDTH,l] - 2*tile[k+HALO_WIDTH+1,l] + tile[k+HALO_WIDTH+2,l]) / h^2
	end

	sync_threads()

	#Center
	if k + HALO_WIDTH <= TILE_DIM1 + 2*HALO_WIDTH - 1 && l <= TILE_DIM2 && 2 <= i <= Nx-1 && j <= Ny
		# @inbounds y_out[global_index] = (tile[k+HALO_WIDTH-1,l] - 2*tile[k+HALO_WIDTH,l] + tile[k+HALO_WIDTH+1,l]) / h^2
		y_out[global_index] = (tile[k+HALO_WIDTH-1,l] - 2*tile[k+HALO_WIDTH,l] + tile[k+HALO_WIDTH+1,l]) / h^2
	end

	sync_threads()

	#Lower Boundary
	if 3 <= k + HALO_WIDTH <= TILE_DIM1 + 2*HALO_WIDTH && l <= TILE_DIM2 && i == Nx && j <= Ny
		# @inbounds y_out[global_index] = (tile[k+HALO_WIDTH-2,l] - 2*tile[k+HALO_WIDTH-1,l] + tile[k+HALO_WIDTH,l]) / h^2
		y_out[global_index] = (tile[k+HALO_WIDTH-2,l] - 2*tile[k+HALO_WIDTH-1,l] + tile[k+HALO_WIDTH,l]) / h^2
		# y_out[global_index] = (tile[k+HALO_WIDTH-2,l] - 2*tile[k+HALO_WIDTH-1,l])
		# y_out[global_index] = tile[k+HALO_WIDTH,l]
		# y_out[global_index] = 0
	end

	# if i == 1 && j <= Nx
	# 	y_out[global_index] = y_out[global_index+1]
	# end

	# sync_threads()

	# if i == Ny && j <= Nx
	# 	y_out[global_index] = y_out[global_index-1]
	# 	# y_out[global_index] = 0.0
	# end

    sync_threads()

    nothing

end

function Dy_GPU_shared(y_in, y_out, Nx, Ny, h, ::Val{TILE_DIM1}, ::Val{TILE_DIM2}) where {TILE_DIM1, TILE_DIM2}
	tidx = threadIdx().x
    tidy = threadIdx().y

	i = (blockIdx().x - 1) * TILE_DIM1 + tidx
	j = (blockIdx().y - 1) * TILE_DIM2 + tidy

	global_index = i + (j-1)*Nx

	HALO_WIDTH = 2
	tile = @cuStaticSharedMem(eltype(y_in),(TILE_DIM1+2*HALO_WIDTH,TILE_DIM2))

	k = tidx
	l = tidy

    # Writing pencil-shaped shared memory

    # for tile itself
	# if k <= TILE_DIM1 && l <= TILE_DIM2 && global_index <= Nx*Ny
	if k <= TILE_DIM1 && l <= TILE_DIM2 && i <= Ny && j <= Nx
		# @inbounds tile[k+HALO_WIDTH,l] = y_in[global_index]
		tile[k+HALO_WIDTH,l] = y_in[global_index]
	end

	sync_threads()

	# For upper halo
	# if k <= HALO_WIDTH && l <= TILE_DIM2 && HALO_WIDTH + 1 <= global_index <= Nx*Ny + HALO_WIDTH
	if k <= HALO_WIDTH && l <= TILE_DIM2 && HALO_WIDTH + 1 <= i <= Ny + HALO_WIDTH && j <= Nx
		# @inbounds tile[k,l] = y_in[global_index - HALO_WIDTH]
		tile[k,l] = y_in[global_index - HALO_WIDTH]
	end

	sync_threads()

	# For lower halo
	# if k >= TILE_DIM1 - HALO_WIDTH && l <= TILE_DIM2 && HALO_WIDTH + 1 <= global_index <= Nx*Ny - HALO_WIDTH
	if  TILE_DIM1 - HALO_WIDTH + 1 <= k <= TILE_DIM1 && l <= TILE_DIM2 && i <= Ny - HALO_WIDTH && j <= Nx
		# @inbounds tile[k+2*HALO_WIDTH,l] = y_in[global_index + HALO_WIDTH]
		tile[k+2*HALO_WIDTH,l] = y_in[global_index + HALO_WIDTH]
	end

    sync_threads()

    # Finite Difference Operations starts

    #Upper Boundary
	if k + HALO_WIDTH <= TILE_DIM1 + 2*HALO_WIDTH - 2 && l <= TILE_DIM2 && i == 1 && j <= Ny
		# @inbounds y_out[global_index] = (tile[k+HALO_WIDTH,l] - 2*tile[k+HALO_WIDTH+1,l] + tile[k+HALO_WIDTH+2,l]) / h^2
		y_out[global_index] = (tile[k+HALO_WIDTH+1,l] - tile[k+HALO_WIDTH,l]) / h
	end

	sync_threads()

	#Center
	if k + HALO_WIDTH <= TILE_DIM1 + 2*HALO_WIDTH && l <= TILE_DIM2 && 2 <= i <= Nx-1 && j <= Ny
		# @inbounds y_out[global_index] = (tile[k+HALO_WIDTH-1,l] - 2*tile[k+HALO_WIDTH,l] + tile[k+HALO_WIDTH+1,l]) / h^2
		y_out[global_index] = (tile[k+HALO_WIDTH+1,l] - tile[k+HALO_WIDTH-1,l]) / (2*h)
	end

	sync_threads()

	#Lower Boundary
	if 3 <= k + HALO_WIDTH <= TILE_DIM1 + 2*HALO_WIDTH && l <= TILE_DIM2 && i == Nx && j <= Ny
		# @inbounds y_out[global_index] = (tile[k+HALO_WIDTH-2,l] - 2*tile[k+HALO_WIDTH-1,l] + tile[k+HALO_WIDTH,l]) / h^2
		y_out[global_index] = (tile[k+HALO_WIDTH,l] - tile[k+HALO_WIDTH-1,l]) / h
		# y_out[global_index] = tile[k+HALO_WIDTH-1,l] #- tile[k+HALO_WIDTH-1,l]
 	end

    sync_threads()

    nothing
end

function Hyinv_GPU_shared(y_in, y_out, Nx, Ny, h, ::Val{TILE_DIM1}, ::Val{TILE_DIM2}) where {TILE_DIM1, TILE_DIM2}
	tidx = threadIdx().x
    tidy = threadIdx().y

	i = (blockIdx().x - 1) * TILE_DIM1 + tidx
	j = (blockIdx().y - 1) * TILE_DIM2 + tidy

	global_index = i + (j-1)*Nx

	HALO_WIDTH = 2
	tile = @cuStaticSharedMem(eltype(y_in),(TILE_DIM1+2*HALO_WIDTH,TILE_DIM2))

	k = tidx
	l = tidy

    # Writing pencil-shaped shared memory

    # for tile itself
	# if k <= TILE_DIM1 && l <= TILE_DIM2 && global_index <= Nx*Ny
	if k <= TILE_DIM1 && l <= TILE_DIM2 && i <= Ny && j <= Nx
		# @inbounds tile[k+HALO_WIDTH,l] = y_in[global_index]
		tile[k+HALO_WIDTH,l] = y_in[global_index]
	end

	sync_threads()

	# For upper halo
	# if k <= HALO_WIDTH && l <= TILE_DIM2 && HALO_WIDTH + 1 <= global_index <= Nx*Ny + HALO_WIDTH
	if k <= HALO_WIDTH && l <= TILE_DIM2 && HALO_WIDTH + 1 <= i <= Ny + HALO_WIDTH && j <= Nx
		# @inbounds tile[k,l] = y_in[global_index - HALO_WIDTH]
		tile[k,l] = y_in[global_index - HALO_WIDTH]
	end

	sync_threads()

	# For lower halo
	# if k >= TILE_DIM1 - HALO_WIDTH && l <= TILE_DIM2 && HALO_WIDTH + 1 <= global_index <= Nx*Ny - HALO_WIDTH
	if  TILE_DIM1 - HALO_WIDTH <= k <= TILE_DIM1 && l <= TILE_DIM2 && i <= Ny - HALO_WIDTH && j <= Nx
		# @inbounds tile[k+2*HALO_WIDTH,l] = y_in[global_index + HALO_WIDTH]
		tile[k+2*HALO_WIDTH,l] = y_in[global_index + HALO_WIDTH]
	end

    sync_threads()

    # Finite Difference Operations starts

    #Upper Boundary
	if k + HALO_WIDTH <= TILE_DIM1 + 2*HALO_WIDTH && l <= TILE_DIM2 && i == 1 && j <= Ny
		# @inbounds y_out[global_index] = (tile[k+HALO_WIDTH,l] - 2*tile[k+HALO_WIDTH+1,l] + tile[k+HALO_WIDTH+2,l]) / h^2
		y_out[global_index] = (2*tile[k+HALO_WIDTH,l]) / h
	end

	sync_threads()

	#Center
	if k + HALO_WIDTH <= TILE_DIM1 + 2*HALO_WIDTH && l <= TILE_DIM2 && 2 <= i <= Nx-1 && j <= Ny
		# @inbounds y_out[global_index] = (tile[k+HALO_WIDTH-1,l] - 2*tile[k+HALO_WIDTH,l] + tile[k+HALO_WIDTH+1,l]) / h^2
		y_out[global_index] = (tile[k+HALO_WIDTH,l]) / h
	end

	sync_threads()

	#Lower Boundary
	if 3 <= k + HALO_WIDTH <= TILE_DIM1 + 2*HALO_WIDTH && l <= TILE_DIM2 && i == Nx && j <= Ny
		# @inbounds y_out[global_index] = (tile[k+HALO_WIDTH-2,l] - 2*tile[k+HALO_WIDTH-1,l] + tile[k+HALO_WIDTH,l]) / h^2
		y_out[global_index] = (2*tile[k+HALO_WIDTH,l]) / h
    end

    sync_threads()

    nothing
end

function Hy_GPU_shared(y_in, y_out, Nx, Ny, h, ::Val{TILE_DIM1}, ::Val{TILE_DIM2}) where {TILE_DIM1, TILE_DIM2}
	tidx = threadIdx().x
    tidy = threadIdx().y

	i = (blockIdx().x - 1) * TILE_DIM1 + tidx
	j = (blockIdx().y - 1) * TILE_DIM2 + tidy

	global_index = i + (j-1)*Ny

	HALO_WIDTH = 2
	tile = @cuStaticSharedMem(eltype(y_in),(TILE_DIM1+2*HALO_WIDTH,TILE_DIM2))

	k = tidx
	l = tidy

    # Writing pencil-shaped shared memory

    # for tile itself
	# if k <= TILE_DIM1 && l <= TILE_DIM2 && global_index <= Nx*Ny
	if k <= TILE_DIM1 && l <= TILE_DIM2 && i <= Ny && j <= Nx
		# @inbounds tile[k+HALO_WIDTH,l] = y_in[global_index]
		tile[k+HALO_WIDTH,l] = y_in[global_index]
	end

	sync_threads()

	# For upper halo
	# if k <= HALO_WIDTH && l <= TILE_DIM2 && HALO_WIDTH + 1 <= global_index <= Nx*Ny + HALO_WIDTH
	if k <= HALO_WIDTH && l <= TILE_DIM2 && HALO_WIDTH + 1 <= i <= Ny + HALO_WIDTH && j <= Nx
		# @inbounds tile[k,l] = y_in[global_index - HALO_WIDTH]
		tile[k,l] = y_in[global_index - HALO_WIDTH]
	end

	sync_threads()

	# For lower halo
	# if k >= TILE_DIM1 - HALO_WIDTH && l <= TILE_DIM2 && HALO_WIDTH + 1 <= global_index <= Nx*Ny - HALO_WIDTH
	if  TILE_DIM1 - HALO_WIDTH + 1 <= k <= TILE_DIM1 && l <= TILE_DIM2 && i <= Ny - HALO_WIDTH && j <= Nx
		# @inbounds tile[k+2*HALO_WIDTH,l] = y_in[global_index + HALO_WIDTH]
		tile[k+2*HALO_WIDTH,l] = y_in[global_index + HALO_WIDTH]
	end

    sync_threads()

    # Finite Difference Operations starts

    #Upper Boundary
	# if k + HALO_WIDTH <= TILE_DIM1 + 2*HALO_WIDTH -2 && l <= TILE_DIM2 && i == 1 && j <= Ny
	if k + HALO_WIDTH <= TILE_DIM1 + 2*HALO_WIDTH && l <= TILE_DIM2 && i == 1 && j <= Nx
		# @inbounds y_out[global_index] = (tile[k+HALO_WIDTH,l] - 2*tile[k+HALO_WIDTH+1,l] + tile[k+HALO_WIDTH+2,l]) / h^2
		y_out[global_index] = (h*tile[k+HALO_WIDTH,l]) / 2
	end

	# sync_threads()

	#Center
	# if k + HALO_WIDTH <= TILE_DIM1 + 2*HALO_WIDTH - 1 && l <= TILE_DIM2 && 2 <= i <= Nx-1 && j <= Ny
	if k + HALO_WIDTH <= TILE_DIM1 + 2*HALO_WIDTH && l <= TILE_DIM2 && 2 <= i <= Ny-1 && j <= Nx
		# @inbounds y_out[global_index] = (tile[k+HALO_WIDTH-1,l] - 2*tile[k+HALO_WIDTH,l] + tile[k+HALO_WIDTH+1,l]) / h^2
		y_out[global_index] = h * (tile[k+HALO_WIDTH,l])
	end

	# sync_threads()

	#Lower Boundary
	if 1 <= k + HALO_WIDTH <= TILE_DIM1 + 2*HALO_WIDTH && l <= TILE_DIM2 && i == Ny && j <= Nx
		# @inbounds y_out[global_index] = (tile[k+HALO_WIDTH-2,l] - 2*tile[k+HALO_WIDTH-1,l] + tile[k+HALO_WIDTH,l]) / h^2
		y_out[global_index] = (h*tile[k+HALO_WIDTH,l]) / 2
    end

    sync_threads()

    nothing
end

function Bx_GPU_shared(y_in, y_out, Nx, Ny, h, ::Val{TILE_DIM1}, ::Val{TILE_DIM2}) where {TILE_DIM1, TILE_DIM2}
	tidx = threadIdx().x
    tidy = threadIdx().y

    # for global memory indexing
    i = (blockIdx().x - 1) * TILE_DIM1 + tidx
    j = (blockIdx().y - 1) * TILE_DIM2 + tidy

    global_index = i + (j - 1) * Ny

    HALO_WIDTH = 2 # For second order derivative

    tile = @cuStaticSharedMem(eltype(y_in), (TILE_DIM1, TILE_DIM2 + 2 * HALO_WIDTH))

    # for tile indexing
    k = tidx
    l = tidy

	# Writing pencil-shaped shared memory

	# for tile itself
	# if k <= TILE_DIM1 && l <= TILE_DIM2 && global_index <= Nx*Ny
	if k <= TILE_DIM1 && l <= TILE_DIM2 && i <= Ny && j <= Nx
		# @inbounds tile[k,l+HALO_WIDTH] = y_in[global_index]
		 tile[k,l+HALO_WIDTH] = y_in[global_index]
    end

    sync_threads()

	# for left halo
	# if k <= TILE_DIM1 && l <= HALO_WIDTH && HALO_WIDTH*Ny+1 <= global_index <= (Nx+HALO_WIDTH)*Ny
	if k <= TILE_DIM1 && l <= HALO_WIDTH && i <= Ny && HALO_WIDTH+1 <= j <= HALO_WIDTH + Nx
		# @inbounds tile[k,l] = y_in[global_index - HALO_WIDTH*Ny]
		tile[k,l] = y_in[global_index - HALO_WIDTH*Ny]
	end

	sync_threads()

	# for right halo
	# if k <= TILE_DIM1 && l >= TILE_DIM2 - HALO_WIDTH && HALO_WIDTH*Ny+1 <= global_index <= (Nx-HALO_WIDTH)*Ny
	if k <= TILE_DIM1 && TILE_DIM2 - HALO_WIDTH + 1<= l <= TILE_DIM2 && i <= Ny && j <= Nx - HALO_WIDTH
		# @inbounds tile[k,l+2*HALO_WIDTH] = y_in[global_index + HALO_WIDTH*Ny]
		tile[k,l+2*HALO_WIDTH] = y_in[global_index + HALO_WIDTH*Ny]
	end

    sync_threads()

    # Finite difference operation starts here

	# Left Boundary
	if k <= TILE_DIM1 && l + HALO_WIDTH <= TILE_DIM2 + 2*HALO_WIDTH -2 && i <= Ny && j == 1
		# @inbounds y_out[global_index] = (tile[k,l + HALO_WIDTH] - 2*tile[k,l + HALO_WIDTH+1] + tile[k,l + HALO_WIDTH+2]) / h^2
		y_out[global_index] = -1.0
	end

	# Center
	if k <= TILE_DIM1 && l + HALO_WIDTH <= TILE_DIM2 + 2*HALO_WIDTH - 1 && i <= Ny && 2 <= j <= Nx-1
		# @inbounds y_out[global_index] = (tile[k,l + HALO_WIDTH-1] - 2*tile[k, l + HALO_WIDTH] + tile[k,l + HALO_WIDTH + 1]) / h^2
		y_out[global_index] = 0.0
	end

	# Right Boundary
	if k <= TILE_DIM1 && 3 <= l + HALO_WIDTH <= TILE_DIM2 + 2*HALO_WIDTH && i <= Ny && j == Nx
		# @inbounds y_out[global_index] = (tile[k,l + HALO_WIDTH-2] - 2*tile[k,l + HALO_WIDTH - 1] + tile[k,l + HALO_WIDTH]) / h^2
		y_out[global_index] = 1.0
	end

    sync_threads()

    nothing
end

function BxSx_GPU_shared(y_in, y_out, Nx, Ny, h, ::Val{TILE_DIM1}, ::Val{TILE_DIM2}) where {TILE_DIM1, TILE_DIM2}
	tidx = threadIdx().x
    tidy = threadIdx().y

    # for global memory indexing
    i = (blockIdx().x - 1) * TILE_DIM1 + tidx
    j = (blockIdx().y - 1) * TILE_DIM2 + tidy

    global_index = i + (j - 1) * Ny

    HALO_WIDTH = 2 # For second order derivative

    tile = @cuStaticSharedMem(eltype(y_in), (TILE_DIM1, TILE_DIM2 + 2 * HALO_WIDTH))

    # for tile indexing
    k = tidx
    l = tidy

	# Writing pencil-shaped shared memory

	# for tile itself
	# if k <= TILE_DIM1 && l <= TILE_DIM2 && global_index <= Nx*Ny
	if k <= TILE_DIM1 && l <= TILE_DIM2 && i <= Ny && j <= Nx
		# @inbounds tile[k,l+HALO_WIDTH] = y_in[global_index]
		 tile[k,l+HALO_WIDTH] = y_in[global_index]
    end

    sync_threads()

	# for left halo
	# if k <= TILE_DIM1 && l <= HALO_WIDTH && HALO_WIDTH*Ny+1 <= global_index <= (Nx+HALO_WIDTH)*Ny
	if k <= TILE_DIM1 && l <= HALO_WIDTH && i <= Ny && HALO_WIDTH+1 <= j <= HALO_WIDTH + Nx
		# @inbounds tile[k,l] = y_in[global_index - HALO_WIDTH*Ny]
		tile[k,l] = y_in[global_index - HALO_WIDTH*Ny]
	end

	sync_threads()

	# for right halo
	# if k <= TILE_DIM1 && l >= TILE_DIM2 - HALO_WIDTH && HALO_WIDTH*Ny+1 <= global_index <= (Nx-HALO_WIDTH)*Ny
	if k <= TILE_DIM1 && TILE_DIM2 - HALO_WIDTH + 1 <= l <= TILE_DIM2 && i <= Ny && j <= Nx - HALO_WIDTH
		# @inbounds tile[k,l+2*HALO_WIDTH] = y_in[global_index + HALO_WIDTH*Ny]
		tile[k,l+2*HALO_WIDTH] = y_in[global_index + HALO_WIDTH*Ny]
	end

    sync_threads()

    # Finite difference operation starts here

	# Left Boundary
	if k <= TILE_DIM1 && l + HALO_WIDTH <= TILE_DIM2 + 2*HALO_WIDTH -2 && i <= Ny && j == 1
		# @inbounds y_out[global_index] = (tile[k,l + HALO_WIDTH] - 2*tile[k,l + HALO_WIDTH+1] + tile[k,l + HALO_WIDTH+2]) / h^2
		y_out[global_index] = (1.5 * tile[k,l+HALO_WIDTH] - 2.0 * tile[k,l+HALO_WIDTH+1] + 0.5*tile[k,l+HALO_WIDTH+2]) / h
	end

	# Center
	if k <= TILE_DIM1 && l + HALO_WIDTH <= TILE_DIM2 + 2*HALO_WIDTH - 1 && i <= Ny && 2 <= j <= Nx-1
		# @inbounds y_out[global_index] = (tile[k,l + HALO_WIDTH-1] - 2*tile[k, l + HALO_WIDTH] + tile[k,l + HALO_WIDTH + 1]) / h^2
		y_out[global_index] = 0.0
	end

	# Right Boundary
	if k <= TILE_DIM1 && 3 <= l + HALO_WIDTH <= TILE_DIM2 + 2*HALO_WIDTH && i <= Ny && j == Nx
		# @inbounds y_out[global_index] = (tile[k,l + HALO_WIDTH-2] - 2*tile[k,l + HALO_WIDTH - 1] + tile[k,l + HALO_WIDTH]) / h^2
		y_out[global_index] = (0.5 * tile[k,l+HALO_WIDTH-2] - 2.0 * tile[k,l+HALO_WIDTH-1] + 1.5 * tile[k,l+HALO_WIDTH]) / h
	end

    sync_threads()

    nothing
end

function By_GPU_shared(y_in, y_out, Nx, Ny, h, ::Val{TILE_DIM1}, ::Val{TILE_DIM2}) where {TILE_DIM1, TILE_DIM2}
	tidx = threadIdx().x
    tidy = threadIdx().y

	i = (blockIdx().x - 1) * TILE_DIM1 + tidx
	j = (blockIdx().y - 1) * TILE_DIM2 + tidy

	global_index = i + (j-1)*Nx

	HALO_WIDTH = 2
	tile = @cuStaticSharedMem(eltype(y_in),(TILE_DIM1+2*HALO_WIDTH,TILE_DIM2))

	k = tidx
	l = tidy

    # Writing pencil-shaped shared memory

    # for tile itself
	# if k <= TILE_DIM1 && l <= TILE_DIM2 && global_index <= Nx*Ny
	if k <= TILE_DIM1 && l <= TILE_DIM2 && i <= Ny && j <= Nx
		# @inbounds tile[k+HALO_WIDTH,l] = y_in[global_index]
		tile[k+HALO_WIDTH,l] = y_in[global_index]
	end

	sync_threads()

	# For upper halo
	# if k <= HALO_WIDTH && l <= TILE_DIM2 && HALO_WIDTH + 1 <= global_index <= Nx*Ny + HALO_WIDTH
	if k <= HALO_WIDTH && l <= TILE_DIM2 && HALO_WIDTH + 1 <= i <= Ny && j <= Nx
		# @inbounds tile[k,l] = y_in[global_index - HALO_WIDTH]
		tile[k,l] = y_in[global_index - HALO_WIDTH]
	end

	sync_threads()

	# For lower halo
	# if k >= TILE_DIM1 - HALO_WIDTH && l <= TILE_DIM2 && HALO_WIDTH + 1 <= global_index <= Nx*Ny - HALO_WIDTH
	if  TILE_DIM1 - HALO_WIDTH <= k <= TILE_DIM1 && l <= TILE_DIM2 && i <= Ny - HALO_WIDTH && j <= Nx
		# @inbounds tile[k+2*HALO_WIDTH,l] = y_in[global_index + HALO_WIDTH]
		tile[k+2*HALO_WIDTH,l] = y_in[global_index + HALO_WIDTH]
	end

    sync_threads()

    # Finite Difference Operations starts

    #Upper Boundary
	if k + HALO_WIDTH <= TILE_DIM1 + 2*HALO_WIDTH && l <= TILE_DIM2 && i == 1 && j <= Ny
		# @inbounds y_out[global_index] = (tile[k+HALO_WIDTH,l] - 2*tile[k+HALO_WIDTH+1,l] + tile[k+HALO_WIDTH+2,l]) / h^2
		y_out[global_index] = -1.0
	end

	sync_threads()

	#Center
	if k + HALO_WIDTH <= TILE_DIM1 + 2*HALO_WIDTH && l <= TILE_DIM2 && 2 <= i <= Nx-1 && j <= Ny
		# @inbounds y_out[global_index] = (tile[k+HALO_WIDTH-1,l] - 2*tile[k+HALO_WIDTH,l] + tile[k+HALO_WIDTH+1,l]) / h^2
		y_out[global_index] = 0.0
	end

	sync_threads()

	#Lower Boundary
	if 3 <= k + HALO_WIDTH <= TILE_DIM1 + 2*HALO_WIDTH && l <= TILE_DIM2 && i == Nx && j <= Ny
		# @inbounds y_out[global_index] = (tile[k+HALO_WIDTH-2,l] - 2*tile[k+HALO_WIDTH-1,l] + tile[k+HALO_WIDTH,l]) / h^2
		y_out[global_index] = 1.0
    end

    sync_threads()

	nothing
end

function BySy_GPU_shared(y_in, y_out, Nx, Ny, h, ::Val{TILE_DIM1}, ::Val{TILE_DIM2}) where {TILE_DIM1, TILE_DIM2}
    tidx = threadIdx().x
    tidy = threadIdx().y

	i = (blockIdx().x - 1) * TILE_DIM1 + tidx
	j = (blockIdx().y - 1) * TILE_DIM2 + tidy

	global_index = i + (j-1)*Nx

	HALO_WIDTH = 2
	tile = @cuStaticSharedMem(eltype(y_in),(TILE_DIM1+2*HALO_WIDTH,TILE_DIM2))

	k = tidx
	l = tidy

    # Writing pencil-shaped shared memory

    # for tile itself
	# if k <= TILE_DIM1 && l <= TILE_DIM2 && global_index <= Nx*Ny
	if k <= TILE_DIM1 && l <= TILE_DIM2 && i <= Ny && j <= Nx
		# @inbounds tile[k+HALO_WIDTH,l] = y_in[global_index]
		tile[k+HALO_WIDTH,l] = y_in[global_index]
	end

	sync_threads()

	# For upper halo
	# if k <= HALO_WIDTH && l <= TILE_DIM2 && HALO_WIDTH + 1 <= global_index <= Nx*Ny + HALO_WIDTH
	if k <= HALO_WIDTH && l <= TILE_DIM2 && HALO_WIDTH + 1 <= i <= Ny + HALO_WIDTH && j <= Nx
		# @inbounds tile[k,l] = y_in[global_index - HALO_WIDTH]
		tile[k,l] = y_in[global_index - HALO_WIDTH]
	end

	sync_threads()

	# For lower halo
	# if k >= TILE_DIM1 - HALO_WIDTH && l <= TILE_DIM2 && HALO_WIDTH + 1 <= global_index <= Nx*Ny - HALO_WIDTH
	if  TILE_DIM1 - HALO_WIDTH <= k <= TILE_DIM1 && l <= TILE_DIM2 && i <= Ny - HALO_WIDTH && j <= Nx
		# @inbounds tile[k+2*HALO_WIDTH,l] = y_in[global_index + HALO_WIDTH]
		tile[k+2*HALO_WIDTH,l] = y_in[global_index + HALO_WIDTH]
	end

    sync_threads()

    # Finite Difference Operations starts

    #Upper Boundary
	if k + HALO_WIDTH <= TILE_DIM1 + 2*HALO_WIDTH && l <= TILE_DIM2 && i == 1 && j <= Ny
		# @inbounds y_out[global_index] = (tile[k+HALO_WIDTH,l] - 2*tile[k+HALO_WIDTH+1,l] + tile[k+HALO_WIDTH+2,l]) / h^2
		y_out[global_index] = (1.5*tile[k+HALO_WIDTH,l] - 2*tile[k+HALO_WIDTH+1,l] + 0.5*tile[k+HALO_WIDTH+2,l]) / h
	end

	sync_threads()

	#Center
	if k + HALO_WIDTH <= TILE_DIM1 + 2*HALO_WIDTH && l <= TILE_DIM2 && 2 <= i <= Nx-1 && j <= Ny
		# @inbounds y_out[global_index] = (tile[k+HALO_WIDTH-1,l] - 2*tile[k+HALO_WIDTH,l] + tile[k+HALO_WIDTH+1,l]) / h^2
		y_out[global_index] = 0.0
	end

	sync_threads()

	#Lower Boundary
	if 3 <= k + HALO_WIDTH <= TILE_DIM1 + 2*HALO_WIDTH && l <= TILE_DIM2 && i == Nx && j <= Ny
		# @inbounds y_out[global_index] = (tile[k+HALO_WIDTH-2,l] - 2*tile[k+HALO_WIDTH-1,l] + tile[k+HALO_WIDTH,l]) / h^2
		y_out[global_index] = (0.5 * tile[k+HALO_WIDTH-2,l] - 2*tile[k+HALO_WIDTH-1,l] + 1.5*tile[k+HALO_WIDTH,l]) / h
    end

    sync_threads()

    nothing

end

function BxSx_tran_GPU_shared(y_in, y_out, Nx, Ny, h, ::Val{TILE_DIM1}, ::Val{TILE_DIM2}) where {TILE_DIM1, TILE_DIM2}
	tidx = threadIdx().x
    tidy = threadIdx().y

    # for global memory indexing
    i = (blockIdx().x - 1) * TILE_DIM1 + tidx
    j = (blockIdx().y - 1) * TILE_DIM2 + tidy

    global_index = i + (j - 1) * Ny

    HALO_WIDTH = 2 # For second order derivative

    tile = @cuStaticSharedMem(eltype(y_in), (TILE_DIM1, TILE_DIM2 + 2 * HALO_WIDTH))

    # for tile indexing
    k = tidx
    l = tidy

	# Writing pencil-shaped shared memory

	# for tile itself
	# if k <= TILE_DIM1 && l <= TILE_DIM2 && global_index <= Nx*Ny
	if k <= TILE_DIM1 && l <= TILE_DIM2 && i <= Ny && j <= Nx
		# @inbounds tile[k,l+HALO_WIDTH] = y_in[global_index]
		 tile[k,l+HALO_WIDTH] = y_in[global_index]
    end

    sync_threads()

	# for left halo
	# if k <= TILE_DIM1 && l <= HALO_WIDTH && HALO_WIDTH*Ny+1 <= global_index <= (Nx+HALO_WIDTH)*Ny
	if k <= TILE_DIM1 && l <= HALO_WIDTH && i <= Ny && HALO_WIDTH+1 <= j <= HALO_WIDTH + Nx
		# @inbounds tile[k,l] = y_in[global_index - HALO_WIDTH*Ny]
		tile[k,l] = y_in[global_index - HALO_WIDTH*Ny]
	end

	sync_threads()

	# for right halo
	# if k <= TILE_DIM1 && l >= TILE_DIM2 - HALO_WIDTH && HALO_WIDTH*Ny+1 <= global_index <= (Nx-HALO_WIDTH)*Ny
	if k <= TILE_DIM1 && TILE_DIM2 - HALO_WIDTH <= l <= TILE_DIM2 && i <= Ny && j <= Nx - HALO_WIDTH
		# @inbounds tile[k,l+2*HALO_WIDTH] = y_in[global_index + HALO_WIDTH*Ny]
		tile[k,l+2*HALO_WIDTH] = y_in[global_index + HALO_WIDTH*Ny]
	end

    sync_threads()

	# Finite difference operation starts here

	# Center
	if k <= TILE_DIM1 && l + HALO_WIDTH <= TILE_DIM2 + 2*HALO_WIDTH - 1 && i <= Ny && 4 <= j <= Nx-3
		# @inbounds y_out[global_index] = (tile[k,l + HALO_WIDTH-1] - 2*tile[k, l + HALO_WIDTH] + tile[k,l + HALO_WIDTH + 1]) / h^2
		y_out[global_index] = 0.0
	end

	sync_threads()

	# Left Boundary
	if k <= TILE_DIM1 && l + HALO_WIDTH <= TILE_DIM2 + 2*HALO_WIDTH -2 && i <= Ny && j == 1
		# @inbounds y_out[global_index] = (tile[k,l + HALO_WIDTH] - 2*tile[k,l + HALO_WIDTH+1] + tile[k,l + HALO_WIDTH+2]) / h^2
		y_out[global_index] = (1.5 * tile[k,l+HALO_WIDTH]) / h
		y_out[global_index + Ny] = (-2.0 * tile[k,l+HALO_WIDTH])/h
		y_out[global_index + 2*Ny] = (0.5 * tile[k,l+HALO_WIDTH])/h
	end
	sync_threads()

	# if k <= TILE_DIM1 && l + HALO_WIDTH <= TILE_DIM2 + 2*HALO_WIDTH -2 && i <= Ny && j == 2
	# 	# @inbounds y_out[global_index] = (tile[k,l + HALO_WIDTH] - 2*tile[k,l + HALO_WIDTH+1] + tile[k,l + HALO_WIDTH+2]) / h^2
	# 	y_out[global_index] = (-2.0 * tile[k,l+HALO_WIDTH]) / h
	# end

	# if k <= TILE_DIM1 && l + HALO_WIDTH <= TILE_DIM2 + 2*HALO_WIDTH -2 && i <= Ny && j == 3
	# 	# @inbounds y_out[global_index] = (tile[k,l + HALO_WIDTH] - 2*tile[k,l + HALO_WIDTH+1] + tile[k,l + HALO_WIDTH+2]) / h^2
	# 	y_out[global_index] = (0.5 * tile[k,l+HALO_WIDTH]) / h
	# end

	# Right Boundary
	if k <= TILE_DIM1 && 3 <= l + HALO_WIDTH <= TILE_DIM2 + 2*HALO_WIDTH && i <= Ny && j == Nx
		# @inbounds y_out[global_index] = (tile[k,l + HALO_WIDTH-2] - 2*tile[k,l + HALO_WIDTH - 1] + tile[k,l + HALO_WIDTH]) / h^2
		y_out[global_index] = (1.5 * tile[k,l+HALO_WIDTH]) / h
		y_out[global_index - Ny] = (-2.0 * tile[k, l+HALO_WIDTH])/h
		y_out[global_index - 2*Ny] = (0.5 * tile[k, l+HALO_WIDTH])/h
	end
	sync_threads()

	# if k <= TILE_DIM1 && 3 <= l + HALO_WIDTH <= TILE_DIM2 + 2*HALO_WIDTH && i <= Ny && j == Nx - 1
	# 	# @inbounds y_out[global_index] = (tile[k,l + HALO_WIDTH-2] - 2*tile[k,l + HALO_WIDTH - 1] + tile[k,l + HALO_WIDTH]) / h^2
	# 	y_out[global_index] = (-2.0 * tile[k,l+HALO_WIDTH]) / h
	# end

	# if k <= TILE_DIM1 && 3 <= l + HALO_WIDTH <= TILE_DIM2 + 2*HALO_WIDTH && i <= Ny && j == Nx - 2
	# 	# @inbounds y_out[global_index] = (tile[k,l + HALO_WIDTH-2] - 2*tile[k,l + HALO_WIDTH - 1] + tile[k,l + HALO_WIDTH]) / h^2
	# 	y_out[global_index] = (0.5 * tile[k,l+HALO_WIDTH]) / h
	# end

    nothing
end

function BySy_tran_GPU_shared(y_in, y_out, Nx, Ny, h, ::Val{TILE_DIM1}, ::Val{TILE_DIM2}) where {TILE_DIM1, TILE_DIM2}
    tidx = threadIdx().x
    tidy = threadIdx().y

	i = (blockIdx().x - 1) * TILE_DIM1 + tidx
	j = (blockIdx().y - 1) * TILE_DIM2 + tidy

	global_index = i + (j-1)*Nx

	HALO_WIDTH = 2
	tile = @cuStaticSharedMem(eltype(y_in),(TILE_DIM1+2*HALO_WIDTH,TILE_DIM2))

	k = tidx
	l = tidy

    # Writing pencil-shaped shared memory

    # for tile itself
	# if k <= TILE_DIM1 && l <= TILE_DIM2 && global_index <= Nx*Ny
	if k <= TILE_DIM1 && l <= TILE_DIM2 && i <= Ny && j <= Nx
		# @inbounds tile[k+HALO_WIDTH,l] = y_in[global_index]
		tile[k+HALO_WIDTH,l] = y_in[global_index]
	end

	sync_threads()

	# For upper halo
	# if k <= HALO_WIDTH && l <= TILE_DIM2 && HALO_WIDTH + 1 <= global_index <= Nx*Ny + HALO_WIDTH
	if k <= HALO_WIDTH && l <= TILE_DIM2 && HALO_WIDTH + 1 <= i <= Ny && j <= Nx
		# @inbounds tile[k,l] = y_in[global_index - HALO_WIDTH]
		tile[k,l] = y_in[global_index - HALO_WIDTH]
	end

	sync_threads()

	# For lower halo
	# if k >= TILE_DIM1 - HALO_WIDTH && l <= TILE_DIM2 && HALO_WIDTH + 1 <= global_index <= Nx*Ny - HALO_WIDTH
	if  TILE_DIM1 - HALO_WIDTH <= k <= TILE_DIM1 && l <= TILE_DIM2 && i <= Ny - HALO_WIDTH && j <= Nx
		# @inbounds tile[k+2*HALO_WIDTH,l] = y_in[global_index + HALO_WIDTH]
		tile[k+2*HALO_WIDTH,l] = y_in[global_index + HALO_WIDTH]
	end

    sync_threads()

    # Finite Difference Operations starts

    #Upper Boundary
	if k + HALO_WIDTH <= TILE_DIM1 + 2*HALO_WIDTH && l <= TILE_DIM2 && i == 1 && j <= Ny
		# @inbounds y_out[global_index] = (tile[k+HALO_WIDTH,l] - 2*tile[k+HALO_WIDTH+1,l] + tile[k+HALO_WIDTH+2,l]) / h^2
		y_out[global_index] = (1.5*tile[k+HALO_WIDTH,l]) / h
		y_out[global_index + 1] = (-2.0*tile[k+HALO_WIDTH,l]) / h
		y_out[global_index + 2] = (0.5*tile[k+HALO_WIDTH,l])/h
	end

	sync_threads()

	#Center
	if k + HALO_WIDTH <= TILE_DIM1 + 2*HALO_WIDTH && l <= TILE_DIM2 && 4 <= i <= Nx-3 && j <= Ny
		# @inbounds y_out[global_index] = (tile[k+HALO_WIDTH-1,l] - 2*tile[k+HALO_WIDTH,l] + tile[k+HALO_WIDTH+1,l]) / h^2
		y_out[global_index] = 0.0
	end

	sync_threads()

	#Lower Boundary
	if 3 <= k + HALO_WIDTH <= TILE_DIM1 + 2*HALO_WIDTH && l <= TILE_DIM2 && i == Nx && j <= Ny
		# @inbounds y_out[global_index] = (tile[k+HALO_WIDTH-2,l] - 2*tile[k+HALO_WIDTH-1,l] + tile[k+HALO_WIDTH,l]) / h^2
		y_out[global_index] = (1.5*tile[k+HALO_WIDTH,l]) / h
		y_out[global_index-1] = (-2.0 * tile[k+HALO_WIDTH,l]) / h
		y_out[global_index-2] = (0.5 * tile[k+HALO_WIDTH,l])/h
    end

    sync_threads()

    nothing

end

# function FACEtoVOL_GPU_shared(y_out, u_face, face, Nx, Ny, ::Val{TILE_DIM}) where {TILE_DIM} #, ::Val{TILE_DIM1}, ::Val{TILE_DIM2}) where {TILE_DIM1, TILE_DIM2}
# 	# tidx = threadIdx().x
# 	# i = (blockIdx().x - 1)*TILE_DIM + tidx
# 	i = (blockIdx().x - 1) * TILE_DIM + threadIdx().x
# 	N = Nx*Ny

# 	if i <= N
# 		y_out[i] = 0.0
# 	end

# 	sync_threads()

# 	if face == 1 && i <= N && mod(i,Ny) == 1
# 		y_out[i] = u_face
# 	end

# 	sync_threads()

# 	if face == 2 && i <= N && mod(i,Ny) == 0
# 		y_out[i] = u_face
# 	end

# 	sync_threads()

# 	if face == 3 && i <= Ny
# 		y_out[i] = u_face
# 	end

# 	sync_threads()

# 	if face == 4 && N-Ny+1 <= i <= N
# 		y_out[i] = u_face
# 	end
# 	sync_threads()

# 	nothing
# end

# function VOLtoFACE_GPU_shared(y_in, y_out,  face, Nx, Ny, ::Val{TILE_DIM}) where {TILE_DIM} #, ::Val{TILE_DIM1}, ::Val{TILE_DIM2}) where {TILE_DIM1, TILE_DIM2}
# 	# tidx = threadIdx().x
# 	# i = (blockIdx().x - 1)*TILE_DIM + tidx
# 	i = (blockIdx().x - 1) * TILE_DIM + threadIdx().x
# 	N = Nx*Ny

# 	if i <= N
# 		y_out[i] = 0.0
# 	end

# 	sync_threads()

# 	if face == 1 && i <= N && mod(i,Ny) == 1
# 		y_out[i] = u_face
# 	end

# 	sync_threads()

# 	if face == 2 && i <= N && mod(i,Ny) == 0
# 		y_out[i] = u_face
# 	end

# 	sync_threads()

# 	if face == 3 && i <= Ny
# 		y_out[i] = u_face
# 	end

# 	sync_threads()

# 	if face == 4 && N-Ny+1 <= i <= N
# 		y_out[i] = u_face
# 	end
# 	sync_threads()

# 	nothing
# end

# tester_function : This gives evaluation on CPU, GPU, GPU_shared
function tester_function(f,Nx,TILE_DIM_1,TILE_DIM_2,TILE_DIM)
    Ny = Nx
	@show f
	@eval gpu_function_naive = $(Symbol(f,"_GPU_naive"))
	@eval gpu_function_shared = $(Symbol(f,"_GPU_shared"))
	@show gpu_function_naive
    @show gpu_function_shared
    h = 1/Nx
	# TILE_DIM_1 = 16
	# TILE_DIM_2 = 2

	u = randn(Nx*Ny)
	y_in = CuArray(u)
	y_out = similar(y_in)
	y_out2 = similar(y_in)

	griddim = (div(Nx,TILE_DIM_1) + 1, div(Ny,TILE_DIM_2) + 1)
	blockdim = (TILE_DIM_1,TILE_DIM_2)

	# TILE_DIM = 32
	THREAD_NUM = TILE_DIM
    BLOCK_NUM = div(Nx * Ny,TILE_DIM)+1

	y = f(u,Nx,Ny,h)
	@cuda threads=THREAD_NUM blocks=BLOCK_NUM gpu_function_naive(y_in, y_out, Nx, Ny, h, Val(TILE_DIM))
    @cuda threads=blockdim blocks=griddim gpu_function_shared(y_in, y_out2, Nx, Ny, h, Val(TILE_DIM_1), Val(TILE_DIM_2))
	@show y ≈ Array(y_out)
	@show y ≈ Array(y_out2)
    #@show y - Array(y_out2)

	rep_times = 10

	t_y = time_ns()
	for i in 1:rep_times
		y = f(u,Nx,Ny,h)
	end
	t_y_end = time_ns()
	t1 = t_y_end - t_y

	memsize = length(u) * sizeof(eltype(u))
	@show Float64(t1)
	@printf("CPU Through-put %20.2f\n", 2 * memsize * rep_times / t1)

	println()

	t_y_out = time_ns()
	for i in 1:rep_times
		@cuda threads=THREAD_NUM blocks=BLOCK_NUM gpu_function_naive(y_in, y_out, Nx, Ny, h, Val(TILE_DIM))
		# @cuda threads=THREAD_NUM blocks=BLOCK_NUM D2y_GPU_v2(y_in, y_out, Nx, Ny, h, Val(TILE_DIM))
	end
	synchronize()
	t_y_out_end = time_ns()
	t2 = t_y_out_end - t_y_out
	@show Float64(t2)
	@show Float64(t1)/Float64(t2)
	@printf("GPU Through-put (naive) %20.2f\n", 2 * memsize * rep_times / t2)

	println()

	t_y_out2 = time_ns()
	for i in 1:rep_times
		@cuda threads=blockdim blocks=griddim gpu_function_shared(y_in, y_out2, Nx, Ny, h, Val(TILE_DIM_1), Val(TILE_DIM_2))
	end
	synchronize()
	t_y_out2_end = time_ns()
	t3 = t_y_out2_end - t_y_out2

	@show Float64(t3)
	@show Float64(t1)/Float64(t3)
	@printf("GPU Through-put (shared memory)%20.2f\n", 2 * memsize * rep_times / t3)

end

# tester_function_v2 : This gives evaluation on CPU, GPU_shared with error output
function tester_function_v2(f,Nx,TILE_DIM_1,TILE_DIM_2)
    Ny = Nx
	@show f
	# @eval gpu_function = $(Symbol(f,"_GPU"))
	@eval gpu_function_shared = $(Symbol(f,"_GPU_shared"))
	# @show gpu_function
    @show gpu_function_shared
    h = 1/Nx
	# TILE_DIM_1 = 16
	# TILE_DIM_2 = 2

	u = randn(Nx*Ny)
	y_in = CuArray(u)
	# y_out = similar(y_in)
	y_out2 = similar(y_in)

	griddim = (div(Nx,TILE_DIM_1) + 1, div(Ny,TILE_DIM_2) + 1)
	blockdim = (TILE_DIM_1,TILE_DIM_2)

	# TILE_DIM = 32
	# THREAD_NUM = TILE_DIM
    # BLOCK_NUM = div(Nx * Ny,TILE_DIM)+1

	y = f(u,Nx,Ny,h)
	# @cuda threads=THREAD_NUM blocks=BLOCK_NUM gpu_function(y_in, y_out, Nx, Ny, h, Val(TILE_DIM))
    @cuda threads=blockdim blocks=griddim gpu_function_shared(y_in, y_out2, Nx, Ny, h, Val(TILE_DIM_1), Val(TILE_DIM_2))
	# @show y ≈ Array(y_out)
	@show y ≈ Array(y_out2)

	@show u
	println()
	@show y
	println()
	@show Array(y_out2)
	println()
	@show y - Array(y_out2)

	@show y ≈ Array(y_out2)

	rep_times = 10

	t_y = time_ns()
	for i in 1:rep_times
		y = f(u,Nx,Ny,h)
	end
	t_y_end = time_ns()
	t1 = t_y_end - t_y

	memsize = length(u) * sizeof(eltype(u))
	@show Float64(t1)
	@printf("CPU Through-put %20.2f\n", 2 * memsize * rep_times / t1)

	println()

	# t_y_out = time_ns()
	# for i in 1:rep_times
	# 	@cuda threads=THREAD_NUM blocks=BLOCK_NUM gpu_function(y_in, y_out, Nx, Ny, h, Val(TILE_DIM))
	# 	# @cuda threads=THREAD_NUM blocks=BLOCK_NUM D2y_GPU_v2(y_in, y_out, Nx, Ny, h, Val(TILE_DIM))
	# end
	# synchronize()
	# t_y_out_end = time_ns()
	# t2 = t_y_out_end - t_y_out
	# @show Float64(t2)
	# @show Float64(t1)/Float64(t2)
	# @printf("GPU Through-put (naive) %20.2f\n", 2 * memsize * rep_times / t2)

	# println()

	t_y_out2 = time_ns()
	for i in 1:rep_times
		@cuda threads=blockdim blocks=griddim gpu_function_shared(y_in, y_out2, Nx, Ny, h, Val(TILE_DIM_1), Val(TILE_DIM_2))
	end
	synchronize()
	t_y_out2_end = time_ns()
	t3 = t_y_out2_end - t_y_out2

	@show Float64(t3)
	@show Float64(t1)/Float64(t3)
	@printf("GPU Through-put (shared memory)%20.2f\n", 2 * memsize * rep_times / t3)

end

# tester_function_v3 : This gives evaluation on CPU, GPU_shared with error output turned off
function tester_function_v3(f,Nx,TILE_DIM_1,TILE_DIM_2)
    Ny = Nx
	@show f
	# @eval gpu_function = $(Symbol(f,"_GPU"))
	@eval gpu_function_shared = $(Symbol(f,"_GPU_shared"))
	# @show gpu_function
    @show gpu_function_shared
    h = 1/Nx
	# TILE_DIM_1 = 16
	# TILE_DIM_2 = 2

	u = randn(Nx*Ny)
	y_in = CuArray(u)
	# y_out = similar(y_in)
	y_out2 = similar(y_in)

	griddim = (div(Nx,TILE_DIM_1) + 1, div(Ny,TILE_DIM_2) + 1)
	blockdim = (TILE_DIM_1,TILE_DIM_2)

	@show blockdim
	@show griddim

	# TILE_DIM = 32
	# THREAD_NUM = TILE_DIM
    # BLOCK_NUM = div(Nx * Ny,TILE_DIM)+1

	y = f(u,Nx,Ny,h)
	# @cuda threads=THREAD_NUM blocks=BLOCK_NUM gpu_function(y_in, y_out, Nx, Ny, h, Val(TILE_DIM))
    @cuda threads=blockdim blocks=griddim gpu_function_shared(y_in, y_out2, Nx, Ny, h, Val(TILE_DIM_1), Val(TILE_DIM_2))
	# @show y ≈ Array(y_out)
	@show y ≈ Array(y_out2)

	# @show y - Array(y_out2)

	rep_times = 10

	t_y = time_ns()
	for i in 1:rep_times
		y = f(u,Nx,Ny,h)
	end
	t_y_end = time_ns()
	t1 = t_y_end - t_y

	memsize = length(u) * sizeof(eltype(u))
	@show Float64(t1)
	@printf("CPU Through-put %20.2f\n", 2 * memsize * rep_times / t1)

	println()

	# t_y_out = time_ns()
	# for i in 1:rep_times
	# 	@cuda threads=THREAD_NUM blocks=BLOCK_NUM gpu_function(y_in, y_out, Nx, Ny, h, Val(TILE_DIM))
	# 	# @cuda threads=THREAD_NUM blocks=BLOCK_NUM D2y_GPU_v2(y_in, y_out, Nx, Ny, h, Val(TILE_DIM))
	# end
	# synchronize()
	# t_y_out_end = time_ns()
	# t2 = t_y_out_end - t_y_out
	# @show Float64(t2)
	# @show Float64(t1)/Float64(t2)
	# @printf("GPU Through-put (naive) %20.2f\n", 2 * memsize * rep_times / t2)

	# println()

	t_y_out2 = time_ns()
	for i in 1:rep_times
		@cuda threads=blockdim blocks=griddim gpu_function_shared(y_in, y_out2, Nx, Ny, h, Val(TILE_DIM_1), Val(TILE_DIM_2))
	end
	synchronize()
	t_y_out2_end = time_ns()
	t3 = t_y_out2_end - t_y_out2

	@show Float64(t3)
	@show Float64(t1)/Float64(t3)
	@printf("GPU Through-put (shared memory)%20.2f\n", 2 * memsize * rep_times / t3)

end

function tester_function_FV(f,u_face,face,Nx,TILE_DIM)
	Ny = Nx
	N = Ny*Nx
	@show f
	# @eval gpu_function = $(Symbol(f,"_GPU"))
	@eval gpu_function_shared = $(Symbol(f,"_GPU_shared"))
	# @show gpu_function
    @show gpu_function_shared
	y = zeros(N)
	y_out = CuArray(y)

	# u_face = 2
	# face = 2

	THREAD_NUM = TILE_DIM
	BLOCK_NUM = (N + TILE_DIM-1, TILE_DIM)

	y = f(u_face, face, Nx, Ny)

	@cuda threads=THREAD_NUM blocks=BLOCK_NUM gpu_function_shared(y_out,u_face,face,Nx,Ny,Val(TILE_DIM))

	@show y ≈ Array(y_out)
	# @show y
	# @show Array(y_out)
	# @show y - Array(y_out)

	rep_times = 10
	memsize = length(y) * sizeof(eltype(y))

	t_y = time_ns()
	for i in 1:rep_times
		y .= f(u_face, face, Nx, Ny)
	end
	t_y_end = time_ns()
	t1 = t_y_end - t_y
	@show Float64(t1)
	@printf("CPU Through-put %20.2f\n", 2 * memsize * rep_times / t1)

	t_y_out = time_ns()
	for i in rep_times
		@cuda threads=THREAD_NUM blocks=BLOCK_NUM gpu_function_shared(y_out,u_face,face,Nx,Ny,Val(TILE_DIM))
	end
	synchronize()
	t_y_out_end = time_ns()
	t2 = t_y_out_end - t_y_out
	@show Float64(t2)
	@printf("GPU Through-put (naive) %20.2f\n", 2 * memsize * rep_times / t2)

end

# tester_function(D2x, 1000, 16, 4, 32)
# tester_function(D2y, 1000, 4, 16, 32)

# tester_function_v3(Dx, 10, 16, 4)

# tester_function_v3(Dx, 10, 16, 4)
# tester_function_v3(Dy, 10, 16, 4)

# tester_function_FV(FACEtoVOL,1,1,10,32)

# Nx = 10
# Ny = Nx
# h = 1/Nx
# TILE_DIM_1 = 4
# TILE_DIM_2 = 16

# u = randn(Nx*Ny);
# y_in = CuArray(u);
# y_out = similar(y_in);
# y_out2 = similar(y_in);

# y = D2y(u,Nx,Ny,h)

# griddim = (div(Nx,TILE_DIM_1) + 1, div(Ny,TILE_DIM_2) + 1)
# blockdim = (TILE_DIM_1,TILE_DIM_2)

# # TILE_DIM = 32
# # THREAD_NUM = 32
# # BLOCK_NUM = div(Nx * Ny,TILE_DIM) + 1

# @cuda threads=blockdim blocks=griddim D2y_GPU_v7(y_in, y_out, Nx, Ny, h, Val(TILE_DIM_1), Val(TILE_DIM_2))
# Array(y_out)

# Array(y_out) ≈ y

# D2y(u,Nx,Ny,h)


#--------------------------------------------------------------
# MultilevelTT package
# example code for https://arxiv.org/abs/1802.09062
#--------------------------------------------------------------
# Auxiliary routines for evaluating representation condition numbers
# (or respective bounds for matrix representations)
module TTCondition

using TT

export repampvals, repamp, repampbound, repcondbound

function repampvals(t::Tensor, l::Int64)
  if l == 1
    T2 = unfold(t[2:end])
    return 1., norm(t[1][:]),
      maximum(svdvals(reshape(T2, (size(T2,1), size(T2,2)))))
  elseif l == length(t)
    T1 = unfold(t[1:(l-1)])
    return maximum(svdvals(reshape(T1, (size(T1,2), size(T1,3))))),
      norm(t[l][:]), 1.
  else
    T1 = unfold(t[1:(l-1)])
    T2 = unfold(t[(l+1):end])
    return maximum(svdvals(reshape(T1, (size(T1,2), size(T1,3))))),
      norm(t[l][:]),
      maximum(svdvals(reshape(T2, (size(T2,1), size(T2,2)))))
  end
end

# computes the exact value of the sum of ramp_l(t) over l
function repamp(t::Tensor)
  s = 0.;
  for l = 1:length(t)
    a, b, c = repampvals(t, l)
    s += a*b*c
  end
  return s
end

# computes the exact value of ramp_l(t)
function repamp(t::Tensor, l::Integer)
  a, b, c = repampvals(t, l)
  return a*b*c
end

function leftUnfold(T::TensorMatrix, l::Int64, i::Int64)
  s = [sizes(T)[i][j] for j = 1:2, i in eachindex(sizes(T))]
  s = s[:,1:(l-1)]
  t = Tensor(T)[1:(l-1)]
  t[l-1] = reshape(t[l-1][:,:,i], (size(t[l-1],1),size(t[l-1],2),1))
  M = unfold(t)[1,:,1]
  L = l-1
  dim = zeros(Int64,2L)
  dim[1:L] = (2L-1):-2:1
  dim[(L+1):(2L)] = (2L):-2:2
  return reshape(dimpermute(reshape(M, (s[:]...)), dim),
                (prod(s[1,:]), prod(s[2,:])))
end

function rightUnfold(T::TensorMatrix, l::Int64, i::Int64)
  s = [sizes(T)[i][j] for j = 1:2, i in eachindex(sizes(T))]
  s = s[:,(l+1):length(T)]
  t = Tensor(T)[(l+1):length(T)]
  t[1] = reshape(t[1][i,:,:], (1,size(t[1],2),size(t[1],3)))
  M = unfold(t)[1,:,1]
  L = length(T) - l
  dim = zeros(Int64,2L);
  dim[1:L] = (2L-1):-2:1;
  dim[(L+1):(2L)] = (2L):-2:2;
  return reshape(dimpermute(reshape(M, (s[:]...)), dim),
            (prod(s[1,:]), prod(s[2,:])))
end

function repampvals(T::TensorMatrix, l::Int64)
  cnorms = [norm(T[l][i,:,:,j]) for i=1:size(T[l],1), j=1:size(T[l],4)]
  if l == 1
    rnorms = [norm(rightUnfold(T, l, i)) for i = 1:size(T[l],4)]
    return [1.], cnorms, rnorms
  elseif l == length(T)
    lnorms = [norm(leftUnfold(T, l, i)) for i = 1:size(T[l],1) ]
    return lnorms, cnorms, [1.]
  else
    lnorms = [norm(leftUnfold(T, l, i)) for i = 1:size(T[l],1) ]
    rnorms = [norm(rightUnfold(T, l, i)) for i = 1:size(T[l],4) ]
    return lnorms, cnorms, rnorms
  end
end

function repampbound(T::TensorMatrix, l::Int64)
  a, b, c = repampvals(T, l)
  return norm(a)*vecnorm(b)*norm(c)
end

function repampbound(T::TensorMatrix)
  return sum(repampbound(T,l) for l = 1:length(T))
end

function repcondbound(T::TensorMatrix)
  return repampbound(T) / minimum(svdvals(unfold(T)))
end

end


using ExactOptimalTransport

using LinearAlgebra
using Random
using Test
using Distributions

Random.seed!(100)

@testset "utils.jl" begin
    @testset "FiniteDiscreteMeasure" begin
        @testset "Univariate Finite Discrete Measure" begin
            n = 100
            m = 80
            μsupp = rand(n)
            νsupp = rand(m)
            μprobs = normalize!(rand(n), 1)

            μ = ExactOptimalTransport.discretemeasure(μsupp, μprobs)
            ν = ExactOptimalTransport.discretemeasure(νsupp)
            # check if it vectors are indeed probabilities
            @test isprobvec(μ.p)
            @test isprobvec(probs(μ))
            @test ν.p == ones(m) ./ m
            @test probs(ν) == ones(m) ./ m

            # check if it assigns to DiscreteNonParametric when Vector/Matrix is 1D
            @test μ isa DiscreteNonParametric
            @test ν isa DiscreteNonParametric

            # check if support is correctly assinged
            @test sort(μsupp) == μ.support
            @test sort(μsupp) == support(μ)
            @test sort(vec(νsupp)) == ν.support
            @test sort(vec(νsupp)) == support(ν)
        end
        @testset "Multivariate Finite Discrete Measure" begin
            n = 10
            m = 3
            μsupp = [rand(m) for i in 1:n]
            νsupp = [rand(m) for i in 1:n]
            μprobs = normalize!(rand(n), 1)
            μ = ExactOptimalTransport.discretemeasure(μsupp, μprobs)
            ν = ExactOptimalTransport.discretemeasure(νsupp)
            # check if it vectors are indeed probabilities
            @test isprobvec(μ.p)
            @test isprobvec(probs(μ))
            @test ν.p == ones(n) ./ n
            @test probs(ν) == ones(n) ./ n

            # check if support is correctly assinged
            @test μsupp == μ.support
            @test μsupp == support(μ)
            @test νsupp == ν.support
            @test νsupp == support(ν)
        end
    end
end


using NS2D
using Plots
using FFTW
using DifferentialEquations

function plot_vorticity(ω, p)

    u, v = compute_velocity(ω, p)

    ω = real(ifft(ω))

    x, y = mesh(p)

    fig = heatmap(
        x, x, ω,
        aspect_ratio=:equal,
        c=cgrad(:RdBu),
        xlims=(-p.Lx/2, p.Lx/2),
        ylims=(-p.Ly/2, p.Ly/2),
        clims=(-maximum(abs.(ω)), maximum(abs.(ω)))
    )

    return fig
end

function plot_velocity(ω, p)

    #
    u, v = compute_velocity(ω, p)

    #
    velocity_magnitude = .√(u.^2 + v.^2)

    x, y = mesh(p)

    fig = heatmap(
        x, y, velocity_magnitude,
        aspect_ratio=:equal,
        c=cgrad(:viridis),
        xlims=(-p.Lx/2, p.Lx/2),
        ylims=(-p.Ly/2, p.Ly/2),
        clims=(0, maximum(velocity_magnitude)),
    )

    return fig
end

function main(; T=1)

    # --> Simulation parameters (default).
    p = SimParams()

    # --> Isotropic enstrophy spectrum
    #     (unit energy for all Fourier modes)
    spectrum(x) = one(x)
    ω = isotropic_turbulence(spectrum, p)

    # -->
    prob = UnforcedProblem(p, ω)

    # --> Callback.
    data = SavedValues(Float64, Float64)
    cb = SavingCallback((u, t, integrator) -> maximum(abs.(u)), data)

    # --> Keyword arguments for DifferentialEquations.jl
    kwargs = Dict(
        :saveat => 0:0.1:T,
        :callback => cb
    )

    # --> Run the simulation.
    sol = simulate(prob, T ; kwargs...)

    # --> Plot the final condition.
    ω = sol[end]
    fig1 = plot_vorticity(ω, p)
    title!("Vorticity")

    fig2 = plot_velocity(ω, p)
    title!("Kinetic energy")

    fig = plot(fig1, fig2, layout=(1, 2), size=(800, 400))
    display(fig)

    return sol, data
end


"""
Vibration analysis of double-cell box structure

Reference values: 20.280 25.43 28.79 30.62 [Hz]
From: Fan, Luah, Free vibration analysis of arbitrary thin shell structures by using spline finite element, Journal Sound Vib, (179), 763-776, 1995.
"""
module dbcs_vibration_examples

using Arpack
using FinEtools
using FinEtools.MeshExportModule.VTKWrite: vtkwrite
using FinEtoolsDeforLinear
using FinEtoolsFlexStructures.FESetShellT3Module: FESetShellT3
using FinEtoolsFlexStructures.FEMMShellT3FFModule
using FinEtoolsFlexStructures.RotUtilModule: initial_Rfield, update_rotation_field!
using VisualStructures: plot_nodes, plot_midline, render, plot_space_box, plot_midsurface, space_aspectratio, save_to_json

function _execute_dsg_model(formul, n, visualize)
    E = 207*phun("GPa")
    nu = 0.3;
    rho= 7850*phun("KG/M^3");
    L0 = 2.54 * phun("m")
    B0 = 1.27 * phun("m")
    thickness = 12.7*phun("mm");

    # Report
    @info "Mesh: $n elements per side"

    tolerance = B0/n/1000
    X = [0.0 0.0 0.0;
        B0 0.0 0.0;
        B0 B0 0.0;
        B0 2*B0 0.0;
        0.0 2*B0 0.0;
        0.0 B0 0.0;
    ]
    l2fens = FENodeSet(X);
    conn = [1 2; 6 3; 5 4; 2 3; 3 4; 5 6; 6 1]
    l2fes = FESetL2(conn);
    nLayers = 5
    ex = function (x, k)
        x[3] += k*L0/nLayers
        x
    end
    fens,fes = Q4extrudeL2(l2fens, l2fes, nLayers, ex); # Mesh
    fens, fes = Q4toT3(fens, fes)
    for r in 1:n
        fens, fes = T3refine(fens, fes)
    end
    @show count(fens)
    vtkwrite("dbcs.vtu", fens, fes)

    mater = MatDeforElastIso(DeforModelRed3D, rho, E, nu, 0.0)

    sfes = FESetShellT3()
    accepttodelegate(fes, sfes)
    femm = formul.make(IntegDomain(fes, TriRule(1), thickness), mater)
    associate = formul.associategeometry!
    stiffness = formul.stiffness
    mass = formul.mass

    # Construct the requisite fields, geometry and displacement
    # Initialize configuration variables
    geom0 = NodalField(fens.xyz)
    u0 = NodalField(zeros(size(fens.xyz,1), 3))
    Rfield0 = initial_Rfield(fens)
    dchi = NodalField(zeros(size(fens.xyz,1), 6))

    # Apply EBC's
    # simple support
    l1 = selectnode(fens; box = Float64[-Inf Inf -Inf Inf 0.0 0.0], inflate = tolerance)
    for i in [1, 2, 3, 4, 5, 6]
        setebc!(dchi, l1, true, i)
    end

    applyebc!(dchi)
    numberdofs!(dchi);

    # Assemble the system matrix
    associategeometry!(femm, geom0)
    K = stiffness(femm, geom0, u0, Rfield0, dchi);
    M = mass(femm, geom0, dchi);

    # Solve
    OmegaShift = 0.0*2*pi
    neigvs = 4
    d, v, nconv = eigs(K+OmegaShift*M, M; nev=neigvs, which=:SM, explicittransform=:none)
    d[:] = d .- OmegaShift;
    fs = real(sqrt.(complex(d)))/(2*pi)
    @show fs

    # Visualization
    U = v[:, 4]
    scattersysvec!(dchi, (L0/4)/maximum(abs.(U)).*U)
    update_rotation_field!(Rfield0, dchi)
    plots = cat(plot_space_box([[0 0 0]; [L0 L0 L0]]),
        #plot_nodes(fens),
        plot_midsurface(fens, fes; x = geom0.values, u = dchi.values[:, 1:3], R = Rfield0.values);
    dims = 1)
    pl = render(plots)
end

function test_convergence()
    formul = FEMMShellT3FFModule
    @info "FV12 free vibration, formulation=$(formul)"
    for n in [1, 2, 3, 4]
        _execute_dsg_model(formul, n, false)
    end
    return true
end

function allrun()
    println("#####################################################")
    println("# test_convergence ")
    test_convergence()
    return true
end # function allrun

@info "All examples may be executed with "
println("using .$(@__MODULE__); $(@__MODULE__).allrun()")

end # module
nothing


function isgrad{T <: Number}(
    g::Function,
    f::Function,
    x::T;
    tol::Real = @centralrule(x),
)
    return maxdiff(g(x), grad(f, x)) <= tol
end

function isgrad!{T <: Number}(
    g!::Function,
    f::Function,
    x::Vector{T};
    tol::Real = @centralrule(maximum(abs(x))),
)
    n = length(x)
    gr = Array(T, n)
    g!(gr, x)
    return maxdiff(gr, grad(f, x)) <= tol
end

function isgrad{T <: Number}(
    g::Function,
    f::Function,
    x::Vector{T};
    tol::Real =  @centralrule(maximum(abs(x))),
)
    return maxdiff(g(x), grad(f, x)) <= tol
end

function ishess{T <: Number}(
    h::Function,
    f::Function,
    x::T;
    tol::Real = @hessianrule(x),
)
    return maxdiff(h(x), hess(f, x)) <= tol
end

function ishess!{T <: Number}(
    h!::Function,
    f::Function,
    x::Vector{T};
    tol::Real = @hessianrule(maximum(abs(x))),
)
    n = length(x)
    H = Array(T, n, n)
    h!(H, x)
    return maxdiff(H, hess(f, x)) <= tol
end

function ishess{T <: Number}(
    h::Function,
    f::Function,
    x::Vector{T};
    tol::Real = @hessianrule(maximum(abs(x))),
)
    return maxdiff(h(x), hess(f, x)) <= tol
end


module HilbertSchmidtIndependenceCriterion

  using StatsFuns, Distances

  # load Base modules
  using Statistics, LinearAlgebra
  import StatsBase: sample
	# includes
  include("common.jl")
  include("gammaHSIC.jl")

	# function exports
  export gammaHSIC, estimateKernelSize, rbfDotProduct

end # module


module Pyramids

using LinearAlgebra, FFTW, Images, Interpolations, Colors
using DSP # I need this only for conv2

export ImagePyramid, PyramidType, ComplexSteerablePyramid, LaplacianPyramid, GaussianPyramid
export subband, toimage, update_subband, update_subband!, test

"abstract supertype for the variety of pyramids that can be constructed."
abstract type PyramidType end
abstract type SimplePyramid <: PyramidType end

"""Type that indicates a complex steerable pyramid. [1]

[1] http://www.cns.nyu.edu/~eero/steerpyr/"""
struct ComplexSteerablePyramid <: PyramidType
end

"""Type that indicates a Laplacian pyramid. [1]

[1] persci.mit.edu/pub_pdfs/pyramid83.pdf"""
struct LaplacianPyramid <: SimplePyramid
end

"""Type that indicates a Gaussian pyramid. [1]

[1] http://persci.mit.edu/pub_pdfs/RCA84.pdf"""
struct GaussianPyramid <: SimplePyramid
end

"""Type that represents a concrete pyramidal representation of a given input image. Each type of pyramid has its own parameters. The basic construction method is, for example

```pyramid = ImagePyramid(im, ComplexSteerablePyramid(), scale=0.5^0.25)```

See the code for more information on optional arguments."""
struct ImagePyramid
    scale::Real
    num_levels::Int
    num_orientations::Int
    t::PyramidType
    pyramid_bands::Dict

    function ImagePyramid(pyr::ImagePyramid)
        return deepcopy(pyr)
    end

    function ImagePyramid(im::Array, t::ComplexSteerablePyramid; scale=0.5, min_size=15, num_orientations=8, max_levels=23, twidth=1)

        t = ComplexSteerablePyramid()
        scale = scale
        num_orientations = num_orientations

        im_dims = size(im)
        h = im_dims[1]
        w = im_dims[2]

        num_levels = min(ceil.(log2(minimum([h w]))/log2(1/scale) - (log2(min_size)/log2(1/scale))),max_levels);

        pyramid_bands, mtx, harmonics = build_complex_steerable_pyramid(im,
            num_levels, num_levels, order=num_orientations-1,
            twidth=twidth, scale=scale)

        return new(scale,num_levels,num_orientations,t,pyramid_bands)
    end

    function ImagePyramid(im::Array, t::GaussianPyramid; min_size=15, max_levels=23, filter=[0.0625; 0.25; 0.375; 0.25; 0.0625])
        this = new()

        pyramid_bands, num_levels = generate_gaussian_pyramid(im,
                min_size=min_size, max_levels=max_levels, filter=filter)

        scale = 0.5
        pyramid_bands = pyramid_bands
        num_orientations = 1
        num_levels = num_levels

        return new(scale,num_levels,num_orientations,t,pyramid_bands)
    end

    function ImagePyramid(im::Array, t::LaplacianPyramid; min_size=15, max_levels=23, filter=[0.0625; 0.25; 0.375; 0.25; 0.0625])

        pyramid_bands, num_levels = generate_laplacian_pyramid(im,
                min_size=min_size, max_levels=max_levels, filter=filter)

        scale = 0.5
        pyramid_bands = pyramid_bands
        num_orientations = 1
        num_levels = num_levels

        return new(scale,num_levels,num_orientations,t,pyramid_bands)
    end
    # I am not sure this is needed
    # function ImagePyramid(pyramid_bands::Dict, scale, t, num_levels, num_orientations)
    #     return new(scale,num_levels,num_orientations,t,pyramid_bands)
    # end
end

##############################
# Functions

"""
    subband(pyramid::ImagePyramid, level; orientation=())

Returns the sub-band of the `pyramid` at `level` and at `orientation.` If orientation is not provided, `subband` assumes that the pyramid is not oriented.

Level 0 is always the high frequency residual."""
function subband(pyramid::ImagePyramid, level; orientation = ())
    if isempty(orientation)
        return pyramid.pyramid_bands[level]
    else
        return pyramid.pyramid_bands[level][orientation]
    end
end

function update_subband!(pyramid::ImagePyramid, level, new_subband; orientation = ())
    if isempty(orientation)
        pyramid.pyramid_bands[level] = copy(new_subband)
    else
        pyramid.pyramid_bands[level][orientation] = copy(new_subband)
    end

    return pyramid
end

function update_subband(pyramid::ImagePyramid, level, new_subband; orientation = ())
    newpyramid = ImagePyramid(pyramid)
    return update_subband!(newpyramid, level, new_subband, orientation=orientation)
end

"Converts a pyramid to a 2-D array (not of type `Image`!)"
function toimage(pyramid::ImagePyramid)
    if typeof(pyramid.t) <: ComplexSteerablePyramid
        im = reconstruct_complex_steerable_pyramid(pyramid, scale=pyramid.scale)
    elseif typeof(pyramid.t) <: LaplacianPyramid
        im = reconstruct_laplacian_pyramid(pyramid)
    elseif typeof(pyramid.t) <: GaussianPyramid
        im = subband(pyramid, 0)
    else
        error("Unsupported pyramid type $(typeof(pyramid.T))")
    end

    return im
end

##############################
# Private functions for building Gaussian and Laplacian pyramids.

function convolve_reflect(im, filter)
    filter_len = length(filter)
    filter_offset::Int = (filter_len-1)/2

    padded_im = zeros((size(im,1) + filter_len*2), (size(im,1) + filter_len*2))

    padded_im[(filter_len+1):(end-filter_len), (filter_len+1):(end-filter_len)] = im

    padded_im[(filter_len+1):(end-filter_len), 1:filter_len] =
        reverse(padded_im[(filter_len+1):(end-filter_len),
                (filter_len+2):(2*filter_len+1)], dims=2)
    padded_im[(filter_len+1):(end-filter_len), (end-filter_len+1):end] =
            reverse(padded_im[(filter_len+1):(end-filter_len),
                (end-2*filter_len):(end-filter_len-1)], dims=2)
    padded_im[1:filter_len, (filter_len+1):(end-filter_len),] =
        reverse(padded_im[(filter_len+2):(1+2*filter_len),
                (filter_len+1):(end-filter_len)], dims=1)
    padded_im[(end-filter_len+1):end, (filter_len+1):(end-filter_len)] =
            reverse(padded_im[(end-2*filter_len):(end-filter_len-1),
                (filter_len+1):(end-filter_len)], dims=1)

    new_im = DSP.conv2(filter, filter, padded_im)

    new_im = new_im[(1+filter_len+filter_offset):(end-filter_len-filter_offset), (1+filter_len+filter_offset):(end-filter_len-filter_offset)]
    return new_im
end

function reduce(im; filter=[0.0625; 0.25; 0.375; 0.25; 0.0625])
    new_im = convolve_reflect(im, filter)
    return new_im[1:2:end, 1:2:end]
end

function expand(im; filter=[0.0625; 0.25; 0.375; 0.25; 0.0625])
    new_im = zeros((size(im, 1)*2, size(im, 2)*2))
    new_im[1:2:end, 1:2:end] = im

    new_im = convolve_reflect(new_im, filter)
    return new_im
end

function generate_gaussian_pyramid(im; min_size=15, max_levels=23, filter=[0.0625; 0.25; 0.375; 0.25; 0.0625])
    im = convert(Array{Float64}, copy(im))

    pyramid_bands = Dict{Integer, Array}()

    im_dims = collect(size(im))
    num_levels = min(max_levels, ceil.(Int, log2(minimum(im_dims)) - log2(min_size)))

    for i = 1:num_levels
        pyramid_bands[i-1] = im
        im = reduce(im, filter=filter)
    end

    return (pyramid_bands, num_levels)
end

function generate_laplacian_pyramid(im; min_size=15, max_levels=23, filter=[0.0625; 0.25; 0.375; 0.25; 0.0625])
    im = convert(Array{Float64}, copy(im))

    pyramid_bands = Dict{Integer, Array}()

    im_dims = collect(size(im))

    num_levels = min(max_levels, ceil.(Int, log2(minimum(im_dims)) - log2(min_size)))

    filter_offset::Int = (length(filter)-1)/2

    for i = 1:num_levels
        reduced_im = reduce(im, filter=filter)
        next_im = expand(reduced_im)
        diff_im = im - next_im

        pyramid_bands[i-1] = diff_im

        im = reduced_im
    end

    pyramid_bands[num_levels] = im

    return (pyramid_bands, num_levels)
end

function reconstruct_laplacian_pyramid(pyramid::ImagePyramid)
    output_im::Array{Float64} = subband(pyramid, pyramid.num_levels)

    for i = (pyramid.num_levels-1):-1:0
        output_im = expand(output_im)
        output_im += subband(pyramid, i)
    end

    return output_im
end

##############################
# Private functions for building complex steerable pyramids.

function construct_steering_matrix(harmonics, angles; even = true)
    numh = 2*length(harmonics) - any(harmonics .== 0)

    imtx = zeros(length(angles), numh)
    col = 1
    for h in harmonics
        args = h * angles

        if h == 0
            imtx[:,col] = ones(angles)
            col += 1
        elseif !even
            imtx[:,col] = sin.(args)
            imtx[:,col+1] = -cos.(args)
            col += 2
        else
            imtx[:,col] = cos.(args)
            imtx[:,col+1] = sin.(args)
            col += 2
        end
    end

    r = rank(imtx)

    if (r != numh) && (r != length(angles))
        warning("Matrix is not full rank")
    end

    return pinv(imtx)
end

function raisedcosine(width=1, position=0, values=[0,1])
    sz = 256 # arbitrary
    X = collect( pi * (-sz-1:1) / (2 * sz) )
    Y = @. values[1] + ( (values[2]-values[1]) * cos(X)^2 )

    Y[1] = Y[2]
    Y[sz+3] = Y[sz+2]
    X = @.  position + (2*width/pi) * (X + pi/4)

    return (X, Y)
end

function build_complex_steerable_pyramid(im,
            height, nScales; order=3, twidth=1, scale=0.5)
    pyramid_bands = Dict{Integer, Union{Array, Dict{Integer, Array}}}()
    num_orientations = order + 1
    # generate steering matrix and harmonics info
    if mod.(num_orientations, 2) == 0
        # on 0.6 it was   ((0:((num_orientations/2)-1))*2+1)'
        harmonics = (1:2:(num_orientations/2-1)*2+1)
    else
        #on 0.6 it was ((0:((num_orientations-1)/2))*2)'
        harmonics = 0:2:(num_orientations-1)
    end

    steeringmatrix = construct_steering_matrix(harmonics,
                pi*(0:(num_orientations-1))/num_orientations, even=true)

    imdft = fftshift(fft(im))
    im_dims = size(im)
    ctr = ceil.(Int, (im_dims .+ 0.5) ./ 2. )

    angle = broadcast(atan,
            (((1:im_dims[1]) .- ctr[1]) ./ (im_dims[1]/2)),
            (((1:im_dims[2]) .- ctr[2]) ./ (im_dims[2]/2))') #SLOOW
    log_rad = broadcast((x,y) -> log2(sqrt(x^2 + y^2)),
            (((1:im_dims[1]) .- ctr[1]) ./ (im_dims[1]/2)),
            (((1:im_dims[2]) .- ctr[2]) ./ (im_dims[2]/2))') #SLOOW
    log_rad[ctr[1], ctr[2]] = log_rad[ctr[1], ctr[2]-1]

    Xrcos, Yrcos = raisedcosine(twidth, (-twidth/2), [0 1])
    Yrcos = sqrt.(Yrcos)
    YIrcos = @. sqrt(1 - Yrcos^2)

    # generate high frequency residual
    Yrcosinterpolant = interpolate((Xrcos,), Yrcos, Gridded(Linear()) )
    Yrcosinterpolant_ext = extrapolate(Yrcosinterpolant,Flat())
    hi0mask = map(Yrcosinterpolant_ext,log_rad)
    hi0dft =  imdft .* hi0mask;

    pyramid_bands[0] = ifft(ifftshift(hi0dft));

    YIrcosinterpolant = interpolate((Xrcos,), YIrcos, Gridded(Linear()))
    YIrcosinterpolant_ext = extrapolate(YIrcosinterpolant,Flat())
    lo0mask = map(YIrcosinterpolant_ext,log_rad)
    lo0dft = imdft .* lo0mask

    for ht = height:-1:1
        Xrcos = Xrcos .- log2(1/scale)
        order = num_orientations - 1

        cnst = (2^(2*order))*(factorial(order)^2)/(num_orientations*factorial(2*order))

        Yrcosinterpolant = interpolate((Xrcos,), Yrcos, Gridded(Linear()))
        Yrcosinterpolant_ext = extrapolate(Yrcosinterpolant,Flat())
        himask = map(Yrcosinterpolant_ext,log_rad)

        # loop through each orientation band
        pyramid_level = Dict{Integer, Array}()

        for b in 1:num_orientations

            banddft = map(zip(lo0dft,himask,angle) ) do (lo0dft_i,himask_i,angle_i)
                ang = angle_i-pi*(b-1)/num_orientations
                a = (abs(mod(pi+ang, 2*pi) - pi) < pi/2) *
                            (2*sqrt(cnst) * (cos(ang)^order))
                (complex(0,-1)^(num_orientations-1) *
                             lo0dft_i * himask_i) * a
            end
            pyramid_level[b] = ifft(ifftshift(banddft))
        end

        pyramid_bands[height-ht+1] = pyramid_level

        dims = size(lo0dft)
        ctr = ceil.(Int, (dims .+ 0.5)./2)

        lodims = round.(Int, im_dims[1:2].* (scale^(nScales-ht+1)) )

        loctr = ceil.(Int, (lodims.+0.5)./2)
        lostart = @. ctr - loctr+1
        loend = @. lostart + lodims - 1

        log_rad = log_rad[lostart[1]:loend[1], lostart[2]:loend[2]]
        angle = angle[lostart[1]:loend[1], lostart[2]:loend[2]]
        lodft = lo0dft[lostart[1]:loend[1], lostart[2]:loend[2]]
        YIrcos = @. abs(sqrt(1 - Yrcos^2))

        YIrcosinterpolant = interpolate((Xrcos,), YIrcos, Gridded(Linear()))
        YIrcosinterpolant_ext = extrapolate(YIrcosinterpolant,Flat())
        lomask = map(YIrcosinterpolant_ext,log_rad)

        lo0dft = lomask .* lodft
    end

    pyramid_bands[height+1] = real(ifft(ifftshift(lo0dft)))

    return (pyramid_bands, steeringmatrix, harmonics)
end

function make_angle_grid(sz, phase=0, origin=-1)
    if length(sz) == 1
        sz = [sz, sz]
    end

    if origin == -1
        origin = (sz + 1)/2
    end

    xramp = ones(round(Int, sz[1]), 1) * collect((1:sz[2]) .- origin[2])'
    yramp = collect((1:sz[1]) .- origin[1]) * ones(1, round.(Int, sz[2]))

    res = atan.(yramp, xramp)

    res = mod.(res .+ (pi-phase), 2*pi) .- pi

    return res
end

function reconstruct_steerable_pyramid(pyr::ImagePyramid;
        levs="all", bands="all", twidth=1, scale=0.5)
    dims = collect(size(subband(pyr, 0)))
    im_dft = zeros(Complex{Float64}, size(subband(pyr, 0)))

    ctr = ceil.(Int, (dims .+ 0.5) ./ 2)

    angle = broadcast(atan,
            (((1:dims[1]) .- ctr[1]) ./ (dims[1]/2)),
            (((1:dims[2]) .- ctr[2]) ./ (dims[2]/2))') #SLOOW
    log_rad = broadcast((x,y) -> log2(sqrt.(x.^2 + y.^2)),
        (((1:dims[1]) .- ctr[1]) ./ (dims[1]/2)),
        (((1:dims[2]) .- ctr[2]) ./ (dims[2]/2))') #SLOOW
    log_rad[ctr[1], ctr[2]] = log_rad[ctr[1], ctr[2]-1]
    log_rad0 = log_rad
    angle0 = angle

    Xrcos, Yrcos = raisedcosine(twidth, (-twidth/2), [0 1])
    Yrcos = sqrt.(Yrcos)
    YIrcos = @. sqrt(1 - Yrcos^2)

    # Start with low frequency residual
    low_dft = fftshift(fft(subband(pyr, pyr.num_levels+1)))

    lodims = collect(size(low_dft))
    loctr = ceil.(Int, (lodims .+ 0.5)./2)
    lostart = @. ctr - loctr + 1
    loend = @. lostart + lodims - 1

    log_rad = log_rad0[lostart[1]:loend[1], lostart[2]:loend[2]]
    angle = angle0[lostart[1]:loend[1], lostart[2]:loend[2]]

    Xrcos = Xrcos .- (log2(1/scale)*pyr.num_levels)
    YIrcosinterpolant = interpolate((Xrcos,), YIrcos, Gridded(Linear()))
    YIrcosinterpolant_ext = extrapolate(YIrcosinterpolant,Flat())
    lomask = map(YIrcosinterpolant_ext,log_rad)

    im_dft[lostart[1]:loend[1], lostart[2]:loend[2]] += low_dft .* lomask

    # Accumulate mid-bamds
    for level = pyr.num_levels:-1:1
        lodims = collect(size(subband(pyr, level, orientation=1)))
        loctr = ceil.(Int, (lodims .+ 0.5) ./ 2)
        lostart = @. ctr - loctr+1
        loend = @. lostart + lodims - 1

        log_rad = copy(log_rad0[lostart[1]:loend[1], lostart[2]:loend[2]])
        angle = copy(angle0[lostart[1]:loend[1], lostart[2]:loend[2]])

        Yrcosinterpolant = interpolate((Xrcos,), Yrcos, Gridded(Linear()))
        Yrcosinterpolant_ext = extrapolate(Yrcosinterpolant,Flat())
        himask = map(Yrcosinterpolant_ext,log_rad)

        Xrcos = Xrcos .+ log2(1/scale)
        order = pyr.num_orientations - 1

        cnst = ((complex(0,1))^(pyr.num_orientations-1)) *
            sqrt((2^(2*order))*(factorial(order)^2)/
                    (pyr.num_orientations*factorial(2*order)))

        for orientation = 1:pyr.num_orientations
            band_dft = fftshift(fft(subband(pyr, level, orientation=orientation)))
            ang = angle .- (pi*(orientation-1)/pyr.num_orientations)
            angle_mask = 2 .* (abs.(mod.(pi .+ ang, 2*pi) .- pi) .< pi/2) .*
                (cnst .* (cos.(ang).^order))

            im_dft[lostart[1]:loend[1], lostart[2]:loend[2]] += band_dft .*
             himask .* angle_mask
        end

        YIrcosinterpolant = interpolate((Xrcos,), YIrcos, Gridded(Linear()))
        YIrcosinterpolant_ext = extrapolate(YIrcosinterpolant,Flat())
        lomask = map(YIrcosinterpolant_ext,log_rad)

        # Everything must be scaled by the low-frequency mask
        im_dft[lostart[1]:loend[1], lostart[2]:loend[2]] .*= lomask
    end

    # Add high frequency residual
    Yrcosinterpolant = interpolate((Xrcos,), Yrcos, Gridded(Linear()) )
    Yrcosinterpolant_ext = extrapolate(Yrcosinterpolant,Flat())
    hi0mask = map(Yrcosinterpolant_ext,log_rad)
    im_dft += fftshift(fft(subband(pyr, 0))) .* hi0mask;

    return real(ifft(ifftshift(im_dft)))
end

# make the complex steerable pyramid real
function convert_complex_steerable_pyramid_to_real(pyramid::ImagePyramid;
            levs="all", bands="all", twidth=1, scale=0.5)
    pyramid = ImagePyramid(pyramid)
    num_levels = pyramid.num_levels
    num_orientations = pyramid.num_orientations

    for nsc in 1:num_levels
        dims = collect(size(subband(pyramid, nsc, orientation=1)))
        ctr = ceil.(Int, (dims .+ 0.5)./ 2)
        ang = make_angle_grid(dims, 0, ctr)
        ang[ctr[1], ctr[2]] = -pi/2

        for nor = 1:num_orientations
            ch = subband(pyramid, nsc, orientation=nor)

            ang0 = pi*(nor-1)/num_orientations
            xang = mod.(ang .- (ang0+pi), 2*pi) .- pi

            # this creates an angular mask
            amask = 2.0 .* (abs.(xang) .< pi/2) .+ (abs.(xang) .== pi/2)
            amask[ctr[1], ctr[2]] = 1.0
            amask[:,1] .= 1.0
            amask[1,:] .= 1.0

            # and masks the fft by it
            amask = fftshift(amask)
            ch = ifft(amask.*fft(ch))
            ch = 0.5 .* real(ch)

            # then creates a new pyramid
            update_subband!(pyramid, nsc, ch, orientation=nor)
        end
    end

    # and returns it as a real
    return pyramid
end

function reconstruct_complex_steerable_pyramid(pyramid::ImagePyramid;
            levs="all", bands="all", twidth=1, scale=0.5)
    real_pyramid = convert_complex_steerable_pyramid_to_real(pyramid,
        levs=levs, bands=bands, twidth=twidth, scale=scale)
    return reconstruct_steerable_pyramid(real_pyramid, levs=levs,
        bands=bands, twidth=twidth, scale=scale)
end

end


#=

  Subset sum problem in Julia ConstraintSolver.jl

  From Katta G. Murty: "Optimization Models for Decision Making", page 340
  http://ioe.engin.umich.edu/people/fac/books/murty/opti_model/junior-7.pdf

  """
  Example 7.8.1

  A bank van had several bags of coins, each containing either
  16, 17, 23, 24, 39, or 40 coins. While the van was parked on the
  street, thieves stole some bags. A total of 100 coins were lost.
  It is required to find how many bags were stolen.
  """

  And we also solve some larger random problem instances.

  Model created by Hakan Kjellerstrand, hakank@gmail.com
  See also my Julia page: http://www.hakank.org/julia/

=#

using ConstraintSolver, JuMP
using Cbc, GLPK, Ipopt
using Distributions
const CS = ConstraintSolver
include("constraints_utils.jl")

function subset_sum_model(a,total,print_solutions=true,all_solutions=true)

    cbc_optimizer = optimizer_with_attributes(Cbc.Optimizer, "logLevel" => 0)
    glpk_optimizer = optimizer_with_attributes(GLPK.Optimizer)
    ipopt_optimizer = optimizer_with_attributes(Ipopt.Optimizer)

    model = Model(optimizer_with_attributes(CS.Optimizer,
                                            "all_solutions"=> all_solutions,
                                            # "all_optimal_solutions"=>all_solutions,
                                            "logging"=>[],

                                            # "traverse_strategy"=>:BFS,
                                            # "traverse_strategy"=>:DFS,
                                            "traverse_strategy"=>:DBFS,

                                            "branch_split"=>:Smallest,
                                            # "branch_split"=>:Biggest,
                                            # "branch_split"=>:InHalf,

                                            # https://wikunia.github.io/ConstraintSolver.jl/stable/options/#branch_strategy-(:Auto)
                                            "branch_strategy" => :IMPS, # default
                                            # "branch_strategy" => :ABS, # Activity Based Search
                                            # "activity.decay" => 0.999, # default 0.999
                                            # "activity.max_probes" => 10, # default, 10
                                            # "activity.max_confidence_deviation" => 20, # default 20

                                            # "simplify"=>false,
                                            # "simplify"=>true, # default

                                            "time_limit"=>6,

                                            # "backtrack" => false, # default true
                                            # "backtrack_sorting" => false, # default true

                                            # "lp_optimizer" => cbc_optimizer,
                                            "lp_optimizer" => glpk_optimizer,
                                            # "lp_optimizer" => ipopt_optimizer,
                                            ))

    if print_solutions
        println("a: $a")
        println("total: $total")
    end

    n = length(a)
    @variable(model, 0 <= x[1:n] <= n, Int)
    # @variable(model, 0 <= s <= n*maximum(a), Int)

    # scalar_product(model,x,a,total)

    @constraint(model,total == sum(a.*x))

    # Solve the problem
    optimize!(model)

    status = JuMP.termination_status(model)
    if status == MOI.OPTIMAL
        num_sols = MOI.get(model, MOI.ResultCount())
        println("num_sols:$num_sols\n")
        if print_solutions
            for sol in 1:num_sols
                println("solution #$sol")
                x_val = convert.(Integer,JuMP.value.(x; result=sol))
                # s_val = convert.(Integer,JuMP.value.(s; result=sol))
                println("x:$x_val")
                # println("s:$s_val")
                println()
            end
        end
    else
        println("status:$status")
    end

    return status
end

coins = [16, 17, 23, 24, 39, 40]
total = 100
@time subset_sum_model(coins,total,true,true)

#
# Some random instances
#
println("\nrandom instances")
for _ in 1:10
    # some random examples
    n = rand(DiscreteUniform(10,1_000))
    a = rand(DiscreteUniform(1,n),n)
    tot = rand(DiscreteUniform(1,3*sum(a)))
    println("\nn:$n total:$tot")
    # println("a:$a")
    @time subset_sum_model(a,tot,false,false)
end


@doc raw"""
    Circle3

An object `c` of type [`Circle3`](@ref) is a circle in the three-dimensional
Euclidean space ``Ε^3``.

Note that the circle can be degenerated, i.e. the squared radius may be zero.
"""
Circle3

@doc raw"""
    Circle3(center::Point3, r²::Real, plane::Plane3)

Introduces a variable `c` of type [`Circle3`](@ref).

It is initalized to the circle of center `center` and squared radius `r²` in
plane `plane`.

!!! info "Precondition"

    `center` lies in `plane` and `r²` ``\ge`` 0.
"""
Circle3(center::Point3, r²::Real, plane::Plane3)

@cxxdereference Circle3(center::Point3, r²::Real, plane::Plane3) =
    Circle3(center, convert(FT, r²), plane)

@doc raw"""
    Circle3(center::Point3, r²::Real, n::Vector3)

Introduces a variable `c` of type [`Circle3`](@ref).

It is initalized to the circle of center `center` and squared radius `r²` in a
plane normal to the vector `n`.

!!! info "Precondition"

    `r²` ``\ge`` 0.
"""
Circle3(center::Point3, r²::Real, n::Vector3)

@cxxdereference Circle3(center::Point3, r²::Real, n::Vector3) =
    Circle3(center, convert(FT, r²), n)

@doc raw"""
    Circle3(p::Point3, q::Point3, r::Point3)

Introduces a variable `c` of type [`Circle3`](@ref).

It is initialized to the circle passing through the three points.

!!! info "Precondition"

    The three points are not collinear.
"""
Circle3(p::Point3, q::Point3, r::Point3)

@doc raw"""
    Circle3(sphere₁::Sphere3, sphere₂::Sphere3)

Introduces a variable `c` of type [`Circle3`](@ref).

It is initialized to the circle along which the two spheres intersect.

!!! info "Precondition"

    The two spheres intersect along a circle.
"""
Circle3(sphere₁::Sphere3, sphere₂::Sphere3)

@doc raw"""
    Circle3(sphere::Sphere3, plane::Plane3)

Introduces a variable `c` of type [`Circle3`](@ref).

It is initialized to the circle along which the sphere and the plane intersect.

!!! info "Precondition"

    The sphere and the plane intersect along a circle.
"""
Circle3(sphere::Sphere3, plane::Plane3)

@doc raw"""
    Circle3(plane::Plane3, sphere::Sphere3)

Introduces a variable `c` of type [`Circle3`](@ref).

It is initialized to the circle along which the sphere and the plane intersect.

!!! info "Precondition"

    The sphere and the plane intersect along a circle.
"""
Circle3(plane::Plane3, sphere::Sphere3)

"""
    center(c::Circle3)

Returns the center of `c`.
"""
center(c::Circle3)

"""
    squared_radius(c::Circle3)

Returns the squared radius of `c`.
"""
squared_radius(c::Circle3)

"""
    supporting_plane(c::Circle3)

Returns the supporting plane of `c`.
"""
supporting_plane(c::Circle3)

"""
    diametral_sphere(c::Circle3)

Returns the diametral sphere of `c`.
"""
diametral_sphere(c::Circle3)

"""
    area_divided_by_pi(c::Circle3)

Returns the area of `c`, divided by ``π``.
"""
area_divided_by_pi(c::Circle3)

"""
    approximate_area(c::Circle3)

Returns an approximation of the area of `c`.
"""
approximate_area(c::Circle3)

"""
    squared_length_divided_by_pi_square(c::Circle3)

Returns the squared length of `c`, divided by ``π^2``.
"""
squared_length_divided_by_pi_square(c::Circle3)

"""
    approximate_squared_length(c::Circle3)

Returns an approximation of the squared length (i.e. perimeter) of `c`.
"""
approximate_squared_length(c::Circle3)

"""
    ==(c::Circle3, circle3::Circle3)

Returns `true`, iff `c` and `circle3` are equal, i.e. if they have the same
center, same squared radius and same supporting plane.
"""
==(c::Circle3, circle3::Circle3)

"""
    bbox(c::Circle3)

Returns a bounding box containing `c`.
"""
bbox(c::Circle3)


using Pumas, DataFrames, LinearAlgebra, Plots

model = @model begin
  @param begin
    θ ∈ VectorDomain(4)
    Ω ∈ PSDDomain(3)
    σ_prop ∈ RealDomain(init=0.1)
  end

  @random begin
    η ~ MvNormal(Ω)
  end

  @covariates isPM Wt

  @pre begin
    TVCL = isPM == 1 ? θ[1] : θ[4]
    CL = θ[1]*(Wt/70)^0.75*exp(η[1])
    V = θ[2]*(Wt/70)^0.75*exp(η[2])
    Ka = θ[3]*exp(η[3])
  end

  @dynamics begin
    Depot'   = -Ka*Depot
    Central' =  Ka*Depot - Central*CL/V
  end

  @vars begin
    conc = Central/V
  end

  @derived begin
    dv ~ @.Normal(conc,sqrt(conc^2*σ_prop+ eps()))
  end

end

fixeffs = (
  θ = [0.4,20,1.1,2],
  Ω = diagm(0 => [0.04,0.04,0.04]),
  σ_prop = 0.04
  )

ev = DosageRegimen(100, time=0)
first(ev.data)

s1 = Subject(id=1,evs=ev,cvs=(isPM=0, Wt=70))
for fn in fieldnames(Subject)
           x = getproperty(s1, fn)
           if !isa(x, Nothing)
               println(fn)
               println(x)
           end
end

s1.events

s1.covariates

obs = simobs(model,s1,fixeffs,obstimes=0:0.1:120)
plot(obs)

s2 = Subject(id=2,evs=ev,cvs=(isPM=1,Wt=70))

twosubjs =  Population([s1,s2])

twosubjs[1]

twosubjs[2]

obs = simobs(model,twosubjs,fixeffs,obstimes=0:0.1:120)

plot(obs)

choose_covariates() = (isPM = rand([1, 0]),
                    Wt = rand(55:80))

cvs = [ choose_covariates() for i in 1:10 ]
DataFrame(cvs)

pop_with_covariates = Population(map(i -> Subject(id=i,evs=ev,cvs=choose_covariates()),1:10))

obs = simobs(model,pop_with_covariates,fixeffs,obstimes=0:0.1:120);

plot(obs)

md =  DosageRegimen(100,ii=24,addl=6)

s3 = Subject(id=3,evs=md, cvs=(isPM=0,Wt=70))

obs = simobs(model, s3, fixeffs,obstimes=0:0.1:240)
plot(obs)

ldmd = DosageRegimen([500,100],cmt=1, time=[0,24], addl=[0,6],ii=[0,24])

s4 = Subject(id=4, evs=ldmd, cvs=(isPM=0,Wt=70))
obs = simobs(model, s4, fixeffs,obstimes=0:0.1:120)
plot(obs, ylims=(0,50))

e1 = DosageRegimen(500,cmt=1, time=0, addl=0,ii=0)
e2 = DosageRegimen(100,cmt=1, time=24, addl=6,ii=24)
evs = DosageRegimen(e1,e2)
obs = simobs(model, s4, fixeffs,obstimes=0:0.1:120)
plot(obs, ylims=(0,50))

e1 = DosageRegimen(100, ii=24, addl=6)
e2 = DosageRegimen(50,  ii=12, addl=13)
e3 = DosageRegimen(200, ii=24, addl=2)

pop1 = Population(map(i -> Subject(id=i,evs=e1,cvs=choose_covariates()),1:5))
pop2 = Population(map(i -> Subject(id=i,evs=e2,cvs=choose_covariates()),6:8))
pop3 = Population(map(i -> Subject(id=i,evs=e3,cvs=choose_covariates()),9:10))
pop = Population(vcat(pop1,pop2,pop3))

obs = simobs(model,pop,fixeffs,obstimes=0:0.1:120)
plot(obs)

inf = DosageRegimen(100, rate=3, cmt=2)

s5 = Subject(id=5, evs=inf, cvs=(isPM=0,Wt=70))
obs = simobs(model, s5, fixeffs, obstimes=0:0.1:120)
plot(obs)

using PumasTutorials
PumasTutorials.tutorial_footer(WEAVE_ARGS[:folder],WEAVE_ARGS[:file])


module Dec8Part1
visited_index = Int64[]
function get_new_index_compute_accumulator(op, value, current_index, accumulator)
    if op == "jmp"
        current_index += value
    elseif op == "acc"
        accumulator += value
        current_index += 1
    else
        current_index += 1
    end
    return current_index, accumulator
end

function parse_operation(x::String)::Tuple{String,Int64}
    op, value = split(x, ' ')
    return op, parse(Int64, value)
end

function get_accumulator_run_operations(operations::Array, index::Int64, accumulator::Int64)
    global visited_index
    push!(visited_index, index)
    while true
        op, value = operations[index]
        index, accumulator = get_new_index_compute_accumulator(op, value, index, accumulator)
        if index ∉ visited_index
            push!(visited_index, index)
        else
            return accumulator
        end
    end
end

function main(file_path::String)::Int64
    operations = readlines(file_path)
    current_index = 1
    accumulator = 0
    operations = parse_operation.(operations)
    return get_accumulator_run_operations(operations, current_index, accumulator)
end
end


# first example in biconnected.jl
pol = W,hcat([EW[:,e] if e>0 else reverse(EW[:,-e]) for e in chains[1]]...)

cycle = []
for edge in chains[1]
	if edge>0
		push!(cycle, EW[:,edge])
	elseif edge<0
		push!( cycle, Array{Int64,1}(reverse(EW[:,-edge])) )
	end
end
EV = hcat(cycle...)

larview(V,EV)
pointInPolygonClassification(V,EV)([200,300])