using NNlib: conv, crosscor, ∇conv_filter, ∇conv_data, ∇maxpool, maxpool, depthwiseconv, ∇depthwiseconv_filter, ∇depthwiseconv_data

@testset "conv2d" begin
    x = reshape(Float64[1:20;], 5, 4, 1, 1)
    w = reshape(Float64[1:4;], 2, 2, 1, 1)

    @test dropdims(conv(x, w), dims = (3,4)) == [
        29 79 129;
        39 89 139;
        49 99 149;
        59 109 159.]

    @test dropdims(conv(view(x, :, :, :, :), w), dims = (3,4)) == [
        29 79 129;
        39 89 139;
        49 99 149;
        59 109 159.]

    @test dropdims(crosscor(x, w), dims = (3,4)) == [
	 51  101  151;
	 61  111  161;
 	 71  121  171;
	 81  131  181.]

    @test dropdims(conv(Float32.(x), Float32.(w)), dims=(3,4)) == Float32.([
        29 79 129;
        39 89 139;
        49 99 149;
        59 109 159.])

    @test dropdims(conv(x, w; stride=2), dims = (3,4)) == [
        29 129;
        49 149.]

    @test dropdims(conv(Float32.(x), Float32.(w); stride=2), dims = (3,4)) == Float32.([
        29 129;
        49 149.])

    @test dropdims(conv(x, w; pad=1), dims = (3,4)) == [
        1.0   9.0   29.0   49.0   48.0;
        4.0  29.0   79.0  129.0  115.0;
        7.0  39.0   89.0  139.0  122.0;
        10.0  49.0   99.0  149.0  129.0;
        13.0  59.0  109.0  159.0  136.0;
        10.0  40.0   70.0  100.0   80.0
    ]

    @test dropdims(conv(Float32.(x), Float32.(w); pad=1), dims = (3,4)) == Float32.([
        1.0   9.0   29.0   49.0   48.0;
        4.0  29.0   79.0  129.0  115.0;
        7.0  39.0   89.0  139.0  122.0;
        10.0  49.0   99.0  149.0  129.0;
        13.0  59.0  109.0  159.0  136.0;
        10.0  40.0   70.0  100.0   80.0
    ])

    @test dropdims(conv(x, w; dilation=2), dims = (3,4)) == [
        48 98;
        58 108;
        68 118.]

	# NaN tests for dilation forward pass

	ys = []
	for idx in 1:1000
    	push!(ys, conv(x, w; dilation=2))
	end
	@test !any([any(isnan.(ys[idx])) for idx in 1:1000])

    # for gradients, check only size
    # correctness of gradients is cross-checked with CUDNN.jl
    # (it's assumed convolution code won't change often)

    @test size(∇conv_filter(reshape(rand(4,3), 4, 3, 1, 1), x)) == size(w)
    @test size(∇conv_data(reshape(rand(4,3), 4, 3, 1, 1), w)) == size(x)

    # Test that stride/pad work backward as well
    y = conv(x, w; stride=2, pad=1, dilation=2)
    @test size(y) == (3, 2, 1, 1)
    @test size(∇conv_filter(y, x; size=size(w), stride=2, pad=1, dilation=2)) == size(w)
    @test size(∇conv_data(y, w; size=size(x), stride=2, pad=1, dilation=2)) == size(x)

	# NaN tests for dilation backward pass: filters
	dy = randn(size(ys[1]))
	dws = []
	for idx in 1:1000
	    push!(dws, ∇conv_filter(dy, x; size=size(w), dilation=2))
	end

	# NaN tests for dilation backward pass: input
	dxs = []
	for idx in 1:1000
	    push!(dxs, ∇conv_data(dy, w; size=size(x), dilation=2))
	end

	@test !any([any(isnan.(dws[idx])) for idx in 1:1000])
	@test !any([any(isnan.(dxs[idx])) for idx in 1:1000])

end

@testset "depthwiseconv2d" begin
    x = reshape(Float64[1:18;], 3, 3, 2, 1)
    w = reshape(Float64[1:16;], 2, 2, 2, 2)

    @test depthwiseconv(x, w)[:] == [23.0, 33.0, 53.0, 63.0, 71.0, 97.0, 149.0, 175.0, 497.0, 539.0, 623.0, 665.0, 689.0, 747.0, 863.0, 921.0]

    @test depthwiseconv(x, w, stride = 2, pad = 1)[:] == [1.0, 7.0, 19.0, 63.0, 5.0, 27.0, 63.0, 175.0, 90.0, 218.0, 287.0, 665.0, 130.0, 310.0, 403.0, 921.0]

    @test depthwiseconv(x, w, stride = 2)[:] == [23.0, 71.0, 497.0, 689.0]

    @test depthwiseconv(x, w, pad = 1)[:] == [1.0, 4.0, 7.0, 6.0, 7.0, 23.0, 33.0, 24.0, 19.0, 53.0, 63.0, 42.0, 21.0, 52.0, 59.0, 36.0, 5.0, 16.0, 27.0, 18.0, 27.0, 71.0, 97.0, 60.0, 63.0, 149.0, 175.0, 102.0, 49.0, 112.0, 127.0, 72.0, 90.0, 199.0, 218.0, 120.0, 227.0, 497.0, 539.0, 294.0, 287.0, 623.0, 665.0, 360.0, 176.0, 379.0, 402.0, 216.0, 130.0, 283.0, 310.0, 168.0, 319.0, 689.0, 747.0, 402.0, 403.0, 863.0, 921.0, 492.0, 240.0, 511.0, 542.0, 288.0]

    # the correctness of the gradients are being verified by calling
    # the corresponding counvolution gradients

    dy = reshape(Float64[1:16;], 2,2,4,1)
    local z = ∇depthwiseconv_data(dy,x,w)
    for i in 1:2
        X = copy(x[:,:,i:i,:]);
        W = copy(permutedims(w[:,:,:,i:i],[1,2,4,3]));
        DY = copy(dy[:,:,2i-1:2i,:]);
        res = ∇conv_data(DY,W;size=size(X))
        @test dropdims(z[:,:,i:i,:], dims=(3,4)) == dropdims(res, dims=(3,4))
    end

    z = ∇depthwiseconv_filter(dy, x, w)
    for i in 1:2
        X = copy(x[:,:,i:i,:]);
        W = copy(permutedims(w[:,:,:,i:i],[1,2,4,3]))
        DY = copy(dy[:,:,2i-1:2i,:])
        res = ∇conv_filter(DY,X; size=size(W))
        @test dropdims(z[:,:,:,i:i]; dims=(4)) == dropdims(res; dims=(3))
    end

    @test size(∇depthwiseconv_filter(rand(2,2,4,1), x, w)) == size(w)
    @test size(∇depthwiseconv_data(rand(2,2,4,1), x, w)) == size(x)

    # Test for the stride/pad for backward pass
    y = depthwiseconv(x,w,stride=2,pad=1)
    @test size(y) == (2,2,4,1)
    @test size(∇depthwiseconv_filter(rand(Float64, size(y)), x, w, stride=2, pad=1)) == size(w)
    @test size(∇depthwiseconv_data(rand(Float64, size(y)), x, w, stride=2, pad=1)) == size(x)
end

@testset "maxpool2d" begin

    x = reshape(Float64[1:20;], 5, 4, 1, 1)

    @test dropdims(maxpool(x, (2,2)), dims = (3,4)) == [7 17; 9 19]
    @test dropdims(maxpool(x, (2,2); stride=(2,2)), dims = (3,4)) == [7 17; 9 19]
    @test dropdims(maxpool(x, (2,2); pad=(1,1)), dims = (3,4)) == [
        1.0  11.0  16.0;
        3.0  13.0  18.0;
        5.0  15.0  20.0;
    ]

    # for gradients, check only size
    # correctness of gradients is cross-checked with CUDNN.jl
    # (it's assumed maxpooling code won't change often)

    y = maxpool(x, (2,2))
    dy = reshape(rand(2,2), 2, 2, 1, 1)
    @test size(∇maxpool(dy, y, x, (2,2))) == size(x)

end

@testset "conv3d" begin

    x = reshape(Float64[1:60;], 5, 4, 3, 1, 1)
    w = reshape(Float64[1:8;], 2, 2, 2, 1, 1)
    res = zeros(4,3,2)
    res[:, :, 1] = [
        322.0  502.0  682.0;
        358.0  538.0  718.0;
        394.0  574.0  754.0;
        430.0  610.0  790.0]
    res[:, :, 2] = [
        1042.0  1222.0  1402.0;
        1078.0  1258.0  1438.0;
        1114.0  1294.0  1474.0;
        1150.0  1330.0  1510.0]
    @test dropdims(conv(x, w), dims = (4,5)) == res

    @test dropdims(conv(Float32.(x), Float32.(w)), dims = (4,5)) == Float32.(res)

    @test dropdims(conv(x, w; stride=2), dims = (3,4,5)) == [
        322.0 682.0;
        394.0 754.0]

    @test dropdims(conv(Float32.(x), Float32.(w); stride=2), dims = (3,4,5)) == Float32.([
        322.0 682.0;
        394.0 754.0])

    res = zeros(6,5,4)
    res[:, :, 1] = [
        1.0   9.0   29.0   49.0   48.0;
        4.0  29.0   79.0  129.0  115.0;
        7.0  39.0   89.0  139.0  122.0;
        10.0  49.0   99.0  149.0  129.0;
        13.0  59.0  109.0  159.0  136.0;
        10.0  40.0   70.0  100.0   80.0]
    res[:, :, 2] = [
        26.0  126.0  206.0  286.0  220.0;
        80.0  322.0  502.0  682.0  502.0;
        94.0  358.0  538.0  718.0  524.0;
        108.0  394.0  574.0  754.0  546.0;
        122.0  430.0  610.0  790.0  568.0;
        80.0  260.0  360.0  460.0  320.0]
    res[:, :, 3] = [
        146.0   446.0   526.0   606.0   420.0;
        360.0  1042.0  1222.0  1402.0   942.0;
        374.0  1078.0  1258.0  1438.0   964.0;
        388.0  1114.0  1294.0  1474.0   986.0;
        402.0  1150.0  1330.0  1510.0  1008.0;
        240.0   660.0   760.0   860.0   560.0]
    res[:, :, 4] = [
        205.0   517.0   577.0   637.0  392.0;
        456.0  1133.0  1263.0  1393.0  847.0;
        467.0  1159.0  1289.0  1419.0  862.0;
        478.0  1185.0  1315.0  1445.0  877.0;
        489.0  1211.0  1341.0  1471.0  892.0;
        270.0   660.0   730.0   800.0  480.0]
    @test dropdims(conv(x, w; pad=1), dims = (4,5)) == res

    @test dropdims(conv(Float32.(x), Float32.(w); pad=1), dims = (4,5)) == Float32.(res)

    @test dropdims(conv(x, w; dilation=2), dims = (3,4,5)) == [
        608 788;
        644 824;
        680 860.
    ]

    @test dropdims(conv(Float32.(x), Float32.(w); dilation=2), dims = (3,4,5)) == Float32.([
        608 788;
        644 824;
        680 860.
    ])
	# NaN tests for dilation forward pass

	ys = []
	for idx in 1:1000
    	push!(ys, conv(x, w; dilation=2))
	end
	@test !any([any(isnan.(ys[idx])) for idx in 1:1000])

    # for gradients, check only size
    # correctness of gradients is cross-checked with CUDNN.jl
    # (it's assumed convolution code won't change often)

    @test size(∇conv_filter(reshape(rand(4,3,2), 4, 3, 2, 1, 1), x; size=size(w))) == size(w)
    @test size(∇conv_data(reshape(rand(4,3,2), 4, 3, 2, 1, 1), w; size=size(x))) == size(x)

	# NaN tests for dilation backward pass: filters
	dy = randn(size(ys[1]))
	dws = []
	for idx in 1:1000
	    push!(dws, ∇conv_filter(dy, x; size=size(w), dilation=2))
	end

	# NaN tests for dilation backward pass: input
	dxs = []
	for idx in 1:1000
	    push!(dxs, ∇conv_data(dy, w; size=size(x), dilation=2))
	end

	@test !any([any(isnan.(dws[idx])) for idx in 1:1000])
	@test !any([any(isnan.(dxs[idx])) for idx in 1:1000])

end

@testset "maxpool3d" begin

    x = reshape(Float64[1:60;], 5, 4, 3, 1, 1)

    @test dropdims(maxpool(x, (2,2,2)), dims = (3,4,5)) == [27 37; 29 39.]
    @test dropdims(maxpool(x, (2,2,2); stride=(2,2,2)), dims = (3,4,5)) == [27 37; 29 39.]
    res = zeros(3,3,2)
    res[:, :, 1] = [
        1.0  11.0  16.0;
        3.0  13.0  18.0;
        5.0  15.0  20.0]
    res[:, :, 2] = [
        41.0  51.0  56.0;
        43.0  53.0  58.0;
        45.0  55.0  60.0]
    @test dropdims(maxpool(x, (2,2,2), pad=(1,1,1)), dims = (4,5)) == res

    # for gradients, check only size
    # correctness of gradients is cross-checked with CUDNN.jl
    # (it's assumed maxpooling code won't change often)

    y = maxpool(x, (2,2,2))
    dy = reshape(rand(2,2), 2, 2, 1, 1, 1)
    @test size(∇maxpool(dy, y, x, (2,2,2))) == size(x)

end


struct Params_S2{ComplexArray,Total}
    # boundary conditions
    periodic::Bool
    # normalization
    total::Total

    # algorithm-specific

    # fft buffers
    complex_img::ComplexArray
end

function Params_S2(img; periodic::Bool=true)
    box = size(img)
    complex_box = periodic ? box : box .* 2

    total = cnt_total(img, periodic)

    p = Params_S2(
        periodic,
        total,
        similar(img, ComplexF32, complex_box),
    )
    cf_type = periodic ? :periodic_point_point : :central_symmetry
    p, cf_type
end

"""
    correllation_function!(res, img, params::Params_S2)

Compute S₂ correlation function in positive directions
"""
function correllation_function!(res, img, params::Params_S2)
    ix = CartesianIndices(img)

    f = params.complex_img .= 0

    v_f = view(f, ix)
    v_f .= img

    self_correlation!(f)

    res .= real.(v_f) ./ params.total
end

"""
    s2(image; periodic = false)

Calculate `S₂` (two point) correlation function map
for the N-dimensional image and return a `CFMap` object.

The `image` contains the probability of the voxel being in the correct phase.

# Examples
```jldoctest
julia> s2([1 0; 0 1]; periodic=true).result
2×2 Matrix{Float32}:
 0.5  0.0
 0.0  0.5
```
"""
function s2(image; periodic::Bool=false)
    corr_function_map(image, Params_S2; periodic)
end

function s2(image, phase::Int; periodic::Bool=false)
    one_phase_img = image .== phase
    corr_function_map(one_phase_img, Params_S2; periodic)
end


function simple_lp(bridged)

    MOI.empty!(bridged)
    @test MOI.is_empty(bridged)

    # add 10 variables - only diagonal is relevant
    X = MOI.add_variables(bridged, 2)

    # add sdp constraints - only ensuring positivenesse of the diagonal
    vov = MOI.VectorOfVariables(X)

    c1 = MOI.add_constraint(bridged,
        MOI.ScalarAffineFunction([
            MOI.ScalarAffineTerm(2.0, X[1]),
            MOI.ScalarAffineTerm(1.0, X[2])
        ], 0.0), MOI.EqualTo(4.0))

    c2 = MOI.add_constraint(bridged,
        MOI.ScalarAffineFunction([
            MOI.ScalarAffineTerm(1.0, X[1]),
            MOI.ScalarAffineTerm(2.0, X[2])
        ], 0.0), MOI.EqualTo(4.0))

    b1 = MOI.add_constraint(bridged,
        MOI.ScalarAffineFunction([
            MOI.ScalarAffineTerm(1.0, X[1])
        ], 0.0), MOI.GreaterThan(0.0))

    b2 = MOI.add_constraint(bridged,
        MOI.ScalarAffineFunction([
            MOI.ScalarAffineTerm(1.0, X[2])
        ], 0.0), MOI.GreaterThan(0.0))

    MOI.set(bridged,
        MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(),
        MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([-4.0, -3.0], [X[1], X[2]]), 0.0)
        )
    MOI.set(bridged, MOI.ObjectiveSense(), MOI.MIN_SENSE)
    MOI.optimize!(bridged)

    obj = MOI.get(bridged, MOI.ObjectiveValue())

    @test obj ≈ -9.33333 atol = 1e-2

    Xr = MOI.get(bridged, MOI.VariablePrimal(), X)

    @test Xr ≈ [1.3333, 1.3333] atol = 1e-2

end

function simple_lp_2_1d_sdp(bridged)

    MOI.empty!(bridged)
    @test MOI.is_empty(bridged)

    # add 10 variables - only diagonal is relevant
    X = MOI.add_variables(bridged, 2)

    # add sdp constraints - only ensuring positivenesse of the diagonal
    vov = MOI.VectorOfVariables(X)

    c1 = MOI.add_constraint(bridged,
        MOI.ScalarAffineFunction([
            MOI.ScalarAffineTerm(2.0, X[1]),
            MOI.ScalarAffineTerm(1.0, X[2])
        ], 0.0), MOI.EqualTo(4.0))

    c2 = MOI.add_constraint(bridged,
        MOI.ScalarAffineFunction([
            MOI.ScalarAffineTerm(1.0, X[1]),
            MOI.ScalarAffineTerm(2.0, X[2])
        ], 0.0), MOI.EqualTo(4.0))

    b1 = MOI.add_constraint(bridged,
        MOI.VectorOfVariables([X[1]]), MOI.PositiveSemidefiniteConeTriangle(1))

    b2 = MOI.add_constraint(bridged,
        MOI.VectorOfVariables([X[2]]), MOI.PositiveSemidefiniteConeTriangle(1))

    MOI.set(bridged,
        MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(),
        MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([-4.0, -3.0], [X[1], X[2]]), 0.0)
        )
    MOI.set(bridged, MOI.ObjectiveSense(), MOI.MIN_SENSE)
    MOI.optimize!(bridged)

    obj = MOI.get(bridged, MOI.ObjectiveValue())

    @test obj ≈ -9.33333 atol = 1e-2

    Xr = MOI.get(bridged, MOI.VariablePrimal(), X)

    @test Xr ≈ [1.3333, 1.3333] atol = 1e-2

end

function lp_in_SDP_equality_form(bridged)

    MOI.empty!(bridged)
    @test MOI.is_empty(bridged)

    # add 10 variables - only diagonal is relevant
    X = MOI.add_variables(bridged, 10)

    # add sdp constraints - only ensuring positivenesse of the diagonal
    vov = MOI.VectorOfVariables(X)
    cX = MOI.add_constraint(bridged, vov, MOI.PositiveSemidefiniteConeTriangle(4))

    c1 = MOI.add_constraint(bridged,
        MOI.ScalarAffineFunction([
            MOI.ScalarAffineTerm(2.0, X[1]),
            MOI.ScalarAffineTerm(1.0, X[3]),
            MOI.ScalarAffineTerm(1.0, X[6])
        ], 0.0), MOI.EqualTo(4.0))

    c2 = MOI.add_constraint(bridged,
        MOI.ScalarAffineFunction([
            MOI.ScalarAffineTerm(1.0, X[1]),
            MOI.ScalarAffineTerm(2.0, X[3]),
            MOI.ScalarAffineTerm(1.0, X[10])
        ], 0.0), MOI.EqualTo(4.0))

    MOI.set(bridged,
        MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(),
        MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([-4.0, -3.0], [X[1], X[3]]), 0.0)
        )
    MOI.set(bridged, MOI.ObjectiveSense(), MOI.MIN_SENSE)
    MOI.optimize!(bridged)

    obj = MOI.get(bridged, MOI.ObjectiveValue())

    @test obj ≈ -9.33333 atol = 1e-2

    Xr = MOI.get(bridged, MOI.VariablePrimal(), X)

    @test Xr ≈ [1.3333, .0, 1.3333, .0, .0, .0, .0, .0, .0, .0] atol = 1e-2

end

function lp_in_SDP_inequality_form(optimizer)

    MOI.empty!(optimizer)
    @test MOI.is_empty(optimizer)

    # add 10 variables - only diagonal is relevant
    X = MOI.add_variables(optimizer, 3)

    # add sdp constraints - only ensuring positivenesse of the diagonal
    vov = MOI.VectorOfVariables(X)
    cX = MOI.add_constraint(optimizer, vov, MOI.PositiveSemidefiniteConeTriangle(2))

    c1 = MOI.add_constraint(optimizer,
        MOI.VectorAffineFunction(MOI.VectorAffineTerm.([1,1],[
            MOI.ScalarAffineTerm(2.0, X[1]),
            MOI.ScalarAffineTerm(1.0, X[3]),
        ]), [-4.0]), MOI.Nonpositives(1))

    c2 = MOI.add_constraint(optimizer,
        MOI.VectorAffineFunction(MOI.VectorAffineTerm.([1,1],[
            MOI.ScalarAffineTerm(1.0, X[1]),
            MOI.ScalarAffineTerm(2.0, X[3]),
        ]), [-4.0]), MOI.Nonpositives(1))

    MOI.set(optimizer,
        MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(),
        MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([4.0, 3.0], [X[1], X[3]]), 0.0)
        )
    MOI.set(optimizer, MOI.ObjectiveSense(), MOI.MAX_SENSE)
    MOI.optimize!(optimizer)

    obj = MOI.get(optimizer, MOI.ObjectiveValue())

    @test obj ≈ 9.33333 atol = 1e-2

    Xr = MOI.get(optimizer, MOI.VariablePrimal(), X)

    @test Xr ≈ [1.3333, .0, 1.3333] atol = 1e-2

    c1_d = MOI.get(optimizer, MOI.ConstraintDual(), c1)
    c2_d = MOI.get(optimizer, MOI.ConstraintDual(), c2)

end

function sdp_from_moi(optimizer)
    # min X[1,1] + X[2,2]    max y
    #     X[2,1] = 1         [0   y/2     [ 1  0
    #                         y/2 0    <=   0  1]
    #     X >= 0              y free
    # Optimal solution:
    #
    #     ⎛ 1   1 ⎞
    # X = ⎜       ⎟           y = 2
    #     ⎝ 1   1 ⎠
    MOI.empty!(optimizer)
    @test MOI.is_empty(optimizer)

    X = MOI.add_variables(optimizer, 3)

    vov = MOI.VectorOfVariables(X)
    cX = MOI.add_constraint(optimizer, vov, MOI.PositiveSemidefiniteConeTriangle(2))

    c = MOI.add_constraint(optimizer, MOI.VectorAffineFunction([MOI.VectorAffineTerm(1,MOI.ScalarAffineTerm(1.0, X[2]))], [-1.0]), MOI.Zeros(1))

    MOI.set(optimizer, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.(1.0, [X[1], X[3]]), 0.0))

    MOI.set(optimizer, MOI.ObjectiveSense(), MOI.MIN_SENSE)
    MOI.optimize!(optimizer)

    @test MOI.get(optimizer, MOI.TerminationStatus()) == MOI.OPTIMAL

    @test MOI.get(optimizer, MOI.PrimalStatus()) == MOI.FEASIBLE_POINT
    @test MOI.get(optimizer, MOI.DualStatus()) == MOI.FEASIBLE_POINT

    @test MOI.get(optimizer, MOI.ObjectiveValue()) ≈ 2 atol=1e-2

    Xv = ones(3)
    @test MOI.get(optimizer, MOI.VariablePrimal(), X) ≈ Xv atol=1e-2
    # @test MOI.get(optimizer, MOI.ConstraintPrimal(), cX) ≈ Xv atol=1e-2

    # @test MOI.get(optimizer, MOI.ConstraintDual(), c) ≈ 2 atol=1e-2
    # @show MOI.get(optimizer, MOI.ConstraintDual(), c)

end

function double_sdp_from_moi(optimizer)
    # solve simultaneously two of these:
    # min X[1,1] + X[2,2]    max y
    #     X[2,1] = 1         [0   y/2     [ 1  0
    #                         y/2 0    <=   0  1]
    #     X >= 0              y free
    # Optimal solution:
    #
    #     ⎛ 1   1 ⎞
    # X = ⎜       ⎟           y = 2
    #     ⎝ 1   1 ⎠
    MOI.empty!(optimizer)
    @test MOI.is_empty(optimizer)

    X = MOI.add_variables(optimizer, 3)
    Y = MOI.add_variables(optimizer, 3)

    vov = MOI.VectorOfVariables(X)
    vov2 = MOI.VectorOfVariables(Y)
    cX = MOI.add_constraint(optimizer, vov, MOI.PositiveSemidefiniteConeTriangle(2))
    cY = MOI.add_constraint(optimizer, vov2, MOI.PositiveSemidefiniteConeTriangle(2))

    c = MOI.add_constraint(optimizer, MOI.VectorAffineFunction([MOI.VectorAffineTerm(1,MOI.ScalarAffineTerm(1.0, X[2]))], [-1.0]), MOI.Zeros(1))
    c2 = MOI.add_constraint(optimizer, MOI.VectorAffineFunction([MOI.VectorAffineTerm(1,MOI.ScalarAffineTerm(1.0, Y[2]))], [-1.0]), MOI.Zeros(1))

    MOI.set(optimizer, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.(1.0, [X[1], X[end], Y[1], Y[end]]), 0.0))

    MOI.set(optimizer, MOI.ObjectiveSense(), MOI.MIN_SENSE)
    MOI.optimize!(optimizer)

    @test MOI.get(optimizer, MOI.TerminationStatus()) == MOI.OPTIMAL

    @test MOI.get(optimizer, MOI.PrimalStatus()) == MOI.FEASIBLE_POINT
    @test MOI.get(optimizer, MOI.DualStatus()) == MOI.FEASIBLE_POINT

    @test MOI.get(optimizer, MOI.ObjectiveValue()) ≈ 2*2 atol=1e-2

    Xv = ones(3)
    @test MOI.get(optimizer, MOI.VariablePrimal(), X) ≈ Xv atol=1e-2
    Yv = ones(3)
    @test MOI.get(optimizer, MOI.VariablePrimal(), Y) ≈ Yv atol=1e-2
    # @test MOI.get(optimizer, MOI.ConstraintPrimal(), cX) ≈ Xv atol=1e-2

    # @test MOI.get(optimizer, MOI.ConstraintDual(), c) ≈ 2 atol=1e-2
    # @show MOI.get(optimizer, MOI.ConstraintDual(), c)

end

function double_sdp_with_duplicates(optimizer)

    MOI.empty!(optimizer)

    x = MOI.add_variable(optimizer)
    X = [x, x, x]

    vov = MOI.VectorOfVariables(X)
    cX = MOI.add_constraint(optimizer, vov, MOI.PositiveSemidefiniteConeTriangle(2))

    c = MOI.add_constraint(optimizer, MOI.VectorAffineFunction([MOI.VectorAffineTerm(1,MOI.ScalarAffineTerm(1.0, X[2]))], [-1.0]), MOI.Zeros(1))

    MOI.set(optimizer, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.(1.0, [X[1], X[3]]), 0.0))

    MOI.set(optimizer, MOI.ObjectiveSense(), MOI.MIN_SENSE)
    MOI.optimize!(optimizer)

    @test MOI.get(optimizer, MOI.TerminationStatus()) == MOI.OPTIMAL

    @test MOI.get(optimizer, MOI.PrimalStatus()) == MOI.FEASIBLE_POINT
    @test MOI.get(optimizer, MOI.DualStatus()) == MOI.FEASIBLE_POINT

    @test MOI.get(optimizer, MOI.ObjectiveValue()) ≈ 2 atol=1e-2

    Xv = ones(3)
    @test MOI.get(optimizer, MOI.VariablePrimal(), X) ≈ Xv atol=1e-2

end

function sdp_wiki(optimizer)
    # https://en.wikipedia.org/wiki/Semidefinite_programming
    MOI.empty!(optimizer)
    @test MOI.is_empty(optimizer)

    X = MOI.add_variables(optimizer, 6)

    vov = MOI.VectorOfVariables(X)
    cX = MOI.add_constraint(optimizer, vov, MOI.PositiveSemidefiniteConeTriangle(3))

    cd1 = MOI.add_constraint(optimizer, MOI.VectorAffineFunction([MOI.VectorAffineTerm(1,MOI.ScalarAffineTerm(1.0, X[1]))], [-1.0]), MOI.Zeros(1))
    cd1 = MOI.add_constraint(optimizer, MOI.VectorAffineFunction([MOI.VectorAffineTerm(1,MOI.ScalarAffineTerm(1.0, X[3]))], [-1.0]), MOI.Zeros(1))
    cd1 = MOI.add_constraint(optimizer, MOI.VectorAffineFunction([MOI.VectorAffineTerm(1,MOI.ScalarAffineTerm(1.0, X[6]))], [-1.0]), MOI.Zeros(1))

    c12_ub = MOI.add_constraint(optimizer, MOI.VectorAffineFunction([MOI.VectorAffineTerm(1,MOI.ScalarAffineTerm(1.0, X[2]))], [0.1]), MOI.Nonpositives(1))   # x <= -0.1 -> x + 0.1 <= 0
    c12_lb = MOI.add_constraint(optimizer, MOI.VectorAffineFunction([MOI.VectorAffineTerm(1,MOI.ScalarAffineTerm(-1.0, X[2]))], [-0.2]), MOI.Nonpositives(1)) # x >= -0.2 -> -x + -0.2 <= 0

    c23_ub = MOI.add_constraint(optimizer, MOI.VectorAffineFunction([MOI.VectorAffineTerm(1,MOI.ScalarAffineTerm(1.0, X[5]))], [-0.5]), MOI.Nonpositives(1)) # x <= 0.5 ->  x - 0.5 <= 0
    c23_lb = MOI.add_constraint(optimizer, MOI.VectorAffineFunction([MOI.VectorAffineTerm(1,MOI.ScalarAffineTerm(-1.0, X[5]))], [0.4]), MOI.Nonpositives(1)) # x >= 0.4 -> -x + 0.4 <= 0

    MOI.set(optimizer, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.(1.0, [X[4]]), 0.0))

    MOI.set(optimizer, MOI.ObjectiveSense(), MOI.MIN_SENSE)

    MOI.optimize!(optimizer)

    obj = MOI.get(optimizer, MOI.ObjectiveValue())
    @test obj ≈ -0.978 atol=1e-2

    MOI.set(optimizer, MOI.ObjectiveSense(), MOI.MAX_SENSE)

    MOI.optimize!(optimizer)

    obj = MOI.get(optimizer, MOI.ObjectiveValue())
    @test obj ≈ 0.872 atol=1e-2

end

simple_lp(optimizer_bridged)
simple_lp_2_1d_sdp(optimizer_bridged)
lp_in_SDP_equality_form(optimizer_bridged)
lp_in_SDP_inequality_form(optimizer_bridged)
sdp_from_moi(optimizer_bridged)
double_sdp_from_moi(optimizer_bridged)
double_sdp_with_duplicates(optimizer_bridged)
sdp_wiki(optimizer_bridged)

# print test
const optimizer_print = MOI.instantiate(
    ()->ProxSDP.Optimizer(
        log_freq = 10, log_verbose = true, timer_verbose = true, extended_log = true, extended_log2 = true,
        tol_gap = 1e-4, tol_feasibility = 1e-4),
    with_bridge_type = Float64)
sdp_wiki(optimizer_print)

# eig solvers
default_solver = MOI.get(optimizer_bridged, MOI.RawOptimizerAttribute("eigsolver"))
min_size_krylov_eigs = MOI.get(optimizer_bridged, MOI.RawOptimizerAttribute("min_size_krylov_eigs"))
MOI.set(optimizer_bridged, MOI.RawOptimizerAttribute("eigsolver"), 1)
MOI.set(optimizer_bridged, MOI.RawOptimizerAttribute("min_size_krylov_eigs"), 1)
sdp_wiki(optimizer_bridged)
MOI.set(optimizer_bridged, MOI.RawOptimizerAttribute("eigsolver"), 2)
sdp_wiki(optimizer_bridged)
MOI.set(optimizer_bridged, MOI.RawOptimizerAttribute("min_size_krylov_eigs"), min_size_krylov_eigs)
MOI.set(optimizer_bridged, MOI.RawOptimizerAttribute("eigsolver"), default_solver)
MOI.set(optimizer_bridged, MOI.RawOptimizerAttribute("full_eig_decomp"), true)
sdp_wiki(optimizer_bridged)
MOI.set(optimizer_bridged, MOI.RawOptimizerAttribute("full_eig_decomp"), false)


using PooksoftOptionsKit
using PyPlot
using Dates

# setup the call test -
stock_price = 54.17
implied_volatility = 0.67
DTE = (31.0/365.0)
number_of_levels = 4
risk_free_rate = 0.15 # this is in percent
dividend_rate = 0.0 # this is in percent

# setup options calculation -
amd_option_parameters = PSOptionKitPricingParameters(implied_volatility, DTE, number_of_levels, risk_free_rate, dividend_rate)

# setup an amd call option -
assetSet = Set{PSAbstractAsset}()
callOptionContract = PSCallOptionContract("AMD", Date(2020,6,25), 60.0, 0.0, 1; sense=:buy, contractMultiplier=1.0)
push!(assetSet, callOptionContract)

# build the pricing tree -
tree = build_binary_price_tree(assetSet, amd_option_parameters, stock_price)

# compute -
result = option_contract_price(tree, amd_option_parameters)


if !@isdefined(Femshop)
    include("../Femshop.jl");
    using .Femshop
end
init_femshop("FVheat2d");

useLog("FVheat2dlog")

# Configuration setup
domain(2)
solverType(FV)
timeStepper(RK4)

# Mesh
n = 20 # number of elements
mesh(QUADMESH, elsperdim=n)

# Variables and BCs
u = variable("u", SCALAR, CELL)
boundary(u, 1, FLUX, "0")

# Time interval and initial condition
T = 0.1;
timeInterval(T)
initial(u, "(sin(pi*x)*sin(2*pi*y))^4")

# The flux and source terms of the conservation equation
# F and S in the following equation:
# Dt(int(u dx)/A) = int(S dx) - int(F.n ds)
coefficient("D", 0.1) # Diffusion rate
fluxAndSource(u, "-D*dot(grad(u),normal())", "0")

#exportCode("fvheat2dcode") # uncomment to export generated code to a file
#importCode("fvheat2dcode") # uncomment to import code from a file

solve(u)

@finalize()

##### Uncomment below to plot
# xy = Femshop.fv_info.cellCenters

# using Plots
# pyplot();
# display(plot(xy[1,:], xy[2,:], u.values[:], st=:surface))


#= Raytracer in Julia =#

using Random
using SharedArrays
using Distributed

#==============================================================
Vectors
==============================================================#

struct Vec3
  x::Float64
  y::Float64
  z::Float64
end

Vec3Zero()::Vec3 = Vec3(0,0,0)
vec_sqrt(v::Vec3)::Vec3 = Vec3(sqrt(v.x), sqrt(v.y), sqrt(v.z))
float_eq(a::Float64, b::Float64)::Bool = abs(a - b) < 0.0001
vec_eq(a::Vec3, b::Vec3)::Bool = float_eq(a.x, b.x) && float_eq(a.y, b.y) && float_eq(a.z, b.z)
dot(a::Vec3, b::Vec3)::Float64 = a.x*b.x + a.y*b.y + a.z*b.z
cross(a::Vec3, b::Vec3)::Vec3 = Vec3(a.y*b.z-a.z*b.y, a.z*b.x-a.x*b.z, a.x*b.y-a.y*b.x)
lengthSquared(a::Vec3)::Float64 = a.x*a.x + a.y*a.y + a.z*a.z
length(a::Vec3)::Float64 = sqrt(lengthSquared(a))
add(a::Vec3, b::Vec3)::Vec3 = Vec3(a.x+b.x, a.y+b.y, a.z+b.z)
add(a::Vec3, b::Float64)::Vec3 = Vec3(a.x+b, a.y+b, a.z+b)
subtract(a::Vec3, b::Vec3)::Vec3 = Vec3(a.x-b.x, a.y-b.y, a.z-b.z)
mul(a::Vec3, b::Vec3)::Vec3 = Vec3(a.x*b.x, a.y*b.y, a.z*b.z)
mul(a::Vec3, b::Float64)::Vec3 = Vec3(a.x*b, a.y*b, a.z*b)
mul(a::Float64, b::Vec3)::Vec3 = Vec3(a*b.x, a*b.y, a*b.z)
div(a::Vec3, b::Vec3)::Vec3 = Vec3(a.x/b.x, a.y/b.y, a.z/b.z)
div(a::Vec3, b::Float64)::Vec3 = Vec3(a.x/b, a.y/b, a.z/b)
eq(a::Vec3, b::Vec3)::Bool = a.x==b.x && a.y==b.x && a.z==b.z
negate(a::Vec3)::Vec3 = Vec3(-a.x, -a.y, -a.z)
unit_vector(a::Vec3)::Vec3 = div(a, length(a))

function normalized(a::Vec3)::Vec3
  l_squared = lengthSquared(a)
  if float_eq(l_squared, 0) || float_eq(l_squared, 1)
    return a
  end
  return div(a, sqrt(l_squared))
end

#==============================================================
Rays
==============================================================#
struct Ray
  origin::Vec3
  direction::Vec3
end
pointAtParameter(ray::Ray, t::Float64)::Vec3 = add(ray.origin, mul(ray.direction, t))

#==============================================================
Utils
==============================================================#

function randomInUnitSphere()::Vec3
  p = Vec3(typemax(Float64), typemax(Float64), typemax(Float64))
  while lengthSquared(p) >= 1
    p = subtract(mul(2.0, Vec3(rand(), rand(), rand())), Vec3(1,1,1))
  end; p
end

function reflect(v::Vec3, n::Vec3)::Vec3
  subtract(v, mul(2.0*dot(v, n),n))
end

# This function either returns true and a refracted vector
# or false and Vec3Zero
function refract(v::Vec3, n::Vec3, ni_over_nt::Float64)::Tuple{Bool, Vec3}
  uv = unit_vector(v)
  dt = dot(uv, n)
  discriminant = 1.0 - ni_over_nt * ni_over_nt * (1.0 - dt * dt)
  if discriminant > 0
    refracted = subtract(mul(ni_over_nt, subtract(uv, mul(n, dt))), mul(n, sqrt(discriminant)))
    return (true, refracted)
  end
  return (false, Vec3Zero())
end

# Polynomial approximation
function schlick(cosine::Float64, reflectiveIndex::Float64)::Float64
  r = ((1-reflectiveIndex) / (1+reflectiveIndex))^2
  r+(1-r)*(1-cosine)^5
end

#==============================================================
Image output
==============================================================#

function writePixelArrayToFile(pixels::Array{Vec3, 2})
  open("output.ppm", "w") do f
    width = size(pixels, 1)
    height = size(pixels, 2)
    write(f, "P3\n$width $height\n255\n")
    for y = reverse(1:height), x = 1:width
        ir = convert(Int64, round(255 * pixels[x, y].x))
        ig = convert(Int64, round(255 * pixels[x, y].y))
        ib = convert(Int64, round(255 * pixels[x, y].z))
        write(f, "$ir $ig $ib\n")
    end
  end
end

#==============================================================
Hitable & HitRecord
==============================================================#
struct HitRecord
  t::Float64
  p::Vec3
  normal::Vec3
  material::Any
end
abstract type Hitable end

#==============================================================
Materials
==============================================================#
abstract type Material end
struct Lambertian <: Material
  albedo::Vec3
end

struct Metal <: Material
  albedo::Vec3
  fuzz::Float64
  Metal(albedo::Vec3, fuzz::Float64) = new(albedo, fuzz < 1 ? fuzz : 1)
end

struct Dialetric <: Material
  reflectiveIndex::Float64
end

function scatter(material::Lambertian, ray::Ray, hitRecord::HitRecord)::Tuple{Bool, Vec3, Ray}
  target = add(add(hitRecord.p, hitRecord.normal), randomInUnitSphere())
  attenuation = material.albedo
  scattered = Ray(hitRecord.p, subtract(target, hitRecord.p))
  return (true, attenuation, scattered)
end

function scatter(material::Metal, ray::Ray, hitRecord::HitRecord)::Tuple{Bool, Vec3, Ray}
  reflected = reflect(unit_vector(ray.direction), hitRecord.normal)
  scattered = Ray(hitRecord.p, add(reflected, mul(material.fuzz, randomInUnitSphere())))
  attenuation = material.albedo
  result = dot(scattered.direction, hitRecord.normal) > 0
  return (result, attenuation, scattered)
end

function scatter(material::Dialetric, ray::Ray, hitRecord::HitRecord)::Tuple{Bool, Vec3, Ray}
  ni_over_nt, reflectProb, cosine = 0.0, 0.0, 0.0
  outwardNormal, refracted = Vec3Zero(), Vec3Zero()
  reflected = reflect(ray.direction, hitRecord.normal)
  attenuation = Vec3(1,1,1)
  scattered = Ray(Vec3Zero(), Vec3Zero())

  if dot(ray.direction, hitRecord.normal) > 0
    outwardNormal = negate(hitRecord.normal)
    ni_over_nt = material.reflectiveIndex
    cosine = material.reflectiveIndex * dot(ray.direction, hitRecord.normal) / length(ray.direction)
  else
    outwardNormal = hitRecord.normal
    ni_over_nt = 1.0 / material.reflectiveIndex
    cosine = -dot(ray.direction, hitRecord.normal) / length(ray.direction)
  end
  result, refracted = refract(ray.direction, outwardNormal, ni_over_nt)
  reflectProb = result ? schlick(cosine, material.reflectiveIndex) : 1
  scattered = (rand() < reflectProb) ? Ray(hitRecord.p, reflected) : Ray(hitRecord.p, refracted)
  return (true, attenuation, scattered)
end

#==============================================================
Geometry
==============================================================#
struct Sphere <: Hitable
  center::Vec3
  radius::Float64
  material::Material
end

# Specialized Sphere hitable
# Returns result, hitRecord
function hit(hitable::Sphere, ray::Ray, t_min::Float64, t_max::Float64, hitRecord)::Tuple{Bool, Any}
  oc = subtract(ray.origin, hitable.center)         # Vec3
  a = dot(ray.direction, ray.direction)             # Float64
  b = dot(oc, ray.direction)                        # Float64
  c = dot(oc, oc) - hitable.radius * hitable.radius # Float64
  discriminant = b*b - a*c                          # Float64
  if discriminant > 0
    temp = (-b - sqrt(b*b-a*c)) / a
    if temp < t_max && temp > t_min
      t = temp
      p = pointAtParameter(ray, t)
      normal = div(subtract(p, hitable.center), hitable.radius)
      return (true, HitRecord(t, p, normal, hitable.material))
    end

    temp = (-b + sqrt(b*b-a*c)) / a
    if temp < t_max && temp > t_min
      t = temp
      p = pointAtParameter(ray, t)
      normal = div(subtract(p, hitable.center), hitable.radius)
      return (true, HitRecord(t, p, normal, hitable.material))
    end
  end
  return (false, nothing)
end

struct HitableList <: Hitable
  list::Array{Hitable}
end

# Specialized hitable list hit function
# Returns result, hitRecord
function hit(hitable::HitableList, ray::Ray, t_min::Float64, t_max::Float64, hitRecord)::Tuple{Bool, Any}
    hitAnything = false
    closestSoFar = t_max
    tempHitRecord, hitRecordResult = nothing, nothing
    for element in hitable.list
      result, tempHitRecord = hit(element, ray, t_min, closestSoFar, hitRecord)
      if result
        hitAnything = true
        closestSoFar = tempHitRecord.t
        hitRecordResult = tempHitRecord
      end
    end
    return (hitAnything, hitRecordResult)
end

#==============================================================
Camera
==============================================================#

mutable struct Camera
  origin::Vec3
  lowerLeftCorner::Vec3
  horizontal::Vec3
  vertical::Vec3
  u::Vec3
  v::Vec3
  w::Vec3
  lensRadius::Float64

  Camera(lookFrom::Vec3, lookAt::Vec3, viewUp::Vec3, verticalFOV::Float64, aspect::Float64, aperture::Float64, focusDistance::Float64) = new(
    origin,
    lowerLeftCorner,
    horizontal,
    vertical,
    u, v, w, lensRadius)

  # Convenience constructor that takes the tuple produced by _Camera method
  Camera(t::Tuple{Vec3, Vec3, Vec3, Vec3, Vec3, Vec3, Vec3, Float64}) = new(t[1], t[2], t[3], t[4], t[5], t[6], t[7], t[8])
end

# Helper methods for Camera constructor
halfHeight(vfov::Float64)::Float64 = tan((vfov*pi/180)/2)
halfWidth(aspect::Float64, half_height::Float64)::Float64 = aspect * half_height
function _Camera(lookFrom::Vec3, lookAt::Vec3, vup::Vec3, vfov::Float64, aspect::Float64, aperture::Float64, focusDistance::Float64)
    lensRadius = aperture / 2
    half_height = halfHeight(vfov)
    half_width = halfWidth(aspect, half_height)
    origin = lookFrom
    w = unit_vector(subtract(lookFrom, lookAt))
    u = unit_vector(cross(vup, w))
    v = cross(w, u)
    a = mul((half_width*focusDistance), u)
    b = mul((half_height*focusDistance), v)
    c = mul(focusDistance, w)
    lowerLeftCorner = subtract(subtract(subtract(origin, a), b), c)
    horizontal = mul(2.0,mul(half_width,mul(focusDistance,u)))
    vertical = mul(2.0,mul(half_height,mul(focusDistance, v)))
    return origin, lowerLeftCorner, horizontal, vertical, u, v, w, lensRadius
end

function randomInUnitDisk()::Vec3
  p = Vec3(typemax(Float64), typemax(Float64), typemax(Float64))
  while dot(p, p) >= 1
    p = subtract(mul(2.0, Vec3(rand(), rand(), 0)), Vec3(1,1,0))
  end
  p
end

function getRay(camera::Camera, s::Float64, t::Float64)::Ray
  rd = mul(camera.lensRadius, randomInUnitDisk())
  offset = mul(camera.u, rd.x * rd.y)
  direction = subtract(subtract(add(add(camera.lowerLeftCorner, mul(s, camera.horizontal)), mul(t, camera.vertical)), camera.origin), offset)
  Ray(add(camera.origin, offset), direction)
end

#==============================================================
Rendering
==============================================================#

function colorFromRay(ray::Ray, world::Hitable, depth::Float64)::Vec3
  hitRecord = nothing
  result, hitRecord = hit(world, ray, 0.001, typemax(Float64), hitRecord)
  if result
    scatterResult, attenuation, scattered = scatter(hitRecord.material, ray, hitRecord)
    if depth < 50 && scatterResult
      return mul(attenuation, colorFromRay(scattered, world, depth+1))
    else
      return Vec3Zero()
    end
  else
    unit_direction = unit_vector(ray.direction)
    t = 0.5 * (unit_direction.y + 1)
    return add(mul((1.0 - t), Vec3(1,1,1)), mul(t, Vec3(0.5, 0.7, 1.0)))
  end
end

function scene()::HitableList
  list = Array{Hitable,1}()
  push!(list, Sphere(Vec3(0,-1000,0), 1000, Lambertian(Vec3(0.5, 0.5, 0.5))))

  for a in -11:10
    for b in -11:10
      chooseMat = rand()
      center = Vec3(Float64(a)+0.9*rand(), 0.2, Float64(b)+0.9*rand())
      if length(subtract(center, Vec3(4,0.2,0))) > 0.9
        # diffuse
        if chooseMat < 0.8
          push!(list, Sphere(center, 0.2, Lambertian(Vec3(rand()*rand(), rand()*rand(), rand()*rand()))))
        # metal
        elseif chooseMat < 0.95
            push!(list, Sphere(center, 0.2, Metal(Vec3(0.5*(1+rand()), 0.5*(1+rand()), 0.5*(1+rand())), 0.5*rand())))
        # glass
        else
          push!(list, Sphere(center, 0.2, Dialetric(1.5)))
        end
      end
    end
  end
  push!(list, Sphere(Vec3(0,1,0), 1.0, Dialetric(1.5)))
  push!(list, Sphere(Vec3(-4,1,0), 1.0, Lambertian(Vec3(0.4,0.2,0.1))))
  push!(list, Sphere(Vec3(4,1,0), 1.0, Metal(Vec3(0.7,0.6,0.5),0.0)))
  HitableList(list)
end

function main()

  # Seed random generator
  Random.seed!(0)

  width = 800
  height = 600
  samples = 50
  pixelArray = fill(Vec3Zero(), width, height)
  world = scene()

  lookFrom = Vec3(13,2,3)
  lookAt = Vec3(0,0,0)
  distToFocus = Float64(10)
  aperture = Float64(0.1)
  camera = Camera(_Camera(lookFrom, lookAt, Vec3(0,1,0), Float64(20), Float64(width) / Float64(height), aperture, distToFocus))

  Threads.@threads for j = reverse(1:height)
   for i = 1:width
    color = Vec3Zero()
    for sample = 1:samples
      u = Float64(i + rand()) / Float64(width)
      v = Float64(j + rand()) / Float64(height)
      ray = getRay(camera, u, v)
      color = add(color, colorFromRay(ray, world, 0.0))
    end
    color = div(color, Float64(samples))
    color = vec_sqrt(color) # Gamma correction
    pixelArray[i, j] = color
   end
  end
  writePixelArrayToFile(pixelArray)
end
@time main()


#TODO add more annotation
println("This file demos some of the mathematical operations on Associative Array")

A = ReadCSV("A.csv")

A = logical(A)

print(A)

print(sum(A,1))

print(sum(A,2))

Aa = A[:,"a,"]

Ab = A[:,"b,"]

Aab = nocol(Aa) & nocol(Ab)

F =  ( Aab )' * A;

print(F)

Fn = F ./ sum(A,1)

print(Fn)

AtA = sqIn(A)

d = diag(adj(AtA))

AtA.A = AtA.A - sparse(diagm(d))

print(AtA)


@testset "3 stages serial with $solver" for solver in lp_solvers
    include("prob5.2_data.jl")

    numScen = 2

    x = Matrix{Vector{JuMP.Variable}}(undef, 2,2)
    v = Matrix{Vector{JuMP.Variable}}(undef, 2,2)
    y = Matrix{Matrix{JuMP.Variable}}(undef, 2,2)
    models = Vector{JuMP.Model}(undef, 3)

    model = StructuredModel(num_scenarios=numScen)

    x[1,1] = @variable(model, [1:n], lowerbound=0)
    v[1,1] = @variable(model, [1:n], lowerbound=0)
    @constraints model begin
        x[1,1] .== v[1,1]
    end
    @objective(model, Min, dot(ic, v[1]))

    models[1] = model

    for s in 1:numScen
        model = StructuredModel(parent=models[1], prob=p2[s], same_children_as=(s == 1 ? nothing : models[2]), id=s, num_scenarios=numScen)
        y[1,s] = @variable(model, [1:n, 1:m], lowerbound=0)
        x[2,s] = @variable(model, [1:n], lowerbound=0)
        v[2,s] = @variable(model, [1:n], lowerbound=0)
        @constraints model begin
            x[2,s] .== x[1,1] + v[2,s]
            demand[j=1:m], sum(y[1,s][:,j]) == D2[j,s]
            ylim[i=1:n], sum(y[1,s][i,:]) <= x[1,1][i]
        end
        @objective(model, Min, dot(ic, v[2,s]) + dot(C, y[1,s] * T))
        if s == 1
            models[2] = model
        end
    end
    for s in 1:numScen
        model = StructuredModel(parent=models[2], prob=p2[s], same_children_as=(s == 1 ? nothing : models[3]), id=s)
        y[2,s] = @variable(model, [1:n, 1:m], lowerbound=0)
        @constraints model begin
            demand[j=1:m], sum(y[2,s][:,j]) == D2[j,s]
            ylim[i=1:n], sum(y[2,s][i,:]) <= x[2,1][i]
        end
        @objective(model, Min, dot(C, y[2,s] * T))
        if s == 1
            models[3] = model
        end
    end

    if !isdefined(@__MODULE__, :testniter_3stages)
        include("prob5.2_3stages_niter.jl")
    end
    fulltest(models[1], 3, 406712.49, [2986,0,7329,854], 402593.71614, 17319.095064, testniter_3stages, solver)
end


using Revise
using AdFem
using LinearAlgebra
using PyPlot

n = 20
mesh = Mesh(n, n, 1/n, degree=2)
bd = bcedge(mesh)
# bdedge = []
# for i = 1:size(bd, 1)
#     a, b = bd[i,:]
#     if mesh.nodes[a,2]<1e-5 && mesh.nodes[b, 2]<1e-5
#         push!(bdedge, [a b])
#     end
# end
# bdedge = vcat(bdedge...)
bdedge = []
for i = 1:size(bd, 1)
    a, b = bd[i,:]
    if mesh.nodes[a,2]>1-1e-5 && mesh.nodes[b, 2]>1-1e-5
        push!(bdedge, [a b])
    end
end
bdedge = vcat(bdedge...)

# nodes = get_bdedge_integration_pts(bdedge, mesh)
# close("all")
# visualize_mesh(mesh)
# # scatter(mesh.nodes[bdedge[:,1],1], mesh.nodes[bdedge[:,1],2])
# # scatter(mesh.nodes[bdedge[:,2],1], mesh.nodes[bdedge[:,2],2], marker = "x")
# scatter(nodes[:,1], nodes[:,2], s = 2)
# savefig("test_quadrature.png")

# nx, ny = AdFem._traction_get_nodes(bdedge, mesh)
# close("all")
# visualize_mesh(mesh)
# scatter(nx[:,1], ny[:,1])
# scatter(nx[:,2], ny[:,2], marker = "+")
# scatter(nx[:,3], ny[:,3], marker = "x")
# savefig("test_vertex.png")

t = eval_f_on_boundary_edge((x, y)->  1.0, bdedge, mesh)
rhs = compute_fem_traction_term1(t, bdedge, mesh)
sum(rhs)
# close("all")
# plot(rhs)
# savefig("test_traction.png")


str1 = "arne" ^ 100
str2 = "bsof" ^ 100
str3 = "arnebsof" ^ 100

dm = ncdmatrix([str1, str2, str3])

@test typeof(dm) == Array{Float64,2}

@test dm[1,1] == 0.0
@test dm[2,2] == 0.0
@test dm[3,3] == 0.0

@test dm[1,2] == dm[2,1]
@test dm[1,3] == dm[3,1]
@test dm[2,3] == dm[3,2]

@test dm[1,2] == ncd(str1, str2)
@test dm[1,3] == ncd(str1, str3)
@test dm[2,3] == ncd(str2, str3)


#################### Model-Based Posterior Statistics ####################
"""
    dic(mc::ModelChains)

Compute the Deviance Information Criterion (DIC) of Spiegelhalter et al. and Gelman et al. from MCMC sampler output.

Returns a `ChainSummary` type object with DIC results from the methods of Spiegelhalter and Gelman in the first and second rows of the `value` field, and the DIC value and effective numbers of parameters in the first and second columns; where

``\\text{DIC} = -2 \\mathcal{L}(\\bar{\\Theta}) + 2 p,``

such that ``\\mathcal{L}(\\bar{\\Theta})`` is the log-likelihood of model outputs given the expected values of model parameters ``\\Theta``, and ``p`` is the effective number of parameters. The latter is defined as ``p_D = -2 \\bar{\\mathcal{L}}(\\Theta) + 2 \\mathcal{L}(\\bar{\\Theta})`` for the method of Spiegelhalter and as ``p_V = \\frac{1}{2} \\operatorname{var}(-2 \\mathcal{L}(\\Theta))`` for the method of Gelman. Results are for all chains combined.

* `mc` : sampler output from a model fit with the `mcmc()` function.
"""
function dic(mc::ModelChains)
    nodekeys = keys(mc.model, :output)

    Dhat = -2.0 * logpdf(mc, mean, nodekeys)
    D = -2.0 * logpdf(mc, nodekeys).value
    p = [mean(D) - Dhat, 0.5 * var(D)]

    ChainSummary(
        [Dhat .+ 2.0 .* p p],
        ["pD", "pV"],
        ["DIC", "Effective Parameters"],
        header(mc),
    )
end

"""
    logpdf(mc::ModelChains, f::Function, nodekeys::Vector{Symbol})

Compute the sum of log-densities at each iteration of MCMC output for stochastic nodes.

Returns a `ModelChains` object of resulting summed log-densities at each MCMC iteration of the supplied chain.

* `mc` : sampler output from a model fit with the `mcmc()`` function.

*  `nodekey/nodekeys` : stochastic model node(s) over which to sum densities (default: all).

* `f` : ??
"""
function logpdf(mc::ModelChains, f::Function, nodekeys::Vector{Symbol})
    m = mc.model

    relistkeys, updatekeys = getsimkeys(mc, nodekeys)
    relistkeys = union(relistkeys, intersect(nodekeys, keys(m, :block)))
    inds = names2inds(mc, relistkeys)
    for key in relistkeys
        isa(m[key], Stochastic{<:GeneralNode}) && throw("not possible with tree objects")
    end

    m[relistkeys] = relist(m, map(i -> f(mc.value[:, i, :]), inds), relistkeys)
    update!(m, updatekeys)
    mapreduce(key -> logpdf(m[key]), +, nodekeys)
end

logpdf(mc::ModelChains, nodekey::Symbol) = logpdf(mc, [nodekey])
"""
    logpdf(mc::ModelChains,
           nodekeys::Vector{Symbol}=keys(mc.model, :stochastic))

"""
function logpdf(mc::ModelChains, nodekeys::Vector{Symbol} = keys(mc.model, :stochastic))
    N = length(mc.range)
    K = size(mc, 3)

    relistkeys, updatekeys = getsimkeys(mc, nodekeys)
    relistkeys = union(relistkeys, intersect(nodekeys, keys(mc.model, :block)))
    inds = names2inds(mc, relistkeys)

    println("MCMC Processing of $N Iterations x $K Chain" * "s"^(K > 1) * "...")

    # set up 1 RemoteChannel & a ProgressMeter for each Chain
    channel = RemoteChannel(() -> Channel{Integer}(1))
    meters = [
        Progress(
            size(mc.value, 1);
            dt = 0.5,
            desc = "Chain $k: ",
            enabled = true,
            offset = k - 1,
            showspeed = true,
        ) for k = 1:K
    ]

    lsts = [
        Any[mc[:, :, [k]], nodekeys, relistkeys, inds, updatekeys, (channel, k)] for
        k = 1:K
    ]

    sims::Vector{Chains} = []
    @sync begin
        # check RemoteChannel for new entries and update the ProgressMeters
        finished_chains = 0
        # loop until all ProgressMeters are finished
        @async while finished_chains < K
            chain = take!(channel)
            chain > 0 ? ProgressMeter.next!(meters[chain]) : finished_chains += 1
        end # while
        @async sims = pmap2(logpdf_modelchains_worker, lsts)
    end # @sync
    println("\n")
    ModelChains(
        cat(sims..., dims = 3),
        mc.model,
        mc.stats,
        mc.stat_names,
        mc.sim_params,
        mc.conv_storage,
    )
end

function logpdf_modelchains_worker(args::Vector)
    mc::ModelChains,
    nodekeys::Vector{Symbol},
    relistkeys::Vector{Symbol},
    inds::Vector{Int},
    updatekeys::Vector{Symbol},
    channel::Tuple{RemoteChannel,Integer} = args
    m = mc.model

    sim = Chains(size(mc, 1), 1, start = first(mc), thin = step(mc), names = ["logpdf"])

    for i = 1:size(mc.value, 1)
        m[relistkeys] = relist(m, mc.value[i, inds, 1], relistkeys)
        update!(m, updatekeys)
        sim.value[i, 1, 1] = mapreduce(key -> logpdf(m[key]), +, nodekeys)
        # send update to RemoteChannel so that the ProgressMeter can be updated in logpdf
        put!(channel[1], channel[2])
    end
    # signal to the logpdf function, that this chain is finished
    put!(channel[1], -1)
    sim
end

predict(mc::ModelChains, nodekey::Symbol) = predict(mc, [nodekey])
"""
    predict(mc::ModelChains,
             nodekeys::Vector{Symbol}=keys(mc.model, :output))

Generate MCMC draws from a posterior predictive distribution.

Returns a `ModelChains` object of draws simulated at each MCMC iteration of the supplied chain. For observed data node ``y``, simulation is from the posterior predictive distribution

``p(\\tilde{y} | y) = \\int p(\\tilde{y} | \\Theta) p(\\Theta | y) d\\Theta,``

where ``\\tilde{y}`` is an unknown observation on the node, ``p(\\tilde{y} | \\Theta)`` is the data likelihood, and ``p(\\Theta | y)`` is the posterior distribution of unobserved parameters ``\\Theta``.

* `mc` : sampler output from a model fit with the `mcmc()`` function.

* `nodekey/nodekeys` : observed Stochastic model node(s) for which to generate draws from the predictive distribution (default: all observed data nodes).
"""
function predict(mc::ModelChains, nodekeys::Vector{Symbol} = keys(mc.model, :output))
    m = mc.model

    outputs = keys(m, :output)
    all(key -> key in outputs, nodekeys) || throw(
        ArgumentError(
            string(
                "nodekeys are not all observed Stochastic nodess : ",
                join(map(string, outputs), ", "),
            ),
        ),
    )

    nodenames = names(m, nodekeys)
    relistkeys, updatekeys = getsimkeys(mc, nodekeys)
    for key in relistkeys
        isa(m[key], Stochastic{<:GeneralNode}) && throw("not possible with tree objects")
    end

    inds = names2inds(mc, relistkeys)

    c = Chains(
        size(mc, 1),
        length(nodenames),
        chains = size(mc, 3),
        start = first(mc),
        thin = step(mc),
        names = nodenames,
    )

    iters, _, chains = size(c.value)
    for k = 1:chains
        for i = 1:iters
            m[relistkeys] = relist(m, mc.value[i, inds, k], relistkeys)
            update!(m, updatekeys)
            f = key -> unlist(m[key], rand(m[key]))
            c.value[i, :, k] = vcat(map(f, nodekeys)...)
        end
    end

    ModelChains(c, m, c.stats, c.stat_names)
end

#################### Auxiliary Functions ####################

function getsimkeys(
    mc::ModelChains,
    nodekeys::Vector{Symbol},
)::Tuple{Vector{Symbol},Vector{Symbol}}

    relistkeys = Symbol[]
    updatekeys = Symbol[]

    m = mc.model
    dag = ModelGraph(m)

    nodekeys = intersect(nodekeys, keys(m, :stochastic))
    blockkeys = keys(m, :block)
    dynamickeys = union(blockkeys, keys(m, :target, blockkeys))
    terminalkeys = union(keys(m, :stochastic), keys(mc, :dependent))

    for v in vertices(dag.graph)
        vkey = dag.keys[v]
        if vkey in dynamickeys
            if any(key -> key in nodekeys, gettargets(dag, v, terminalkeys))
                vkey in terminalkeys ? push!(relistkeys, vkey) : push!(updatekeys, vkey)
            end
        end
    end
    if !isempty(relistkeys)
        append!(updatekeys, nodekeys)
    end

    relistkeys, intersect(keys(m, :dependent), updatekeys)
end


function get_robust_path(start_node::Array, end_node::Array, c::Array, d::Array, Gamma, origin, destination)
	@assert length(start_node)==length(end_node)
	@assert length(start_node)==length(c)
	@assert length(start_node)==length(d)

	no_arc = length(start_node)

	dset = [sort(d, rev=true); 0]
	best_obj = Inf
	best_path = []
	best_x = []

	# Constructing Lset as in Lee and Kwon (2014) http://dx.doi.org/10.1007/s10288-014-0270-7
	k_max = round(Int, ceil((no_arc - Gamma)/2))

	Lset = Array{Int}(k_max+1)
	for k=1:k_max
		Lset[k] = Gamma + 2*k-1
	end
	Lset[k_max+1]=no_arc + 1

	# This for-loop examines the smallest theta first.
	for i=length(Lset):-1:1
		theta = dset[Lset[i]]
		link_cost = c + max(d - theta, 0)

		# println("$i : $theta : $(Gamma*theta) : $best_obj")

		if Gamma*theta > best_obj
			# If this condition is true, next theta values will always have a bigger objective value.
			# We can stop examining theta values.
			break
		end

		(pp, xx, ll) = get_shortest_path(start_node, end_node, link_cost, origin, destination)
		# current_obj = Gamma * theta + dot(link_cost, xx)
		current_obj = Gamma * theta + ll

		if current_obj < best_obj
			best_obj = current_obj
			best_path = pp
			best_x = xx
		end

	end

	return best_path, best_x, best_obj
end


__precompile__()

module SkyCoords
using StaticArrays

export AbstractSkyCoords,
       ICRSCoords,
       GalCoords,
       FK5Coords,
       separation,
       position_angle,
       offset

include("types.jl")

# -----------------------------------------------------------------------------
# Helper functions: Create rotation matrix about a given axis (x, y, z)

function xrotmat(angle)
    s, c = sincos(angle)
    SMatrix{3,3}(1, 0, 0, 0, c, -s, 0, s, c)
end

function yrotmat(angle)
    s, c = sincos(angle)
    SMatrix{3,3}(c, 0, s, 0, 1, 0, -s, 0, c)
end

function zrotmat(angle)
    s, c = sincos(angle)
    SMatrix{3,3}(c, -s, 0, s, c, 0, 0, 0, 1)
end

# (lon, lat) -> [x, y, z] unit vector
function coords2cart(lon, lat)
    sinlon, coslon = sincos(lon)
    sinlat, coslat = sincos(lat)
    SVector{3}(coslat * coslon, coslat * sinlon, sinlat)
end

# [x, y, z] unit vector -> (lon, lat)
cart2coords(v) = atan(v[2], v[1]), atan(v[3], sqrt(v[1] * v[1] + v[2] * v[2]))

# -----------------------------------------------------------------------------
# Constant rotation matricies and precession matrix function

# ICRS --> FK5 at J2000 (See USNO Circular 179, section 3.5)
eta0 = deg2rad(-19.9 / 3600000.0)
xi0 = deg2rad(9.1 / 3600000.0)
da0 = deg2rad(-22.9 / 3600000.0)
const ICRS_TO_FK5J2000 = xrotmat(-eta0) * yrotmat(xi0) * zrotmat(da0)
const FK5J2000_TO_ICRS = ICRS_TO_FK5J2000'

# FK5J2000 --> Gal
# Note that galactic pole and zeropoint of l are somewhat arbitrary
# and not officially defined (as far as I know). The values below are
# from astropy.coordinates, which includes the following comment:
# | "This gives better consistency with other codes than using the values
# |  from Reid & Brunthaler 2004 and the best self-consistency between FK5
# |  -> Galactic and FK5 -> FK4 -> Galactic. The lon0 angle was found by
# |  optimizing the self-consistency."
ngp_fk5j2000_ra = deg2rad(192.8594812065348)
ngp_fk5j2000_dec = deg2rad(27.12825118085622)
lon0_fk5j2000 = deg2rad(122.9319185680026)
const FK5J2000_TO_GAL = (zrotmat(pi - lon0_fk5j2000) *
                         yrotmat(pi / 2.0 - ngp_fk5j2000_dec) * zrotmat(ngp_fk5j2000_ra))
const GAL_TO_FK5J2000 = FK5J2000_TO_GAL'

# Gal --> ICRS: simply chain through FK5J2000
const GAL_TO_ICRS = FK5J2000_TO_ICRS * GAL_TO_FK5J2000
const ICRS_TO_GAL = GAL_TO_ICRS'

# FK5J2000 --> FK5{epoch}
# Computes the precession matrix from J2000 to the given Julian equinox.
# Expression from from Capitaine et al. 2003 as expressed in the USNO
# Circular 179.  This should match the IAU 2006 standard from SOFA.
const pzeta = [2.650545, 2306.083227, 0.2988499, 0.01801828, -0.000005971, -0.0000003173]
const pz = [-2.650545, 2306.077181, 1.0927348, 0.01826837, -0.000028596, -0.0000002904]
const ptheta = [0.0, 2004.191903, -0.4294934, -0.04182264, -0.000007089, -0.0000001274]
function precess_from_j2000(equinox)
    t = (equinox - 2000.0) / 100.0
    tn = 1.0
    zeta = pzeta[1]
    z = pz[1]
    theta = ptheta[1]
    for i = 2:6
        tn *= t
        zeta += pzeta[i] * tn
        z += pz[i] * tn
        theta += ptheta[i] * tn
    end
    zeta = deg2rad(zeta / 3600.0)
    z = deg2rad(z / 3600.0)
    theta = deg2rad(theta / 3600.0)
    zrotmat(-z) * yrotmat(theta) * zrotmat(-zeta)
end

# -----------------------------------------------------------------------------
# Type-dependent methods

lon(c::GalCoords) = c.l
lat(c::GalCoords) = c.b
lon(c::AbstractSkyCoords) = c.ra
lat(c::AbstractSkyCoords) = c.dec

# Abstract away specific field names (ra, dec vs l, b)
coords2cart(c::AbstractSkyCoords) = coords2cart(lon(c), lat(c))

# Rotation matrix between coordinate systems: `rotmat(to, from)`
# Note that all of these return SMatrix{3,3}{Float64}, regardless of
# element type of input coordinates.
rotmat(::Type{T1}, ::Type{T2}) where {T1<:GalCoords,T2<:ICRSCoords} = ICRS_TO_GAL
rotmat(::Type{T1}, ::Type{T2}) where {T1<:ICRSCoords,T2<:GalCoords} = GAL_TO_ICRS

# Define both these so that `convert(FK5Coords{e}, ...)` and
# `convert(FK5Coords{e,T}, ...)` both work. Similar with other
# FK5Coords rotmat methods below.
@generated rotmat(::Type{FK5Coords{e1}}, ::Type{T2}) where {e1,T2<:ICRSCoords} =
    precess_from_j2000(e1) * ICRS_TO_FK5J2000
@generated rotmat(::Type{FK5Coords{e1,T1}}, ::Type{T2}) where {e1,T1,T2<:ICRSCoords} =
    precess_from_j2000(e1) * ICRS_TO_FK5J2000

@generated rotmat(::Type{FK5Coords{e1}}, ::Type{T2}) where {e1,T2<:GalCoords} =
    precess_from_j2000(e1) * GAL_TO_FK5J2000
@generated rotmat(::Type{FK5Coords{e1,T1}}, ::Type{T2}) where {e1,T1,T2<:GalCoords} =
    precess_from_j2000(e1) * GAL_TO_FK5J2000

@generated rotmat(::Type{T1}, ::Type{FK5Coords{e2,T2}}) where {T1<:ICRSCoords,e2,T2} =
    FK5J2000_TO_ICRS * precess_from_j2000(e2)'

@generated rotmat(::Type{T1}, ::Type{FK5Coords{e2,T2}}) where {T1<:GalCoords,e2,T2} =
    FK5J2000_TO_GAL * precess_from_j2000(e2)'

@generated rotmat(::Type{FK5Coords{e1}}, ::Type{FK5Coords{e2,T2}}) where {e1,e2,T2} =
    precess_from_j2000(e1) * precess_from_j2000(e2)'
@generated rotmat(::Type{FK5Coords{e1,T1}}, ::Type{FK5Coords{e2,T2}}) where {e1,T1,e2,T2} =
    precess_from_j2000(e1) * precess_from_j2000(e2)'

# ------------------------------------------------------------------------------
# Distance between coordinates

function _separation(λ_1, ϕ_1, λ_2, ϕ_2)
    Δλ = λ_2 - λ_1
    sin_Δλ, cos_Δλ = sincos(Δλ)
    sin_ϕ1, cos_ϕ1 = sincos(ϕ_1)
    sin_ϕ2, cos_ϕ2 = sincos(ϕ_2)
    return atan(
        hypot(cos_ϕ2 * sin_Δλ, cos_ϕ1 * sin_ϕ2 - sin_ϕ1 * cos_ϕ2 * cos_Δλ),
        sin_ϕ1 * sin_ϕ2 + cos_ϕ1 * cos_ϕ2 * cos_Δλ,
    )
end

"""
    separation(c1::AbstractSkyCoords, c2::AbstractSkyCoords) -> distance

Return angular separation between two sky coordinates, in radians.

The angular separation is calculated using the [Vincenty formula](http://en.wikipedia.org/wiki/Great-circle_distance), which is slightly more
complex and computationally expensive than some alternatives, but is stable at
at all distances, including the poles and antipodes.
"""
separation(c1::T, c2::T) where {T<:AbstractSkyCoords} =
    _separation(lon(c1), lat(c1), lon(c2), lat(c2))

separation(c1::T1, c2::T2) where {T1<:AbstractSkyCoords,T2<:AbstractSkyCoords} =
    separation(c1, convert(T1, c2))

"""
    position_angle(c1::AbstractSkyCoords, c2::AbstractSkyCoords) -> angle

Return position angle between two sky coordinates, in positive radians.

# Examples
```jldoctest
julia> c1 = ICRSCoords(0, 0); c2 = ICRSCoords(deg2rad(1), 0);

julia> position_angle(c1, c2) |> rad2deg
90.0
```
"""
position_angle(c1::T, c2::T) where {T <: AbstractSkyCoords} = _position_angle(lon(c1), lat(c1), lon(c2), lat(c2))
position_angle(c1::T1, c2::T2) where {T1 <: AbstractSkyCoords,T2 <: AbstractSkyCoords} = position_angle(c1, convert(T1, c2))

function _position_angle(λ1, ϕ1, λ2, ϕ2)
    sin_Δλ, cos_Δλ = sincos(λ2 - λ1)
    sin_ϕ1, cos_ϕ1 = sincos(ϕ1)
    sin_ϕ2, cos_ϕ2 = sincos(ϕ2)

    x = sin_ϕ2 * cos_ϕ1 - cos_ϕ2 * sin_ϕ1 * cos_Δλ
    y = sin_Δλ * cos_ϕ2

    return mod2pi(atan(y, x))
end

"""
    offset(::AbstractSkyCoords, separation, pa) -> coordinate

Offset a coordinate by a given angular separation, `separation`, in radians and position angle, `pa`, in radians.

Uses the sine and cosine rules in spherical coordinates with corrections for the antipodes. Returns a sky coordinate of the same type as input.

# Examples
```jldoctest
julia> c1 = ICRSCoords(0, 0);

julia> c2 = offset(c1, deg2rad(1), deg2rad(90))
ICRSCoords{Float64}(0.017453292519943295, 1.0686516840418957e-18)

julia> offset(c1, c2) .|> rad2deg
(1.0, 90.0)
```

# See Also
* [`separation`](@ref), [`position_angle`](@ref)
"""
offset(c::T, sep, pa) where T <: AbstractSkyCoords = T(_offset(lon(c), lat(c), sep, pa)...)

"""
    offset(::AbstractSkyCoords, AbstractSkyCoords) -> angle, angle

Return the separation and position angle in radians between two sky coordinates.

# Examples
```jldoctest
julia> c1 = ICRSCoords(0, 0); c2 = ICRSCoords(deg2rad(1), 0);

julia> offset(c1, c2) .|> rad2deg
(1.0, 90.0)
```

# See Also
* [`separation`](@ref), [`position_angle`](@ref)
"""
offset(c1::AbstractSkyCoords, c2::AbstractSkyCoords) = separation(c1, c2), position_angle(c1, c2)

#= use the cosine rule in spherical geometry with three points, the north pole, the starting point,
and the final point.
angles: (change in lon), (position angle), (-1/position angle)
sides: (separation), (final co-latitude), (starting co-latitude)
=#
function _offset(λ, ϕ, separation, pa)
    sin_a, cos_a = sincos(separation)
    cos_c, sin_c = sincos(ϕ)
    sin_B, cos_B = sincos(pa)

    # solving cosine rule
    cos_b = cos_c * cos_a + sin_c * sin_a * cos_B

    # solving sine rule
    xsin_A = sin_a * sin_B * sin_c
    xcos_A = cos_a - cos_b * cos_c

    # correction for antipodes, otherwise atan2
    ang = sin_c < 1e-12 ? π/2 + cos_c * (π/2 - pa) : atan(xsin_A, xcos_A)

    return mod2pi(λ + ang), asin(cos_b)
end

end # module


immutable BicubicSpline <: AbstractInterpolation
    BicubicSpline() = BicubicSpline
end

immutable BicubicSplineInterpolation{T}
    x::Vector{T}
    y::Vector{T}
    z::Array{T,2}
    A::Array{T,4}
end

function default_settings{D,T}(::Type{BicubicSpline},axes::NTuple{D,T})
    nx = length(axes[1])
    ny = length(axes[2])
    return Irregular{2,Val{(nx,ny)}}, Nearest
end

function deriv(x,y,i)
    nx = length(x)
    if i == 1
        dy = y[i+1] - y[i]
        return dy
    end
    if i == nx
        dy = y[i] - y[i-1]
        return dy
    end
    dy1 = y[i+1] - y[i]
    dy2 = y[i] - y[i-1]
    return 0.5*(dy1 + dy2)
end

function BicubicSpline(x::AbstractVector,y::AbstractVector, z::AbstractArray)
    issorted(x) || throw(ArgumentError("x points must be in ascending order"))
    issorted(y) || throw(ArgumentError("y points must be in ascending order"))
    x = convert(Array{Float64,1},x)
    y = convert(Array{Float64,1},y)
    z = convert(Array{Float64,2},z)
    nx = length(x)
    ny = length(y)
    nx == size(z,1) || throw(ArgumentError("length(x) != size(z,1)"))
    ny == size(z,2) || throw(ArgumentError("length(y) != size(z,2)"))

    px = similar(z)
    py = similar(z)
    pxy = similar(z)

    for i=1:nx
        for j = 1:ny
            px[i,j] = deriv(x,z[:,j],i)
            py[i,j] = deriv(y,z[i,:],j)
        end
        for j=1:ny
            pxy[i,j] = deriv(y,px[i,:],j)
        end
    end
    C = [1 0 0 0; 0 0 1 0; -3 3 -2 -1; 2 -2 1 1]
    A = zeros(4,4,nx,ny)
    for i=1:nx-1
        for j=1:ny-1
            F = [z[i,j]    z[i,j+1]    py[i,j]    py[i,j+1];
                 z[i+1,j]  z[i+1,j+1]  py[i+1,j]  py[i+1,j+1];
                 px[i,j]   px[i,j+1]   pxy[i,j]   pxy[i,j+1];
                 px[i+1,j] px[i+1,j+1] pxy[i+1,j] pxy[i+1,j+1]]
            A[:,:,i,j] = C*(F*C')
        end
    end

    x,y,z, A = promote(x,y,z,A)
    return BicubicSplineInterpolation{eltype(x)}(x,y,z,A)
end

@inline function preprocess(::Type{BicubicSpline}, axes, f)
    return BicubicSpline(axes...,f)
end

function apply_bc(::Type{BicubicSpline},BC::Type{Nearest}, I::Symbol, x::Symbol)
    xbc = apply_nearest(I, 1, x, 1)
    ybc = apply_nearest(I, 2, x, 2)
    quote
        $(xbc)
        $(ybc)
    end
end

function apply_bc(::Type{BicubicSpline},BC::Type{Reflect}, I::Symbol, x::Symbol)
    xbc = apply_reflect(I,1,x,1)
    ybc = apply_reflect(I,2,x,2)
    quote
        $(xbc)
        $(ybc)
    end
end

function apply_bc{V}(::Type{BicubicSpline},BC::Type{Value{V}}, I::Symbol, x::Symbol)
    xbc = apply_value(BC,I,1,x,1)
    ybc = apply_value(BC,I,2,x,2)
    quote
        $(xbc)
        $(ybc)
    end
end

function apply_bc(::Type{BicubicSpline},BC::Type{None}, I::Symbol, x::Symbol)
    apply_none()
end

function apply_bc(::Type{BicubicSpline},BC::Type{Error}, I::Symbol, x::Symbol)
    xbc = apply_error(I,1,x,1)
    ybc = apply_error(I,2,x,2)
    quote
        $(xbc)
        $(ybc)
    end
end

@inline function get_indices{S}(::Type{BicubicSpline}, G::Type{Uniform{2,S}}, I, x)
    dx = I.x[2]-I.x[1]
    ix = clamp(Int(floor((x[1] - I.x[1])/dx)) + 1, 1, S.parameters[1][1]-1)
    dy = I.y[2]-I.y[1]
    iy = clamp(Int(floor((x[2] - I.y[1])/dy)) + 1, 1, S.parameters[1][2]-1)
    return (ix, iy)
end

@inline function get_indices{S}(::Type{BicubicSpline}, G::Type{Irregular{2,S}}, I, x)
    xr = searchsortedlast(I.x,x[1],Base.Order.Forward)
    ix = clamp(xr, 1, S.parameters[1][1]-1)
    yr = searchsortedlast(I.y,x[2],Base.Order.Forward)
    iy = clamp(yr, 1, S.parameters[1][2]-1)
    return (ix, iy)
end

@inline function get_value(::Type{BicubicSpline}, I, x, inds)
    ix, iy = inds
    A = I.A[:,:,ix,iy]
    dx = I.x[ix+1] - I.x[ix]
    xx = (x[1] - I.x[ix])/dx
    dy = I.y[iy+1] - I.y[iy]
    yy = (x[2] - I.y[iy])/dy
    v = (1.0) *(A[1,1] + A[1,2]*yy + A[1,3]*(yy^2) + A[1,4]*(yy^3)) +
        (xx)  *(A[2,1] + A[2,2]*yy + A[2,3]*(yy^2) + A[2,4]*(yy^3)) +
        (xx^2)*(A[3,1] + A[3,2]*yy + A[3,3]*(yy^2) + A[3,4]*(yy^3)) +
        (xx^3)*(A[4,1] + A[4,2]*yy + A[4,3]*(yy^2) + A[4,4]*(yy^3))
    return v
end


function Base.findmax(in::AFArray, dim::Int)
  dim > 0 || throw("Invalid dimension: $(dim).")
  out = af_array[0]
  idx = af_array[0]
  af_imax(out, idx, in, dim-1)
  AFArray(out[1]), AFArray(idx[1])
end

function Base.maximum(in::AFArray, dim::Int)
  dim > 0 || throw("Invalid dimension: $(dim).")
  out = af_array[0]
  af_max(out, in, dim-1)
  AFArray(out[1])
end

function Base.sum(in::AFArray, dim::Int)
  #dim > 0 || throw("Invalid dimension: $(dim).")
  out = af_array[0]
  af_sum(out, in, dim-1)
  AFArray(out[1])
end


function ML_get_f_g(featuresTP::Matrix{Float64}, labels::Vector{Float64}, W::Matrix{Float64}, index::Int64)
	numFeatures = size(featuresTP)[1]
	numClasses = div(length(W),numFeatures)
	W=reshape(W,(numFeatures,numClasses))
	class=labels[index]
	x = featuresTP[:,index]
	a=exp(x'*W)
	aDb=a/sum(a)
	g=x*aDb
	g[:,round(Int,class)]-=x
	(-log(aDb[round(Int,class)]),reshape(g, (numFeatures*numClasses,1)),aDb)
end

function ML_restore_gradient(featuresTP::Matrix{Float64}, labels::Vector{Float64}, aDb, index::Int64)
	numFeatures = size(featuresTP)[1]
	numClasses = 129
	class=labels[index]
	x = featuresTP[:,index]
	g=x*aDb
	g[:,class]-=x
	reshape(g, (numFeatures*numClasses,1))
end

function ML_get_f(features::Matrix{Float64}, labels::Vector{Float64}, W::Matrix{Float64})
	numFeatures = size(features)[2]
	numClasses = div(length(W),numFeatures)
	W=reshape(W,(numFeatures,numClasses))
	f=0
	@inbounds @simd for k=1:length(labels)
		class=labels[k]
		x=features[k,:]
		a=exp(x*W)
		aDb=a/sum(a)
		f+=log(aDb[round(Int,class)])
	end
	(-f/length(labels))
end

# Returns f, percent correctly classified
function ML_for_output(features::Matrix{Float64}, labels::Vector{Float64}, W::Matrix{Float64})
	numFeatures = size(features)[2]
	numClasses = div(length(W),numFeatures)
	W=reshape(W,(numFeatures,numClasses))
	g=zeros(size(W))
	f=0
	pcc=0
	@inbounds @simd for k=1:length(labels)
		class=labels[k]
		x=features[k,:]
		a=exp(x*W)
		(v,chosenclass) = findmax(a);
		if chosenclass == class
			pcc+=1
		end
		aDb=a/sum(a)
		f+=log(aDb[round(Int,class)])
	end
	(-f/length(labels),pcc/length(labels))
end


doc"""
    Frechet(α,θ)

The *Fréchet distribution* with shape `α` and scale `θ` has probability density function

$f(x; \alpha, \theta) = \frac{\alpha}{\theta} \left( \frac{x}{\theta} \right)^{-\alpha-1}
e^{-(x/\theta)^{-\alpha}}, \quad x > 0$

```julia
Frechet()        # Fréchet distribution with unit shape and unit scale, i.e. Frechet(1.0, 1.0)
Frechet(a)       # Fréchet distribution with shape a and unit scale, i.e. Frechet(a, 1.0)
Frechet(a, b)    # Fréchet distribution with shape a and scale b

params(d)        # Get the parameters, i.e. (a, b)
shape(d)         # Get the shape parameter, i.e. a
scale(d)         # Get the scale parameter, i.e. b
```

External links

* [Fréchet_distribution on Wikipedia](http://en.wikipedia.org/wiki/Fréchet_distribution)

"""
immutable Frechet <: ContinuousUnivariateDistribution
    α::Float64
    θ::Float64

    function Frechet(α::Real, θ::Real)
    	@check_args(Frechet, α > zero(α) && θ > zero(θ))
    	new(α, θ)
    end
    Frechet(α::Real) = Frechet(α, 1.0)
    Frechet() = new(1.0, 1.0)
end

@distr_support Frechet 0.0 Inf

#### Parameters

shape(d::Frechet) = d.α
scale(d::Frechet) = d.θ
params(d::Frechet) = (d.α, d.θ)

#### Statistics

mean(d::Frechet) = (α = d.α; α > 1.0 ? d.θ * gamma(1.0 - 1.0 / α) : Inf)

median(d::Frechet) = d.θ * logtwo^(-1.0 / d.α)

mode(d::Frechet) = (iα = -1.0/d.α; d.θ * (1.0 - iα) ^ iα)

function var(d::Frechet)
    if d.α > 2.0
        iα = 1.0 / d.α
        return d.θ^2 * (gamma(1.0 - 2.0 * iα) - gamma(1.0 - iα)^2)
    else
        return Inf
    end
end

function skewness(d::Frechet)
    if d.α > 3.0
        iα = 1.0 / d.α
        g1 = gamma(1.0 - iα)
        g2 = gamma(1.0 - 2.0 * iα)
        g3 = gamma(1.0 - 3.0 * iα)
        return (g3 - 3.0 * g2 * g1 + 2 * g1^3) / ((g2 - g1^2)^1.5)
    else
        return Inf
    end
end

function kurtosis(d::Frechet)
    if d.α > 3.0
        iα = 1.0 / d.α
        g1 = gamma(1.0 - iα)
        g2 = gamma(1.0 - 2.0 * iα)
        g3 = gamma(1.0 - 3.0 * iα)
        g4 = gamma(1.0 - 4.0 * iα)
        return (g4 - 4.0 * g3 * g1 + 3 * g2^2) / ((g2 - g1^2)^2) - 6.0
    else
        return Inf
    end
end

function entropy(d::Frechet)
    const γ = 0.57721566490153286060  # γ is the Euler-Mascheroni constant
    1.0 + γ / d.α + γ + log(d.θ / d.α)
end

#### Evaluation

function logpdf(d::Frechet, x::Float64)
    (α, θ) = params(d)
    if x > 0.0
        z = θ / x
        return log(α / θ) + (1.0 + α) * log(z) - z^α
    else
        return -Inf
    end
end

pdf(d::Frechet, x::Float64) = exp(logpdf(d, x))

cdf(d::Frechet, x::Float64) = x > 0.0 ? exp(-((d.θ / x) ^ d.α)) : 0.0
ccdf(d::Frechet, x::Float64) = x > 0.0 ? -expm1(-((d.θ / x) ^ d.α)) : 1.0
logcdf(d::Frechet, x::Float64) = x > 0.0 ? -(d.θ / x) ^ d.α : -Inf
logccdf(d::Frechet, x::Float64) = x > 0.0 ? log1mexp(-((d.θ / x) ^ d.α)) : 0.0

quantile(d::Frechet, p::Float64) = d.θ * (-log(p)) ^ (-1.0 / d.α)
cquantile(d::Frechet, p::Float64) = d.θ * (-log1p(-p)) ^ (-1.0 / d.α)
invlogcdf(d::Frechet, lp::Float64) = d.θ * (-lp)^(-1.0 / d.α)
invlogccdf(d::Frechet, lp::Float64) = d.θ * (-log1mexp(lp))^(-1.0 / d.α)

function gradlogpdf(d::Frechet, x::Float64)
    (α, θ) = params(d)
    insupport(Frechet, x) ? -(α + 1.0) / x + α * (θ^α) * x^(-α-1.0)  : 0.0
end

## Sampling

rand(d::Frechet) = d.θ * randexp() ^ (-1.0 / d.α)


if basename(pwd()) == "aoc"
    cd("2015/23")
end

mutable struct Registers
    a::BigInt
    b::BigInt
end
Registers() = Registers(0, 0)

function interpret(filename, rs = Registers())
    program = readlines(filename)
    pc = 1
    while checkbounds(Bool, program, pc)
        line = program[pc]
        if startswith(line, "j")
            if startswith(line, "jmp")
                pc += parse(Int, line[4:end])
            else
                cond = startswith(line, "jie") ? iseven : isone
                x = parse(Int, line[7:end])
                pc += cond(getfield(rs, Symbol(line[5]))) ? x : 1
            end
        else
            r = Symbol(line[5])
            if startswith(line, "hlf")
                setfield!(rs, r, getfield(rs, r) ÷ 2)
            elseif startswith(line, "tpl")
                setfield!(rs, r, getfield(rs, r) * 3)
            elseif startswith(line, "inc")
                setfield!(rs, r, getfield(rs, r) + 1)
            end
            pc += 1
        end
    end
    rs
end

@assert interpret("example.txt").a == 2
interpret("input.txt").b
interpret("input.txt", Registers(1, 0)).b


# Range: 1 : 100,000
# Time:  0.540s

# Written by Alex Vear in 2019
# Public domain.  No rights reserved.

startval = 1
endval = 100000

sqrs = [x^2 for x in startval:endval]
tris = [x*(x+1)/2 for x in startval:endval]
println(intersect(sqrs, tris))


struct BindingCurve{T,B,F}
    bond_lengths::B
    potential::Vector{T}
    equilibrium_bond_length::T
    potential_minimum::T
    fit::F
end

function calculate_binding_curve(
    bond_lengths::AbstractVector, model::AdiabaticModel, environment::EvaluationEnvironment
)
    potential = calculate_diatomic_energy.(bond_lengths, model, environment) # Calculate binding curve
    potential_minimum, index = findmin(potential)
    equilibrium_bond_length = bond_lengths[index]
    fit = fit_binding_curve(bond_lengths, potential)
    return BindingCurve(bond_lengths, potential, equilibrium_bond_length, potential_minimum, fit)
end

function fit_binding_curve(bond_lengths, binding_curve)
    itp = interpolate(binding_curve, BSpline(Cubic(Line(OnGrid()))))
    sitp = scale(itp, bond_lengths)
    return sitp
end

function calculate_force_constant(binding_curve)
    return hessian(binding_curve.fit, binding_curve.equilibrium_bond_length)[1]
end

function guess_initial_energy(k, μ, ν, potential_minimum)
    ω = sqrt(k / μ)
    E_guess = (ν + 1/2) * ω
    return E_guess + potential_minimum
end

function plot_binding_curve(bond_lengths, binding_curve, fit)
    plt = lineplot(bond_lengths, binding_curve;
        title="Binding curve", xlabel="Bond length / bohr", ylabel="Energy / Hartree",
        name="Actual values", canvas=DotCanvas, border=:ascii
    )
    lineplot!(plt, bond_lengths, fit.(bond_lengths); name="Fitted curve")
    show(plt)
    println()
    @info "The two lines shown above should closely match. \
        This indicates the evaluation of the potential is working correctly."
end


using Distributions
using Base.Test
import Distributions: GammaMTSampler

gamcdf(x::Float64, α::Float64, s::Float64) =
    ccall(("pgamma", "libRmath-julia"),
        Float64, (Float64, Float64, Float64, Int32, Int32),
        x, α, s, 1, 0)

function test_gamsampler(s, params::(Float64, Float64), ns::Int, tol::Float64)
    # divide bins
    α, scale = params
    step = scale * 0.5
    edges = step:step:(10*scale)
    nbins = length(edges)

    # compute the portion of each bin
    pv = zeros(nbins)
    a = 0.0
    for i = 1:nbins
        apre = a
        a = gamcdf(edges[i], α, scale)
        pv[i] = a - apre
    end

    # sampling
    cnts = zeros(Int, nbins)
    for i = 1:ns
        x = rand(s)
        k = ifloor(x / step) + 1
        if k <= nbins
            cnts[k] += 1
        end
    end
    q = cnts ./ ns

    @test_approx_eq_eps q pv tol
end

# test cases

for params in [(1.0, 1.0), (2.0, 1.0), (3.0, 1.0), (0.5, 1.0),
               (1.0, 2.0), (3.0, 2.0), (0.5, 2.0)]

    test_gamsampler(GammaMTSampler(params...), params, 10^5, 0.015)
end


import LinearAlgebra: Factorization, norm

function compute_relative_error(L::SparseMatrixCSC, A::Factorization, max_iter=100, number_trials=1)
    x = randn(size(L, 1), number_trials)
    x = x / norm(x)
    for k = 1 : max_iter
        x .= L * (L' * x) - A \ x
        x .= x ./ mapslices(norm, x, dims=1)
        # @show x' * (L * (L' * x) - A \ x)
    end
    err = sum(mapslices(norm, x' * (L * (L' * x) - A \ x), dims=1)) / number_trials
    x = randn(size(L, 1), number_trials)
    x = x / norm(x)
    for k = 1 : max_iter
        x .= A \ x
        x .= x ./ mapslices(norm, x, dims=1)
        # @show x' * (A \ x)
    end
    @show nrm = sum(mapslices(norm, x' * (A \ x), dims = 1)) / number_trials
    return abs(err) / nrm
end


@testset "PrGP" begin

    v1 = [1, 0, 0]
    v2 = [0, 1, 0]
    t  = [3, 3, 4]
    @test optimal_coeff(v1, v2, t) ≈ [3, 3]

    t  = [3, 3, 100101010]
    @test optimal_coeff(v1, v2, t) ≈ [3, 3]

    v1 = [1, 0, 1]
    v2 = [0, 1, 0]
    t  = [3, 3, 4]
    @test optimal_coeff(v1, v2, t) ≈ [3.5, 3.0]

    v1 = [1, 1, 1]
    v2 = [-1, 1, 1]
    @test optimal_coeff(v1, v2, t) ≈ [3.25, 0.25]

    v1 = v2 = [1,1,1]
    t = [10, 10, 10,]
    @test optimal_coeff(v1, v2, t) ≈ [5, 5]

    cfg = Config(population_size=100, num_generations=10, p_crossover=0.5, p_mutation=0.5, mutation_step=0.01, min_depth=1, max_depth=6, tournament_size=4, pset=PrimitiveSet(), create_rand_tree=generate_grow, create_population=ramped_half_and_half, fitness=rmse)
    dataset = Dataset(zeros(1, 1), [3, 3, 4])

    e1 = Entry(1, 1, JuliaGSGP.none, JuliaGSGP.dummy_node(), (-1, -1, -1), (-1, -1, -1), [1, 0, 1], [1, 0, 1], 10, 10)
    e2 = Entry(1, 1, JuliaGSGP.none, JuliaGSGP.dummy_node(), (-1, -1, -1), (-1, -1, -1), [0, 1, 0], [0, 1, 0], 10, 10)
    e3 = projection_crossover_entry(e1, e2, dataset, dataset, cfg)

    @test e3.train_semantics ≈ [3.5, 3., 3.5]

end


@testset "Arithmethic" begin
    @testset "Mag" begin
        @test Mag(4) <= Mag(1) + Mag(3) <= Mag(5)
        @test Mag(2) <= Mag(3) - Mag(1) <= Mag(3)
        @test Mag(0) <= Mag(3) - Mag(5) <= Mag(1)
        @test Mag(6) <= Mag(2) * Mag(3) <= Mag(7)
        @test Mag(3) <= Mag(6) / Mag(2) <= Mag(4)

        @test Mag(4) <= Mag(1) + 3 == 1 + Mag(3) <= Mag(5)
        @test Mag(6) <= Mag(2) * 3 == 3 * Mag(2) <= Mag(7)
        @test Mag(2) <= Mag(4) / 2 <= Mag(3)

        @test Mag(1 / 4) <= inv(Mag(4)) <= Mag(1 / 3)
        @test Mag(8) <= Mag(2)^3 <= Mag(9)
        @test Mag(2) <= sqrt(Mag(4)) <= Mag(3)
        @test Mag(1 / 2) <= Arblib.rsqrt(Mag(4)) <= Mag(1)
        @test Mag(5) <= hypot(Mag(3), Mag(4)) <= Mag(6)
        @test Mag(2) <= Arblib.root(Mag(8), 3) <= Mag(3)
        @test Mag(log(8)) <= log(Mag(8)) <= Mag(log(9))
        @test log(Mag(2)) <= Arblib.neglog(inv(Mag(2.0))) <= log(Mag(3))
        @test log(Mag(2)) <= log1p(Mag(1)) <= log(Mag(3))
        @test Mag(exp(2)) <= exp(Mag(2)) <= Mag(exp(3))
        @test Mag(exp(-2)) <= Arblib.expinv(Mag(2)) <= Mag(exp(-1))
        @test Mag(expm1(2)) <= expm1(Mag(2)) <= Mag(expm1(3))
        @test Mag(atan(2)) <= atan(Mag(2)) <= Mag(atan(3))
        @test Mag(cosh(2)) <= cosh(Mag(2)) <= Mag(cosh(3))
        @test Mag(sinh(2)) <= sinh(Mag(2)) <= Mag(sinh(3))

        @test min(Mag(1), Mag(2)) == Mag(1)
        @test max(Mag(1), Mag(2)) == Mag(2)
        @test minmax(Mag(1), Mag(2)) == minmax(Mag(2), Mag(1)) == (Mag(1), Mag(2))
        @test minimum(Mag[10:20; 0:9]) == Mag(0)
        @test maximum(Mag[10:20; 0:9]) == Mag(20)
        @test extrema(Mag[10:20; 0:9]) == (Mag(0), Mag(20))
    end

    @testset "Arf" begin
        @test sign(Arf(2)) == Arf(1)
        @test sign(Arf(-2)) == Arf(-1)
        @test sign(Arf(0)) == Arf(0)

        @test abs(Arf(-2)) == abs(Arf(2)) == Arf(2)
        @test -Arf(2) == Arf(-2)

        @test Arf(1) + Arf(2) ==
              Arf(1) + 2 ==
              2 + Arf(1) ==
              Arf(1) + UInt(2) ==
              UInt(2) + Arf(1) ==
              Arf(1) + UInt8(2) ==
              Arf(1) + UInt8(2) ==
              UInt8(2) + Arf(1) ==
              Arf(3)
        @test Arf(1) - Arf(2) == Arf(1) - 2 == Arf(1) - UInt(2) == Arf(-1)
        @test Arf(2) * Arf(3) ==
              Arf(2) * 3 ==
              3 * Arf(2) ==
              Arf(2) * UInt(3) ==
              UInt(3) * Arf(2) ==
              Arf(6)
        @test Arf(6) / Arf(2) ==
              Arf(6) / 2 ==
              6 / Arf(2) ==
              Arf(6) / UInt(2) ==
              UInt(6) / Arf(2) ==
              Arf(6) / UInt8(2) ==
              UInt8(6) / Arf(2) ==
              Arf(3)

        @test sqrt(Arf(4)) == Arf(2)
        @test Arblib.rsqrt(Arf(4)) == Arf(1 // 2)
        @test Arblib.root(Arf(8), 3) == Arf(2)

        @test min(Arf(1), Arf(2)) == Arf(1)
        @test max(Arf(1), Arf(2)) == Arf(2)
        @test minmax(Arf(1), Arf(2)) == minmax(Arf(2), Arf(1)) == (Arf(1), Arf(2))
        @test minimum(Arf[10:20; 0:9]) == Arf(0)
        @test maximum(Arf[10:20; 0:9]) == Arf(20)
        @test extrema(Arf[10:20; 0:9]) == (Arf(0), Arf(20))
    end

    @testset "$T" for T in [Arb, Acb]
        @test T(1) + T(2) ==
              T(1) + 2 ==
              2 + T(1) ==
              T(1) + UInt(2) ==
              UInt(2) + T(1) ==
              T(1) + UInt8(2) ==
              UInt8(2) + T(1) ==
              T(3)
        @test T(1) - T(2) == T(1) - 2 == T(1) - UInt(2) == T(-1)
        @test T(2) * T(3) ==
              T(2) * 3 ==
              3 * T(2) ==
              T(2) * UInt(3) ==
              UInt(3) * T(2) ==
              T(6)
        @test T(6) / T(2) == T(6) / 2 == T(6) / UInt(2) == T(3)

        @test Base.literal_pow(^, T(2), Val(-2)) ==
              T(2)^-2 ==
              Arblib.sqr(inv(T(2))) ==
              T(1 // 4)
        @test Base.literal_pow(^, T(2), Val(0)) == T(2)^0 == one(T)
        @test Base.literal_pow(^, T(2), Val(1)) == T(2)^1 == T(2)
        @test Base.literal_pow(^, T(2), Val(2)) == T(2)^2 == Arblib.sqr(T(2)) == T(4)

        @test Arblib.root(T(8), 3) == T(2)

        for f in [
            inv,
            sqrt,
            log,
            log1p,
            exp,
            expm1,
            sin,
            cos,
            tan,
            cot,
            sec,
            csc,
            atan,
            asin,
            acos,
            sinh,
            cosh,
            tanh,
            coth,
            sech,
            csch,
            atanh,
            asinh,
        ]
            @test f(T(0.5)) ≈ f(0.5)
        end
        @test acosh(T(2)) ≈ acosh(2)

        @test Arblib.rsqrt(T(1 // 4)) == T(2)
        @test Arblib.sqr(T(3)) == T(9)

        @test sinpi(T(1)) == T(0)
        @test cospi(T(1)) == T(-1)
        @test Arblib.tanpi(T(1)) == T(0)
        @test Arblib.cotpi(T(0.5)) == 0
        @test Arblib.cscpi(T(0.5)) == 1
        @test sinc(T(0.5)) ≈ sinc(0.5)

        @test isequal(sincos(T(1)), (sin(T(1)), cos(T(1))))
        if VERSION >= v"1.6"
            @test isequal(sincospi(T(1)), (sinpi(T(1)), cospi(T(1))))
        else
            @test isequal(Arblib.sincospi(T(1)), (sinpi(T(1)), cospi(T(1))))
        end
        @test isequal(Arblib.sinhcosh(T(1)), (sinh(T(1)), cosh(T(1))))
    end

    @testset "Arb - specific" begin
        @test Arb(1) + Arf(2) == Arf(2) + Arb(1) == Arb(3)
        @test Arb(1) - Arf(2) == Arb(-1)
        @test Arb(2) * Arf(3) == Arf(3) * Arb(2) == Arb(6)
        @test Arb(6) / Arf(2) == UInt(6) / Arb(2) == UInt8(6) / Arb(2) == Arb(3)

        @test Arb(2)^Arb(3) ==
              Arb(2)^3 ==
              Arb(2)^Int8(3) ==
              Arb(2)^UInt(3) ==
              Arb(2)^UInt8(3) ==
              (Arb(2)^-3)^-1 ==
              Arb(8)
        @test hypot(Arb(3), Arb(4)) == Arb(5)

        @test Arblib.sqrtpos(Arb(4)) == Arb(2)
        @test Arblib.sqrtpos(Arb(-4)) == Arb(0)
        @test Arblib.sqrt1pm1(Arb(3)) == Arb(1)

        @test atan(Arb(2), Arb(3)) ≈ atan(2, 3)

        @test min(Arb(1), Arb(2)) == Arb(1)
        @test max(Arb(1), Arb(2)) == Arb(2)
        @test minmax(Arb(1), Arb(2)) == minmax(Arb(2), Arb(1)) == (Arb(1), Arb(2))
        @test Arblib.contains(min(Arb((0, 2)), Arb((-1, 3))), -1)
        @test Arblib.contains(min(Arb((0, 2)), Arb((-1, 3))), 2)
        @test !Arblib.contains(min(Arb((0, 2)), Arb((-1, 3))), 3)
        @test Arblib.contains(max(Arb((0, 2)), Arb((-1, 3))), 0)
        @test Arblib.contains(max(Arb((0, 2)), Arb((-1, 3))), 3)
        @test !Arblib.contains(max(Arb((0, 2)), Arb((-1, 3))), -1)
        @test all(Arblib.contains.(minmax(Arb((0, 2)), Arb((-1, 3))), (-1, 0)))
        @test all(Arblib.contains.(minmax(Arb((0, 2)), Arb((-1, 3))), (2, 3)))
        @test all(.!Arblib.contains.(minmax(Arb((0, 2)), Arb((-1, 3))), (3, -1)))
        @test minimum(Arb[10:20; 0:9]) == Arb(0)
        @test maximum(Arb[10:20; 0:9]) == Arb(20)
        @test extrema(Arb[10:20; 0:9]) == (Arb(0), Arb(20))
        A = [Arb((i, i + 1)) for i = 0:10]
        @test Arblib.contains(minimum(A), Arb((0, 1)))
        @test Arblib.contains(minimum(reverse(A)), Arb((0, 1)))
        @test Arblib.contains(maximum(A), Arb((10, 11)))
        @test Arblib.contains(maximum(reverse(A)), Arb((10, 11)))
        @test all(Arblib.contains.(extrema(A), (Arb((0, 1)), Arb((10, 11)))))
        # These fails with the default implementation of minimum and maximum
        @test Arblib.contains(
            minimum([Arb((-i, -i + 1)) for i = 0:1000]),
            Arb((-1000, -999)),
        )
        @test Arblib.contains(maximum([Arb((i, i + 1)) for i = 0:1000]), Arb((1000, 1001)))
    end

    @testset "Acb - specific" begin
        @test Acb(1) + Arb(2) == Arb(2) + Acb(1) == Acb(3)
        @test Acb(1) - Arb(2) == Acb(-1)
        @test Acb(2) * Arb(3) == Arb(3) * Acb(2) == Acb(6)
        @test Acb(6) / Arb(2) == Acb(3)

        @test Acb(2)^Acb(3) ==
              Acb(2)^Arb(3) ==
              Acb(2)^3 ==
              Acb(2)^Int8(3) ==
              Acb(2)^UInt(3) ==
              Acb(2)^UInt8(3) ==
              (Acb(2)^-3)^-1 ==
              Acb(8)

        @test Acb(2, 3) * Complex{Bool}(0, 0) ==
              Complex{Bool}(0, 0) * Acb(2, 3) ==
              Acb(2, 3) * (0 + 0im)
        @test Acb(2, 3) * Complex{Bool}(0, 1) ==
              Complex{Bool}(0, 1) * Acb(2, 3) ==
              Acb(2, 3) * (0 + 1im)
        @test Acb(2, 3) * Complex{Bool}(1, 0) ==
              Complex{Bool}(1, 0) * Acb(2, 3) ==
              Acb(2, 3) * (1 + 0im)
        @test Acb(2, 3) * Complex{Bool}(1, 1) ==
              Complex{Bool}(1, 1) * Acb(2, 3) ==
              Acb(2, 3) * (1 + 1im)

        @test real(Acb(1, 2)) isa Arb
        @test real(Acb(1, 2)) == Arb(1)
        @test imag(Acb(1, 2)) isa Arb
        @test imag(Acb(1, 2)) == Arb(2)

        @test conj(Acb(1, 2)) == Acb(1, -2)

        @test abs(Acb(3, 4)) isa Arb
        @test abs(Acb(3, 4)) == Arb(5)
    end
end


using DecomposedMDPSolver
using POMDPs, POMDPModels, POMDPSimulators, POMDPModelTools
using LocalFunctionApproximation, GridInterpolations, LocalApproximationPolicyEvaluation
using Flux
using Random
using BSON

# Step 1 - Setup the gridworld problem
action_probability(mdp::SimpleGridWorld, s, a) = 0.25
POMDPs.convert_s(::Type{AbstractArray}, s::GWPos, mdp::SimpleGridWorld) = SVector{4, Float64}(1., s[1], s[2], s[1]*s[2])
POMDPs.gen(mdp::SimpleGridWorld, s, a, rng = Random.GLOBAL_RNG) = (sp =rand(transition(mdp, s, a )), r=reward(mdp, s, a))
POMDPs.initialstate(mdp::SimpleGridWorld) = rand(initialstate_distribution(mdp))
g_size = (9,9)
g = SimpleGridWorld(size = g_size, rewards = Dict(GWPos(g_size...) => 1, GWPos(1,1) => 0), tprob = 1., discount=1)

# Step 2 - Solve the problem exactly using local approximation
grid = RectangleGrid([1:g_size[1] ...], [1:g_size[2]...])
interp = LocalGIFunctionApproximator(grid)
solver = LocalPolicyEvalSolver(interp, action_probability, is_mdp_generative = true, n_generative_samples = 1,  verbose = true, max_iterations = 2000, belres = 1e-6)
dp_policy = solve(solver, g)
render(g, (s=GWPos(1,1), r=1), color = (s) -> 20. *(value(dp_policy, s) - 0.5))

# Step 3 - Solve the problem using a MC policy eval with function approximation
V_network = Chain((x) -> x .- 5.f0 ./ 5.f0, Dense(2,32, relu), Dense(32, 1, sigmoid))
render(g, (s=GWPos(1,1), r=1), color = (s) -> 20. *(V_network(convert_s(Array{Float64, 1}, s, g))[1] - 0.5))

mc_policy_eval!(g, V_network, action_probability, iterations=100)

render(g, (s=GWPos(1,1), r=1), color = (s) -> 20. *(V_network(convert_s(Array{Float64, 1}, s, g))[1] - 0.5))

new_V = BSON.load("log/_4/last_model.bson")[:V]
render(g, (s=GWPos(1,1), r=1), color = (s) -> 20. *(new_V(convert_s(Array{Float64, 1}, s, g))[1] - 0.5))


Alias = Tuple{SparseVector{Int}, SparseVector{Float64}}

"""
    Computes node embeddings on graph `g`, as per [1]

Conducts biased random walks on the graph, then computes word embeddings
by treating those random walks like sentences.

# Arguments
- `walks_per_node::Int`: Number of walks starting on each node,
total number of walks is `nv(g) * walks_per_node`
- `len::Int`: Length of random walks
- `p::Float64`: Return parameter from [1]
- `q::Float64`: In-out parameter from [1]

[1] https://cs.stanford.edu/~jure/pubs/node2vec-kdd16.pdf
"""
function node2vec(g::AbstractGraph; walks_per_node::Int, len::Int, p::Float64, q::Float64)::Vector{Vector{Int}}
    walks = simulate_walks(g; walks_per_node=walks_per_node, len=len, p=p, q=q)
    model = walks2vec(walks)
    return copy(model.vectors)
end

"""
Modified version of Node2Vec.learn_embeddings[1]. Uses
a Julia interface[2] to the original word2vec C code[3].

Treats each random walk like a sentence, and computed word
embeddings using node ID as words.

[1] https://github.com/ollin18/Node2Vec.jl
[2] https://github.com/JuliaText/Word2Vec.jl
[3] https://code.google.com/archive/p/word2vec/
"""
function walks2vec(walks::Vector{Vector{Int}};size::Int=100)

    str_walks=map(x -> string.(x),walks)

    if Sys.iswindows()
        rpath = pwd()
    else
        rpath = "/tmp"
    end
    the_walks = joinpath(rpath,"str_walk.txt")
    the_vecs = joinpath(rpath,"str_walk-vec.txt")

    writedlm(the_walks,str_walks)
    word2vec(the_walks,the_vecs,verbose=true,size=size)
    model=wordvectors(the_vecs)
    rm(the_walks)
    rm(the_vecs)
    model
end

"""
    Conducts a random walk over `g` in O(l) time,
weighted by alias sampling probabilities `alias_nodes`
and `alias_edges`
"""
function node2vec_walk(
    g::AbstractGraph,
    alias_nodes::Dict{Int, Alias},
    alias_edges::Dict{Tuple{Int, Int}, Alias};
    start_node::Int,
    walk_length::Int)::Vector{Int}
    walk::Vector{Int} = [start_node]
    current::Int = start_node
    for _ in 2:walk_length
        curr = walk[end]
        cur_nbrs = sort(neighbors(g, curr))
        if length(walk) == 1
            push!(walk, cur_nbrs[alias_sample(alias_nodes[curr]...)])
        else
            prev = walk[end-1]
            next = cur_nbrs[alias_sample(alias_edges[(prev, curr)]...)]
            push!(walk, next)
        end
    end
    return walk
end

"Returns J and q for a given edge"
function get_alias_edge(g::AbstractGraph, src::Int, dst::Int, p::Float64, q::Float64)::Alias
    unnormalized_probs = spzeros(length(neighbors(g, dst)))
    for (i, dst_nbr) in enumerate(sort(neighbors(g, dst)))
        if dst_nbr == src
            unnormalized_probs[i] = 1/p
        elseif has_edge(g, dst_nbr, src)
            unnormalized_probs[i] = 1
        else
            unnormalized_probs[i] = 1/q
        end
    end
    normalized_probs = unnormalized_probs ./ sum(unnormalized_probs)
    return alias_setup(normalized_probs)
end

"""
    Computes weighted probability transition aliases J and q for nodes and edges
using return parameter `p` and In-out parameter `q`

Implementation as specified in the node2vec paper by Grover and Leskovec (2016)
https://cs.stanford.edu/~jure/pubs/node2vec-kdd16.pdf
"""
function preprocess_modified_weights(g::AbstractGraph, p::Float64, q::Float64)

    alias_nodes::Dict{Int, Alias} = Dict()
    alias_edges::Dict{Tuple{Int, Int}, Alias} = Dict()

    for node in vertices(g)
        probs = [1 / length(neighbors(g, node)) for _ in neighbors(g, node)]
        alias_nodes[node] =  alias_setup(probs)
    end
    for edge in edges(g)
        alias_edges[(edge.src, edge.dst)] = get_alias_edge(g, edge.src, edge.dst, p, q)
        if !is_directed(g)
            alias_edges[(edge.dst, edge.src)] = get_alias_edge(g, edge.dst, edge.src, p, q)
        end
    end
    return alias_nodes, alias_edges
end

"Given a graph, compute `walks_per_node` * nv(g) random walks."
function simulate_walks(g::AbstractGraph; walks_per_node::Int, len::Int, p::Float64, q::Float64)::Vector{Vector{Int}}
    alias_nodes, alias_edges = preprocess_modified_weights(g, p, q)
    walks::Vector{Vector{Int}} = []
    for _ in 1:walks_per_node
        for node in shuffle(vertices(g))
            walk::Vector{Int} = node2vec_walk(g, alias_nodes, alias_edges; start_node=node, walk_length=len)
            push!(walks, walk)
        end
    end
    return walks
end


function km(ind, T, r)
    perm = sortperm(vec(T))
    lastT = T[perm[1]]
    n = 0
    d = length(perm)
    prod = 1.0
    Fr = ones(length(r))
    ri = 1
    for (i, p) in enumerate(perm)
        if T[p] != lastT
            prod *= (1 - n/d)
            while ri <= length(r) && r[ri] <= T[p]
                Fr[ri] = 1-prod
                ri += 1
            end
            n = 0
            d = length(perm) - i + 1
            lastT = T[p]
        end
        n += ind[p]
    end
    prod *= (1 - n/d)
    while ri <= length(r) && r[ri] <= lastT
        Fr[ri] = 1-prod
        ri += 1
    end
    Fr
end


using Test, OrdinaryDiffEq, HDF5, ModelConstructors
include(joinpath(dirname(@__FILE__), "../../../src/includeall.jl"))

m = Li2020()
stategrid, _ = initialize_nojump!(m)
ode_f, ode_callback = eqcond_nojump(m)
tspan = (stategrid[:w][1], stategrid[:w][end])
θ = parameters_to_named_tuple(map(x -> m.parameters[m.keys[x]], get_setting(m, :nojump_parameters)))
prob = ODEProblem(ode_f, get_setting(m, :boundary_conditions)[:p][1], tspan, θ, tstops = stategrid[:w][2:end - 1])
sol = solve(prob, get_setting(m, :ode_integrator),
            reltol = get_setting(m, :ode_reltol),
            abstol = get_setting(m, :ode_abstol), callback = ode_callback)

true_soln = h5read(joinpath(dirname(@__FILE__), "../../reference/models/li2020/li2020_nojump_eqm.h5"), "p_DP5")
@testset "Baseline solution for no jump equilibrium" begin
    @test @test_matrix_approx_eq sol.u true_soln
end


export BandMatrix

type BandMatrix{T} <: AbstractMatrix{T}
	B::Matrix{T}
	m::Integer
	n::Integer
	function BandMatrix{T}(A::AbstractMatrix{T},m::Integer) where T
		n,An1 = size(A)
		B = zeros(T,n,2m+1)
		for k=1:m
			for j=1:k+m
				B[k,j-k+m+1] = A[k,j]
			end
		end
		for k=m+1:n-m
			for j=k-m:k+m
				B[k,j-k+m+1] = A[k,j]
			end
		end
		for k=n-m+1:n
			for j=k-m:n
				B[k,j-k+m+1] = A[k,j]
			end
		end
		new(B,m,n)
	end
end
BandMatrix{T}(A::AbstractMatrix{T},m::Integer) = BandMatrix{T}(A,m)

function Base.ndims(B::BandMatrix)
	return 2
end
function Base.size(B::BandMatrix)
	return B.n,B.n
end
function Base.size(B::BandMatrix,d::Integer)
	return B.n
end

function Base.getindex{T}(B::BandMatrix{T},k::Int64,j::Int64)
	abs(k-j) > B.m ? zero(T) : B.B[k,j-k+B.m+1]
end

function Base.setindex!{T}(B::BandMatrix{T},val::T,k::Int64,j::Int64)
	if abs(k-j) <= B.m
		B.B[k,j-k+B.m+1] = val
	end
end

function (\){T1<:Number,T2<:Number}(B::BandMatrix{T1},b::Vector{T2})
	A = deepcopy(B)
	x = deepcopy(b)
	bandelim!(A,x)
	return x
end

function bandelim!{T1<:Number,T2<:Number}(A::BandMatrix{T1},b::Vector{T2})
	m,n = A.m,A.n
	n == length(b) || throw(DimensionMismatch(""))
	T=promote_type(T1,T2)
	b = convert(Vector{T},b)
	for i=2:m
		for j=0:m-1
			coef = A[i+j,i-1]/A[i-1,i-1]
			for k=i-m-1:m
				A[i+j,i+k] -= coef*A[i-1,i+k]
			end
			b[i+j] -= coef*b[i-1]
		end
	end
	for i=m+1:n-m
		for j=0:m-1
			coef = A[i+j,i-1]/A[i-1,i-1]
			for k=-m:m
				A[i+j,i+k] -= coef*A[i-1,i+k]
			end
			b[i+j] -= coef*b[i-1]
		end
	end
	for i=n-m+1:n
		for j=0:n-i
			coef = A[i+j,i-1]/A[i-1,i-1]
			for k=-m:n-i
				A[i+j,i+k] -= coef*A[i-1,i+k]
			end
			b[i+j] -= coef*b[i-1]
		end
	end

	for i=n:-1:m+1
		b[i] /=A[i,i]
		A[i,i] = one(T)
		for j=1:m
			b[i-j] -= A[i-j,i]*b[i]
			A[i-j,i] = zero(T)
		end
	end
	for i=m:-1:1
		b[i] /=A[i,i]
		A[i,i] = one(T)
		for j=1:i-1
			b[i-j] -= A[i-j,i]*b[i]
			A[i-j,i] = zero(T)
		end
	end
end


using Unitful, UnitfulAssets

module ProductionUnits
    using Unitful
    using Unitful: @unit
    @unit tshirt "tshirt" TShirt 1 false
    @unit worker "worker" Worker 1 false
end

Unitful.register(ProductionUnits);

fabric = 15u"USD"/8u"yd"/44u"inch"
dyes = 20u"USD/lb"
fixer = 8u"USD"/5u"lb"
thread = 19u"USD"/1000u"yd"

# raw_material_cost = 1.6u"m^2" * fabric + 2u"oz" * dyes + 1u"oz" * fixer + 48u"yd" * thread

raw_price_table = (fabric, dyes, fixer, thread)

"""
    raw_material(n::Unitful.Quantity)

Return the amount of each raw material needed to manufacture `n` T-shirts.

The argument `n` must be given in `tshirt` units.

Returns a tuple with the following quantities, respectively:
* The necessary amount of cotton fabric.
* The necessary amount of dye.
* The necessary amount of fixer.
* The necessary amount of thread.

"""
raw_material(n::Unitful.Quantity) = (1.6u"m^2" * n / u"tshirt", 2u"oz" * n / u"tshirt", 1u"oz" * n / u"tshirt", 48u"yd" * n / u"tshirt")

raw_material_price = sum(raw_material(1u"tshirt") .*  raw_price_table)

eletricity_price = 0.13u"USD/kW/hr"
labor_price = 10.50u"USD/worker/hr"
fixed_cost = 24000u"USD/yr"

"""
    manufacturing_cost(n::Unitful.Quantity, t::Unitful.Quantity, tlim::Unitful.Quantity=40u"hr/worker/wk")

Return the cost of manufacturing `n` T-shirts during a time period `t`.

The argument `n` must be given in `tshirt` units, and `t`, in time units.
The optional argument `tlim` is the time limit of work per worker, which
defaults to `40u"hr/worker/wk"`.

Return a tuple with the following quantities, respectively:
* The cost of the production, in US Dollars.
* The cost per T-shirt.
* The number of labor hours required to produce `n` t-shirts.
* The minimum number of workers considering the limit given by `tlim`.
* The eletricity required for the whole manufacturing process.
"""
function production_cost(n::Unitful.Quantity, t::Unitful.Quantity, tlim::Unitful.Quantity=40u"hr/worker/wk")
    labor_hours = 2u"hr/tshirt" * n
    eletricity_spent = 2u"kW * hr/tshirt" * n
    total_cost = n * raw_material_price + labor_hours * labor_price + eletricity_spent * eletricity_price + fixed_cost * t
    cost_per_tshirt = total_cost / n
    min_num_workers = Int(ceil(labor_hours/tlim/t)) * u"worker"
    return total_cost, cost_per_tshirt, labor_hours, min_num_workers, eletricity_spent
end

production_cost(50u"tshirt", 1u"wk")
raw_material(50u"tshirt")

production_cost(2000u"tshirt", 30u"d", 44u"hr/worker/wk")
raw_material(2000u"tshirt")