include("aesgf-code.jl")

global const Nks = [4, 6, 8]
global const Nbs = [4, 4, 4]
global const Nb = 4
global const Nrs = [10, 12, 14]
global const WORDLENGTH = 4

global const SBOX = [
0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76,
0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0,
0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15,
0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75,
0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84,
0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf,
0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8,
0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2,
0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73,
0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb,
0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79,
0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08,
0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a,
0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e,
0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf,
0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16
]

global const INVSBOX = [
0x52, 0x09, 0x6a, 0xd5, 0x30, 0x36, 0xa5, 0x38, 0xbf, 0x40, 0xa3, 0x9e, 0x81, 0xf3, 0xd7, 0xfb,
0x7c, 0xe3, 0x39, 0x82, 0x9b, 0x2f, 0xff, 0x87, 0x34, 0x8e, 0x43, 0x44, 0xc4, 0xde, 0xe9, 0xcb,
0x54, 0x7b, 0x94, 0x32, 0xa6, 0xc2, 0x23, 0x3d, 0xee, 0x4c, 0x95, 0x0b, 0x42, 0xfa, 0xc3, 0x4e,
0x08, 0x2e, 0xa1, 0x66, 0x28, 0xd9, 0x24, 0xb2, 0x76, 0x5b, 0xa2, 0x49, 0x6d, 0x8b, 0xd1, 0x25,
0x72, 0xf8, 0xf6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xd4, 0xa4, 0x5c, 0xcc, 0x5d, 0x65, 0xb6, 0x92,
0x6c, 0x70, 0x48, 0x50, 0xfd, 0xed, 0xb9, 0xda, 0x5e, 0x15, 0x46, 0x57, 0xa7, 0x8d, 0x9d, 0x84,
0x90, 0xd8, 0xab, 0x00, 0x8c, 0xbc, 0xd3, 0x0a, 0xf7, 0xe4, 0x58, 0x05, 0xb8, 0xb3, 0x45, 0x06,
0xd0, 0x2c, 0x1e, 0x8f, 0xca, 0x3f, 0x0f, 0x02, 0xc1, 0xaf, 0xbd, 0x03, 0x01, 0x13, 0x8a, 0x6b,
0x3a, 0x91, 0x11, 0x41, 0x4f, 0x67, 0xdc, 0xea, 0x97, 0xf2, 0xcf, 0xce, 0xf0, 0xb4, 0xe6, 0x73,
0x96, 0xac, 0x74, 0x22, 0xe7, 0xad, 0x35, 0x85, 0xe2, 0xf9, 0x37, 0xe8, 0x1c, 0x75, 0xdf, 0x6e,
0x47, 0xf1, 0x1a, 0x71, 0x1d, 0x29, 0xc5, 0x89, 0x6f, 0xb7, 0x62, 0x0e, 0xaa, 0x18, 0xbe, 0x1b,
0xfc, 0x56, 0x3e, 0x4b, 0xc6, 0xd2, 0x79, 0x20, 0x9a, 0xdb, 0xc0, 0xfe, 0x78, 0xcd, 0x5a, 0xf4,
0x1f, 0xdd, 0xa8, 0x33, 0x88, 0x07, 0xc7, 0x31, 0xb1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xec, 0x5f,
0x60, 0x51, 0x7f, 0xa9, 0x19, 0xb5, 0x4a, 0x0d, 0x2d, 0xe5, 0x7a, 0x9f, 0x93, 0xc9, 0x9c, 0xef,
0xa0, 0xe0, 0x3b, 0x4d, 0xae, 0x2a, 0xf5, 0xb0, 0xc8, 0xeb, 0xbb, 0x3c, 0x83, 0x53, 0x99, 0x61,
0x17, 0x2b, 0x04, 0x7e, 0xba, 0x77, 0xd6, 0x26, 0xe1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0c, 0x7d
]

global const MIXCOLUMNSMATRIX = [
0x02, 0x03, 0x01, 0x01,
0x01, 0x02, 0x03, 0x01,
0x01, 0x01, 0x02, 0x03,
0x03, 0x01, 0x01, 0x02
]

global const INVMIXCOLUMNSMATRIX = [
0x0e, 0x0b, 0x0d, 0x09,
0x09, 0x0e, 0x0b, 0x0d,
0x0d, 0x09, 0x0e, 0x0b,
0x0b, 0x0d, 0x09, 0x0e
]

# Encrypts the given plaintext block using the given key
# and returns the resulting ciphertext.
# Both the plaintext and key must be represented as strings consisting of
# only hexadecimal characters.
# The returned ciphertext is a string consisting of only hexadecimal
# characters.
function AESEncrypt(plain::String, key::String)
	bytes2hex(AESEncrypt(hex2bytes(plain), hex2bytes(key)))
end

# Decrypts the given ciphertext block using the given key
# and returns the resulting plaintext.
# Both the ciphertext and key must be represented as strings consisting of
# only hexadecimal characters.
# The returned plaintext is a string consisting of only hexadecimal
# characters.
function AESDecrypt(cipher::String, key::String)
	bytes2hex(AESDecrypt(hex2bytes(cipher), hex2bytes(key)))
end

# Encrypts the given plaintext block using the given key
# and returns the resulting ciphertext.
# Both the plaintext and key are arrays holding bytes of type UInt8.
# The returned ciphertext is an array holding bytes of type UInt8.
function AESEncrypt(plain::Array{UInt8, 1}, key::Array{UInt8, 1})
	(w, Nr) = AEScrypt(plain, key)
	return AESCipher(plain, w, Nr)
end

# Decrypts the given ciphertext block using the given key
# and returns the resulting plaintext.
# Both the ciphertext and key are arrays holding bytes of type UInt8.
# The returned plaintext is an array holding bytes of type UInt8.
function AESDecrypt(cipher::Array{UInt8, 1}, key::Array{UInt8, 1})
	(w, Nr) = AEScrypt(cipher, key)
	return AESInvCipher(cipher, w, Nr)
end

# General function for checking the parameters of the encryption/decryption
# functions and expanding the key.
function AEScrypt(input::Array{UInt8, 1}, key::Array{UInt8, 1})
	if length(input) != (WORDLENGTH * Nb)
		error("input must be a 16-byte block!")
	end
	(Nk, Nr) = AESParameters(key)
	w = KeyExpansion(key, Nk, Nr)
	return (w, Nr)
end

# Return a tuple (Nk, Nr) where Nk is the number of key blocks
# and Nr is the number of rounds for the key size.
function AESParameters(key::Array{UInt8, 1})
	if mod(length(key), WORDLENGTH) != 0
		error("the key length must be a multiple of four!")
	end

	Nk = div(length(key), WORDLENGTH)
	i = first(indexin([Nk], Nks))

	if i == 0
		error("key length is non-standard!")
	end

	Nr = Nrs[i]
	return (Nk, Nr)
end

function KeyExpansion(key::Array{UInt8, 1}, Nk::Int, Nr::Int)
	@assert(length(key) == (WORDLENGTH * Nk))

	w = Array{UInt8}(undef, WORDLENGTH * Nb * (Nr + 1))
	w[1:(WORDLENGTH * Nk)] = copy(key)
	i = Nk

	while i < (Nb * (Nr + 1))
		temp = w[((i - 1) * WORDLENGTH + 1):(i * WORDLENGTH)]
		if mod(i, Nk) == 0
			temp = xor.(SubWord(RotWord(temp)) , Rcon(div(i, Nk)))
		elseif (Nk > 6) && (mod(i, Nk) == Nb)
			temp = SubWord(temp)
		end
		w[(i * WORDLENGTH + 1):((i + 1) * WORDLENGTH)] = xor.(w[((i - Nk) * WORDLENGTH + 1):((i - Nk + 1) * WORDLENGTH)] , temp)
		i += 1
	end

	return w
end

function SubWord(w::Array{UInt8, 1})
	@assert(length(w) == WORDLENGTH)
	map!(x -> SBOX[Int(x) + 1], w, w)
	return w
end

function RotWord(w::Array{UInt8, 1})
	@assert(length(w) == WORDLENGTH)
	permute!(w, [2, 3, 4, 1])
end

function Rcon(i::Int)
	@assert(i > 0)
	x = 0x01
	for j=1:(i-1)
		x = gmul(x, 0x02)
	end
	return [x, 0x00, 0x00, 0x00]
end

function AESCipher(inBytes::Array{UInt8, 1}, w::Array{UInt8, 1}, Nr::Int)
	@assert(WORDLENGTH == Nb)
	@assert(length(inBytes) == (WORDLENGTH * Nb))
	@assert(length(w) == (WORDLENGTH * Nb * (Nr + 1)))

	state = copy(inBytes)
	AddRoundKey(state, w[1:(Nb * WORDLENGTH)])

	for round=1:(Nr-1)
		SubBytes(state)
		ShiftRows(state)
		MixColumns(state)
		AddRoundKey(state, w[(round * Nb * WORDLENGTH + 1):((round + 1) * Nb * WORDLENGTH)])
	end

	SubBytes(state)
	ShiftRows(state)
	AddRoundKey(state, w[(Nr * Nb * WORDLENGTH + 1):((Nr + 1) * Nb * WORDLENGTH)])

	return state
end

function AESInvCipher(inBytes::Array{UInt8, 1}, w::Array{UInt8, 1}, Nr::Int)
	@assert(WORDLENGTH == Nb)
	@assert(length(inBytes) == (WORDLENGTH * Nb))
	@assert(length(w) == (WORDLENGTH * Nb * (Nr + 1)))

	state = copy(inBytes)
	AddRoundKey(state, w[(Nr * Nb * WORDLENGTH + 1):((Nr + 1) * Nb * WORDLENGTH)])

	for round=(Nr-1):-1:1
		InvShiftRows(state)
		InvSubBytes(state)
		AddRoundKey(state, w[(round * Nb * WORDLENGTH + 1):((round + 1) * Nb * WORDLENGTH)])
		InvMixColumns(state)
	end

	InvShiftRows(state)
	InvSubBytes(state)
	AddRoundKey(state, w[1:(Nb * WORDLENGTH)])

	return state
end

# Returns one-based column indices of the given column (one-based).
function columnIndices(column::Int)
	return column:Nb:Nb*Nb
end

# Returns row indices of the given row (one-based).
function rowIndices(row::Int)
	return ((row - 1) * Nb + 1):(row * Nb)
end

function SubBytes(a::Array{UInt8, 1})
	SubBytesGen(a, false)
end

function InvSubBytes(a::Array{UInt8, 1})
	SubBytesGen(a, true)
end

function SubBytesGen(a::Array{UInt8, 1}, inv::Bool)
	box = inv ? INVSBOX : SBOX
	f = (x) -> box[Int(x) + 1]
	map!(f, a, a)
	return a
end

function ShiftRows(a::Array{UInt8, 1})
	ShiftRowsGen(a, false)
end

function InvShiftRows(a::Array{UInt8, 1})
	ShiftRowsGen(a, true)
end

function ShiftRowsGen(a::Array{UInt8, 1}, inv::Bool)
	# permute columns in _last three_ rows
	# note that a column is actually a row in memory
	for r=2:Nb
		indices = columnIndices(r)
		step = inv ? Nb + 2 - r : r
		p = map(c -> mod(c + step - 1, Nb) + 1, 0:(Nb-1))
		a[indices] = a[indices][p]
	end
	return a
end

function MixColumns(a::Array{UInt8, 1})
	MixColumnsGen(a, false)
end

function InvMixColumns(a::Array{UInt8, 1})
	MixColumnsGen(a, true)
end

function MixColumnsGen(a::Array{UInt8, 1}, inv::Bool)
	# note that columns are actually rows in memory
	matrix = inv ? INVMIXCOLUMNSMATRIX : MIXCOLUMNSMATRIX
	for c=1:Nb
		indices = rowIndices(c)
		ai = copy(a[indices])
		@assert(length(ai) == Nb)
		# Matrix multiplication with Galois field operations
		for r=1:Nb
			mi = matrix[rowIndices(r)]
			a[indices[r]] = gadd(map(gmul, ai, mi))
		end
	end
	return a
end

function AddRoundKey(s::Array{UInt8, 1}, w::Array{UInt8, 1})
	@assert(length(w) == (WORDLENGTH * Nb) && (WORDLENGTH == Nb))
	return map!(gadd, s, s, w)
end


using StaticArrays, EllZeroTrendFiltering
using Test

#SNP example
N = 400
data = snp500_data()[1:N]

M = 10
@testset "Examples lazy=$lazy" for lazy in [true, false]
    Ivec, Yvec, fvec = fit_pwl_constrained(data, M, lazy=lazy)

    #Make sure nothing crashes
    @test Ivec[5] == [1,78,146,162,370,400]
    @test Yvec[5] ≈ [7.180172223381005, 7.215552904744207, 7.160379047774212,
                     7.2454671184257435, 7.302625286124225, 7.215712991948725]
    @test fvec[5] ≈ 0.2057753829867579

    @test Ivec[M] == [1,64,68,144,164,204,241,252,281,366,400]
    @test Yvec[M] ≈ [7.192869876795329,7.187186703983764,7.234241678532294,7.156765323782069,
                  7.249687632432071,7.274247888087285,7.211190688270137,7.324697309669883,
                  7.258287507540115,7.316043144146352,7.215020359632374]
    @test fvec[M] ≈ 0.11431141407229006

    #Test regularize discrete
    # should generate solution with 10 segments
    I, Y, f = fit_pwl_regularized(data, 0.02, lazy=lazy)
    @test length(I) == 9
    @test I == Ivec[8]
    @test Y ≈ Yvec[8]     rtol=sqrt(eps())
    @test f ≈ fvec[8]     rtol=sqrt(eps())

    #Sin example
    g_(x) = sin(x) + 0.5sin(3.5x) + 0.5sin(5.1x)
    local t # TODO, can remove in julia 1.0?
    t = range(0, stop=2π, length=201)

    ζ = 0.1
    I, Y, cost = fit_pwl_regularized(g_, t, ζ, lazy=lazy)

    @test I == [1, 87, 107, 129, 147, 201]
    @test cost ≈ 0.35055983342102515

    # Compare to constrained
    Ivec, Yvec, fvec = fit_pwl_constrained(g_, t, 5, lazy=lazy)
    @test I == Ivec[5]
    @test Y ≈ Yvec[5]       rtol=sqrt(eps())
    @test cost ≈ fvec[5]       rtol=sqrt(eps())

    ζ = 0.002
    I, Y, cost = fit_pwl_regularized(g_, t, ζ, lazy=lazy)

    @test I == [1, 9, 15, 33, 52, 68, 87, 104, 111, 125, 132, 146, 153, 170, 188, 201]
    @test cost ≈ 0.006061712727833957
end


using ImageTransformations: center

"""
    mask_circle!(::AbstractMatrix, npix; fill=0, center=center(arr))
    mask_circle!(::AbstractArray, npix; fill=0, center=center(arr))

In-place version of [`mask_circle`](@ref)
"""
function mask_circle!(arr::AbstractMatrix{T}, npix; fill=zero(T), center=center(arr)) where T
    @inbounds for idx in CartesianIndices(arr)
        d = sqrt((idx.I[1] - center[1])^2 + (idx.I[2] - center[2])^2)
        if d < npix
            arr[idx] = fill
        end
    end
    return arr
end

function mask_circle!(cube::AbstractArray{T,3}, npix; kwargs...) where T
    @inbounds for i in axes(cube, 3)
        slice = @view cube[:, :, i]
        mask_circle!(slice, npix; kwargs...)
    end
    return cube
end

"""
    mask_circle(::AbstractMatrix, npix; fill=0, center=center(arr))
    mask_circle(::AbstractArray, npix; fill=0, center=center(arr))

Mask the inner-circle of an image with radius `npix` with value `fill`. Note that the input type must be compatible with the fill value's type. If the input is a cube it will mask each frame individually.

# See Also
[`mask_circle!`](@ref)
"""
mask_circle(arr::AbstractMatrix, npix; kwargs...) = mask_circle!(copy(arr), npix; kwargs...)
mask_circle(cube::AbstractArray{T,3}, npix; kwargs...) where T = mask_circle!(copy(cube), npix; kwargs...)

"""
    mask_annulus!(::AbstractMatrix, npix_in, npix_out; fill=0, center=center(arr))

In-place version of [`mask_annulus`](@ref)
"""
function mask_annulus!(arr::AbstractMatrix{T}, npix_in, npix_out; fill = zero(T), center=center(arr)) where T
    @inbounds for idx in CartesianIndices(arr)
        d = sqrt((idx.I[1] - center[1])^2 + (idx.I[2] - center[2])^2)
        if npix_in ≤ d < npix_out
            arr[idx] = fill
        end
    end
    return arr
end

"""
    mask_annulus(::AbstractMatrix, npix_in, npix_out; fill=0, center=center(arr))

Mask an annular region of an image with inner-radius `npix_in`, outer-radius `npix_out` with value `fill`.
Note that the input type must be compatible with the fill value's type.

# See Also
[`mask_annulus!`](@ref)
"""
mask_annulus(arr::AbstractMatrix, npix_in, npix_out; kwargs...) = mask_annulus!(copy(arr), npix_in, npix_out; kwargs...)

"""
    get_annulus_segments(data, inner_radius, width, [nsegments];
                         theta_init=0, scale_factor=1, mode=:index,
                         center=center(data))

Returns indices or values in segments of a centered annulus.

### Modes
* `:mask` - returns a positive boolean mask for indexing
* `:value` - returns the data values indexed with the boolean mask
* `:apply` - returns the input data weighted by the boolean mask
"""
function get_annulus_segments(data, inner_radius, width, nsegments; theta_init=0, scale_factor=1, mode=:mask, center=center(data))
    cx, cy = center
    xs, ys = axes(data)
    dist = @. sqrt((xs - cx)^2 + (ys' - cy)^2)
    ϕ_stride = deg2rad(ceil(Int, 360 / nsegments))
    ϕ = @. atan(ys - cy, xs - cx)
    ϕ_rot = mod2pi.(ϕ)
    outer_radius = @. inner_radius + scale_factor * width

    masks = similar(data, Matrix{Bool}, nsegments)
    for i in 1:nsegments
        ϕ_start = deg2rad(theta_init) + (i - 1) * ϕ_stride
        ϕ_end = ϕ_start + ϕ_stride

        if ϕ_start < 2π && ϕ_end > 2π
            mask = @. (dist ≥ inner_radius) & (dist < outer_radius) & (ϕ_rot ≥ ϕ_start) & (ϕ_rot ≤ 2π) | (dist ≥ inner_radius) & (dist < outer_radius) & (ϕ_rot ≥ 0) & (ϕ_rot < ϕ_end - 2π)
        elseif ϕ_start ≥ 2π && ϕ_end > 2π
            mask = @. (dist ≥ inner_radius) & (dist < outer_radius) & (ϕ_rot ≥ ϕ_start - 2π) & (ϕ_rot < ϕ_end - 2π)
        else
            mask = @. (dist ≥ inner_radius) & (dist < outer_radius) & (ϕ_rot ≥ ϕ_start) & (ϕ_rot < ϕ_end)
        end
        @inbounds masks[i] = mask
    end

    return _convert_mask.(Val(mode), masks, (data,))
end

get_annulus_segments(data, inner_radius, width; kwargs...) = get_annulus_segments(data, inner_radius, width, 1; kwargs...) |> only

_convert_mask(::Val{:mask}, mask, data) = mask
_convert_mask(::Val{:value}, mask, data) = data[mask]
_convert_mask(::Val{:apply}, mask, data) = data .* mask


line_regex = r"(\d+)-(\d+) ([a-z]): ([a-z]+)";
lines = readlines("2/input.txt");

function part1(input)
    function check_pw(item)
        return parse(Int64, item[1]) <= count(x -> (x == only(item[3])), item[4]) <= parse(Int64, item[2])
    end

    return count(check_pw.(map(x -> x.captures, match.(line_regex, lines))));
end

function part2(input)
    function check_pw(item)
        return (item[4][parse(Int64, item[1])] == only(item[3])) ⊻ (item[4][parse(Int64, item[2])] == only(item[3]))
    end

    return count(check_pw.(map(x -> x.captures, match.(line_regex, lines))));
end

println("Part 1: $(part1(lines))")
println("Part 2: $(part2(lines))")


"""
    EpsilonGreedySelector <: AbstractActionSelector
    EpsilonGreedySelector(ϵ)

The best action is selected for a proportion ``1 - \\epsilon``
and a random action (with uniform probability) is selected for a proportion ``\\epsilon``.
ϵ can also be a decay. See the following examples.

## Example

```julia
julia> selector = EpsilonGreedySelector(0.1)
EpsilonGreedySelector(0.1)

julia> countmap(selector([1,2,1,1]) for _ in 1:1000)
Dict{Any,Int64} with 4 entries:
  4 => 37
  2 => 915
  3 => 22
  1 => 26
```

julia> ϵ = exp_decay(init=1.0, λ=1.0, decay_step=500, clip=0.1)
(::getfield(Ju, Symbol("#f#34")){Float64,Float64,Int64,Float64}) (generic function with 1 method)

julia> selector = EpsilonGreedySelector(ϵ)
EpsilonGreedySelector{getfield(Ju, Symbol("#f#34")){Float64,Float64,Int64,Float64}}(getfield(Ju, Symbol("#f#34")){Float64,Float64,Int64,Float64}(1.0, 1.0, 500, 0.1, Core.Box(-1)))

julia> countmap(selector([1,2,1,1]) for _ in 1:1000)
Dict{Any,Int64} with 4 entries:
  4 => 101
  2 => 677
  3 => 106
  1 => 116
"""
mutable struct EpsilonGreedySelector{T<:Union{Float64, <:Function}} <: AbstractActionSelector
    ϵ::T
end

_epsilon_select(values, ϵ) = rand() > ϵ ? sample(findallmax(values)[2]) : rand(1:length(values))

"""
    (p::EpsilonGreedySelector)(values::AbstractArray{T, 1}) where T

!!! note
    If multiple values with the same maximum value are found.
    Then a random one will be returned!

    `NaN` will be filtered unless all the values are `NaN`.
    In that case, a random one will be returned.
"""
(p::EpsilonGreedySelector{Float64})(values) = _epsilon_select(values, p.ϵ)

(p::EpsilonGreedySelector{<:Function})(values) = _epsilon_select(values, p.ϵ())

# TODO: add `dims` argument to support higher dimension?


using HOODESolver
using LinearAlgebra
using DoubleFloats
using Random
using SparseArrays
using Test
using DiffEqBase

function test_operator(T::DataType)
#    mat = [0 1;-1 0]
    for mat in [
    T.([0 0 1 0; 0 0 0 0; -1 0 0 0; 0 0 0 0]),
    T.([0 1;-1 0]),
    T.([0 0 0 1 0 0; 0 0 0 0 1 0; 0 0 0 0 0 0;
     0 0 0 0 1 0; 0 0 0 -1 0 0; 0 0 0 0 0 0])
]
        epsilon = T(1//1024)
        linop = LinearHOODEOperator((1/epsilon)*mat)
        @test isconstant(linop)
        @test isapprox(linop.epsilon, epsilon)
        @test isapprox(linop.A, mat)
        L = T(1//16) * linop
        @test isconstant(L)
        @test isapprox(L.epsilon, epsilon*16)
        @test isapprox(L.A, mat)
        linop2 = LinearHOODEOperator(linop)
        @test isconstant(linop2)
        @test isapprox(linop2.epsilon, epsilon)
        @test isapprox(linop2.A, mat)
        L = T(1//16) * linop2
        @test isconstant(L)
        @test isapprox(L.epsilon, epsilon*16)
        @test isapprox(L.A, mat)
        linop3 = LinearHOODEOperator(epsilon, mat)
        @test isconstant(linop3)
        @test isapprox(linop3.epsilon, epsilon)
        @test isapprox(linop3.A, mat)
        L = T(1//16) * linop3
        @test isconstant(L)
        @test isapprox(L.epsilon, epsilon*16)
        @test isapprox(L.A, mat)
    end
end

@testset "test operator" begin
    test_operator(Float64)
    test_operator(Double64)
    test_operator(BigFloat)
end

function fct4bis( du, u, p, t)
    B=[0.1 1.1 0.2 05 ; -0.9 -0.12 -0.7 0.4 ; 0.5 0.66 0.7 0.8 ; -0.34 0.8 0 0.3]
    du[:] = B*u
    missing
end

function testcommon_interface_fct(linop)
    linopok = LinearHOODEOperator(linop)
    B=[0.1 1.1 0.2 05 ; -0.9 -0.12 -0.7 0.4 ; 0.5 0.66 0.7 0.8 ; -0.34 0.8 0 0.3]
    u0 = 2rand(4)-ones(4)
    sol_ref = exp(1.0*(1/linopok.epsilon*linopok.A+B))*u0
    fct = (u,p,t) -> B*u
    prob = SplitODEProblem(linop, fct, u0, (0.0, 1.0))
    sol = solve(prob, HOODEAB(), nb_t=100)
    @test isapprox(sol_ref, sol[end], atol=1e-7, rtol=1e-6)
    fct = (u,p) -> B*u
    prob = SplitODEProblem(linop, fct, u0, (0.0, 1.0))
    sol = solve(prob, HOODEAB(), dt=0.01)
    @test isapprox(sol_ref, sol[end], atol=1e-7, rtol=1e-6)
    fct = (u) -> B*u
    prob = SplitODEProblem(linop, fct, u0, (0.0, 1.0))
    sol = solve(prob, HOODEAB())
    @test isapprox(sol_ref, sol[end], atol=1e-7, rtol=1e-6)
    # prob = SplitODEProblem(linop, fct4bis, u0, (0.0, 1.0), missing, A, epsilon)
    # sol = solve(prob)
    # @test isapprox(sol_ref, sol[end], atol=1e-7, rtol=1e-6)

end
function testcommon_interface_epsilon()
    @time @testset "test interface while epsilon varying" begin
        Random.seed!(199881)
        u0=rand(BigFloat,4)
        println("u0=$u0")
        B = 2rand(BigFloat,4,4)-ones(BigFloat,4,4)
        A =  [0 0 1 0; 0 0 0 0;-1 0 0 0; 0 0 0 0]
        fct = (u,p,t) -> B*u
        order = 5
        for i=1:1
            epsilon = big"1.0"/big"10"^(i*3)
            eps_v = convert(Float32, epsilon)
            t_max = big"1.0"
            linop = LinearHOODEOperator(epsilon, A)
            sol_ref = exp(t_max*(1/epsilon*A+B))*u0
            prob = SplitODEProblem(linop, fct, u0, (big"0.0", t_max))
            println("epsilon=$eps_v sol_ref=$sol_ref")
            nb = 100
            res_err = zeros(BigFloat,5)
            ind = 1
            while nb <= 1000
                sol = solve(prob, HOODEAB(5), nb_t=nb, dense=false)
                res_err[ind] = norm(sol[end]-sol_ref,Inf)
 #               println("\nnb=$nb epsilon=$eps_v err=$(res_err[ind])")
                nb *= 10
                ind += 1
            end
            coef = Float64(log(res_err[1]) - log(res_err[2]))/log(10)
            @test isapprox( order, coef, atol=0.01)
            println("epsilon=$eps_v coef log = $coef")
        end
    end
end
function testcommon_interface_interpolate()
    @time @testset "test interface for interpolation" begin
        seed=123873
        A =  [0 0 1 0; 0 0 0 0;-1 0 0 0; 0 0 0 0]
        order = 6
        nb = 100
        t_max = big"1.0"
        for i=1:1
            seed = seed*1341+6576123
            Random.seed!(seed)
            u0=rand(BigFloat,4)
            println("u0=$u0")
            B = 2rand(BigFloat,4,4)-ones(BigFloat,4,4)
            fct = (u,p,t) -> B*u
            epsilon = big"0.4"/2.0^i
            eps_v = convert(Float32, epsilon)
            t_max = big"1.0"
            linop = LinearHOODEOperator(epsilon, A)
            prob = SplitODEProblem(linop, fct, u0, (big"0.0",t_max))
   #         sol = solve(prob, getprecision=false)
            sol = solve(prob,HOODEAB())
            m = 1/epsilon*A+B
            reftol=norm(exp(t_max*m)*u0-sol[end], Inf)*10
            for j=1:10
                t=rand(BigFloat)
                res_ex=exp(t*m)*u0
                res_ap=sol(t)
                println("t=$t norm=$(norm(res_ex-res_ap, Inf))")
                @test isapprox(res_ex, res_ap, atol=reftol, rtol=reftol*10)
            end
        end
    end
end
function testcommon_interface_interpolate_float()
    @time @testset "test interface for interpolation" begin
        seed=124363
        A =  [0 0 1 0; 0 0 0 0;-1 0 0 0; 0 0 0 0]
        order = 6
        nb = 100
        t_max = 1.0
        for i=1:1
            seed = seed*1341+6576127
            Random.seed!(seed)
            u0=rand(4)
            println("u0=$u0")
            B = 2rand(4,4)-ones(4,4)
            fct = (u,p,t) -> B*u
            epsilon = 0.4/2.0^i
            eps_v = convert(Float32, epsilon)
            t_max = 1.0
            linop = LinearHOODEOperator(epsilon, A)
            prob = SplitODEProblem(linop, fct, u0, (0.0,t_max))
   #         sol = solve(prob, getprecision=false)
            sol = solve(prob, HOODEAB())
            m = 1/epsilon*A+B
            reftol=norm(exp(t_max*m)*u0-sol[end], Inf)*10
            @test reftol < 1e-6
            for j=1:10
                t=rand()
                res_ex=exp(t*m)*u0
                res_ap=sol(t)
#                println("type ex=$(typeof(res_ex)) type ap=$(typeof(res_ap))")
#                println("t=$t norm=$(norm(res_ex-res_ap, Inf))")
                @test isapprox(res_ex, res_ap, atol=reftol, rtol=reftol*10)
            end
        end
    end
end
function testcommon_interface_short()
    seed=981881
    A =  [0 0 1 0; 0 0 0 0;-1 0 0 0; 0 0 0 0]
    order = 7
    nb = 5
     for i=1:10
        seed += 10
        Random.seed!(seed)
        u0=rand(BigFloat,4)
        println("u0=$u0")
        B = 2rand(BigFloat,4,4)-ones(BigFloat,4,4)
        fct = (u,p,t) -> B*u
        epsilon = big"0.00004"/2.0^i
        eps_v = convert(Float32, epsilon)
        t_max = big"0.01"
        linop = LinearHOODEOperator(epsilon, A)
        prob = SplitODEProblem(linop, fct, u0, (big"0.0",t_max))
        sol = solve(prob, HOODEAB(order), getprecision=false, nb_t=nb)
        resnorm=norm(exp(t_max*(1/epsilon*A+B))*u0-sol[end], Inf)
        println("sol=$(sol[end])")
        println("solexact=$(exp(t_max*(1/epsilon*A+B))*u0)")
        println("resnorm=$resnorm")
        @test resnorm < 1e-10
    end
end
function tts_time(t_begin, t_end)
    @time @testset "test interface time from $t_begin to $t_end" begin
        A =  [0 0 1 0; 0 0 0 0;-1 0 0 0; 0 0 0 0]
        B =  2rand(BigFloat,4,4)-ones(BigFloat,4,4)
        u0 = rand(BigFloat, 4)
        fct = (u,p,t) -> B*u
        epsilon=big"0.000000345"
        linop = LinearHOODEOperator(epsilon, A)
        prob = SplitODEProblem(linop, fct, u0, (t_begin,t_end))
        sol = solve(prob, HOODEAB(4), getprecision=false, nb_t=100)
        m = 1/epsilon*A+B
        solref = exp((t_end-t_begin)*m)*u0
        println("sol=$(sol[end]) solref=$solref norm=$(norm(sol[end]-solref,Inf))")
        @test isapprox(sol[end], solref,atol=1e-8, rtol=1e-7)
        for i=1:10
            t = rand(BigFloat)*(t_end-t_begin) + t_begin
            res_ex=exp((t-t_begin)*m)*u0
            res_ap=sol(t)
            @test isapprox(res_ex, res_ap, atol=1e-8, rtol=1e-7)
        end
    end
end
function tts_time_time(t_begin, t_end)
    @time @testset "test interface time time from $t_begin to $t_end" begin
        A =  [0 0 1 0; 0 0 0 0;-1 0 0 0; 0 0 0 0]
        B =  2rand(BigFloat,4,4)-ones(BigFloat,4,4)
        u0 = 2rand(BigFloat, 4)-ones(BigFloat,4)
        fct = (u,p,t) -> B*u +t*p[1] +p[2]
        tuple_p = (2rand(BigFloat,4)-ones(BigFloat,4),
    2rand(BigFloat,4)-ones(BigFloat,4))
        epsilon=big"0.000003467"
        nb = 100
        order = 4
        linop = LinearHOODEOperator(epsilon, A)
        prob = SplitODEProblem(linop, fct, u0, (t_begin,t_end), tuple_p; B=B)
        sol = solve(prob, HOODEAB(4), getprecision=false, nb_t=100)
#        solref = getexactsol(sol.par_u0.parphi, u0, t_end)
        solref = getexactsol(sol.destats.par_u0.parphi, u0, t_end)
        println("sol=$(sol[end]) solref=$solref norm=$(norm(sol[end]-solref,Inf))")
        @test isapprox(sol[end], solref,atol=1e-8, rtol=1e-7)
        for i=1:10
            t = rand(BigFloat)*(t_end-t_begin) + t_begin
            res_ex=getexactsol(sol.destats.par_u0.parphi, u0, t)
            res_ap=sol(t)
            @test isapprox(res_ex, res_ap, atol=1e-8, rtol=1e-7)
        end
    end
end
function testcommon_interface_time()
    seed=7887
    Random.seed!(seed)
    tts_time(big"0.0",big"1.0")
    tts_time(big"0.0",big"1.4565656565")
    tts_time(big"2.234566",big"3.45766790123")
    tts_time(-big"0.8457676",big"0.56716")
    tts_time(-big"1.8111457676",-big"0.345456716")
    tts_time(big"1.8457676",big"0.56716")
    tts_time(-big"0.8457676",-big"1.56716")
    tts_time(-big"0.845722676",-big"0.56716")
end
function testcommon_interface_time_time()
    seed=788227
    Random.seed!(seed)
    tts_time_time(big"0.0",big"1.0")
    tts_time_time(big"0.0",big"1.4565656565")
    tts_time_time(big"11.234566",big"12.45766790123")
    tts_time_time(-big"0.8457676",big"0.56716")
    tts_time_time(-big"1.8111457676",-big"0.345456716")
    tts_time_time(big"1.8457676",big"0.56716")
    tts_time_time(-big"0.8457676",-big"1.56716")
    tts_time_time(-big"0.845722676",-big"0.56716")
end
@time @testset "test interface type of function" begin
    A =  [0 0 1 0; 0 0 0 0; -1 0 0 0; 0 0 0 0]
    epsilon = 0.0001
    linop = LinearHOODEOperator(epsilon, A)
    testcommon_interface_fct(linop)
    linop2 = LinearHOODEOperator(epsilon, sparse(A))
    testcommon_interface_fct(linop2)
    testcommon_interface_fct(DiffEqArrayOperator((1/epsilon)*A))
end

testcommon_interface_interpolate_float()
testcommon_interface_time()
testcommon_interface_time_time()
testcommon_interface_epsilon()
testcommon_interface_interpolate()
testcommon_interface_short()


#=
Compute the condition number given the initial graph and the preconditioner.
Uses cholfact, so, if the initial graph is not sdd or if inverse has a lot of nonzeros this fails.
=#
function condNumber{Tv,Ti}(la::SparseMatrixCSC{Tv,Ti}, prec::SparseMatrixCSC{Tv,Ti}; tol = 1e-5)
	# cf = cholfact(prec)
	n = la.n
	ldl = ldltfact(prec)
	diag = 1 ./ (ldl[:D] \ ones(n))

	return computeCN(la, ldl[:UP], diag, tol=tol)
end

# M = Ut * d * U
function condNumber{Tv,Ti}(la::SparseMatrixCSC{Tv,Ti}, U::UpperTriangular{Tv,SparseMatrixCSC{Tv,Ti}}, d::Array{Tv,1}; tol = 1e-5)
	return computeCN(la, U, d, tol=tol)
end

# M = Ut * U
function condNumber{Tv,Ti}(la::SparseMatrixCSC{Tv,Ti}, U::UpperTriangular{Tv,SparseMatrixCSC{Tv,Ti}}; tol = 1e-5)
	return computeCN(la, U, ones(a.n), tol=tol)
end

function computeCN{Tv,Ti}(la::SparseMatrixCSC{Tv,Ti}, U, d::Array{Tv,1}; tol=1e-5)

	n = la.n

	# get the max eigenvalue of d^-1/2 * L^-1 * a * U^-1 * d^-1/2
	f = function(b::Array{Float64,1})
		res = copy(b)

		# diag sqrt inverse
		for i in 1:(n - 1)
            res[i] = res[i] * d[i]^(-1/2)
        end
		res[n] = 0

		res = U \ res
		res = la * res
		res = U' \ res

		# diag sqrt inverse
        for i in 1:(n - 1)
            res[i] = res[i] * d[i]^(-1/2)
        end
        res[n] = 0

		return res
	end
	fOp = SqLinOp(true,1.0,n,f)
	lmax = eigs(fOp;nev=1,which=:LM,tol=0.0)[1][1]

	if lmax < 1e-20
		# If lambdamax is too small, the computation of 1 - 1/K won't be possible.
		# So, just return a huge condition number (which should be true anyway)
		return 1,1e20
	end

	# get the max eigenvalue of 1 - (M^-1/2 L M^-1/2) / lambdaMax
	g = function(b::Array{Float64,1})
		res2 = copy(b)
		res = copy(b) / lmax

		# diag sqrt inverse
		for i in 1:(n - 1)
            res[i] = res[i] * d[i]^(-1/2)
        end
		res[n] = 0

		res = U \ res
		res = la * res
		res = U' \ res

		# diag sqrt inverse
		for i in 1:(n - 1)
            res[i] = res[i] * d[i]^(-1/2)
        end
		res[n] = res2[n] = 0

		return res2 - res
	end
	gOp = SqLinOp(true,1.0,n,g)

    #R = eigs(gOp;nev=1,which=:LM,tol=tol)[1][1]
	R = powerIteration(gOp, tol=tol, maxit=100)

    Kmin = 1 / (1 - R)
    Kmax = 1 / (1 - R - tol)

    if Kmax < 0
    	while Kmax < 0
    		tol = tol / 10

    		R = powerIteration(gOp, tol=tol)

		    Kmin = 1 / (1 - R)
		    Kmax = 1 / (1 - R - tol)
    	end

    	println("Initial tolerance too large. Recomputed with tolerance = ", tol)
    end

    return (Kmin, Kmax)

end

# function computeCN{Tv,Ti}(la::SparseMatrixCSC{Tv,Ti}, U::UpperTriangular{Tv,SparseMatrixCSC{Tv,Ti}}, d::Array{Tv,1}; tol=1e-3)

# 	n = la.n

# 	# get the max eigenvalue of d^-1/2 * L^-1 * a * U^-1 * d^-1/2
# 	f = function(b::Array{Float64,1})
# 		res = copy(b)

# 		# diag sqrt inverse
# 		res = res .* d.^-0.5
# 		res[n] = 0

# 		res = U \ res
# 		res = la * res
# 		res = U' \ res

# 		# diag sqrt inverse
#         res = res .* d.^-0.5
#         res[n] = 0

# 		return res
# 	end
# 	fOp = SqLinOp(true,1.0,n,f)
# 	lmax = eigs(fOp;nev=1,which=:LM,tol=0.0)[1][1]

# 	# get the max eigenvalue of 1 - (M^-1/2 L M^-1/2) / lambdaMax
# 	g = function(b::Array{Float64,1})
# 		res2 = copy(b)
# 		res = copy(b) / lmax

# 		# diag sqrt inverse
# 		res = res .* d.^-0.5
# 		res[n] = 0

# 		res = U \ res
# 		res = la * res
# 		res = U' \ res

# 		# diag sqrt inverse
# 		res = res .* d.^-0.5
# 		res[n] = res2[n] = 0

# 		return res2 - res
# 	end
# 	gOp = SqLinOp(true,1.0,n,g)

# 	R = eigs(gOp;nev=1,which=:LM,tol=tol)[1][1]

#     Kmin = 1 / (1 - R)
#     Kmax = 1 / (1 - R - tol)

#     return (Kmin, Kmax)

# end


# ---
# title: 1399. Count Largest Group
# id: problem1399
# author: Tian Jun
# date: 2020-10-31
# difficulty: Easy
# categories: Array
# link: <https://leetcode.com/problems/count-largest-group/description/>
# hidden: true
# ---
#
# Given an integer `n`. Each number from `1` to `n` is grouped according to the
# sum of its digits.
#
# Return how many groups have the largest size.
#
#
#
# **Example 1:**
#
#
#
#     Input: n = 13
#     Output: 4
#     Explanation: There are 9 groups in total, they are grouped according sum of its digits of numbers from 1 to 13:
#     [1,10], [2,11], [3,12], [4,13], [5], [6], [7], [8], [9]. There are 4 groups with largest size.
#
#
# **Example 2:**
#
#
#
#     Input: n = 2
#     Output: 2
#     Explanation: There are 2 groups [1], [2] of size 1.
#
#
# **Example 3:**
#
#
#
#     Input: n = 15
#     Output: 6
#
#
# **Example 4:**
#
#
#
#     Input: n = 24
#     Output: 5
#
#
#
#
# **Constraints:**
#
#   * `1 <= n <= 10^4`
#
#
## @lc code=start
using LeetCode

## add your code here:
## @lc code=end


using ProgressMeter, SharedArrays

println("Single thread:...")
@time begin
  a = collect(1:1000000)
  @showprogress for i in 1:length(a)
    while a[i] != 1
      if a[i] % 2 == 0
        a[i] = a[i] / 2
      else
        a[i] = 3 * a[i] + 1
      end
    end
  end
end

println("Multithread:...")
@time begin
  a = collect(1:1000000)::SharedArray{Int}
  @showprogress @distributed for i in 1:length(a)
    while a[i] != 1
      if a[i] % 2 == 0
        a[i] = a[i] / 2
      else
        a[i] = 3 * a[i] + 1
      end
    end
  end
end


# For vectors of probabilities in columns of P compute the convolution

# Input:
# P:        Matrix P_{ij} of probabilities P(X_j = i - 1)
#           for i = 1, ..., n and j = 1, ..., k

# Output:
# p:        Vector of probabilities P(X_1 + ... + X_k = i)
#           for i = 0, ..., n

function GetProbFromP(P::Array{Float64, 2})::Vector{Float64}
    dims = size(P)[2]

    if dims == 1
        return P[:, 1]
    end

    if dims > 2
        for i = dims - 1:-1:2
            P[:, i] = convolution(P[:, i], P[:, i+1])
        end
    end

    convolution(P[:, 1], P[:, 2])
end


# ------------------------------------------------------------------
# Licensed under the MIT License. See LICENSE in the project root.
# ------------------------------------------------------------------

@userplot DistPlot1D

@recipe function f(dp::DistPlot1D; quantiles=[0.25,0.50,0.75])
  # retrieve inputs
  sdata = dp.args[1]
  var   = dp.args[2]

  # fit spatial statistics
  h = EmpiricalHistogram(sdata, var)
  q = quantile(sdata, var, quantiles)
  μ = mean(sdata, var)

  legend --> false
  framestyle --> :box
  grid --> false
  xguide --> var

  # plot histogram
  @series begin
    seriestype --> :step
    seriescolor --> :black
    h
  end

  # plot quantiles
  @series begin
    seriestype --> :vline
    primary --> false
    seriescolor --> :black
    linestyle --> :dash
    q
  end

  # plot mean
  @series begin
    seriestype --> :vline
    primary --> false
    seriescolor --> :green
    [μ]
  end
end


#######################
# MathAST Definitions

abstract type MathExpressionFragment end

struct MathSequence <:MathExpressionFragment
    fragments::Vector{MathExpressionFragment}
end
Base.length(ms::MathSequence) = length(ms.fragments)

const MathSymbolInner = Union{Number, Symbol}
struct MathSymbol{T<:MathSymbolInner} <: MathExpressionFragment
    sym::T
end
MathSymbol(s::AbstractString) = MathSymbol(Symbol(s))

struct MathFrac <: MathExpressionFragment
    numer::MathExpressionFragment
    denom::MathExpressionFragment
end

struct MathSuperscript <: MathExpressionFragment
    base::MathExpressionFragment
    expo::MathExpressionFragment
end

struct MathSubscript <: MathExpressionFragment
    base::Symbol
    sub::Symbol
end

struct MathFunctionCall <: MathExpressionFragment
    fname::Symbol
    arg::MathSequence
end

struct MathMatrix <: MathExpressionFragment
    mat::Matrix{MathExpressionFragment}
end


using FastRounding

using Test

const SysFloat = Union{Float64, Float32}

a = 4/3
b = 1/7
c = -b

function testrounding(op::Function, a::T, b::T, mode::RoundingMode) where T<: SysFloat
    S = typeof(op(a,b))

    c = setrounding(BigFloat, mode) do
            S(op(big(a), big(b)))
    end
    return c
end

@testset "Test Rounding" begin
    @testset "a + b" begin
        @test testrounding(+, a, b, RoundUp) == add_round(a, b, RoundUp)
        @test testrounding(+, a, b, RoundDown) == add_round(a, b, RoundDown)
        @test testrounding(+, a, b, RoundToZero) == add_round(a, b, RoundToZero)
        @test testrounding(+, a, b, RoundNearest) == add_round(a, b, RoundNearest)
    end
    @testset "a - b" begin
        @test testrounding(-, a, b, RoundUp) == sub_round(a, b, RoundUp)
        @test testrounding(-, a, b, RoundDown) == sub_round(a, b, RoundDown)
        @test testrounding(-, a, b, RoundToZero) == sub_round(a, b, RoundToZero)
        @test testrounding(-, a, b, RoundNearest) == sub_round(a, b, RoundNearest)
    end
    @testset "a * b" begin
        @test testrounding(*, a, b, RoundUp) == mul_round(a, b, RoundUp)
        @test testrounding(*, a, b, RoundDown) == mul_round(a, b, RoundDown)
        @test testrounding(*, a, b, RoundToZero) == mul_round(a, b, RoundToZero)
        @test testrounding(*, a, b, RoundNearest) == mul_round(a, b, RoundNearest)
    end
    @testset "a / b" begin
        @test testrounding(/, a, b, RoundUp) == div_round(a, b, RoundUp)
        @test testrounding(/, a, b, RoundDown) == div_round(a, b, RoundDown)
        @test testrounding(/, a, b, RoundToZero) == div_round(a, b, RoundToZero)
        @test testrounding(/, a, b, RoundNearest) == div_round(a, b, RoundNearest)
    end
    @testset "a + c" begin
        @test testrounding(+, a, c, RoundUp) == add_round(a, c, RoundUp)
        @test testrounding(+, a, c, RoundDown) == add_round(a, c, RoundDown)
        @test testrounding(+, a, c, RoundToZero) == add_round(a, c, RoundToZero)
        @test testrounding(+, a, c, RoundNearest) == add_round(a, c, RoundNearest)
    end
    @testset "a - c" begin
        @test testrounding(-, a, c, RoundUp) == sub_round(a, c, RoundUp)
        @test testrounding(-, a, c, RoundDown) == sub_round(a, c, RoundDown)
        @test testrounding(-, a, c, RoundToZero) == sub_round(a, c, RoundToZero)
        @test testrounding(-, a, c, RoundNearest) == sub_round(a, c, RoundNearest)
    end
    @testset "a * c" begin
        @test testrounding(*, a, c, RoundUp) == mul_round(a, c, RoundUp)
        @test testrounding(*, a, c, RoundDown) == mul_round(a, c, RoundDown)
        @test testrounding(*, a, c, RoundToZero) == mul_round(a, c, RoundToZero)
        @test testrounding(*, a, c, RoundNearest) == mul_round(a, c, RoundNearest)
    end
    @testset "a / c" begin
        @test testrounding(/, a, c, RoundUp) == div_round(a, c, RoundUp)
        @test testrounding(/, a, c, RoundDown) == div_round(a, c, RoundDown)
        @test testrounding(/, a, c, RoundToZero) == div_round(a, c, RoundToZero)
        @test testrounding(/, a, c, RoundNearest) == div_round(a, c, RoundNearest)
    end
    @testset "1.0 + 0.0" begin
        @test testrounding(+, 1.0, 0.0, RoundUp) == 1.0
        @test testrounding(+, 1.0, 0.0, RoundDown) == 1.0
        @test testrounding(+, 1.0, 0.0, RoundToZero) == 1.0
        @test testrounding(+, 1.0, 0.0, RoundNearest) == 1.0
    end
    @testset "1.0 - 0.0" begin
        @test testrounding(-, 1.0, 0.0, RoundUp) == 1.0
        @test testrounding(-, 1.0, 0.0, RoundDown) == 1.0
        @test testrounding(-, 1.0, 0.0, RoundToZero) == 1.0
        @test testrounding(-, 1.0, 0.0, RoundNearest) == 1.0
    end
    @testset "1.0 * 1.0" begin
        @test testrounding(*, 1.0, 1.0, RoundUp) == 1.0
        @test testrounding(*, 1.0, 1.0, RoundDown) == 1.0
        @test testrounding(*, 1.0, 1.0, RoundToZero) == 1.0
        @test testrounding(*, 1.0, 1.0, RoundNearest) == 1.0
    end
    @testset "1.0 / 1.0" begin
        @test testrounding(/, 1.0, 1.0, RoundUp) == 1.0
        @test testrounding(/, 1.0, 1.0, RoundDown) == 1.0
        @test testrounding(/, 1.0, 1.0, RoundToZero) == 1.0
        @test testrounding(/, 1.0, 1.0, RoundNearest) == 1.0
    end
end


module LSHFunctions

using Distributions, LinearAlgebra, SparseArrays

#========================
Common types/utilities used throughout the module
========================#

include(joinpath("utils", "vecops.jl"))
include("LSHBase.jl")
include("intervals.jl")
include("similarities.jl")

#========================
Hash functions
========================#

include(joinpath("hashes", "simhash.jl"))
include(joinpath("hashes", "minhash.jl"))
include(joinpath("hashes", "lphash.jl"))
include(joinpath("hashes", "sign_alsh.jl"))

# Must be placed last, since it uses the definitions of LSHFunction subtypes
# defined in the other files.
include(joinpath("hashes", "lshfunction.jl"))

#========================
Function hashing
========================#

include(joinpath("function_hashing", "monte_carlo.jl"))

#========================
Exports
========================#

# Similarity functions, norms, inner products
export cossim, inner_prod, ℓ1, ℓ2, ℓp, L1, L2, Lp, ℓ1_norm, ℓ2_norm,
       ℓp_norm, L1_norm, L2_norm, Lp_norm, jaccard

# Hash functions
export SimHash, L1Hash, L2Hash, MIPSHash, SignALSH, MinHash,
       LSHFunction, MonteCarloHash, SymmetricLSHFunction,
       AsymmetricLSHFunction

# Helper / utility functions for LSHFunctions
export index_hash, query_hash, n_hashes, hashtype, similarity, lsh_family,
       embedded_similarity, collision_probability, @interval, HashCompressor

end # module


using PtFEM

data = Dict(
  # GenericSolid(ndim, nst, nels, nn, nip, finite_element(nod, nodof), axisymmentrix)
  :struc_el => GenericSolid(3, 6, 6, 8, 1, Tetrahedron(3), false),
  :properties => [100.0 0.3 0.0;],
  :g_coord => [
    0.0 0.0 0.0; 1.0 0.0 0.0; 0.0 0.0 -1.0; 1.0 0.0 -1.0;
    0.0 1.0 0.0; 1.0 1.0 0.0; 0.0 1.0 -1.0; 1.0 1.0 -1.0],
  :g_num => [
    1, 3, 4, 7,
    1, 4, 2, 7,
    1, 2, 5, 7,
    6, 4, 8, 7,
    6, 2, 4, 7,
    6, 5, 2, 7
  ],
  :support => [
    (1, [0 0 1]), (2, [1 0 1]), (3, [0 0 0]), (4, [1 0 0]),
    (5, [0 1 1]), (7, [0 1 0]), (8, [1 1 0])
  ],
  :loaded_nodes => [
    ( 1, [0.0 0.0 -1/6]),
    ( 2, [0.0 0.0 -1/3]),
    ( 5, [0.0 0.0 -1/3]),
    ( 6, [0.0 0.0 -1/6])
  ]
)

@time m = p54(data)
println()


macro import_rectangle_huge()
    :(
        using ComplexVisual:
            CV_Rectangle, cv_rect_blwh, cv_width, cv_height, cv_is_inside,
            cv_anchor, cv_pad, cv_intersect, CV_RectangleStore,
            cv_add_rectangle!, cv_find_first_nonempty_intersection,
            cv_compute_bounding_box
    )
end

import Base:show

"""
A rectangle with non-positive width and/or non-positive height is considered as "empty".

A rectangle is always given/specified by the coordinates of the upper-left and lower-right corner.
"""
struct CV_Rectangle{N<:Number}
    top    :: N
    left   :: N
    bottom :: N
    right  :: N
    empty  :: Bool

    function CV_Rectangle(top::T, left::T, bottom::T, right::T) where {T<:Number}
        if !(isfinite(top) && isfinite(left) && isfinite(bottom) && isfinite(right))
            cv_error(
                "Only finite values are supported",
                "; top = ", top, "; left = ", left, "; bottom = ", bottom, "; right = ", right)
        end
        empty = (left >= right) || (bottom >= top)
        return new{T}(top, left, bottom, right, empty)
    end
end

"""
    cv_rect_blwh(::Type{T}, bottom, left, width, height) where {T<:Real}

    bottom   T
    left     T      (T <: Real)
    width    T
    height   T

creates rectangle (with given number type).
"""
function cv_rect_blwh(::Type{T},
        bottom::Real, left::Real, width::Real, height::Real) where {T<:Real}
    return CV_Rectangle(T(bottom + height), T(left), T(bottom), T(left + width))
end

function CV_Rectangle(type::Type{T}) where {T<:Number}
    null = zero(T)
    return CV_Rectangle(null, null, null, null)
end
function CV_Rectangle(width::T, height::T) where {T<:Number}
    return cv_rect_blwh(T, zero(T), zero(T), width, height)
end
function CV_Rectangle(type::Type{T}, width::Real, height::Real) where {T<:Number}
    return cv_rect_blwh(T, zero(T), zero(T), T(width), T(height))
end

function show(io::IO, rect::CV_Rectangle) # {{{
    char = rect.empty ? '▭' : '▬'
    print(io, char);
    show(io, rect.left);   print(io, '→'); show(io, rect.right)
    print(io, ", ")
    show(io, rect.bottom); print(io, '↑'); show(io, rect.top)
    print(io, char)
    return nothing
end

function show(io::IO, ::MIME{Symbol("text/plain")}, rect::CV_Rectangle)
    compact = get(io, :compact, false) :: Bool
    if !compact
        print(io, "CV_Rectangle(")
    end
    show(io, rect)
    if !compact
        print(io, ")")
    end
    return nothing
end # }}}

cv_width(rect::CV_Rectangle) = rect.right - rect.left
cv_height(rect::CV_Rectangle) = rect.top - rect.bottom

"""
    cv_pad(rect::CV_Rectangle{N}, top, left=top, bottom=top, right=left)

create a padded version of `rect`. `top`, `left`, `bottom`, `right` are
padding values.
"""
function cv_pad(rect::CV_Rectangle{N},
        top::Number, left::Number=top,
        bottom::Number=top, right::Number=left) where {N<:Number}
    return CV_Rectangle(
        rect.top + N(top),
        rect.left - N(left),
        rect.bottom - N(bottom),
        rect.right + N(right))
end

function cv_is_inside(rect::CV_Rectangle{N}, x::N, y::N) where {N}
    return (rect.left ≤ x ≤ rect.right) && (rect.bottom ≤ y ≤ rect.top)
end

"""
return anchor for a rectangle.

Coor-system

     ┌─────────>
     │   north/bottom
     │  w┌───┐e
     │  e│   │a
     │  s│   │s
     │  t└───┘t
     v   south/top

Supported anchors (for a rectangle) are:
```
:north        :south        :east    :west    :center
:northeast    :southeast
:southwest    :northwest
```
"""
function cv_anchor(r::CV_Rectangle{N},
                   anchor_name::Symbol) where {N<:Number}
    return (
        anchor_name == :north     ? (cv_half(r.left + r.right), r.bottom)  :
        anchor_name == :south     ? (cv_half(r.left + r.right), r.top)     :
        anchor_name == :east      ? (r.right, cv_half(r.top + r.bottom))   :
        anchor_name == :west      ? (r.left, cv_half(r.top + r.bottom))    :
        anchor_name == :northeast ? (r.right, r.bottom)                    :
        anchor_name == :southeast ? (r.right, r.top)                       :
        anchor_name == :southwest ? (r.left, r.top)                        :
        anchor_name == :northwest ? (r.left, r.bottom)                     :
        anchor_name == :center    ? (cv_half(r.left + r.right),
                                      cv_half(r.top + r.bottom))           :
        cv_error("Unknown anchor_name: ", string(anchor_name))) :: Tuple{N, N}
end

"""
return rectangle with intersection of rectA and rectB.

If already rectA and/or rectB is empty then an empty rectangle is returned.
"""
function cv_intersect(rectA::CV_Rectangle{N}, rectB::CV_Rectangle{N}) where {N<:Number}
    if rectA.empty
        return rectA
    end
    if rectB.empty
        return rectB
    end
    # Non-empty case
    top = min(rectA.top, rectB.top)
    left = max(rectA. left, rectB.left)
    bottom = max(rectA.bottom, rectB.bottom)
    right = min(rectA.right, rectB.right)
    return CV_Rectangle(top, left, bottom, right)
end

"""
This helper is used to get the smallest bounding box for rectangles and for intersection tests.
No painting/drawing is done.
"""
mutable struct CV_RectangleStore{N<:Number}
    rectangles    :: Vector{CV_Rectangle{N}}
    bounding_box  :: CV_Rectangle{N}

    function CV_RectangleStore(type::Type{T}) where {T<:Number}
        rectangles = Vector{CV_Rectangle{T}}()
        bounding_box = CV_Rectangle(type)
        return new{T}(rectangles, bounding_box)
    end
end

function show(io::IO, store::CV_RectangleStore{N}) where {N} # {{{
    print(io, "CV_RectangleStore")
    print(io, "(bb: ")
    show(io, store.bounding_box)
    no_rect = length(store.rectangles)
    print(io, ", #", no_rect)
    print(io, ')')
    return nothing
end

function show(io::IO, mime::MIME{Symbol("text/plain")},
              store::CV_RectangleStore{N}) where {N}
    print(io, "CV_RectangleStore{")
    show(io, N)
    print(io, "}(bounding_box: ")
    show(io, mime, store.bounding_box)
    print(io, "; with ")
    no_rect = length(store.rectangles)
    print(io, no_rect, " rectangle")
    if no_rect != 1
        print(io, 's')
    end
    print(io, ')')
    return nothing
end # }}}

"""
Internal method for bonding_box update with new/last rectangle in rectangles.
"""
function cv_update_bounding_box!(store::CV_RectangleStore{N}) where {N<:Number}
    rect = store.rectangles[end]
    bb = store.bounding_box
    if bb.empty
        store.bounding_box = rect
    else
        top = max(rect.top, bb.top)
        left = min(rect.left, bb.left)
        bottom = min(rect.bottom, bb.bottom)
        right = max(rect.right, bb.right)
        store.bounding_box = CV_Rectangle(top, left, bottom, right)
    end
    return nothing
end

"""
add rectangle to store and update bounding box.
"""
function cv_add_rectangle!(store::CV_RectangleStore{N}, rect::CV_Rectangle{N}) where {N<:Number}

    if rect.empty
        cv_error("Only non-empty rectangles can be added.")
    end
    push!(store.rectangles, rect)
    cv_update_bounding_box!(store)
    return nothing
end

"""
find first non-empty intersection. If all intersections are empty then an empty rectangle is returned.
"""
function cv_find_first_nonempty_intersection(
            store::CV_RectangleStore{N}, rect::CV_Rectangle{N}) where {N<:Number}
    last_intersection = CV_Rectangle(zero(N), zero(N), zero(N), zero(N))
    if !(rect.empty)
        for test_rect in store.rectangles
            result = cv_intersect(rect, test_rect)
            if !(result.empty)
                last_intersection = result
                break
            end
        end
    end
    return last_intersection
end

function cv_compute_bounding_box(rects::CV_Rectangle{N}...) where {N}
    store = CV_RectangleStore(N)
    for rect in rects
        cv_add_rectangle!(store, rect)
    end
    return store.bounding_box
end

# vim:syn=julia:cc=79:fdm=marker:sw=4:ts=4:


using CUDA.CUTENSOR
using CUDA
using LinearAlgebra

@test has_cutensor()
@test CUTENSOR.version() isa VersionNumber

@testset "CuTensor type basics" begin
    N = 2
    dmax = 2^div(18,N)
    dims = rand(2:dmax, N)
    p = randperm(N)
    indsA = collect(('a':'z')[1:N])
    dimsA = dims
    A = rand(Float64, dimsA...)
    dA = CuArray(A)
    p = randperm(N)
    indsA = collect(('a':'z')[1:N])
    ctA = CuTensor(dA, indsA)
    @test length(ctA) == length(A)
    @test size(ctA) == size(A)
    @test size(ctA, 1) == size(A, 1)
    @test ndims(ctA) == ndims(A)
    @test strides(ctA) == strides(A)
    @test eltype(ctA) == eltype(A)
end

@testset "Elementwise binary" begin
    eltypes = ((Float16, Float16),
               #(Float16, Float32),
               (Float32, Float32),
               #(Float32, Float64),
               (Float64, Float64),
               #(ComplexF16, ComplexF16), (ComplexF16, ComplexF32),
               (ComplexF32, ComplexF32), #(ComplexF32, ComplexF64),
               (ComplexF64, ComplexF64))
    @testset for N=2:5
        @testset for (eltyA, eltyC) in eltypes
            # setup
            eltyD = eltyC
            dmax  = 2^div(18,N)
            dims  = rand(2:dmax, N)
            p     = randperm(N)
            indsA = collect(('a':'z')[1:N])
            indsC = indsA[p]
            dimsA = dims
            dimsC = dims[p]
            A     = rand(eltyA, dimsA...)
            dA    = CuArray(A)
            C     = rand(eltyC, dimsC...)
            dC    = CuArray(C)
            Mem.pin(A)
            Mem.pin(C)

            # simple case
            opA   = CUTENSOR.CUTENSOR_OP_IDENTITY
            opC   = CUTENSOR.CUTENSOR_OP_IDENTITY
            dD    = similar(dC, eltyD)
            opAC  = CUTENSOR.CUTENSOR_OP_ADD
            dD    = CUTENSOR.elementwiseBinary!(1, dA, indsA, opA, 1, dC, indsC, opC,
                                                dD, indsC, opAC)
            D = collect(dD)
            @test D ≈ permutedims(A, p) .+ C
            Dsimple = similar(C)
            Mem.pin(Dsimple)
            Dsimple = CUTENSOR.elementwiseBinary!(1, A, indsA, opA, 1, C, indsC, opC,
                                                  Dsimple, indsC, opAC)
            synchronize()
            @test Dsimple ≈ permutedims(A, p) .+ C

            # using integers as indices
            dD = CUTENSOR.elementwiseBinary!(1, dA, 1:N, opA, 1, dC, p, opC, dD, p, opAC)
            D = collect(dD)
            @test D ≈ permutedims(A, p) .+ C
            Dint = zeros(eltyC, dimsC...)
            Mem.pin(Dint)
            Dint = CUTENSOR.elementwiseBinary!(1, A, 1:N, opA, 1, C, p, opC, Dint, p, opAC)
            synchronize()
            @test Dint ≈ permutedims(A, p) .+ C

            # multiplication as binary operator
            opAC = CUTENSOR.CUTENSOR_OP_MUL
            dD = CUTENSOR.elementwiseBinary!(1, dA, indsA, opA, 1, dC, indsC, opC,
                                                dD, indsC, opAC)
            D = collect(dD)
            @test D ≈ permutedims(A, p) .* C
            Dmult = zeros(eltyC, dimsC...)
            Mem.pin(Dmult)
            Dmult = CUTENSOR.elementwiseBinary!(1, A, indsA, opA, 1, C, indsC, opC,
                                                Dmult, indsC, opAC)
            synchronize()
            @test Dmult ≈ permutedims(A, p) .* C

            # with non-trivial coefficients and conjugation
            opA = eltyA <: Complex ? CUTENSOR.CUTENSOR_OP_CONJ :
                                    CUTENSOR.CUTENSOR_OP_IDENTITY
            opC = CUTENSOR.CUTENSOR_OP_IDENTITY
            opAC = CUTENSOR.CUTENSOR_OP_ADD
            α = rand(eltyD)
            γ = rand(eltyD)
            dD = CUTENSOR.elementwiseBinary!(α, dA, indsA, opA, γ, dC, indsC, opC,
                                                dD, indsC, opAC)
            D = collect(dD)
            @test D ≈ α .* conj.(permutedims(A, p)) .+ γ .* C
            Dnontrivial = similar(C)
            Mem.pin(Dnontrivial)
            Dnontrivial = CUTENSOR.elementwiseBinary!(α, A, indsA, opA, γ, C, indsC, opC,
                                                Dnontrivial, indsC, opAC)
            synchronize()
            @test Dnontrivial ≈ α .* conj.(permutedims(A, p)) .+ γ .* C

            # test in-place, and more complicated unary and binary operations
            opA = eltyA <: Complex ? CUTENSOR.CUTENSOR_OP_IDENTITY :
                                    CUTENSOR.CUTENSOR_OP_SQRT
            # because we use rand, entries of A will be positive when elty is real
            opC = eltyC <: Complex ? CUTENSOR.CUTENSOR_OP_CONJ :
                                    CUTENSOR.CUTENSOR_OP_IDENTITY
            opAC = eltyD <: Complex ? CUTENSOR.CUTENSOR_OP_ADD :
                                    CUTENSOR.CUTENSOR_OP_MAX
            α = rand(eltyD)
            γ = rand(eltyD)
            dD = CUTENSOR.elementwiseBinary!(α, dA, indsA, opA, γ, dC, indsC, opC,
                                                dC, indsC, opAC)
            D = collect(dC)
            if eltyD <: Complex
                if eltyA <: Complex
                    @test D ≈ α .* permutedims(A, p) .+ γ .* conj.(C)
                else
                    @test D ≈ α .* sqrt.(convert.(eltyD, permutedims(A, p))) .+
                                γ .* conj.(C)
                end
            else
                @test D ≈ max.(α .* sqrt.(convert.(eltyD, permutedims(A, p))), γ .* C)
            end            # # using host memory

            # using CuTensor type
            dA = CuArray(A)
            dC = CuArray(C)
            ctA = CuTensor(dA, indsA)
            ctC = CuTensor(dC, indsC)
            ctD = ctA + ctC
            hD = collect(ctD.data)
            @test hD ≈ permutedims(A, p) .+ C
            ctD = ctA - ctC
            hD = collect(ctD.data)
            @test hD ≈ permutedims(A, p) .- C

            α = rand(eltyD)
            ctD = LinearAlgebra.axpy!(α, ctA, ctC)
            hD = collect(ctD.data)
            @test hD ≈ α.*permutedims(A, p) .+ C

            γ = rand(eltyD)
            ctD = LinearAlgebra.axpby!(α, ctA, γ, ctC)
            hD = collect(ctD.data)
            @test hD ≈ α.*permutedims(A, p) .+ γ.*C
        end
    end
end

@testset "Permutations" begin
    eltypes = ((Float16, Float16),
               #(Float16, Float32),
               (Float32, Float32),
               #(Float32, Float64),
               (Float64, Float64),
               #(ComplexF16, ComplexF16),
               #(ComplexF16, ComplexF32),
               (ComplexF32, ComplexF32),
               #(ComplexF32, ComplexF64),
               (ComplexF64, ComplexF64))
    @testset for N=2:5
        @testset for (eltyA, eltyC) in eltypes
            # setup
            dmax = 2^div(18,N)
            dims = rand(2:dmax, N)
            p = randperm(N)
            indsA = collect(('a':'z')[1:N])
            indsC = indsA[p]
            dimsA = dims
            dimsC = dims[p]
            A = rand(eltyA, dimsA...)
            Mem.pin(A)
            dA = CuArray(A)
            dC = similar(dA, eltyC, dimsC...)

            # simple case
            dC = CUTENSOR.permutation!(one(eltyA), dA, indsA, dC, indsC)
            C  = collect(dC)
            @test C == permutedims(A, p) # exact equality
            Csimple = zeros(eltyC, dimsC...)
            Mem.pin(Csimple)
            Csimple = CUTENSOR.permutation!(one(eltyA), A, indsA, Csimple, indsC)
            synchronize()
            @test Csimple == permutedims(A, p) # exact equality

            # with scalar
            α  = rand(eltyA)
            dC = CUTENSOR.permutation!(α, dA, indsA, dC, indsC)
            C  = collect(dC)
            @test C ≈ α * permutedims(A, p) # approximate, floating point rounding

            Cscalar = zeros(eltyC, dimsC...)
            Mem.pin(Cscalar)
            Cscalar = CUTENSOR.permutation!(α, A, indsA, Cscalar, indsC)
            synchronize()
            @test Cscalar ≈ α * permutedims(A, p) # approximate, floating point rounding
        end
    end
end

@testset "Elementwise trinary" begin
    eltypes = ((Float16, Float16, Float16),
               #(Float16, Float32, Float32),
               # (Float32, Float16, Float32),
                (Float32, Float32, Float32),
               # (Float32, Float32, Float64),
               # (Float32, Float64, Float64),
               # (Float64, Float32, Float64),
                (Float64, Float64, Float64),
                (ComplexF32, ComplexF32, ComplexF32),
                (ComplexF64, ComplexF64, ComplexF64))
    @testset for N=2:5
        @testset for (eltyA, eltyB, eltyC) in eltypes
            # setup
            eltyD = eltyC
            dmax = 2^div(18,N)
            dims = rand(2:dmax, N)
            pA = randperm(N)
            ipA = invperm(pA)
            pB = randperm(N)
            ipB = invperm(pB)
            indsC = collect(('a':'z')[1:N])
            dimsC = dims
            indsA = indsC[ipA]
            dimsA = dims[ipA]
            indsB = indsC[ipB]
            dimsB = dims[ipB]
            A = rand(eltyA, dimsA...)
            dA = CuArray(A)
            B = rand(eltyB, dimsB...)
            dB = CuArray(B)
            C = rand(eltyC, dimsC...)
            dC = CuArray(C)
            dD = similar(dC)

            # simple case
            opA = CUTENSOR.CUTENSOR_OP_IDENTITY
            opB = CUTENSOR.CUTENSOR_OP_IDENTITY
            opC = CUTENSOR.CUTENSOR_OP_IDENTITY
            opAB = CUTENSOR.CUTENSOR_OP_ADD
            opABC = CUTENSOR.CUTENSOR_OP_ADD
            dD = CUTENSOR.elementwiseTrinary!(1, dA, indsA, opA, 1, dB, indsB, opB,
                                                1, dC, indsC, opC, dD, indsC, opAB, opABC)
            D = collect(dD)
            @test D ≈ permutedims(A, pA) .+ permutedims(B, pB) .+ C

            # test with Mem.pinned host memory
            Mem.pin(A)
            Mem.pin(B)
            Mem.pin(C)
            Dc = similar(C)
            Mem.pin(Dc)
            Dc = CUTENSOR.elementwiseTrinary!(1, A, indsA, opA, 1, B, indsB, opB,
                                                1, C, indsC, opC, Dc, indsC, opAB, opABC)
            synchronize()
            @test Dc ≈ permutedims(A, pA) .+ permutedims(B, pB) .+ C

            # using integers as indices
            dD = CUTENSOR.elementwiseTrinary!(1, dA, ipA, opA, 1, dB, ipB, opB,
                                                1, dC, 1:N, opC, dD, 1:N, opAB, opABC)
            D = collect(dD)
            @test D ≈ permutedims(A, pA) .+ permutedims(B, pB) .+ C
            Dd = similar(C)
            Mem.pin(Dd)
            Dd = CUTENSOR.elementwiseTrinary!(1, A, ipA, opA, 1, B, ipB, opB,
                                                1, C, 1:N, opC, Dd, 1:N, opAB, opABC)
            synchronize()
            @test Dd ≈ permutedims(A, pA) .+ permutedims(B, pB) .+ C

            # multiplication as binary operator
            opAB = CUTENSOR.CUTENSOR_OP_MUL
            opABC = CUTENSOR.CUTENSOR_OP_ADD
            dD = CUTENSOR.elementwiseTrinary!(1, dA, indsA, opA, 1, dB, indsB, opB,
                                                1, dC, indsC, opC, dD, indsC, opAB, opABC)
            D = collect(dD)
            @test D ≈ (convert.(eltyD, permutedims(A, pA)) .* convert.(eltyD, permutedims(B, pB))) .+ C
            De = similar(C)
            Mem.pin(De)
            De = CUTENSOR.elementwiseTrinary!(1, A, indsA, opA, 1, B, indsB, opB,
                                                1, C, indsC, opC, De, indsC, opAB, opABC)
            synchronize()
            @test De ≈ (convert.(eltyD, permutedims(A, pA)) .* convert.(eltyD, permutedims(B, pB))) .+ C

            opAB = CUTENSOR.CUTENSOR_OP_ADD
            opABC = CUTENSOR.CUTENSOR_OP_MUL
            dD = CUTENSOR.elementwiseTrinary!(1, dA, indsA, opA, 1, dB, indsB, opB,
                                                1, dC, indsC, opC, dD, indsC, opAB, opABC)
            D = collect(dD)
            @test D ≈ (convert.(eltyD, permutedims(A, pA)) .+ convert.(eltyD, permutedims(B, pB))) .* C
            Df = similar(C)
            Mem.pin(Df)
            Df = CUTENSOR.elementwiseTrinary!(1, A, indsA, opA, 1, B, indsB, opB,
                                                1, C, indsC, opC, Df, indsC, opAB, opABC)
            synchronize()
            @test Df ≈ (convert.(eltyD, permutedims(A, pA)) .+ convert.(eltyD, permutedims(B, pB))) .* C

            opAB = CUTENSOR.CUTENSOR_OP_MUL
            opABC = CUTENSOR.CUTENSOR_OP_MUL
            dD = CUTENSOR.elementwiseTrinary!(1, dA, indsA, opA, 1, dB, indsB, opB,
                                                1, dC, indsC, opC, dD, indsC, opAB, opABC)
            D = collect(dD)
            @test D ≈ convert.(eltyD, permutedims(A, pA)) .*
                        convert.(eltyD, permutedims(B, pB)) .* C
            Dg = similar(C)
            Mem.pin(Dg)
            Dg = CUTENSOR.elementwiseTrinary!(1, A, indsA, opA, 1, B, indsB, opB,
                                                1, C, indsC, opC, Dg, indsC, opAB, opABC)
            synchronize()
            @test Dg ≈ convert.(eltyD, permutedims(A, pA)) .*
                        convert.(eltyD, permutedims(B, pB)) .* C

            # with non-trivial coefficients and conjugation
            α = rand(eltyD)
            β = rand(eltyD)
            γ = rand(eltyD)
            opA = eltyA <: Complex ? CUTENSOR.CUTENSOR_OP_CONJ :
                                    CUTENSOR.CUTENSOR_OP_IDENTITY
            opAB = CUTENSOR.CUTENSOR_OP_ADD
            opABC = CUTENSOR.CUTENSOR_OP_ADD
            dD = CUTENSOR.elementwiseTrinary!(α, dA, indsA, opA, β, dB, indsB, opB,
                                                γ, dC, indsC, opC, dD, indsC, opAB, opABC)
            D = collect(dD)
            @test D ≈ α .* conj.(permutedims(A, pA)) .+ β .* permutedims(B, pB) .+ γ .* C
            Dh = similar(C)
            Mem.pin(Dh)
            Dh = CUTENSOR.elementwiseTrinary!(α, A, indsA, opA, β, B, indsB, opB,
                                                γ, C, indsC, opC, Dh, indsC, opAB, opABC)
            synchronize()
            @test Dh ≈ α .* conj.(permutedims(A, pA)) .+ β .* permutedims(B, pB) .+ γ .* C

            opB = eltyB <: Complex ? CUTENSOR.CUTENSOR_OP_CONJ :
                                    CUTENSOR.CUTENSOR_OP_IDENTITY
            opAB = CUTENSOR.CUTENSOR_OP_ADD
            opABC = CUTENSOR.CUTENSOR_OP_ADD
            dD = CUTENSOR.elementwiseTrinary!(α, dA, indsA, opA, β, dB, indsB, opB,
                                                γ, dC, indsC, opC, dD, indsC, opAB, opABC)
            D = collect(dD)
            @test D ≈ α .* conj.(permutedims(A, pA)) .+
                        β .* conj.(permutedims(B, pB)) .+ γ .* C
            Di = similar(C)
            Mem.pin(Di)
            Di = CUTENSOR.elementwiseTrinary!(α, A, indsA, opA, β, B, indsB, opB,
                                                γ, C, indsC, opC, Di, indsC, opAB, opABC)
            synchronize()
            @test Di ≈ α .* conj.(permutedims(A, pA)) .+
                        β .* conj.(permutedims(B, pB)) .+ γ .* C

            opA = CUTENSOR.CUTENSOR_OP_IDENTITY
            opAB = CUTENSOR.CUTENSOR_OP_MUL
            opABC = CUTENSOR.CUTENSOR_OP_ADD
            dD = CUTENSOR.elementwiseTrinary!(α, dA, indsA, opA, β, dB, indsB, opB,
                                                γ, dC, indsC, opC, dD, indsC, opAB, opABC)
            D = collect(dD)
            @test D ≈ α .* permutedims(A, pA) .* β .* conj.(permutedims(B, pB)) .+ γ .* C
            Dj = similar(C)
            Mem.pin(Dj)
            Dj = CUTENSOR.elementwiseTrinary!(α, A, indsA, opA, β, B, indsB, opB,
                                                γ, C, indsC, opC, Dj, indsC, opAB, opABC)
            synchronize()
            @test Dj ≈ α .* permutedims(A, pA) .* β .* conj.(permutedims(B, pB)) .+ γ .* C

            # test in-place, and more complicated unary and binary operations
            opA = eltyA <: Complex ? CUTENSOR.CUTENSOR_OP_IDENTITY :
                                    CUTENSOR.CUTENSOR_OP_SQRT
            opB = eltyB <: Complex ? CUTENSOR.CUTENSOR_OP_IDENTITY :
                                    CUTENSOR.CUTENSOR_OP_SQRT
            # because we use rand, entries of A will be positive when elty is real
            opC = eltyC <: Complex ? CUTENSOR.CUTENSOR_OP_CONJ :
                                    CUTENSOR.CUTENSOR_OP_IDENTITY
            opAB = eltyD <: Complex ? CUTENSOR.CUTENSOR_OP_MUL :
                                    CUTENSOR.CUTENSOR_OP_MIN
            opABC = eltyD <: Complex ? CUTENSOR.CUTENSOR_OP_ADD :
                                    CUTENSOR.CUTENSOR_OP_MAX
            α = rand(eltyD)
            β = rand(eltyD)
            γ = rand(eltyD)
            dD = CUTENSOR.elementwiseTrinary!(α, dA, indsA, opA, β, dB, indsB, opB,
                                                γ, dC, indsC, opC, dC, indsC, opAB, opABC)
            D = collect(dD)
            if eltyD <: Complex
                if eltyA <: Complex && eltyB <: Complex
                    @test D ≈ α .* permutedims(A, pA) .* β .* permutedims(B, pB) .+
                                γ .* conj.(C)
                elseif eltyB <: Complex
                    @test D ≈ α .* sqrt.(convert.(eltyD, permutedims(A, pA))) .*
                                β .* permutedims(B, pB) .+ γ .* conj.(C)
                elseif eltyB <: Complex
                    @test D ≈ α .* permutedims(A, pA) .*
                                β .* sqrt.(convert.(eltyD, permutedims(B, pB))) .+
                                γ .* conj.(C)
                else
                    @test D ≈ α .* sqrt.(convert.(eltyD, permutedims(A, pA))) .*
                                β .* sqrt.(convert.(eltyD, permutedims(B, pB))) .+
                                γ .* conj.(C)
                end
            else
                @test D ≈ max.(min.(α .* sqrt.(convert.(eltyD, permutedims(A, pA))),
                                    β .* sqrt.(convert.(eltyD, permutedims(B, pB)))),
                                γ .* C)
            end
        end
    end
end

@testset "Reduction" begin
    eltypes = (#(Float16, Float16), #(Float16, Float32),
                            (Float32, Float32), #(Float32, Float64),
                            (Float64, Float64),
                            #(ComplexF16, ComplexF16), (ComplexF16, ComplexF32),
                            (ComplexF32, ComplexF32), #(ComplexF32, ComplexF64),
                            (ComplexF64, ComplexF64))
    @testset for NA=2:5, NC = 1:NA-1
        @testset for (eltyA, eltyC) in eltypes
            # setup
            eltyD = eltyC
            dmax = 2^div(18,NA)
            dims = rand(2:dmax, NA)
            p = randperm(NA)
            indsA = collect(('a':'z')[1:NA])
            indsC = indsA[p][1:NC]
            dimsA = dims
            dimsC = dims[p][1:NC]
            A = rand(eltyA, (dimsA...,))
            dA = CuArray(A)
            C = rand(eltyC, (dimsC...,))
            dC = CuArray(C)
            # setup
            Mem.pin(A)

            opA = CUTENSOR.CUTENSOR_OP_IDENTITY
            opC = CUTENSOR.CUTENSOR_OP_IDENTITY
            opReduce = CUTENSOR.CUTENSOR_OP_ADD
            # simple case
            dC = CUTENSOR.reduction!(1, dA, indsA, opA, 0, dC, indsC, opC, opReduce)
            C = collect(dC)
            @test reshape(C, (dimsC..., ones(Int,NA-NC)...)) ≈
                sum(permutedims(A, p); dims = ((NC+1:NA)...,))
            Csimple = zeros(eltyC, dimsC...)
            Mem.pin(Csimple)
            Csimple = CUTENSOR.reduction!(1, A, indsA, opA, 0, Csimple, indsC, opC, opReduce)
            synchronize()
            @test reshape(Csimple, (dimsC..., ones(Int,NA-NC)...)) ≈
                sum(permutedims(A, p); dims = ((NC+1:NA)...,))

            # using integers as indices
            Cinteger = zeros(eltyC, dimsC...)
            Mem.pin(Cinteger)
            dC = CUTENSOR.reduction!(1, dA, collect(1:NA), opA, 0, dC, p[1:NC], opC, opReduce)
            synchronize()
            C = collect(dC)
            @test reshape(C, (dimsC..., ones(Int,NA-NC)...)) ≈
                sum(permutedims(A, p); dims = ((NC+1:NA)...,))
            Cinteger = CUTENSOR.reduction!(1, A, collect(1:NA), opA, 0, Cinteger, p[1:NC], opC, opReduce)
            synchronize()
            @test reshape(Cinteger, (dimsC..., ones(Int,NA-NC)...)) ≈
                sum(permutedims(A, p); dims = ((NC+1:NA)...,))

            # multiplication as reduction operator
            opReduce = CUTENSOR.CUTENSOR_OP_MUL
            Cmult = zeros(eltyC, dimsC...)
            Mem.pin(Cmult)
            dC = CUTENSOR.reduction!(1, dA, indsA, opA, 0, dC, indsC, opC, opReduce)
            synchronize()
            C = collect(dC)
            @test reshape(C, (dimsC..., ones(Int,NA-NC)...)) ≈
                prod(permutedims(A, p); dims = ((NC+1:NA)...,)) atol=eps(Float16) rtol=Base.rtoldefault(Float16)
            Cmult = CUTENSOR.reduction!(1, A, indsA, opA, 0, Cmult, indsC, opC, opReduce)
            synchronize()
            @test reshape(Cmult, (dimsC..., ones(Int,NA-NC)...)) ≈
                prod(permutedims(A, p); dims = ((NC+1:NA)...,)) atol=eps(Float16) rtol=Base.rtoldefault(Float16)
            # NOTE: this test often yields values close to 0 that do not compare approximately

            # with non-trivial coefficients and conjugation
            opA = eltyA <: Complex ? CUTENSOR.CUTENSOR_OP_CONJ :
                                    CUTENSOR.CUTENSOR_OP_IDENTITY
            opC = CUTENSOR.CUTENSOR_OP_IDENTITY
            opReduce = CUTENSOR.CUTENSOR_OP_ADD
            C = rand(eltyC, (dimsC...,))
            dC = CuArray(C)
            α = rand(eltyC)
            γ = rand(eltyC)
            dC = CUTENSOR.reduction!(α, dA, indsA, opA, γ, dC, indsC, opC, opReduce)
            @test reshape(collect(dC), (dimsC..., ones(Int,NA-NC)...)) ≈
                α .* conj.(sum(permutedims(A, p); dims = ((NC+1:NA)...,))) .+ γ .* C
        end
    end
end

@testset "Contraction" begin
    eltypes = ( (Float32, Float32, Float32, Float32),
                (Float32, Float32, Float32, Float16),
                (ComplexF32, ComplexF32, ComplexF32, ComplexF32),
                (Float64, Float64, Float64, Float64),
                (Float64, Float64, Float64, Float32),
                (ComplexF64, ComplexF64, ComplexF64, ComplexF64),
                (ComplexF64, ComplexF64, ComplexF64, ComplexF32)
                )

    @testset for NoA=1:3, NoB=1:3, Nc=1:3
        @testset for (eltyA, eltyB, eltyC, eltyCompute) in eltypes
            # setup
            dmax = 2^div(18, max(NoA+Nc, NoB+Nc, NoA+NoB))
            dimsoA = rand(2:dmax, NoA)
            loA = prod(dimsoA)
            dimsoB = rand(2:dmax, NoB)
            loB = prod(dimsoB)
            dimsc = rand(2:dmax, Nc)
            lc = prod(dimsc)
            allinds = collect('a':'z')
            indsoA = allinds[1:NoA]
            indsoB = allinds[NoA .+ (1:NoB)]
            indsc = allinds[NoA .+ NoB .+ (1:Nc)]
            pA = randperm(NoA + Nc)
            ipA = invperm(pA)
            pB = randperm(Nc + NoB)
            ipB = invperm(pB)
            pC = randperm(NoA + NoB)
            ipC = invperm(pC)
            compute_rtol = (real(eltyCompute) == Float16 || real(eltyC) == Float16) ? 1e-2 : (real(eltyCompute) == Float32 ? 1e-4 : 1e-6)
            dimsA = [dimsoA; dimsc][pA]
            indsA = [indsoA; indsc][pA]
            dimsB = [dimsc; dimsoB][pB]
            indsB = [indsc; indsoB][pB]
            dimsC = [dimsoA; dimsoB][pC]
            indsC = [indsoA; indsoB][pC]

            A = rand(eltyA, (dimsA...,))
            Mem.pin(A)
            dA = CuArray(A)
            mA = reshape(permutedims(A, ipA), (loA, lc))
            B = rand(eltyB, (dimsB...,))
            Mem.pin(B)
            dB = CuArray(B)
            mB = reshape(permutedims(B, ipB), (lc, loB))
            C = zeros(eltyC, (dimsC...,))
            dC = CuArray(C)

            # simple case host side
            Cpin = zeros(eltyC, (dimsC...,))
            Mem.pin(Cpin)
            @test !any(isnan.(Cpin))
            opA = CUTENSOR.CUTENSOR_OP_IDENTITY
            opB = CUTENSOR.CUTENSOR_OP_IDENTITY
            opC = CUTENSOR.CUTENSOR_OP_IDENTITY
            opOut = CUTENSOR.CUTENSOR_OP_IDENTITY
            Cpin  = CUTENSOR.contraction!(1, A, indsA, opA, B, indsB, opB, 0, Cpin, indsC, opC, opOut)
            synchronize()
            mCpin = reshape(permutedims(Cpin, ipC), (loA, loB))
            @test !any(isnan.(A))
            @test !any(isnan.(B))
            @test !any(isnan.(mCpin))
            @test mCpin ≈ mA * mB rtol=compute_rtol

            # simple case
            opA = CUTENSOR.CUTENSOR_OP_IDENTITY
            opB = CUTENSOR.CUTENSOR_OP_IDENTITY
            opC = CUTENSOR.CUTENSOR_OP_IDENTITY
            opOut = CUTENSOR.CUTENSOR_OP_IDENTITY
            dC = CUTENSOR.contraction!(1, dA, indsA, opA, dB, indsB, opB, 0, dC, indsC, opC, opOut, compute_type=eltyCompute)
            C = collect(dC)
            mC = reshape(permutedims(C, ipC), (loA, loB))
            @test mC ≈ mA * mB rtol=compute_rtol

            # simple case with plan storage
            opA = CUTENSOR.CUTENSOR_OP_IDENTITY
            opB = CUTENSOR.CUTENSOR_OP_IDENTITY
            opC = CUTENSOR.CUTENSOR_OP_IDENTITY
            opOut = CUTENSOR.CUTENSOR_OP_IDENTITY
            plan  = CUTENSOR.plan_contraction(dA, indsA, opA, dB, indsB, opB, dC, indsC, opC, opOut)
            dC = CUTENSOR.contraction!(1, dA, indsA, opA, dB, indsB, opB, 0, dC, indsC, opC, opOut, plan=plan)
            C = collect(dC)
            mC = reshape(permutedims(C, ipC), (loA, loB))
            @test mC ≈ mA * mB

            # simple case with plan storage host-side
            opA = CUTENSOR.CUTENSOR_OP_IDENTITY
            opB = CUTENSOR.CUTENSOR_OP_IDENTITY
            opC = CUTENSOR.CUTENSOR_OP_IDENTITY
            opOut = CUTENSOR.CUTENSOR_OP_IDENTITY
            plan  = CUTENSOR.plan_contraction(A, indsA, opA, B, indsB, opB, C, indsC, opC, opOut)
            Cpin = CUTENSOR.contraction!(1, A, indsA, opA, B, indsB, opB, 0, Cpin, indsC, opC, opOut, plan=plan)
            synchronize()
            mC = reshape(permutedims(Cpin, ipC), (loA, loB))
            @test !any(isnan.(mC))
            @test mC ≈ mA * mB

            # simple case with plan storage and compute type
            opA = CUTENSOR.CUTENSOR_OP_IDENTITY
            opB = CUTENSOR.CUTENSOR_OP_IDENTITY
            opC = CUTENSOR.CUTENSOR_OP_IDENTITY
            opOut = CUTENSOR.CUTENSOR_OP_IDENTITY
            plan  = CUTENSOR.plan_contraction(dA, indsA, opA, dB, indsB, opB, dC, indsC, opC, opOut, compute_type=eltyCompute)
            dC = CUTENSOR.contraction!(1, dA, indsA, opA, dB, indsB, opB,
                                        0, dC, indsC, opC, opOut, plan=plan, compute_type=eltyCompute)
            C = collect(dC)
            mC = reshape(permutedims(C, ipC), (loA, loB))
            @test mC ≈ mA * mB rtol=compute_rtol

            # with non-trivial α
            α = rand(eltyCompute)
            dC = CUTENSOR.contraction!(α, dA, indsA, opA, dB, indsB, opB, zero(eltyCompute), dC, indsC, opC, opOut, compute_type=eltyCompute)
            C = collect(dC)
            mC = reshape(permutedims(C, ipC), (loA, loB))
            @test mC ≈ α * mA * mB rtol=compute_rtol

            α = rand(eltyCompute)
            Calpha = zeros(eltyC, (dimsC...,))
            Mem.pin(Calpha)
            @test !any(isnan.(Calpha))
            Calpha = CUTENSOR.contraction!(α, A, indsA, opA, B, indsB, opB, 0, Calpha, indsC, opC, opOut)
            synchronize()
            mCalpha = reshape(permutedims(collect(Calpha), ipC), (loA, loB))
            @test !any(isnan.(mCalpha))
            @test mCalpha ≈ α * mA * mB rtol=compute_rtol

            # with non-trivial β
            C = rand(eltyC, (dimsC...,))
            dC = CuArray(C)
            α = rand(eltyCompute)
            β = rand(eltyCompute)
            copyto!(dC, C)
            dD = CUTENSOR.contraction!(α, dA, indsA, opA, dB, indsB, opB, β, dC, indsC, opC, opOut, compute_type=eltyCompute)
            D = collect(dD)
            mC = reshape(permutedims(C, ipC), (loA, loB))
            mD = reshape(permutedims(D, ipC), (loA, loB))
            @test mD ≈ α * mA * mB + β * mC rtol=compute_rtol
            # with CuTensor objects
            if eltyCompute != Float32 && eltyC != Float16
                ctA = CuTensor(dA, indsA)
                ctB = CuTensor(dB, indsB)
                ctC = CuTensor(dC, indsC)
                ctC = LinearAlgebra.mul!(ctC, ctA, ctB)
                C2, C2inds = collect(ctC)
                mC = reshape(permutedims(C2, ipC), (loA, loB))
                @test mC ≈ mA * mB
                ctC = ctA * ctB
                C2, C2inds = collect(ctC)
                pC2 = convert.(Int, indexin(convert.(Char, C2inds), [indsoA; indsoB]))
                mC = reshape(permutedims(C2, invperm(pC2)), (loA, loB))
                @test mC ≈ mA * mB
            end
            # with conjugation flag for complex arguments
            if !((NoA, NoB, Nc) in ((1,1,3), (1,2,3), (3,1,2)))
            # not supported for these specific cases for unknown reason
                if eltyA <: Complex
                    opA   = CUTENSOR.CUTENSOR_OP_CONJ
                    opB   = CUTENSOR.CUTENSOR_OP_IDENTITY
                    opOut = CUTENSOR.CUTENSOR_OP_IDENTITY
                    dC    = CUTENSOR.contraction!(complex(1.0, 0.0), dA, indsA, opA, dB, indsB, opB,
                                                  0, dC, indsC, opC, opOut, compute_type=eltyCompute)
                    C     = collect(dC)
                    mC    = reshape(permutedims(C, ipC), (loA, loB))
                    @test mC ≈ conj(mA) * mB rtol=compute_rtol
                end
                if eltyB <: Complex
                    opA = CUTENSOR.CUTENSOR_OP_IDENTITY
                    opB = CUTENSOR.CUTENSOR_OP_CONJ
                    opOut = CUTENSOR.CUTENSOR_OP_IDENTITY
                    dC = CUTENSOR.contraction!(complex(1.0, 0.0), dA, indsA, opA, dB, indsB, opB,
                                               complex(0.0, 0.0), dC, indsC, opC, opOut, compute_type=eltyCompute)
                    C = collect(dC)
                    mC = reshape(permutedims(C, ipC), (loA, loB))
                    @test mC ≈ mA*conj(mB) rtol=compute_rtol
                end
                if eltyA <: Complex && eltyB <: Complex
                    opA = CUTENSOR.CUTENSOR_OP_CONJ
                    opB = CUTENSOR.CUTENSOR_OP_CONJ
                    opOut = CUTENSOR.CUTENSOR_OP_IDENTITY
                    dC = CUTENSOR.contraction!(one(eltyCompute), dA, indsA, opA, dB, indsB, opB,
                           zero(eltyCompute), dC, indsC, opC, opOut, compute_type=eltyCompute)
                    C = collect(dC)
                    mC = reshape(permutedims(C, ipC), (loA, loB))
                    @test mC ≈ conj(mA)*conj(mB) rtol=compute_rtol
                end
            end
        end
    end
end


function optimfunwrapper(x::Vector, g::Vector, site, var)
    g === nothing && (g = zeros(Float64, length(x)))
    return PLsiteAndGrad!(x, g, site,  var)
end

function optimfunwrapper(x::Vector, g::Vector, var)
    g === nothing && (g = zeros(Float64, length(x)))
    return PLsiteAndGradSym!(x, g, var)
end

function inflate_matrix(J::Array{Float64,3},N)
    q,q,NN = size(J)

    @assert (N*(N-1))>>1 == NN

    Jt = zeros(q,q,N,N)
    ctr = 0
    for i in 1:N-1
        for j in i+1:N
            ctr += 1
            Jt[:,:,i,j] = J[:,:,ctr]
            Jt[:,:,j,i] = J[:,:,ctr]'
        end
    end
    return Jt
end

function correct_APC(S::Matrix)
    N = size(S, 1)
    Si = sum(S, dims=1)
    Sj = sum(S, dims=2)
    Sa = sum(S) * (1 - 1/N)

    S -= (Sj * Si) / Sa
    return S
end

function compute_APC(J::Array{Float64,4},N,q)
    FN = fill(0.0, N,N)
    for i=1:N-1
        for j=i+1:N
            FN[i,j] = norm(J[1:q-1,1:q-1,i,j],2)
            FN[j,i] =FN[i,j]
        end
    end
    FN=correct_APC(FN)
    return FN
end

function ReadFasta(filename::AbstractString,max_gap_fraction::Real, theta::Any, remove_dups::Bool)

    Z = read_fasta_alignment(filename, max_gap_fraction)
    if remove_dups
        Z, _ = remove_duplicate_sequences(Z)
    end

    N, M = size(Z)
    q = round(Int,maximum(Z))

    q > 32 && error("parameter q=$q is too big (max 31 is allowed)")
    W , Meff = compute_weights(Z,q,theta)
    rmul!(W, 1.0/Meff)
    Zint=round.(Int,Z)
    return W, Zint,N,M,q
end

function compute_ranking(S::Matrix{Float64}, min_separation::Int = 5)
    N = size(S, 1)
    R = Array{Tuple{Int,Int,Float64}}(undef, div((N-min_separation)*(N-min_separation+1), 2))
    counter = 0
    for i = 1:N-min_separation, j = i+min_separation:N
        counter += 1
        R[counter] = (i, j, S[j,i])
    end

    sort!(R, by=x->x[3], rev=true)
    return R
end

function sumexp(vec::Array{Float64,1})
    mysum = 0.0
    @inbounds @simd for i=1:length(vec)
        mysum += exp(vec[i])
    end
    return mysum
end


type INIDRandomSequence{S<:ValueSupport,T<:UnivariateDistribution} <: MultivariateDistribution{S}
    distributions::Vector{T}
end
function INIDRandomSequence{T<:UnivariateDistribution}(distributions::Vector{T})
    S = super(T).parameters[2]
    return INIDRandomSequence{S,T}(distributions)
end

length(X::INIDRandomSequence) = length(X.distributions)

function insupport(X::INIDRandomSequence,x::AbstractVector)
  length(x)==length(X) && all(dx->insupport(dx[1],dx[2]),zip(X.distributions,x))
end

function _rand!{T<:Real}(X::INIDRandomSequence, x::AbstractVector{T})
  for (i,d_i) in enumerate(X.distributions)
    x[i] = rand(d_i)
  end
  return x
end

function _logpdf{T<:Real}(X::INIDRandomSequence, x::AbstractVector{T})
  return sum(arg->logpdf(arg[1],arg[2]),zip(X.distributions,x))
end

mean(X::INIDRandomSequence) = Float64[mean(d_i) for d_i in X.distributions]
var(X::INIDRandomSequence) = Float64[var(d_i) for d_i in X.distributions]
cov(X::INIDRandomSequence) = Diagonal(var(X))
entropy(X::INIDRandomSequence) = sum(entropy,X.distributions)

function mgf(X::IIDRandomSequence, t::AbstractVector)
  prod([mgf(d_i,t_i) for (d_i,t_i) in zip(X.distributions,t)])
end
function cf(X::IIDRandomSequence, t::AbstractVector)
  prod([cf(d_i,t_i) for (d_i,t_i) in zip(X.distributions,t)])
end


module thermostat

using ReachabilityAnalysis, ModelingToolkit

const A1 = 40.0
const A2 = 30.0
const B = 0.5
const t1 = 75
const t2 = 65

const var = @variables t

function thermostat_on()
    invariant = HalfSpace(t <= t1)
    @system(x' = -Bx + A1, x ∈ invariant)
end

function thermostat_off()
    invariant = HalfSpace(t >= t2)
    @system(x' = -Bx + A2, x ∈ invariant)
end

function thermostat_hybrid()
    automaton = LightAutomaton(2)
    add_transition!(automaton, 1, 2, 1)
    add_transition!(automaton, 2, 1, 2)

    mode1 = thermostat_on()
    mode2 = thermostat_off()
    modes = [mode1, mode2]

    ## transition on -> off
    guard = HalfSpace(t >= t1)
    trans1 = ConstrainedIdentityMap(1, guard)
    ## transition off -> on
    guard = HalfSpace(t <= t2)
    trans2 = ConstrainedIdentityMap(1, guard)
    resetmaps = [trans1, trans2]

    return HybridSystem(automaton, modes, resetmaps, [AutonomousSwitching()])
end

function model(X0)
    H = thermostat_hybrid()
    return IVP(H, X0)
end

end  # module


import LinearAlgebra

n = 2
nProc = numProc(comm)
Data = Float32

######Build matrix######
map = BlockMap(nProc*n, comm)

numMyElts = numMyElements(map)
numGlobalElts = numGlobalElements(map)
myGlobalElts = myGlobalElements(map)

numNz = Array{GID, 1}(undef, numMyElts)
for i = 1:numMyElts
    if myGlobalElts[i] == 1 || myGlobalElts[i] == numGlobalElts
        numNz[i] = 2
    else
        numNz[i] = 3
    end
end

const A = CSRMatrix{Data}(map, numNz, STATIC_PROFILE)

const values = Data[-1, -1]
two = Data[2]

for i = 1:numMyElts
    if myGlobalElts[i] == 1
        indices = GID[2]
    elseif myGlobalElts[i] == numGlobalElts
        indices = GID[numGlobalElts-2]
    else
        indices = GID[myGlobalElts[i]-1, myGlobalElts[i]+1]
    end

    insertGlobalValues(A, myGlobalElts[i], indices, values)
    insertGlobalValues(A, myGlobalElts[i], GID[myGlobalElts[i]], two)
end

fillComplete(A, map, map)

Y = DenseMultiVector(map, LinearAlgebra.diagm(0 => Data(1):n))
X = DenseMultiVector(map, fill(Data(2), n, n))

@test Y === apply!(Y, A, X, NO_TRANS, Float32(3), Float32(.5))

@test fill(2, n, n) == X.data #ensure X isn't mutated

exp = LinearAlgebra.diagm(0 => Data(1):n)*.5
for i in 1:n
    if i == 1 && pid == 1
        exp[1, :] .+= 6
    elseif i == n && pid == nProc
        exp[i, :] .+= 6
    #else
        #exp[i, :] .+= -6 +12-6
    end
end

@test exp == Y.data

X = DenseMultiVector(map, fill(Data(2), n, n))
Y = A*X

@test Y isa DenseMultiVector
@test numVectors(Y) == n
@test fill(2, n, n) == X.data #ensure X isn't mutated
exp = fill(Data(0), n, n)
for i in 1:n
    if i == 1 && pid == 1
        exp[1, :] .= 2
    elseif i == n && pid == nProc
        exp[i, :] .= 2
    #else
        #exp[i, :] .= -2 +4 -2
    end
end

@test exp == Y.data

Y = DenseMultiVector(map, fill(Data(2), n, n))
X = DenseMultiVector(map, fill(Data(2), n, n))

@test Y === mul!(Y, A, X)

@test fill(2, n, n) == X.data #ensure X isn't mutated

exp = fill(Data(0), n, n)
for i in 1:n
    if i == 1 && pid == 1
        exp[1, :] .+= 2
    elseif i == n && pid == nProc
        exp[i, :] .+= 2
    #else
        #exp[i, :] .+= -2 +4 -2
    end
end

@test exp == Y.data


using SparseMatrix
using LinearAlgebra
using SparseArrays
using Test

@testset "nnz1" begin
    for i=1:100
        m = rand(1:20)
        n = rand(1:20)
        p = rand()
        A=Matrix(sprandn(m,n,p))
        x1=sparse(A)
        x2=SparseCSR(A)
        x3=SparseCSC(A)
        x4=SparseCOO(A)
        @test nnz(x1) == nnz(x2) == nnz(x3) == nnz(x4)
    end
end

@testset "tocsc1" begin
    for i=1:100
        m = rand(1:20)
        n = rand(1:20)
        p = rand()
        A=Matrix(sprandn(m,n,p))
        x0 = sparse(A)
        x1=SparseCSR(A)
        x2=SparseCSC(A)
        x3=SparseCOO(A)
        y1=sparse(x1)
        y2=sparse(x2)
        y3=sparse(x3)
        if nnz(x1) != 0
            x1.val[1] = 0.0
            x2.val[1] = 0.0
            x3.val[1] = 0.0
        end
        @test x0 == y1 == y2 == y3
    end
end

@testset "multiple1" begin
    for i=1:100
        m = rand(1:20)
        n = rand(1:20)
        p = rand()
        A=Matrix(sprandn(m,n,p))
        x=randn(n)
        C1 = A * x
        C2 = SparseCSR(A) * x
        C3 = SparseCSC(A) * x
        C4 = SparseCOO(A) * x
        @test C1 ≈ C2 ≈ C3 ≈ C4
    end
end

@testset "multiple2" begin
    for i=1:100
        m = rand(1:20)
        n = rand(1:20)
        p = rand()
        A=Matrix(sprandn(n,m,p))
        x=randn(n)
        C1 = A' * x
        C2 = SparseCSR(A)' * x
        C3 = SparseCSC(A)' * x
        C4 = SparseCOO(A)' * x
        @test C1 ≈ C2 ≈ C3 ≈ C4
    end
end

@testset "multiple3" begin
    for i=1:100
        m = rand(1:20)
        n = rand(1:20)
        k = rand(1:20)
        p = rand()
        A=Matrix(sprandn(m,k,p))
        B=randn(k,n)
        C1 = A * B
        C2 = SparseCSR(A) * B
        C3 = SparseCSC(A) * B
        C4 = SparseCOO(A) * B
        @test C1 ≈ C2 ≈ C3 ≈ C4
    end
end

@testset "multiple4" begin
    for i=1:100
        m = rand(1:20)
        n = rand(1:20)
        k = rand(1:20)
        p = rand()
        A=Matrix(sprandn(k,m,p))
        B=randn(k,n)
        C1 = A' * B
        C2 = SparseCSR(A)' * B
        C3 = SparseCSC(A)' * B
        C4 = SparseCOO(A)' * B
        @test C1 ≈ C2 ≈ C3 ≈ C4
    end
end

@testset "multiple5" begin
    for i=1:100
        m = rand(1:20)
        n = rand(1:20)
        k = rand(1:20)
        p = rand()
        A=Matrix(sprandn(m,k,p))
        B=randn(n,k)
        C1 = A * B'
        C2 = SparseCSR(A) * B'
        C3 = SparseCSC(A) * B'
        C4 = SparseCOO(A) * B'
        @test C1 ≈ C2 ≈ C3 ≈ C4
    end
end

@testset "multiple6" begin
    for i=1:100
        m = rand(1:20)
        n = rand(1:20)
        k = rand(1:20)
        p = rand()
        A=Matrix(sprandn(k,m,p))
        B=randn(n,k)
        C1 = A' * B'
        C2 = SparseCSR(A)' * B'
        C3 = SparseCSC(A)' * B'
        C4 = SparseCOO(A)' * B'
        @test C1 ≈ C2 ≈ C3 ≈ C4
    end
end

@testset "multiple7" begin
    for i=1:100
        m = rand(1:20)
        n = rand(1:20)
        k = rand(1:20)
        p = rand()
        A=randn(m,k)
        B=Matrix(sprandn(k,n,p))
        C1 = A * B
        C2 = A * SparseCSR(B)
        C3 = A * SparseCSC(B)
        C4 = A * SparseCOO(B)
        @test C1 ≈ C2 ≈ C3 ≈ C4
    end
end

@testset "multiple8" begin
    for i=1:100
        m = rand(1:20)
        n = rand(1:20)
        k = rand(1:20)
        p = rand()
        A=randn(k,m)
        B=Matrix(sprandn(k,n,p))
        C1 = A' * B
        C2 = A' * SparseCSR(B)
        C3 = A' * SparseCSC(B)
        C4 = A' * SparseCOO(B)
        @test C1 ≈ C2 ≈ C3 ≈ C4
    end
end

@testset "multiple9" begin
    for i=1:100
        m = rand(1:20)
        n = rand(1:20)
        k = rand(1:20)
        p = rand()
        A=randn(m,k)
        B=Matrix(sprandn(n,k,p))
        C1 = A * B'
        C2 = A * SparseCSR(B)'
        C3 = A * SparseCSC(B)'
        C4 = A * SparseCOO(B)'
        @test C1 ≈ C2 ≈ C3 ≈ C4
    end
end

@testset "multiple10" begin
    for i=1:100
        m = rand(1:20)
        n = rand(1:20)
        k = rand(1:20)
        p = rand()
        A=randn(k,m)
        B=Matrix(sprandn(n,k,p))
        C1 = A' * B'
        C2 = A' * SparseCSR(B)'
        C3 = A' * SparseCSC(B)'
        C4 = A' * SparseCOO(B)'
        @test C1 ≈ C2 ≈ C3 ≈ C4
    end
end

@testset "diag1" begin
    for i = 1:100
        m = rand(1:20)
        n = rand(1:20)
        p = rand()
        A = Matrix(sprandn(m,n,p))
        d1 = spdiag(A)
        d2 = convert(Vector{Float64}, spdiag(SparseCSR(A)))
        d3 = convert(Vector{Float64}, spdiag(SparseCSC(A)))
        d4 = convert(Vector{Float64}, spdiag(SparseCOO(A)))
        @test d1 ≈ d2 ≈ d3 ≈ d4
    end
end

@testset "diag2" begin
    for i = 1:100
        m = rand(1:20)
        n = rand(1:20)
        p = rand()
        A = Matrix(sprandn(m,n,p))
        d1 = spdiag(A)
        d2 = spdiag(SparseCSR(A))
        d3 = spdiag(SparseCSC(A))
        d4 = spdiag(SparseCOO(A))
        for k = 1:length(d1)
            @test d1[k] ≈ d2[k] ≈ d3[k] ≈ d4[k]
        end
    end
end

@testset "diag3" begin
    for i = 1:100
        m = rand(1:20)
        n = rand(1:20)
        p = rand()
        A = Matrix(sprandn(m,n,p))
        d1 = abs.(spdiag(A))
        d2 = abs.(spdiag(SparseCSR(A)))
        d3 = abs.(spdiag(SparseCSC(A)))
        d4 = abs.(spdiag(SparseCOO(A)))
        @test d1 ≈ d2 ≈ d3 ≈ d4
    end
end

@testset "diag4" begin
    for i = 1:100
        m = rand(1:20)
        n = rand(1:20)
        p = rand()
        A = Matrix(sprandn(m,n,p))
        d1 = maximum(spdiag(A))
        d2 = maximum(spdiag(SparseCSR(A)))
        d3 = maximum(spdiag(SparseCSC(A)))
        d4 = maximum(spdiag(SparseCOO(A)))
        @test d1 ≈ d2 ≈ d3 ≈ d4
    end
end

@testset "copy" begin
    for i = 1:100
        m = rand(1:20)
        n = rand(1:20)
        p = rand()
        A = Matrix(sprandn(m,n,p))
        d1 = Matrix(copy(SparseCSR(A)))
        d2 = Matrix(copy(SparseCSC(A)))
        d3 = Matrix(copy(SparseCOO(A)))
        @test A ≈ d1 ≈ d2 ≈ d3
    end
end

@testset "zero" begin
    for i = 1:100
        m = rand(1:20)
        n = rand(1:20)
        p = rand()
        A = Matrix(sprandn(m,n,p))
        z = zero(A)
        d1 = Matrix(zero(SparseCSR(A)))
        d2 = Matrix(zero(SparseCSC(A)))
        d3 = Matrix(zero(SparseCOO(A)))
        d4 = Matrix(zero(SparseArrays.SparseMatrixCSC(A)))
        @test z ≈ d1 ≈ d2 ≈ d3 ≈ d4
    end
end

@testset "linearlize" begin
    for i = 1:100
        m = rand(1:20)
        n = rand(1:20)
        p = rand()
        A = SparseCSC(sprandn(m,n,p))
        B = copy(A)
        for i = 1:length(B)
            B[i] *= 100.0
        end
        @test A.val * 100.0 ≈ B.val
        A = SparseCSC(sprandn(m,n,p))
        B = copy(A)
        for i = eachindex(B)
            B[i] *= 100.0
        end
        @test A.val * 100.0 ≈ B.val
    end
end

@testset "block 1" begin
    for i = 1:100
        m = rand(1:20)
        n = rand(1:20)
        p = rand()
        A1 = SparseCSC(sprandn(m,n,p))
        x1 = rand(n,1)
        m = rand(1:20)
        n = rand(1:20)
        p = rand()
        A2 = SparseCSC(sprandn(m,n,p))
        x2 = rand(n,1)

        y0 = [A1 * x1; A2 * x2]

        X = BlockCOO(2,2, [(1,1,A1), (2,2,A2)])
        y = SparseCOO(X) * [x1; x2]

        @test Matrix(SparseCOO(X)) == Matrix(SparseCSR(X)) == Matrix(SparseCSC(X)) == Matrix(sparse(X))
        @test y0 ≈ y
    end
end

@testset "block 2" begin
    for i = 1:100
        m = rand(1:20)
        n = rand(1:20)
        p = rand()
        A1 = SparseCSR(sprandn(m,n,p))
        x1 = rand(n,1)
        m = rand(1:20)
        n = rand(1:20)
        p = rand()
        A2 = SparseCOO(sprandn(m,n,p))
        x2 = rand(n,1)

        y0 = [A1 * x1; A2 * x2]

        X = BlockCOO(2,2, [(1,1,A1), (2,2,A2)])
        y = SparseCOO(X) * [x1; x2]

        @test Matrix(SparseCOO(X)) == Matrix(SparseCSR(X)) == Matrix(SparseCSC(X)) == Matrix(sparse(X))
        @test y0 ≈ y
    end
end

@testset "block 3" begin
    for i = 1:100
        m = rand(1:20)
        n = rand(1:20)
        p = rand()
        A1 = Matrix(sprandn(m,n,p))
        x1 = rand(n,1)
        m = rand(1:20)
        n = rand(1:20)
        p = rand()
        A2 = SparseCSC(sprandn(m,n,p))
        x2 = rand(n,1)

        y0 = [A1 * x1; A2 * x2]

        X = BlockCOO(2,2, [(1,1,A1), (2,2,A2)])
        y = SparseCOO(X) * [x1; x2]

        @test Matrix(SparseCOO(X)) == Matrix(SparseCSR(X)) == Matrix(SparseCSC(X)) == Matrix(sparse(X))
        @test y0 ≈ y
    end
end

@testset "spdiag 1" begin
    for ite = 1:100
        m = rand(1:20)
        n = rand(1:20)
        p = rand()
        A = Matrix(sprandn(m,n,p))
        d0 = spdiag(A)
        d1 = spdiag(SparseCSR(A))
        d2 = spdiag(SparseCSC(A))
        d3 = spdiag(SparseCOO(A))
        d4 = spdiag(SparseMatrixCSC(A))
        v0 = [d0.val[i] for i = d0.index if !iszero(d0.val[i])]
        v1 = [d1.val[i] for i = d1.index if !iszero(i)]
        v2 = [d2.val[i] for i = d2.index if !iszero(i)]
        v3 = [d3.val[i] for i = d3.index if !iszero(i)]
        v4 = [d4.val[i] for i = d4.index if !iszero(i)]
        @test v0 == v1 == v2 == v3 == v4
    end
end

@testset "block 4" begin
    for i = 1:100
        m = rand(1:20)
        n = rand(1:20)
        p = rand()
        A1 = Matrix(sprandn(m,n,p))
        m = rand(1:20)
        n = rand(1:20)
        p = rand()
        A2 = SparseCSC(sprandn(m,n,p))
        X = spzeros(AbstractMatrix{Float64}, 2, 2)
        X[1,1] = A1
        X[2,2] = A2
        if sum(A1 .!= 0) != 0 && SparseArrays.nnz(A2) != 0
            AA = Matrix(sparse(block(X)))
            A1 = Matrix(A1)
            A2 = Matrix(A2)
            BB = zeros(size(A1) .+ size(A2))
            BB[1:size(A1)[1],1:size(A1)[2]] = A1
            BB[size(A1)[1]+1:end,size(A1)[2]+1:end] = A2
            @test AA == BB
        end
    end
end


using Plots, LaTeXStrings
gr()
fontFamily = "Times"
default(titlefont=(16, fontFamily), guidefont=(16, fontFamily),
      tickfont=(10, fontFamily),legendfont=(13, fontFamily),
      bg_legend = nothing, framestyle = :semi, grid = true)
x = LinRange(0,2π,20)
markers = [:d, :o, :rect, :h, :s]
colores = [:orangered, :dodgerblue, :black, :orange, :purple]
pltscater = plot(xlab =L"x", legend =:bottom, fg_legend = :grey,
    legendfont=(9,), size = (600, 300))
for (indx, i) in enumerate(0:0.25:1)
    plot!(x, i .- cos.(x), c = colores[indx],
    m = (markers[indx], 4, :white, stroke(1,colores[indx])),
    label = ":$(markers[indx])")
end
pltscater
savefig("figsmarkers.pdf")


x = [0, 0, 1, 1, 1]
y = [0, 1, 0, 1, 1]
cm = IntConfusionMatrix(x, y)

@test typeof(cm) <: InfoTheory.ConfusionMatrix

@test sort(InfoTheory.xvalues(cm)) == [0, 1]
@test sort(InfoTheory.yvalues(cm)) == [0, 1]

xi0 = InfoTheory.xindex(cm, 0)
xi1 = InfoTheory.xindex(cm, 1)
@test in(xi0, 1:2)
@test in(xi1, 1:2)
@test xi0 != xi1

yi0 = InfoTheory.yindex(cm, 0)
yi1 = InfoTheory.yindex(cm, 1)
@test in(yi0, 1:2)
@test in(yi1, 1:2)
@test yi0 != yi1

@test count(cm, 0, 0) == 1
@test count(cm, 0, 1) == 1
@test count(cm, 1, 0) == 1
@test count(cm, 1, 1) == 2

x2 = [0, 0, 1, 1, -1]
y2 = [0, 1, 0, 1, 1]
cm2 = IntConfusionMatrix(x2, y2)
@test sort(InfoTheory.xvalues(cm2)) == [-1, 0, 1]
@test sort(InfoTheory.yvalues(cm2)) == [0, 1]

@test count(cm2, 0, 0) == 1
@test count(cm2, 0, 1) == 1
@test count(cm2, 1, 0) == 1
@test count(cm2, 1, 1) == 1
@test count(cm2, -1, 1) == 1
@test count(cm2, -1, 0) == 0