@testset "Test optimization problem" begin
    # Define logistic map
    function logistic_f(x_next, x, k, θ)
        x_next .=  θ*x - θ*x.^2
        return
    end
    function logistic_g(y, x, k, θ)
        y .= x
        return
    end
    # Define initial conditions
    x0 = [0.5]
    x0_list = Vector{Vector{Float64}}(10)
    x0_list[1] = copy(x0)
    grid = collect(1:10:101)
    k0_list = grid[1:end-1]
    # Initialize buffer
    y = Vector{Vector{Float64}}(100)
    for i = 1:length(y)
        y[i] = zeros(1)
    end
    # Simulate
    x_final = copy(x0)
    for i = 1:length(grid)-1
        x_final = ms.simulate_space_state!(y[grid[i]:grid[i+1]-1],
                                           logistic_f, logistic_g,
                                           x0, (grid[i], grid[i+1]-1), (3.78,))
        if i != length(grid)-1
            x0 .= x_final
            x0_list[i+1] = copy(x0)
        end
    end
    # Sanity check: Check if multiple simulations corresponds to a single one
    y2 = Vector{Vector{Float64}}(100)
    for i = 1:length(y)
        y2[i] = zeros(1)
    end
    x_final2 = ms.simulate_space_state!(y2, logistic_f, logistic_g,
                                       [0.5], (1, 100), (3.78,))
    @test vcat(y...) == vcat(y2...)

    # Simulate using multiple
    θ = [3.78, 3.78, 1.00]
    # Define polynomial model
    # f(x) = θ1*x - θ2*x^2
    @everywhere function f(y, dx, dθ, x, k, θ)
        y .= θ[1]*x - θ[2]*x.^2
        dx .= θ[1] - 2*θ[2]*x
        dθ .=  [ x  -x.^2  0]
        return
    end
    # g(x) = x
    @everywhere function g(y, dx, dθ, x, k, θ)
        y .= θ[3]*x
        dx .= θ[3]
        dθ .=  [ 0 0 x ]
        return
    end
    opt = ms.OptimizationProblem(f, g, x0_list,
                                  y, k0_list, θ)

    @testset "Test cost funciton" begin
      θ_ext = [3.78; 3.78; 1.00; vcat(x0_list...)]
      @test ms.cost_function(opt, θ_ext) ≈ 0.0
      θ_ext = [3.78; 3.78; 2.00; vcat(x0_list...)]
      @test ms.cost_function(opt, θ_ext) ≈ sum(vcat(y...).^2)
    end

    @testset "Test gradient" begin
        function wrapper_cost(θ_ext)
            return ms.cost_function(opt, θ_ext)
        end

        function wrapper_gradient(θ_ext)
            return ms.gradient(opt, θ_ext)
        end

        function finite_difference_gradient(θ_ext)
            return Calculus.gradient(wrapper_cost, θ_ext, :central)
        end

        for θ in ([3.21, 3.2, 1.00],
                [3.21, 1.2, 1.54],
                [3.56, 3.07, 1.54])
            for x0_ext in (collect(linspace(0.2, 0.3, 10)),
                           collect(linspace(0.1, 0.8, 10)))
                θ_ext = [θ; x0_ext]
                @test isapprox(wrapper_gradient(θ_ext),
                               finite_difference_gradient(θ_ext),
                               rtol=1e-5)
            end
        end
    end

    @testset "Test Hessian" begin
        θ = [3.21, 3.2, 1.00]
        x0_ext = collect(linspace(0.2, 0.3, 10))
        θ_ext = [θ; x0_ext]
        hess = ms.hessian(opt, θ_ext)
        p = ones(13)
        @test length(dot(hess, p)) == 13
    end

    @testset "Test constraints" begin
        θ_ext = [3.78; 3.78; 1.00; vcat(x0_list...)]
        @test ms.constraint(opt, θ_ext) ≈ zeros(9) atol=1e-10
        θ_ext = [3.78; 3.78; 2.00; vcat(x0_list...)]
        @test ms.constraint(opt, θ_ext) ≈ zeros(9) atol=1e-10
    end

    @testset "Test constraint Jacobian θ" begin
        function wrapper_constr(θ_ext)
            return copy(ms.constraint(opt, θ_ext))
        end

        function wrapper_jacobian(θ_ext)
            J = ms.jacobian(opt, θ_ext)
            return full(sparse(J.i, J.j, J.v))
        end

        function finite_difference_jacobian(θ_ext)
            return Calculus.jacobian(wrapper_constr, θ_ext, :central)
        end

        for θ in ([3.21, 3.2, 1.00],
                [3.21, 1.2, 1.54],
                [3.56, 3.07, 1.54])
            for x0_ext in (collect(linspace(0.2, 0.3, 10)),
                           collect(linspace(0.1, 0.8, 10)))
                θ_ext = [θ; x0_ext]
                finite_difference_jacobian(θ_ext)
                @test isapprox(wrapper_jacobian(θ_ext),
                               finite_difference_jacobian(θ_ext),
                               rtol=1e-5)
            end
        end
    end
end

@testset "Test extended vector conversions" begin
    srand(1)
    Nθ = 10
    Nx = 3
    M = 5
    θ = randn(Nθ)
    x0_list = Vector{Vector{Float64}}(M)
    for i = 1:M
        x0_list[i] = randn(Nx)
    end
    θ_extended = [θ; vcat(x0_list...)]
    @testset "Test build extended vector" begin
        θ_extended_buffer = zeros(θ_extended)
        ms.build_extended_vector!(θ_extended_buffer, θ, x0_list, Nθ, M, Nx)
        @test θ_extended_buffer ≈ θ_extended
    end

    @testset "Test read extended vector" begin
        θ_buffer = zeros(θ)
        x0_list_buffer = Vector{Vector{Float64}}(M)
        for i = 1:M
            x0_list_buffer[i] = zeros(Nx)
        end
        ms.read_extended_vector!(θ_buffer, x0_list_buffer, θ_extended, Nθ, M, Nx)
        @test θ_buffer ≈ θ
        @test x0_list_buffer ≈ x0_list
    end
end

@testset "Test jacobian indices" begin
    Nθ = 3; Nx = 2; M = 3
    i, j = ms.jacobian_indices(Nθ, M, Nx)
    @test i == [1, 2, 1, 2, 1, 2,
                3, 4, 3, 4, 3, 4,
                1, 2, 1, 2,
                3, 4, 3, 4,
                1, 2,
                3, 4]
    @test j == [1, 1, 2, 2, 3, 3,
                1, 1, 2, 2, 3, 3,
                4, 4, 5, 5,
                6, 6, 7, 7,
                6, 7,
                8, 9]
end

@testset "Test Jacobian from matrices" begin
    Nθ = 3; Nx = 2; M = 3
    i, j = ms.jacobian_indices(Nθ, M, Nx)
    nnz = length(i)
    v = Vector{Float64}(nnz)
    jacobian = ms.SparseMatrixCOO{Float64,Int64}(Nx*(M-1), Nθ + M*Nx, i, j, v)
    jacobian_θ = Vector{Matrix{Float64}}(M-1)
    for i = 1:M-1
        jacobian_θ[i] = ones(Nx, Nθ)
    end
    jacobian_x0 = Vector{Matrix{Float64}}(M-1)
    for i = 1:M-1
        jacobian_x0[i] = ones(Nx, Nx)
    end
    ms.jacobian_from_matrices!(jacobian, jacobian_θ, jacobian_x0,
                            Nθ, M, Nx; reset_constants=true)
    J = full(sparse(jacobian.i, jacobian.j, jacobian.v))
    @test J == Float64[1 1 1 1 1 -1  0  0  0;
                       1 1 1 1 1  0 -1  0  0;
                       1 1 1 0 0  1  1 -1  0;
                       1 1 1 0 0  1  1  0 -1]
end

@testset "Test single shooting" begin
    # From example 2.11, "Linear System Theory and design" Chen
    # With not very realistic values
    # Define linear function
    function linear_f(x_next, x, k)
        A = [-0.005    0   -0.1;
             0         0    0.1;
             1.0      -1.0    0]
        x_next .=  A*x
        return
    end
    # g(x) = x
    function linear_g(y, x, k)
      y .= dot([1, -1, 0], x)
      return
    end
    # Define initial conditions
    x0 = [5, 0.6, 0.5]
    # Initialize buffer
    N = 1000
    y = Vector{Vector{Float64}}(N)
    for i = 1:length(y)
       y[i] = zeros(1)
    end
    # Simulate
    x_final = ms.simulate_space_state!(y, linear_f, linear_g,
                                       x0, (1, N))

    # Define initial values
    k0_list = [1]
    x0_list = [[5, 0.6, 0.5]]
    θ0 = [-0.0047, 0.097, 0.097, 0.98]

    function f(y, dx, dθ, x, k, θ)
        A = [θ[1]  0 -θ[2];
             0     0  θ[3];
             θ[4] -θ[4]  0]
         y .= A*x
         dx .= A
         dθ .= [x[1] -x[3]    0      0;
                0      0      x[3]   0;
                0      0       0    x[1]-x[2]]
         return
    end
    function g(y, dx, dθ, x, k, θ)
      y .= dot([1, -1, 0], x)
      dx .= [1 -1 0]
      dθ .=  0
      return
    end

    opt = ms.OptimizationProblem(f, g, x0_list, y, k0_list, θ0)

    res = ms.solve(opt, options=Dict("gtol" => 1e-12,
                                     "xtol" => 1e-10))

    θext_opt = [-0.005, 0.1, 0.1, 1.0, 5, 0.6, 0.5]
    final_error = norm((res["x"] - θext_opt)./θext_opt,  Inf)
    initial_error = norm(([θ0; x0_list[1]] - θext_opt)./θext_opt, Inf)

    @test final_error / initial_error < 0.01
end

@testset "Test multiple shooting" begin
    M = 10
    function linear_f(x_next, x, k)
        A = [-0.005    0   -0.1;
             0         0    0.1;
             1.0      -1.0    0]
        x_next .=  A*x
        return
    end
    function linear_g(y, x, k)
        y .= dot([1, -1, 0], x)
        return
    end

    # Define initial conditions
    x0 = [5, 0.6, 0.5]
    x0_list = Vector{Vector{Float64}}(10)
    x0_list[1] = copy(x0)
    grid = collect(1:100:1001)
    k0_list = grid[1:end-1]
    # Initialize buffer
    y = Vector{Vector{Float64}}(1000)
    for i = 1:length(y)
        y[i] = zeros(1)
    end
    # Simulate
    x_final = copy(x0)
    for i = 1:length(grid)-1
        x_final = ms.simulate_space_state!(y[grid[i]:grid[i+1]-1],
                                          linear_f, linear_g,
                                          x0, (grid[i], grid[i+1]-1))
        if i != length(grid)-1
           x0 .= x_final
           x0_list[i+1] = copy(x0)
        end
    end

    θ0 = [-0.0049, 0.097, 0.097, 1.0]
    function f(y, dx, dθ, x, k, θ)
        A = [θ[1]  0 -θ[2];
             0     0  θ[3];
             θ[4] -θ[4]  0]
         y .= A*x
         dx .= A
         dθ .= [x[1] -x[3]    0      0;
                0      0      x[3]   0;
                0      0       0    x[1]-x[2]]
         return
    end
    function g(y, dx, dθ, x, k, θ)
      y .= dot([1, -1, 0], x)
      dx .= [1 -1 0]
      dθ .=  0
      return
    end

    opt = ms.OptimizationProblem(f, g, x0_list, y, k0_list, θ0)

    res = ms.solve(opt, options=Dict("gtol" => 1e-8,
                                     "xtol" => 1e-8,
                                     "maxiter" => 200))
    θ_opt = [-0.005, 0.1, 0.1, 1.0]
    final_error = norm((res["x"][1:4] - θ_opt)./θ_opt,  Inf)
    initial_error = norm((θ0 - θ_opt)./θ_opt, Inf)

    @test final_error / initial_error < 0.1
end


include(joinpath(dirname(@__FILE__),"..", "src/KABSCH", "kabsch.jl"))
include(joinpath(dirname(@__FILE__),"..", "src/PDB", "pdb.jl"))
using Kabsch
using PDB
# julia uses MATLAB-style syntax for matrices
P = [0.1 0.2 0.3; 0.5 -0.5 -0.3; 0.2 -0.3 8.3]
centroid = calc_centroid(P)
@test_approx_eq_eps centroid[1] 0.266667 1e-4
@test_approx_eq_eps centroid[2] -0.199999 1e-4
@test_approx_eq_eps centroid[3] 2.766666666666667 1e-4

P = [51.65 -1.90 50.07;
    50.40 -1.23 50.65;
    50.68 -0.04 51.54;
    50.22 -0.02 52.85]

Q = [51.30 -2.99 46.54;
     51.09 -1.88 47.58;
     52.36 -1.20 48.03;
     52.71 -1.18 49.38]

T1, T2, centroid_p1, centroid_p2 = translate_points(P, Q)

# (bad) lazy test if translated matrices are OK ("collisions" obviously can exist)
@test_approx_eq_eps sum(T1) -2.2828578949e-05 1e-4
@test_approx_eq_eps sum(T2) 1.09672546387e-05 1e-4

c_1 = [ 50.73750305  -0.79750001  51.27750397]
c_2 = [ 51.86499786  -1.81249988  47.88249969]

@test_approx_eq_eps centroid_p1[1] c_1[1] 1e-4
@test_approx_eq_eps centroid_p1[2] c_1[2] 1e-4
@test_approx_eq_eps centroid_p1[3] c_1[3] 1e-4

@test_approx_eq_eps rmsd(P,Q) 3.87845580529 1e-4

#@test_approx_eq_eps kabsch_rmsd(P,Q) 72.14091715529968 1e-5

structure = get_structure("2HHB.pdb")
chains = get_chains(structure)

alpha_1 = chains[1]
alpha_2 = chains[3]

beta_1 = chains[2]
beta_2 = chains[4]

#@test_approx_eq_eps kabsch_rmsd(alpha_1,alpha_2) 17.789580172997187 1e-6

#@test_approx_eq_eps kabsch_rmsd(beta_1,beta_2) 19.320217378889485 1e-6

P = [-1. 0. 0.; 0. 2. 0.; 0. 1. 0.; 0. 1. 1]
Q = [0. -1. -1.; 0. -1. 0; 0. 0. 0.; -1. 0. 0.]

@test_approx_eq_eps kabsch_rmsd(P, Q) 0.6947710216026161 1e-6


using Test
using CLIMA
using CLIMA.RootSolvers

@testset "RootSolvers - compact solution correctness" begin
  f(x) = x^2 - 100^2
  f′(x) = 2x
  for FT in [Float32, Float64]
    sol = find_zero(f, FT(0.0), FT(1000.0), SecantMethod(), CompactSolution())
    @test sol.converged
    @test sol.root isa FT
    @test sol.root ≈ 100

    sol = find_zero(f, FT(0.0), FT(1000.0), RegulaFalsiMethod(), CompactSolution())
    @test sol.converged
    @test sol.root isa FT
    @test sol.root ≈ 100

    sol = find_zero(f, FT(1.0), NewtonsMethodAD(), CompactSolution())
    @test sol.converged
    @test sol.root isa FT
    @test sol.root ≈ 100

    sol = find_zero(f, f′, FT(1.0), NewtonsMethod(), CompactSolution())
    @test sol.converged
    @test sol.root isa FT
    @test sol.root ≈ 100
  end
end

@testset "RootSolvers - verbose solution correctness" begin
  f(x) = x^2 - 100^2
  f′(x) = 2x
  for FT in [Float32, Float64]
    sol = find_zero(f, FT(0.0), FT(1000.0), SecantMethod(), VerboseSolution())
    @test sol.converged
    @test sol.root isa FT
    @test sol.root ≈ 100
    @test sol.err < 1e-3
    @test sol.iter_performed < 20
    @test sol.iter_performed+1 == length(sol.root_history) == length(sol.err_history)

    sol = find_zero(f, FT(0.0), FT(1000.0), RegulaFalsiMethod(), VerboseSolution())
    @test sol.converged
    @test sol.root isa FT
    @test sol.root ≈ 100
    @test sol.err < 1e-3
    @test sol.iter_performed < 20
    @test sol.iter_performed+1 == length(sol.root_history) == length(sol.err_history)

    sol = find_zero(f, FT(1.0), NewtonsMethodAD(), VerboseSolution())
    @test sol.converged
    @test sol.root isa FT
    @test sol.root ≈ 100
    @test sol.err < 1e-3
    @test sol.iter_performed < 20
    @test sol.iter_performed+1 == length(sol.root_history) == length(sol.err_history)

    sol = find_zero(f, f′, FT(1.0), NewtonsMethod(), VerboseSolution())
    @test sol.converged
    @test sol.root isa FT
    @test sol.root ≈ 100
    @test sol.err < 1e-3
    @test sol.iter_performed < 20
    @test sol.iter_performed+1 == length(sol.root_history) == length(sol.err_history)
  end
end

if CLIMA.array_type() != Array
  CLIMA.gpu_allowscalar(false)

  @testset "RootSolvers CUDA - compact solution " begin
    for FT in [Float32, Float64]
      X0 = cu(rand(FT, 5,5))
      X1 = cu(rand(FT, 5,5)) .+ 1000
      f(x) = x^2 - 100^2

      sol = RootSolvers.find_zero.(f, X0, X1, SecantMethod(), CompactSolution())
      converged = map(x->x.converged, sol)
      X_roots = map(x->x.root, sol)
      @test all(converged)
      @test eltype(X_roots) == eltype(X0)
      @test all(X_roots .≈ 100)
    end
  end

  @testset "RootSolvers CUDA - verbose solution " begin
    for FT in [Float32, Float64]
      X0 = cu(rand(FT, 5,5))
      X1 = cu(rand(FT, 5,5)) .+ 1000
      f(x) = x^2 - 100^2

      sol = RootSolvers.find_zero.(f, X0, X1, SecantMethod(), VerboseSolution())
      converged = map(x->x.converged, sol)
      X_roots = map(x->x.root, sol)
      err = map(x->x.err, sol)
      iter_performed = map(x->x.iter_performed, sol)
      @test all(converged)
      @test eltype(X_roots) == eltype(X0)
      @test all(X_roots .≈ 100)
      @test all(err .< 1e-2)
      @test all(iter_performed .< 20)
    end
  end
end


using Gadfly
using RDatasets

p1 = plot(dataset("datasets", "iris"), x="SepalLength", y="SepalWidth", Stat.binmean, Geom.point)
# color works
p2 = plot(dataset("datasets", "iris"), x="SepalLength", y="SepalWidth", color = "Species", Stat.binmean, Geom.point)
# n changes number of bins
p3 = plot(dataset("datasets", "iris"), x="SepalLength", y="SepalWidth", color="Species", Stat.binmean(n=3), Geom.point)
# integer works
p4 = plot(x= rand(1:10, 100), y = rand(1:10, 100), Stat.binmean, Geom.point)
#  surimpose regression
p5 = plot(
		layer(dataset("datasets", "iris"), x="SepalLength", y="SepalWidth", color="Species", Stat.binmean(n=5), Geom.point),
		layer(dataset("datasets", "iris"), x="SepalLength", y="SepalWidth", color="Species", Geom.smooth(method=:lm))
	)
vstack(p1, p2, p3, p4, p5)


using ArgParse
s = ArgParseSettings()
@add_arg_table s begin
    "--part2"
    help = "Do part 2"
    action = :store_true
    "--test"
    help = "Run test data"
    action = :store_true
end
parsed_args = parse_args(ARGS, s)

test_suffix = parsed_args["test"] ? "_test" : ""
input = readlines("input/day_17$test_suffix.txt")
txt = split(input[1][14:end], ", ")

function get_min_max(rng)
    mn = [parse(Int16, p) for p in split(rng[3:end],"..")]
    (min = mn[1], max = mn[2])
end

bd = map( get_min_max, txt)
bounds = (x = bd[1], y = bd[2])

# target area: x=20..30, y=-10..-5
function hitsTarget(vel)
    pos = (x=0, y=0)
    ymax = 0
    while (pos.x < bounds.x.max && pos.y >= bounds.y.min)
        pos = (x= (pos.x + vel.x), y= (pos.y + vel.y))
        vel = (x= vel.x > 0 ? vel.x - 1 : 0, y= vel.y -1)
        ymax = max(ymax, pos.y)
        if pos.x >= bounds.x.min && pos.y >= bounds.y.min &&
            pos.x <= bounds.x.max && pos.y <= bounds.y.max
            return (true, false, ymax)
        end
    end
   (false, (pos.x >= bounds.x.max) || vel.x==0, ymax)
end

function part1()
    # 2 * bounds.min.x = (nn+n) >
    minx = Int16(floor(sqrt(2 * bounds.x.min)))
    maxx = bounds.x.max
    ymax = 0
    for vx in minx:maxx
        vy  = 0
        while true
            (hit, over, ym) = hitsTarget((x=vx, y=vy))
            if hit && ym > ymax
                ymax = ym
            end
            if vy > 1000
                break
            end
            vy +=1
        end
    end
    ymax
end

function part2()
    minx = Int16(floor(sqrt(2 * bounds.x.min)))
    maxx = bounds.x.max
    hits = 0
    for vx in minx:maxx
        vy  = bounds.y.min
        while true
            (hit, _, _) = hitsTarget((x=vx, y=vy))
            if hit
                hits += 1
            end
            if vy > 1000
                break
            end
            vy +=1
        end
    end
    hits
end

p = parsed_args["part2"] ? part2 : part1
answer = p()
println("The answer is: $answer")


using StaticArrays
using IntervalArithmetic
import IntervalArithmetic: interval

using LinearAlgebra

import Base: +, *, ^, -

"""
Affine form with center `c`, affine components `γ` and error `Δ`.

Variant where Δ is an interval
"""
struct Aff{N,T<:AbstractFloat}
    c::T   # mid-point
    γ::SVector{N,T}  # affine terms
    Δ::Interval{T}   # error term
end

function Base.show(io::IO, C::Aff{N,T}) where {N,T}
    print(io, "⟨", C.c, "; ", C.γ, "; ", C.Δ, "⟩")
end

==(C::Aff, D::Aff) = C.c == D.c && C.γ == D.γ && C.Δ == D.Δ

"""
Make an `Aff` based on an interval, which is number `i` of `n` total variables.
"""
function Aff(X::Interval{T}, n, i) where {T}
    c = mid(X)
    r = radius(X)

    γ = SVector(ntuple(j->i==j ? r : zero(r), n))

    return Aff(c, γ, Interval{T}(0))
end

+(x::Aff{N,T}, y::Aff{N,T}) where {N,T} = Aff(x.c + y.c, x.γ .+ y.γ, x.Δ + y.Δ)

-(x::Aff{N,T}, y::Aff{N,T}) where {N,T} = Aff(x.c - y.c, x.γ .- y.γ, x.Δ - y.Δ)

interval(C::Aff) = C.c + sum(abs.(C.γ))*(-1..1) + C.Δ

function *(x::Aff{N,T}, y::Aff{N,T}) where {N,T}
    c = x.c * y.c

    γ = x.c .* y.γ + y.c .* x.γ

    Δ = (x.γ ⋅ y.γ) * (0..1)  # ϵ_i^2

    if N > 1
        Δ += sum(x.γ[i] * y.γ[j] for i in 1:N, j in 1:N if i ≠ j) * (-1..1)  # ϵ_i * ϵ_j
    end

    Δ += (x.c + sum(abs.(x.γ))*(-1..1)) * y.Δ
    Δ += (y.c + sum(abs.(y.γ))*(-1..1)) * x.Δ

    Δ += x.Δ * y.Δ

    return Aff(c, γ, Δ)

end

*(x::Aff, α::Real) = Aff(α*x.c, α.*x.γ, α*x.Δ)
*(α::Real, x::Aff) = x * α

+(x::Aff, α::Real) = Aff(α+x.c, x.γ, x.Δ)
+(α::Real, x::Aff) = x + α

-(x::Aff) = Aff(-x.c, .-(x.γ), -x.Δ)
-(x::Aff, α::Real) = Aff(x.c - α, x.γ, x.Δ)
-(α::Real, x::Aff) = α + (-x)

/(x::Aff, α::Real) = Aff(x.c/α, x.γ/α, x.Δ/α)

function ^(x::Aff, n::Integer)

    invert = false

    if n < 0
        invert = true
        n = -n
        @show n
    end

    result = Base.power_by_squaring(x, n)

    if invert
        result = inv(result)
    end

    return result
end

Base.literal_pow(::typeof(^), x::Aff, ::Val{p}) where {T,p} = x^p

x = Aff{2,Float64}(0.0, SVector(1.0, 0.0), 0..0)
y = Aff{2,Float64}(0.0, SVector(0.0, 1.0), 0..0)

x = Aff(3..5, 2, 1)
y = Aff(2..4, 2, 2)
#
# 3-x
# interval(3-x)
#
# x + y
#
#
# interval(x+y)
#
# x * y
# interval(x * y)
#
# interval(x * y)
# interval(x) * interval(y)
#
# z = Aff(-1..1, 1, 1)
# z^2
# interval(z^2)
#
# using Polynomials
#
# p = Poly([-3, 1])
# p2 = p^8
#
# x = 4 ± 1e-4
# y = Aff(x, 1, 1)
#
# interval(y)
# interval(p2(x))
# interval(p2(y))
#
# @time interval(p2(y))
#
#
# f( (x, y) ) = [x^3 + y, (x - y)^2]
#
# X = IntervalBox(-1..1, -1..1)
#
# f(X)
#
# xx = Aff(X[1], 2, 1)
# yy = Aff(X[2], 2, 2)
#
# interval.(f((xx, yy)))
#
# f(X)
#
#
#
#
# x = Aff(4..6, 1, 1)    # example from Messine
# f(x) = x * (10 - x)
#
# f(x)
# interval(f(x))
#
# interval(10*x - x^2)

"General formula for affine approximation of nonlinear functions"
function affine_approx(x::Aff, α, ζ, δ)

    c = α * x.c + ζ
    γ = α .* x.γ
    δ += α * x.Δ  # interval

    return Aff(c, γ, δ)
end

function Base.sqrt(x::Aff, X=interval(x))

    a, b = X.lo, X.hi

    # @show a, b

    # min-range:  de Figuereido book, pg. 64
    α = 1 / (2*√b)
    ζ = (√b) / 2
    δ = ( (√(b) - √(a))^2 / (2*√b) ) * (-1..1)

    return affine_approx(x, α, ζ, δ)
end

function Base.inv(x::Aff, X=interval(x))

    a, b = X.lo, X.hi

    # @show a, b

    # min-range:  de Figuereido book, pg. 70
    α = -1 / (b^2)
    d = interval(1/b - α*b, 1/a - α*a)
    ζ = mid(d)
    δ = radius(d) * (-1..1)

    if a < 0
        ζ = -ζ
    end

    return affine_approx(x, α, ζ, δ)
end


using Test, YaoBase, BitBasis, YaoArrayRegister, YaoBlocks, LinearAlgebra

A(i, j) = control(i, j => shift(2π / (1 << (i - j + 1))))
B(n, i) = chain(n, i == j ? put(i => H) : A(j, i) for j in i:n)
qft(n) = chain(B(n, i) for i in 1:n)

struct QFT{N,T} <: PrimitiveBlock{N,T} end

QFT(::Type{T}, n::Int) where {T} = QFT{n,T}()
QFT(n::Int) = QFT(ComplexF64, n)
circuit(::QFT{N}) where {N} = qft(N)
YaoBlocks.mat(x::QFT) = mat(circuit(x))
YaoBlocks.print_block(io::IO, x::QFT{N}) where {N} = print(io, "QFT($N)")

using FFTW, LinearAlgebra

function YaoBlocks.apply!(r::ArrayReg, x::QFT)
    α = sqrt(length(statevec(r)))
    invorder!(r)
    lmul!(α, ifft!(statevec(r)))
    return r
end

r = rand_state(5)
r1 = r |> copy |> QFT(5)
r2 = r |> copy |> circuit(QFT(5))

@test r1 ≈ r2

Hadamards(n) = repeat(H, 1:n)
ControlU(n, m, U) = chain(n + m, control(k, n+1:n+m => matblock(U^(2^(k - 1)))) for k in 1:n)
PE(n, m, U) = chain(
    n + m, # total number of the qubits
    concentrate(Hadamards(n), 1:n), # apply H in local scope
    ControlU(n, m, U),
    concentrate(QFT(n)', 1:n),
)

using LinearAlgebra

N, M = 3, 5
P = eigen(rand_unitary(1 << M)).vectors
θ = Int(0b110) / 1 << N
phases = rand(1 << M)
phases[bit"010"] = θ
U = P * Diagonal(exp.(2π * im * phases)) * P'

psi = P[:, 3]

λ = exp(2π * im * θ)
@test isapprox(U * psi, λ * psi; atol = 1e-8)

r = join(ArrayReg(psi), zero_state(N))
r |> PE(N, M, U)

p, a = findmax(probs(partial_tr(r, N+1:N+M)))
@test p ≈ 1.0
@test breflect(Int(0b110); nbits = 3) == a - 1


#
# Tests for Jacobi sub-module
#

@testset "Jacobi" begin
    @testset "Abramowitz & Stegun Table 16.1" begin
        dataloc = "data/"
        # table 16.1
        t161, _ = readdlm(joinpath(dataloc, "table_16_1.csv"), ',', header=true)
        # table 17.2
        t172, _ = readdlm(joinpath(dataloc, "table_17_2.csv"), ',', header=true)

        K_lut = Dict(zip(t172[:, 1], t172[:, 2]))

        # data vary first by ϵ ∈ 0:5:90 then α ∈ 0:5:85
        αs = 0:5:85
        ϵs = 0:5:90
        θss = reshape(t161[:, 3], length(ϵs), length(αs))
        θns = reshape(t161[:, 4], length(ϵs), length(αs))

        @testset "α = $α" for (i, α) in enumerate(αs)
            K = K_lut[α]
            m = sind(α)^2
            denom = sqrt(secd(α))

            @testset "ϵ = $ϵ" for (j, ϵ) in enumerate(ϵs)
                j₁ = length(ϵs) - j + 1
                ϵ₁ = ϵs[j₁]
                u = ϵ * K / 90

                θs = θss[j, i]
                θn = θns[j, i]
                θc = θss[j₁, i]/denom
                θd = θns[j₁, i]/denom

                @test Jacobi.sn(u, m) ≈ θs / θn atol=2.5e-9
                @test Jacobi.cn(u, m) ≈ θc / θn atol=2.5e-9
                @test Jacobi.dn(u, m) ≈ θd / θn atol=2.5e-9
                @test Jacobi.nn(u, m) == 1.0

                @test Jacobi.sd(u, m) ≈ θs / θd atol=2.5e-9
                @test Jacobi.cd(u, m) ≈ θc / θd atol=2.5e-9
                @test Jacobi.dd(u, m) == 1.0
                @test Jacobi.nd(u, m) ≈ θn / θd atol=2.5e-9

                @test Jacobi.cc(u, m) == 1.0
                if ϵ != 90
                    # very sensitive around u = K,
                    # estimate of K(0) = pi/2 + 4e-9, so cosine causes errors
                    # also, errors build up in ϵ ≥75°, so lower tolerences for that
                    @test Jacobi.sc(u, m) ≈ θs / θc atol=1e-7
                    @test Jacobi.dc(u, m) ≈ θd / θc atol=1e-7
                    @test Jacobi.nc(u, m) ≈ θn / θc atol=1e-7
                end

                @test Jacobi.ss(u, m) == 1.0
                @test Jacobi.cs(u, m) ≈ θc / θs atol=5.5e-8
                @test Jacobi.ds(u, m) ≈ θd / θs atol=5.5e-8
                @test Jacobi.ns(u, m) ≈ θn / θs atol=5.5e-8

                # ellipj
                s, c, d = ellipj(u, m)
                @test s ≈ θs / θn atol=1e-9
                @test c ≈ θc / θn atol=1e-9
                @test d ≈ θd / θn atol=1e-9
            end
        end
    end

    @testset "u = $u" for u in -1.:0.21:1.0
        @test Jacobi.am(u,0) ≈ u
        @test Jacobi.sn(u,0) ≈ sin(u)
        @test Jacobi.cn(u,0) ≈ cos(u)
        @test Jacobi.dn(u,0) ≈ 1.
        @test Jacobi.nn(u,0) ≈ 1.
        @test Jacobi.cd(u,0) ≈ cos(u)
        @test Jacobi.sd(u,0) ≈ sin(u)
        @test Jacobi.nd(u,0) ≈ 1.
        @test Jacobi.dd(u,0) ≈ 1.
        @test Jacobi.dc(u,0) ≈ sec(u)
        @test Jacobi.nc(u,0) ≈ sec(u)
        @test Jacobi.sc(u,0) ≈ tan(u)
        @test Jacobi.cc(u,0) ≈ 1.
        @test Jacobi.ns(u,0) ≈ csc(u)
        @test Jacobi.ds(u,0) ≈ csc(u)
        @test Jacobi.cs(u,0) ≈ cot(u)
        @test Jacobi.ss(u,0) ≈ 1.

        @test Jacobi.am(u,1) ≈ atan(sinh(u))
        @test Jacobi.sn(u,1) ≈ tanh(u)
        @test Jacobi.cn(u,1) ≈ sech(u)
        @test Jacobi.dn(u,1) ≈ sech(u)
        @test Jacobi.nn(u,1) ≈ 1.
        @test Jacobi.cd(u,1) ≈ 1.
        @test Jacobi.sd(u,1) ≈ sinh(u)
        @test Jacobi.nd(u,1) ≈ cosh(u)
        @test Jacobi.dd(u,1) ≈ 1.
        @test Jacobi.dc(u,1) ≈ 1.
        @test Jacobi.nc(u,1) ≈ cosh(u)
        @test Jacobi.sc(u,1) ≈ sinh(u)
        @test Jacobi.cc(u,1) ≈ 1.
        @test Jacobi.ns(u,1) ≈ coth(u)
        @test Jacobi.ds(u,1) ≈ csch(u)
        @test Jacobi.cs(u,1) ≈ csch(u)
        @test Jacobi.ss(u,1) ≈ 1.
    end

    @testset "errors" begin
        u = 0.5 # random value for testing
        @test_throws DomainError Jacobi.am(u, -0.1)
        @test_throws DomainError Jacobi.am(u, -eps(0.0))

        @test_throws DomainError Jacobi.am(u, 1 + eps(1.0))
        @test_throws DomainError Jacobi.am(u, 1.1)
    end
end


"""
function euler_fluxes(UL,UR)
    UL, UR = states = (rho,u,v,beta)
    2D version.  Figure out multiple dispatch version for 3D?

"""

# assumes field ordering: (rhoL,uL,...,betaL) = UL, (rhoR,uR,...,betaR) = UR
function euler_fluxes(UL,UR)
    rhoL = first(UL); betaL = last(UL)
    rhoR = first(UR); betaR = last(UR)
    logL = (log.(rhoL),log.(betaL))
    logR = (log.(rhoR),log.(betaR))
    return euler_fluxes(UL,UR,logL,logR)
end

# dispatches to 2D/3D
function euler_fluxes(UL,UR,logL,logR)
    return euler_fluxes(UL...,UR...,logL...,logR...)
end

# 2d version
function euler_fluxes(rhoL,uL,vL,betaL,rhoR,uR,vR,betaR,
                      rhologL,betalogL,rhologR,betalogR)

    rholog = logmean.(rhoL,rhoR,rhologL,rhologR)
    betalog = logmean.(betaL,betaR,betalogL,betalogR)

    # arithmetic avgs
    rhoavg = (@. .5*(rhoL+rhoR))
    uavg   = (@. .5*(uL+uR))
    vavg   = (@. .5*(vL+vR))

    unorm = (@. uL*uR + vL*vR)
    pa    = (@. rhoavg/(betaL+betaR))
    f4aux = (@. rholog/(2*(γ-1)*betalog) + pa + .5*rholog*unorm)

    FxS1 = (@. rholog*uavg)
    FxS2 = (@. FxS1*uavg + pa)
    FxS3 = (@. FxS1*vavg)
    FxS4 = (@. f4aux*uavg)

    FyS1 = (@. rholog*vavg)
    FyS2 = (@. FxS3)
    FyS3 = (@. FyS1*vavg + pa)
    FyS4 = (@. f4aux*vavg)
    return (FxS1,FxS2,FxS3,FxS4),(FyS1,FyS2,FyS3,FyS4)
end

# 3d version
function euler_fluxes(rhoL,uL,vL,wL,betaL,rhoR,uR,vR,wR,betaR,
                      rhologL,betalogL,rhologR,betalogR)

    rholog = logmean.(rhoL,rhoR,rhologL,rhologR)
    betalog = logmean.(betaL,betaR,betalogL,betalogR)

    # arithmetic avgs
    rhoavg = (@. .5*(rhoL+rhoR))
    uavg   = (@. .5*(uL+uR))
    vavg   = (@. .5*(vL+vR))
    wavg   = (@. .5*(wL+wR))

    unorm = (@. uL*uR + vL*vR + wL*wR)
    pa    = (@. rhoavg/(betaL+betaR))
    E_plus_p  = (@. rholog/(2*(γ-1)*betalog) + pa + .5*rholog*unorm)

    FxS1 = (@. rholog*uavg)
    FxS2 = (@. FxS1*uavg + pa)
    FxS3 = (@. FxS1*vavg) # rho * u * v
    FxS4 = (@. FxS1*wavg) # rho * u * w
    FxS5 = (@. E_plus_p*uavg)

    FyS1 = (@. rholog*vavg)
    FyS2 = (@. FxS3) # rho * u * v
    FyS3 = (@. FyS1*vavg + pa)
    FyS4 = (@. FyS1*wavg) # rho * v * w
    FyS5 = (@. E_plus_p*vavg)

    FzS1 = (@. rholog*wavg)
    FzS2 = (@. FxS4) # rho * u * w
    FzS3 = (@. FyS4) # rho * v * w
    FzS4 = (@. FzS1*wavg + pa) # rho * w^2 + p
    FzS5 = (@. E_plus_p*wavg)

    Fx = (FxS1,FxS2,FxS3,FxS4,FxS5)
    Fy = (FyS1,FyS2,FyS3,FyS4,FyS5)
    Fz = (FzS1,FzS2,FzS3,FzS4,FzS5)
    return Fx,Fy,Fz
end


using Borg

function dtlz2(vars, objs, consts)
    nvars = length(vars)
    nobjs = length(objs)

    k = nvars - nobjs + 1
    g = 0.0

    for i=nvars-k:nvars-1
        g += (vars[i+1] - 0.5)^2.0
    end

    for i=0:nobjs-1
        objs[i+1] = 1.0 + g

        for j=0:nobjs-i-1-1
            objs[i+1] *= cos(0.5*pi*vars[j+1])
        end

        if i != 0
            objs[i+1] *= sin(0.5*pi*vars[nobjs-i-1+1])
        end
    end
end

nvars = 11
nobjs = 2

lowerBounds = ones(nvars) * 0.0
upperBounds = ones(nvars) * 1.0
epsilons = ones(nobjs) * 0.01

problem = createProblem(lowerBounds, upperBounds, epsilons, 0, dtlz2)

result = solve(problem, 1000000)


using UnitTestDesign

entries = [
    (args = 5, vals = 4),
    (args = 5, vals = 8),
    (args = 10, vals = 4),
    (args = 10, vals = 8),
    (args = 40, vals = 4),
    (args = 40, vals = 8),
]
for e in entries
    vals = fill(1:e[:vals], e[:args])
    c = e[:vals]^e[:args]
    p = size(all_pairs(vals...))[1]
    t = size(all_triples(vals...))[1]
    ve = size(values_excursion(vals...))[1]
    pe = size(pairs_excursion(vals...))[1]
    # This is the power of 10 for the arguments.
    # val^arg = 10^(log10(val^arg)) = 10^(arg * log10(val))
    pow10 = e[:args] * log10(e[:vals])
    println("$(e[:vals]) $(e[:args]) $c $(pow10) $p $t $ve $pe")
end

params = fill(1:4, 40)
ts = all_pairs(params...;
    wayness = Dict(3 => [collect(3:6)])
    )


export proper_divisors, is_abundant

is_abundant(n) = begin
    sum(proper_divisors(n)) > n
end


"""
    GaborKernel(; ell::Real=1.0, p::Real=1.0)

Gabor kernel with lengthscale `ell` and period `p`. Given by
```math
    κ(x,y) =  h(x-z), h(t) = exp(-sum(t.^2./(ell.^2)))*cos(pi*sum(t./p))
```

"""
struct GaborKernel{K<:Kernel} <: Kernel
    kernel::K
    function GaborKernel(;ell=nothing, p=nothing)
        k = _gabor(ell=ell, p=p)
        return new{typeof(k)}(k)
    end
end

@functor GaborKernel

(κ::GaborKernel)(x, y) = κ.kernel(x ,y)

function _gabor(; ell = nothing, p = nothing)
    if ell === nothing
        if p === nothing
            return SqExponentialKernel() * CosineKernel()
        else
            return SqExponentialKernel() * transform(CosineKernel(), 1 ./ p)
        end
    elseif p === nothing
        return transform(SqExponentialKernel(), 1 ./ ell) * CosineKernel()
    else
        return transform(SqExponentialKernel(), 1 ./ ell) * transform(CosineKernel(), 1 ./ p)
    end
end

function Base.getproperty(k::GaborKernel, v::Symbol)
    if v == :kernel
        return getfield(k, v)
    elseif v == :ell
        kernel1 = k.kernel.kernels[1]
    	if kernel1 isa TransformedKernel
    	    return 1 ./ kernel1.transform.s[1]
    	else
    	    return 1.0
    	end
    elseif v == :p
        kernel2 = k.kernel.kernels[2]
        if kernel2 isa TransformedKernel
            return 1 ./ kernel2.transform.s[1]
        else
            return 1.0
        end
    else
        error("Invalid Property")
    end
end

Base.show(io::IO, κ::GaborKernel) = print(io, "Gabor Kernel (ell = ", κ.ell, ", p = ", κ.p, ")")

kernelmatrix(κ::GaborKernel, x::AbstractVector) = kernelmatrix(κ.kernel, x)

function kernelmatrix(κ::GaborKernel, x::AbstractVector, y::AbstractVector)
    return kernelmatrix(κ.kernel, x, y)
end

kerneldiagmatrix(κ::GaborKernel, x::AbstractVector) = kerneldiagmatrix(κ.kernel, x)


using Displaz

function example(N)
    t = range(0, stop=1, length=N)'
    P = (10 .+ 0.2*rand(Float64, size(t))) .* vcat(exp.(5*t).*cos.(100*pi*t),
                                                   exp.(5*t).*sin.(100*pi*t),
                                                   100*t);
    C = vcat(t, 1 .- t, zero(t));
    sz  = map(Float32, t)
    shape = mod.(rand(UInt8, N), 6)
    plot3d!(P, color = C, label="Top series", markershape=shape)
    P[3,:] = -P[3,:]
    plot3d!(P, color = C, markersize = sz, markershape="-", label="Bottom series")
end

example(1_000_000)


using Statistics
using StatsModels
using GLM
using DataFrames
using CSV
using Lathe

data = DataFrame(CSV.File("automobili.csv"))
data = Lathe.preprocess.predict(data, :model)
dataTrain, dataTest = Lathe.preprocess.TrainTestSplit(data,.80)
fm = @formula(cena ~ kubikaza+godiste+model)
linearRegressor = lm(fm, dataTrain)

predvidjenaCena = predict(linearRegressor, dataTest)
println("Procenjene cene vozila: ")
for i in 1:length(predvidjenaCena)
   println("X$i: $(dataTest.proizvodjac[i])  $(dataTest.model[i]) $(dataTest.kubikaza[i]) $(dataTest.godiste[i]) Cena: $(dataTest.cena[i]) Procena: $(predvidjenaCena[i])")
end

# Procena greske
# Racunanje i ispis gresaka
errorsTest = dataTest.cena-predvidjenaCena
println()
println("Spisak svih gresaka pri testiranju je: $(round.(abs.(errorsTest); digits = 2))")
# Racunanje i ispis proseka greske
absMeanErrorTest = mean(abs.(errorsTest))
println("Prosecna absolutna greska pri testiranju je: $absMeanErrorTest evra")
# Racunanje prosecne relativne greske u procentima MAPE
mapeTest = mean(abs.(errorsTest./dataTest.cena))
println("Prosecna relativna greska pri testiranju je: $(mapeTest*100) procenata")


using MAT
using MultipleViewGeometry, Test
using MultipleViewGeometry.ModuleCostFunction
using MultipleViewGeometry.ModuleTypes
using BenchmarkTools
using StaticArrays, LinearAlgebra, SparseArrays

file = matopen("debug/mPts1.mat")
mpts1 = read(file,"mPts1")
close(file)

file = matopen("debug/mPts2.mat")
mpts2 = read(file,"mPts2")
close(file)

file = matopen("debug/covs.mat")
covs = read(file,"covs")
close(file)

file = matopen("debug/Ct.mat")
Ct = read(file,"Ct")
close(file)

npts, dim  = size(mpts1)

s²  = 1e-7
s = sqrt(s² )
Λ₁ =  [SMatrix{3,3}(s² * Matrix(Diagonal([1.0,1.0,0.0]))) for i = 1:npts]
Λ₂ =  [SMatrix{3,3}(s² *Matrix(Diagonal([1.0,1.0,0.0]))) for i = 1:npts]

𝒪 = [Point2DH(vcat(mpts1[i,:],[1])) for i =1:npts]
𝒪ʹ= [Point2DH(vcat(mpts2[i,:],[1])) for i =1:npts]

𝐅₀ = estimate(FundamentalMatrix(),DirectLinearTransform(), (𝒪, 𝒪ʹ))
𝐅 = estimate(FundamentalMatrix(),
                           FundamentalNumericalScheme(reshape(𝐅₀,9,1), 5, 1e-10),
                                                             (Λ₁,Λ₂), (𝒪, 𝒪ʹ))
𝐟 = reshape(𝐅,9,1)
𝐟 = 𝐟 / norm(𝐟)

# # Validate the covariance matrix of an estimate based on the AML cost function.
C1 = covariance_matrix(AML(), HessianApproximation(), FundamentalMatrix(), reshape(𝐅,9,1), (Λ₁,Λ₂), (𝒪 , 𝒪ʹ))
C2 = covariance_matrix(AML(), CanonicalApproximation(), FundamentalMatrix(),  𝐟, (Λ₁,Λ₂), (𝒪 , 𝒪ʹ))

@test norm(C1-Ct) / norm(Ct) * 100 < 0.6
@test norm(C2-Ct) / norm(Ct) * 100 < 0.6


abstract type CorrelationStructure end

# not mutable because it will never change
struct Independence <: CorrelationStructure
    N::Number
    α
end

Independence(N, α = 0) = Independence(N, α)

mutable struct Exchangeable <: CorrelationStructure
    N::Real
    α̂::Real
end

mutable struct Unstructered <: CorrelationStructure
    N::Real
    α̂
end

(X::Independence)() = Matrix{Int}(I, X.N, X.N)

(X::Unstructered)() = reshape(X.α̂, (X.N, X.N))

function (X::Exchangeable)()
    R = zeros(X.N, X.N)
    for j in 1:X.N
        for k in 1:X.N
            R[j, k] = j == k ? 1 : X.α̂
        end
    end
    return R
end

function updateα(::Type{Independence}, n, m, e, ϕ)
    Independence(m)()
end

function updateα(::Type{Exchangeable}, n, m, e, ϕ)
    R = updateα(Unstructered, n, m, e, ϕ)
    mean([R[r] for r in CartesianIndices(R) if r[1] ≠ r[2]])
end

# n is the number of clusters
# m is the number of observations within clusters
# e is the pearsons residuals within a cluster
# ϕ is well... ϕ
# returns the unstructured correlation matrix
function updateα(::Type{Unstructered}, n, m, e, ϕ)
    R = zeros(m, m)
    for i in 1:n
        for j in 1:m
            for k in 1:m
                R[j, k] += e[i][j]*e[i][k]
            end
        end
    end
    R = R / (n * ϕ)
    R[diagind(R)] .= 1
    return R
end


using KernelLibrary, KernelRecursiveLeastSquares, Winston

x = randn(5000,20);
h = randn(20,1);
h2 = randn(20,1);
y = (x*h + 1).^3 + 4*(x*h2 + 1).^2+ 500*randn(5000,1);

x2 = randn(200,20);
y2 = (x2*h + 1).^3 + 4*(x2*h2 + 1).^2;

x=x'; y=y';
x2=x2'; y2=y2';

k(x,y) = polynomial_kernel(x,y,2);

(alpha1, dict1, Kinv1, dict1_idx) = krls(x, y, kernelfunc=k, nu=1., maxdict=30);

@time (alpha1, dict1, Kinv1, dict1_idx) = krls(x, y, kernelfunc=k, nu=1., maxdict=300);

ypred1 = zeros(size(x2,2),1);
for ii in [1:size(x2,2)]
	tmp = k(dict1, x2[:,ii])*alpha1;
	ypred1[ii]=tmp[1];
end

cor(ypred1, y2')

k(x,y) = multiquad_kernel(x,y,1);

@time (alpha2, dict2, Kinv2, dict2_idx) = krls(x, y, kernelfunc=k, nu=3., maxdict=300, index="rand");

ypred2 = zeros(size(x2,2),1);
for ii in [1:size(x2,2)]
	tmp = k(dict2, x2[:,ii])*alpha2;
	ypred2[ii]=tmp[1];
end

cor(ypred2, y2')

k(x,y) = power_kernel(x,y,3);

@time (alpha3, dict3, Kinv3, dict3_idx) = krls(x, y, kernelfunc=k, nu=.5, maxdict=300, index="rand");

ypred3 = zeros(size(x2,2),1);
for ii in [1:size(x2,2)]
	tmp = k(dict3, x2[:,ii])*alpha3;
	ypred3[ii]=tmp[1];
end

cor(ypred3, y2')

hold(false)
plot(y2')
hold(true)
plot(ypred1, "r");
plot(ypred2, "g")
plot(ypred3, "b")

mp = mean([ypred1 ypred2 ypred3],2);
plot(mp,"m")
cor(mp, y2')


using Test
import CryptoGroups
import CryptoGroups: PGroup, specialize, Enc, Dec, <|, generator, PGroup, ECGroup

function elgamal_test(g)

    sk = 5
    pk = g^sk
    r = 3
    m = g^5
    r2 = 6

    enc = Enc(pk, g)
    dec = Dec(sk)

    @test dec(enc(m, r)) == m
    @test enc(enc(m, r), r2) == enc(m, r + r2)

    ### Shuffle generation

    sk = 5
    pk = g^sk

    enc = Enc(pk, g)

    m_vec = [g^4, g^2, g^3]
    e_vec = enc.(m_vec, [2, 3, 7])

    ### The shuffling
    r_vec = Int[4, 2, 3]

    e_enc = enc.(e_vec, r_vec)
    ψ = sortperm(e_enc)
    sort!(e_enc)

    @test sort(dec.(e_enc)) == sort(m_vec)

    #m_vec = [g, g^2, g^3]
    #e_vec = enc.(m_vec, 1)
end

let
    q = 11
    p = 2*q + 1

    G = PGroup(p, q)
    g = G(3)

    elgamal_test(g)
end

import CryptoGroups
import CryptoGroups: specialize, <|, generator, PGroup, ECGroup, Specs

let
    spec = Specs.MODP_1024
    G = specialize(PGroup, spec)
    g = G <| generator(spec)

    elgamal_test(g)
end

let
    spec = Specs.Curve_P_256

    G = specialize(ECGroup, spec; name = :P_256)
    g = G <| generator(spec)

    elgamal_test(g)
end

let
    spec = Specs.Curve_B_163_PB
    G = specialize(ECGroup, spec; name = :B_163_PB)
    g = G <| generator(spec)

    elgamal_test(g)
end


## getFundamentals
##----------------

## calculate fundamental helping values
function getFundamentals(mod::MuSigmaModel)
    nAss = size(mod.mu, 1)

    ## get return moments
    mus = mod.mu
    covMatr = mod.sigma

    ## get helper matrices
    onesN = ones(nAss,1)
    a = ones(1, nAss)/covMatr*ones(nAss, 1)
    b = ones(1, nAss)/covMatr*mus
    c = mus'/covMatr*mus
    d = a.*c-b.^2

    return a[1], b[1], c[1], d[1]
end

## get global minimum variance portfolio with short selling
function getGMVSS(mod::MuSigmaModel)
    nAss = size(mod.mu, 1)

    ## get return moments
    covMatr = mod.sigma

    a, b, c, d = getFundamentals(mod)

    ## compute GMV weights
    xGMV = covMatr\ones(nAss, 1)/a

    return xGMV[:]
end

## efficient frontier
##-------------------

function getEffFrontier(mod::MuSigmaModel;
                        nPoints = 50)

    ## get GMV portfolio
    xGMV = AssetMgmt.getGMVSS(mod)

    ## get associated portfolio mu
    muGMV = AssetMgmt.getPMean(xGMV, mod.mu)

    ## find maximum asset mu
    maxMu = maximum(mod.mu)

    ## find maximum asset sigma
    ## sigmas = AssetMgmt.getVolas(mod)
    ## maxSigma = maximum(sigmas)

    ## define mu grid
    muGrid = linspace(muGMV, maxMu, nPoints)

    ## get fundamentals
    a, b, c, d = AssetMgmt.getFundamentals(mod)

    ## get sigmas for given mu grid
    effSigmas = sqrt((a*muGrid.^2 - 2*b*muGrid + c)/d)

    return muGrid, effSigmas
end

function getEffPortfolios(mod::MuSigmaModel;
                          nPoints = 50)

    ## get GMV portfolio
    xGMV = AssetMgmt.getGMVSS(mod)

    ## get associated portfolio mu
    muGMV = AssetMgmt.getPMean(xGMV, mod.mu)

    ## find maximum asset mu
    maxMu = maximum(mod.mu)

    ## define mu grid
    muGrid = linspace(muGMV, maxMu, nPoints)

    ## get number of assets
    nAss = size(mod.mu, 1)

    ## preallocation
    nPfs = length(muGrid)
    effWgts = ones(nPfs, nAss)

    for ii=1:nPfs
        muTarget = muGrid[ii]

        ## get weights
        wgts = getEffPfGivenMu(mod, muTarget)
        effWgts[ii, :] = wgts
    end

    return effWgts
end

function getEffPfGivenMu(mod, muTarget)
    covMatr = mod.sigma

    ## get number of assets
    nAss = size(mod.mu, 1)

    ## create helping matrices
    R = [ones(nAss) mod.mu]

    ## get constraints
    muConstr = [1; muTarget]

    ## get weights
    wgts = inv(covMatr)*R*inv(R'*inv(covMatr)*R)*muConstr

    return wgts'
end


# This file is a part of JuliaFEM.
# License is MIT: see https://github.com/JuliaFEM/FEMPlates.jl/blob/master/LICENSE

using Test, FEMBase, FEMPlates

# Create 1x1 element, assemble stiffness matrix.

X = Dict(1 => [0.0, 0.0],
         2 => [1.0, 0.0],
         3 => [0.0, 1.0])
element = Element(Tri3, [1, 2, 3])
update!(element, "geometry", X)
update!(element, "youngs modulus", 4.0*4880.0)
update!(element, "thickness", 1.0)
update!(element, "poissons ratio", 1/3)
update!(element, "distributed load", 6.0)
problem = Problem(DKT, "test plate", 3)
add_elements!(problem, [element])
time = 0.0
assemble!(problem, time)
K = Matrix(problem.assembly.K)
f = Matrix(problem.assembly.f)

K_expected = [
 4880     0     0  -2440   1220  -1220  -2440  1220     0
    0   915  -305    610    305    610   -610  -610   305
    0  -305   915    610    305   -610   -610   610   305
-2440   610   610   2440      0    610      0  -610   610
 1220   305   305      0    610   -305  -1220   305   305
-1220   610  -610    610   -305    915    610  -915     0
-2440  -610  -610      0  -1220    610   2440  -610  -610
 1220  -610   610   -610    305   -915   -610   915     0
    0   305   305    610    305      0   -610     0   305]
@test isapprox(K, K_expected)

f_expected = zeros(9)
f_expected[1:3:end] .= 1.0
@test isapprox(f, f_expected)


################################################################################
#                                ___  ____ _  _                                #
#                                |__] [__  |_/                                 #
#                                |    ___] | \_                                #
################################################################################

type PSK <: Modulation
    M::Int                    # Number of symbols in the constellation
    m::Int                    # Bits per symbol
    constellation::Vector     # Symbol constellation
    grayConstellation::Vector # Symbol constellation with gray code
    bitsMap::Vector           # bitsMap[i] maps to constellation[i]
end

function PSK( M::Integer )
    ispow2( M ) || error( "M must be a power of 2" )
    Δϕ                = 2π/M
    bitsMap           = [ encode( Gray, i ) for i in 0:M-1 ]
    # constellation     = Complex128[ exp(Δϕ*im*i) for i in 0:M-1 ]
    # grayConstellation = Complex128[ exp(Δϕ*im*decode( Gray, i)) for i in 0:M-1 ]
    constellation     = Array( Complex128, M )
    grayConstellation = Array( Complex128, M )
    for i in 0:M-1
       constellation[i+1]     = exp(Δϕ*im*i)
       grayConstellation[i+1] = exp(Δϕ*im*decode( Gray, i))
    end
    m                 = log2( M )
    PSK( M, m, constellation, grayConstellation, bitsMap )
end

################################################################################
#                                _  _ ____ ___                                 #
#                                |\/| |  | |  \                                #
#                                |  | |__| |__/                                #
################################################################################

function modulate( psk::PSK, bits::Integer )
    psk.grayConstellation[ bits+1 ]
end

function modulate( psk::PSK, data::AbstractVector )
    T = eltype(psk.grayConstellation)
    T[ modulate( psk, datum ) for datum in data ]
end

################################################################################
#                           ___  ____ _  _ ____ ___                            #
#                           |  \ |___ |\/| |  | |  \                           #
#                           |__/ |___ |  | |__| |__/                           #
################################################################################

function demodulate( psk::PSK, symbol::Complex )
    ϕ = angle( symbol )
    ϕ = ϕ < 0 ? ϕ += 2π : ϕ

    index = int( ϕ*psk.M / 2π ) + 1
    index = mod1( index, psk.M )
    psk.bitsMap[index]
end

function demodulate( psk::PSK, symbols::AbstractVector )
    Int[ demodulate( psk, symbol ) for symbol in symbols ]
end

################################################################################
#                      ____ _  _ ____ _  _ ___  _    ____                      #
#                      |___  \/  |__| |\/| |__] |    |___                      #
#                      |___ _/\_ |  | |  | |    |___ |___                      #
################################################################################

#=

using PyPlot

function PyPlot.plot( psk::PSK )
    fig = figure()
    scatter( real(psk.constellation), imag(psk.constellation) )
    plot( real(psk.grayConstellation), imag(psk.grayConstellation) )

    for i in 0:psk.M-1
        symbol = psk.constellation[ i + 1 ]
        x      = real( symbol )
        y      = imag( symbol )
        bits   = psk.bitsMap[i+1]

        annotate( bin(bits, psk.m ), (x, y+0.05), ha="center" )
    end

    setp( axes(), aspect = 1  )
    xlabel( "In Phase" )
    ylabel( "Quadrature" )

    return fig
end

psk   = PSK( 4 )

plot( psk )

modData   = 0:psk.M-1 #rand(0:8-1, 10)
symbols   = modulate( psk, modData)
demodData = demodulate( psk, symbols )

Test.@test_approx_eq modData demodData

=#


function tensor_create{order, dim}(::Type{Tensor{order, dim}}, f)
    if order == 1
        ex = Expr(:tuple, [f(i) for i=1:dim]...)
    elseif order == 2
        ex = Expr(:tuple, [f(i,j) for i=1:dim, j=1:dim]...)
    elseif order == 4
        ex = Expr(:tuple, [f(i,j,k,l) for i=1:dim, j=1:dim, k = 1:dim, l = 1:dim]...)
    end
    return ex
end

function tensor_create{order, dim}(::Type{SymmetricTensor{order, dim}}, f)
    ex = Expr(:tuple)
    if order == 2
        for j in 1:dim, i in j:dim
            push!(ex.args, f(i, j))
        end
    elseif order == 4
        for l in 1:dim, k in l:dim, j in 1:dim, i in j:dim
            push!(ex.args, f(i, j, k, l))
        end
    end
    return ex
end

# create recursive muladd exp from two expression arrays
function make_muladd_exp(ex1, ex2)
    N = length(ex1)
    ex = Expr(:call)
    exn = Expr(:call, :*, ex1[1], ex2[1])

    if N == 1 # return only the multiplication
        return exn
    end

    for i in 2:N
        ex = Expr(:call, :muladd)
        push!(ex.args, ex1[i])
        push!(ex.args, ex2[i])
        push!(ex.args, exn)
        exn = ex
    end
    return ex
end


include("./training_set_generator.jl")
using TrainingSetGenerator

#Perceptron realization class
type Perceptron
    inputs_number::Int8
    weights::Array{Float64}
    learning_rate::Float64
end

#Predicting class based on the observation vector
function predict(observation, perceptron)
    sum = dot(observation[1], perceptron.weights)
    return return_sign(sum)
end

#Signum
function return_sign(number)
    if number > 0
        return 1
    else
        return -1
    end
end

#Method to recalculate weights according to the error
function recalculate_weight(observation, perceptron)
        label = observation[2]
        prediction = predict(observation, perceptron)
        error = label - prediction
        #Recalculate weights
        perceptron.weights += error * observation[1] * perceptron.learning_rate
        return perceptron, prediction, label
end

#Calculating error rate of the current epoch
function error_rate(predictions, targets)
    total_positives = 0
    for i = 1:length(targets)
        if targets[i] == predictions[i]
            total_positives +=1
        end
    end
    accuracy = total_positives/length(targets)
    return accuracy
end

#Method to train perceptron
function train(inputs, perceptron)
    predictions = []
    true_labels = []
    for i=1:length(inputs)
        perceptron, prediction, label = recalculate_weight(inputs[i], perceptron)
        push!(predictions, prediction)
        push!(true_labels, label)
    end
    accuracy = error_rate(predictions, true_labels)
    return perceptron, accuracy
end

#Method to test perceptron
function test(inputs, perceptron)
    testing_predictions =  []
    true_labels = []
    for i=1:length(inputs)
        prediction = predict(inputs[i], perceptron)
        push!(testing_predictions, prediction)
        push!(true_labels, inputs[i][2])
    end
    accuracy = error_rate(testing_predictions, true_labels)
    println(string("Accuracy on the testing data is: ", accuracy * 100, "%"))
    return perceptron
end

#Metod to run perceptron
function run_perceptron(epochs)

    #Initializing perceptron with, three inputs(two coordinates and a bias) with random weights
    inputs_number = 3
    bias = 1
    learning_rate = 0.2
    perceptron = Perceptron(inputs_number, [rand(-1:1), rand(-1:1), bias], learning_rate)
    accuracy = 0

    sample_training_set = TrainingSetGenerator.generate_data(200)

    for i= 1:epochs
        perceptron, accuracy = train(sample_training_set, perceptron)
    end

    println(string("Accuracy on the training data is: ", accuracy * 100, "%"))
    testing_data = TrainingSetGenerator.generate_data(20)
    test(testing_data, perceptron)

end


module TestMonoidalUndirectedWiringDiagrams
using Test

using Catlab.Theories, Catlab.WiringDiagrams
using Catlab.CategoricalAlgebra.CSets: subpart

# Categorical interface
#######################

const HDOb = HypergraphDiagramOb{Symbol,Symbol}
const HDHom = HypergraphDiagramHom{Symbol,Symbol}

# Category
#---------

# Domains and codomains
A, B = HDOb([:A]), HDOb([:B])
f = HDHom(singleton_diagram(A⊗B, name=:f), dom=BitArray([1,0]))
g = HDHom(singleton_diagram(B⊗A, name=:g), codom=BitArray([0,1]))
@test subpart(f.diagram, :name) == [:f]
@test subpart(g.diagram, :name) == [:g]
@test dom(f) == A
@test codom(f) == B
@test dom(g) == B
@test codom(g) == A

# Composition and identities
fg = compose(f,g)
@test dom(fg) == A
@test codom(fg) == A
@test nboxes(fg.diagram) == 2
@test njunctions(fg.diagram) == 3
@test compose(id(dom(f)), f) == f
@test compose(f, id(codom(f))) == f

# Symmetric monoidal category
#----------------------------

# Monoidal products
X, Y = HDOb([:A,:B]), HDOb([:C,:D])
I = munit(HDOb)
@test otimes(A,B) == X
@test otimes(X,I) == X
@test otimes(I,X) == X
@test dom(f⊗g) == A⊗B
@test codom(f⊗g) == B⊗A

# Braiding
@test compose(braid(X,Y),braid(Y,X)) == id(otimes(X,Y))

# Hypergraph category
#--------------------

@test compose(create(X), mcopy(X)) == dunit(X)
@test compose(mmerge(X), delete(X)) == dcounit(X)
# XXX: Not equal due to ordering of outer ports, but isomorphic.
#@test (id(B)⊗dunit(A)) ⋅ (id(B)⊗f⊗id(A)) ⋅ (dcounit(B)⊗id(A)) == dagger(f)

end


# signal_dft.jl
##########################
fs=Int(1000);
Ts=1/fs;
N=Int(12000);
T=N/fs;
#t=(0:Int(N-1))*Ts;

x=zeros(Float64, N)
for m=1:N
    x[m]=sin(2*Ts*(m-1)*(Ts*(m-1)-3)*(Ts*(m-1)-6)*(Ts*(m-1)-9)*(Ts*(m-1)-12))
end

using FFTW, DSP

hatx=fft(x);
k = (0:length(hatx)-1)*50/length(hatx);
G=broadcast(abs, hatx);

using Plots

plot(G,
    xaxis="k = 0, 1, 2, ..., N-1",
    yaxis="|F[x](k)|",
    title="DFT of x(t)=sin(2t(t-3)(t-6)(t-9)(t-12))",
    grid=false,
    legend=false)
plot!(xticks = ([0 5000 10000;], [0, 5000, 10000]))
plot!(yticks = ([0 200 400;], [0 200 400]))
savefig("signal_dft.png")


# using KernelDensity
using Distributions
using myJuliaUtils
using Test

# univariate case

# reps=200
reps=20000000
x = randn(reps)
a=0:1
# @time y = kde(x)
# pdf(y, a)

@time z=fit(Normal, x)
a1=pdf.(z,a)

@test isapprox(a1,pdf.(Normal(0.,1.),a); rtol=1e-3)

q=3
w=4
e=reps
x=randn(q,w,e);
v=reshape(collect(1:1:q*w) ./10,q,w)
o= sum(v.-0.1 .<=x .<=v.+0.1,dims=3)./e ./0.2
# @time y=TVPVARPkg.useKDE2fit(x,v)
# @btime y=findKD($x,$v)

@time z=useN2fit(x,v)
@test isapprox(z, pdf.(Normal(0.,1.),v) ; rtol=1e-3)

# multivariate case
@time z=useMvN2fit(x,v)

f=similar(x,q)
for a=1:q
    f[a]=pdf(MvNormal(zeros(w),eye(w)), v[a,:])
end

@test isapprox(z, f ; rtol=1e-3)


module Run
using DirichletProcess
using Distributions
using Plots

function dp_samples(alpha, n)
    dp = DP(randn, alpha)
    samples = [dp_sample!(dp) for _ in 1:n]
    return samples
end

function hdp_samples(alpha1, alpha2, n)
    hdp = HDP(randn, alpha1, alpha2)
    samples = [hdp_sample!(hdp) for _ in 1:n]
    return samples
end

function dp()
    n = 10000
    fig = plot(; layout=(2, 2), legend=false)
    alphas = [1, 10, 100, 1000]
    for i in 1:4
        alpha = alphas[i]
        histogram!(dp_samples(alpha, n); subplot=i, normalize=true)
    end
    display(fig)

    return nothing
end

function hdp()
    n = 10000
    fig = plot(; layout=(2, 2), legend=false)
    # alphas = [1, 10, 100, 1000]
    alphas = [10, 10, 10, 10]
    for i in 1:4
        alpha = alphas[i]
        histogram!(hdp_samples(100, alpha, n); subplot=i, normalize=true)
    end
    display(fig)

    return nothing
end

end


using TuringModels

@model m8_2(y) = begin

    σ ~ FlatPos(0.0) # improper prior with probability one everywhere above 0.0
    α ~ Flat() # improper prior with pobability one everywhere

    y .~ Normal(α, σ)
end

y = [-1,1];

# Sample

chns = sample(m8_2(y), NUTS(0.65), 1000)

# Describe the posterior samples

chns |> display

# End of `08/m8.2t.jl`


# # [Multi-City COVID-19 Model](@id covid_example)
#
#md # [![](https://img.shields.io/badge/show-nbviewer-579ACA.svg)](@__NBVIEWER_ROOT_URL__/examples/covid/covid.ipynb)

using AlgebraicPetri
using AlgebraicPetri.Epidemiology

using OrdinaryDiffEq
using Plots

using Catlab
using Catlab.Theories
using Catlab.Programs
using Catlab.CategoricalAlgebra.FreeDiagrams
using Catlab.WiringDiagrams
using Catlab.Graphics

import Catlab.Theories: id

display_wd(ex) = to_graphviz(ex, orientation=LeftToRight, labels=true);
id(args...) = foldl((x,y)->id(x) ⊗ id(y), args);

# #### Step 1: Define a new primitive to extend the presentation of InfectiousDiseases

travel_petri = PetriCospan(
        [1,2,3],
        PetriNet(6, (1,4), (2,5), (3,6)),
        [4,5,6]);

# #### Step 2: Extend the Infectious Disease presentation,
# get the new generators, and update the functor

@present EpiWithTravel <: InfectiousDiseases begin
    travel::Hom(S⊗E⊗I,S⊗E⊗I)
end;

S,E,I,R,D,transmission,exposure,illness,recovery,death,travel = generators(EpiWithTravel);
new_functor = copy(FunctorGenerators)
new_functor[travel] = travel_petri

F(ex) = functor((PetriCospanOb, PetriCospan), ex, generators=new_functor);

# ### COVID-19 TRAVEL MODEL:

# SEIRD City Model with travel as S ⊗ E ⊗ I → S ⊗ E ⊗ I
#
# $seird_{city} = (((Δ(S) ⊗ id(E)) ⋅ (id(S) ⊗ σ(S,E))) ⊗ id(I)) ⋅ (id(S, E) ⊗ exposure) ⋅
#                 (id(S) ⊗ (∇(E) ⋅ Δ(E)) ⊗ id(I)) ⋅ (id(S, E) ⊗ ((illness ⊗ id(I)) ⋅
#                 (∇(I) ⋅ Δ(I)) ⋅ (id(I) ⊗ (Δ(I) ⋅(recovery ⊗ death))))) ⋅ (travel ⊗ ◊(R) ⊗ ◊(D))$
#
# This is a very complicated interaction, so we can use the program interface for easier model definition

seird_city = @program EpiWithTravel (s::S, e::E, i::I) begin
    e2 = exposure(s, i)
    i2 = illness(e2)
    d = death(i2)
    r = recovery(i2)
    return travel(s, [e, e2], i2)
end
seird_city = to_hom_expr(FreeBiproductCategory, seird_city)

display_wd(seird_city)
# -
Graph(decoration(F(seird_city)))

# create a multi-city SEIRD models

ncities(city,n::Int) = compose([city for i in 1:n]...)

seird_3 = ncities(seird_city, 3)
pc_seird_3 = F(seird_3)
p_seird_3 = decoration(pc_seird_3)
display_wd(seird_3)
# -
Graph(p_seird_3)

# Define time frame and initial parameters

tspan = (0.0,90.0)
u0 = zeros(Float64, length(base(pc_seird_3)))
u0[1]  = 10000
u0[6]  = 10000
u0[11] = 10000
u0[2]  = 1

seirdparams(n::Int, k::Number) = begin
    βseird = [10/sum(u0), 1/2, 1/5, 1/16]
    βtravel = [1/20, 1/200, 1/20]/100k
    β = vcat(βseird, βtravel)
    return foldl(vcat, [(1-(0/(2n)))*β for i in 1:n])
end
params = seirdparams(3, 5);

# Generate, solve, and visualize resulting ODE

prob = ODEProblem(vectorfield(p_seird_3),u0,tspan,params);
sol = solve(prob,Tsit5());

plot(sol)

# Define different dynamic transition function rates

asymptotic(a,b,k=1) = t->(b + (a-b)/(k*t+1))
triangleasm(a,b,k=1) = t->max(b, (1-t/k)*(a-b)+b)
trianglewave(a,b,k=1) = t->((a-b)/π)*asin(sin(t*2π/k))+2b
coswave(a,b,k=1) = t->(a-b)*(cos(t*2π/k)/2)+( a+b )/2
sincwave(a,b,k=1) = t->(a-b)*(sinc(t*2π/k)/2)+( a+b )/2
modsincwave(a,b,k) = t-> max(b, (a-b)*(sinc(t*2π/k))+(b))
dynseirdparams(f, a,b,period, n::Int, scale::Number) = begin
    βseird = [f(a,b,period), 1/2, 1/5, 1/16]
    βtravel = [1/20, 1/200, 1/20]/100scale
    β = vcat(βseird, βtravel)
    return foldl(vcat, [β for i in 1:n])
end

waveparams(f,a,b,p) = begin
    dynseirdparams(f,a,b,p,3,5)
end;

# Generate, solve, and visualize ODE of a dynamic flow rates

tspan = (0, 240.0)
a,b,p = 10, 1, 1/6

dynparams = waveparams(asymptotic, a/sum(u0), b/sum(u0),p)

prob = ODEProblem(vectorfield(p_seird_3),u0,tspan, dynparams)
sol = solve(prob,Tsit5(), saveat=1:1:tspan[2])

plot(sol)


using QuadGK
export BlackBody,
    spectral_radiance,
    spectral_radiance_frequency,
    spectral_band_radiance,
    spectral_band_radiance_frequency,
    radiance,
    disk_nt_rel_factors,
    gravity_radius,
    disk_flux,
    compute_disk_temperature,
    compute_disk_luminosity,
    compute_disk_spectral_luminosity,
    compute_disk_spectral_flux,
    compute_disk_spectral_flux_at_radius,
    compute_disk_flux_at_radius

struct BlackBody
    T::Float64
end

kev_to_hz(energy) = energy / ERG_TO_KEV / H_PLANCK
lambda_peak(T) = 0.2897771955 / T
nu_peak(T) = C / lambda_peak(T)
nu_peak(bb::BlackBody) = nu_peak(bb.T)

"""
Spectral radiance of a black body bb for a frequency. Input frequency is
in Hz, and output units are erg / (s cm^2 sr Hz)
"""
function spectral_radiance_frequency(bb::BlackBody, frequency)
    a = 2 * H_PLANCK * frequency^3 / C^2
    b = 1 / (exp((H_PLANCK * frequency) / (K_B * bb.T)) - 1)
    return a * b
end

"""
Spectral radiance of a black body at a given energy. The input energy
has units of keV, and the result output is in units of
keV / (s cm² keV sr)
"""
function spectral_radiance(bb::BlackBody, energy)
    frequency = kev_to_hz(energy)
    rad = spectral_radiance_frequency(bb, frequency)
    return rad
end

"""
    spectral_band_radiance_frequency(bb::BlackBody, low, high)
Computes the radiance on a frequency band from low to high. low and high
have to be in Hz, return units are erg / sr / m^2
"""
function spectral_band_radiance_frequency(bb::BlackBody, low, high)
    low = max(low, nu_peak(bb) / 1e4)
    high = min(high, nu_peak(bb) * 1e4)
    integral, err = quadgk(x -> spectral_radiance_frequency(bb, x), low, high, rtol = 1e-8)
    return integral
end

"""
Computes the radiance on an energetic band from low to high.
"""
function spectral_band_radiance(bb::BlackBody, low, high)
    low = kev_to_hz(low)
    high = kev_to_hz(high)
    low = max(low, nu_peak(bb) / 1e3)
    high = min(high, nu_peak(bb) * 1e3)
    integral, err = quadgk(x -> spectral_radiance_frequency(bb, x), low, high, rtol = 1e-8)
    return integral
end

radiance(bb::BlackBody) = SIGMA_SB * bb.T^4 / π

"""
Novikov-Thorne relativistic factors for the AD spectrum.
If the passed radius is smaller than ISCO, it returns 0.

Parameters
----------
radius
    disc radius measured in R_g
spin
    normalized black hole spin (-1, 1)
isco
    innermost stable circular orbit measured in Rg
"""
function disk_nt_rel_factors(radius, spin, isco)
    if radius <= isco
        return 0.0
    end
    yms = sqrt(isco)
    y1 = 2 * cos((acos(spin) - pi) / 3)
    y2 = 2 * cos((acos(spin) + pi) / 3)
    y3 = -2 * cos(acos(spin) / 3)
    y = sqrt(radius)
    C = 1 - 3 / radius + 2 * spin / radius^1.5
    B = 3 * (y1 - spin)^2 * log((y - y1) / (yms - y1)) / (y * y1 * (y1 - y2) * (y1 - y3))
    B += 3 * (y2 - spin)^2 * log((y - y2) / (yms - y2)) / (y * y2 * (y2 - y1) * (y2 - y3))
    B += 3 * (y3 - spin)^2 * log((y - y3) / (yms - y3)) / (y * y3 * (y3 - y1) * (y3 - y2))
    A = 1 - yms / y - 3 * spin * log(y / yms) / (2 * y)
    factor = (A - B) / C
    return factor
end

disk_nt_rel_factors(bh, r) = disk_nt_rel_factors(r, bh.spin, bh.isco)

"""
Self-gravity radius as described by Laor & Netzer (1989).
"""
function gravity_radius(bh)
    mass = (bh.M / (1e9 * M_SUN))
    alpha = 0.1 # assumption
    r_sg = 2150 * mass^(-2 / 9) * bh.mdot^(4 / 9) * alpha^(2 / 9)# / bh.Rg
    return r_sg
end

"""
Computes the disk flux of the accretion disc of black hole ``bh``
at a given radius ``r`` (in Rg). See equation 5 of https://arxiv.org/pdf/2001.04720.pdf
"""
function disk_flux(bh, r)
    Mdot = compute_mass_accretion_rate(bh)
    NT_factors = disk_nt_rel_factors(bh, r)
    return 3 * G * bh.M * Mdot / (8 * π * (r * bh.Rg)^3) * NT_factors
end

"""
Computes the temperature of the disk at a radius r (in units of Rg).
"""
function compute_disk_temperature(bh, r)
    flux = disk_flux(bh, r)
    return (flux / SIGMA_SB)^(1 / 4)
end

"""
    compute_disk_luminosity(bh, r_min, r_max)
computes the luminosity of the disk between r_min and r_max
"""
function compute_disk_luminosity(bh, r_min, r_max)
    constant = SIGMA_SB * 4 * π * bh.Rg^2
    integ, _ = quadgk(
        r -> r * compute_disk_temperature(bh, r)^4,
        r_min,
        r_max,
        atol = 0,
        rtol = 1e-2,
    )
    return integ * constant
end

"""
    compute_disk_spectral_flux_at_radius(bh, radius, energy_range)
Returns an array with the photon flux at each bin. (# photons / cm^2)
"""
function compute_disk_spectral_flux_at_radius(bh, radius, energy)
    ret = Float64[]
    bb = BlackBody(compute_disk_temperature(bh, radius))
    freq = kev_to_hz(energy)
    radiance = spectral_radiance_frequency(bb, freq)
    return radiance * π / HZ_TO_KEV
end

"""
    compute_disk_flux_at_radius(bh, radius, energy_range)
Returns an array with the photon flux at each bin. (# photons / cm^2)
"""
function compute_disk_flux_at_radius(bh, radius, low = 1e-5, high = 1e2)
    ret = Float64[]
    bb = BlackBody(compute_disk_temperature(bh, radius))
    radiance = spectral_band_radiance(bb, low, high)
    return radiance * π
end

function compute_disk_spectral_luminosity(bh, energy_range; r_min=6, r_max=1600)
    ret = Float64[]
    for energy in energy_range
        integ, _ = quadgk(
            r -> r * compute_disk_spectral_flux_at_radius(bh, r, energy),
            r_min,
            r_max,
            rtol = 1e-8,
            atol = 0,
        )
        push!(ret, integ)
    end
    return ret * 2 * 2π * bh.Rg^2
end


function count_unique(x)
    u = unique(x)
    c = Array{Int64}(undef, length(u))
    for i in 1 : length(u)
        c[i] = length(findall(isequal(u[i]), x))
    end
    return u, c
end

function get_branching_shold(xs)
    u = unique(xs)
    return (u[2:end]+u[1:end-1]) / 2
end

function delta_gini_index(left, right, alpha, branch_count)
    n_left, n_right = length(left), length(right)
    n_total = n_left + n_right
    _, counts = count_unique(left)
    ratio_classes = counts / n_left
    left_gain = (n_left/n_total) * (1-sum(ratio_classes.^2))
    _, counts = count_unique(right)
    ratio_classes = counts / n_right
    right_gain = (n_right/n_total) * (1-sum(ratio_classes.^2))
    return left_gain+right_gain+alpha*branch_count
end

function branch(x, t, alpha, branch_count)
    gains = []
    rules = []
    for feature_id in 1:size(x, 2)
        xs = x[:, feature_id]
        thresholds = get_branching_shold(xs)
        for th in thresholds
            left_y = t[findall(xs.<th)]
            right_y = t[findall(xs.>=th)]
            gain = delta_gini_index(left_y, right_y, alpha, branch_count)
            push!(gains, gain)
            if rules == []
                rules = [feature_id th]
            else
                rules = vcat(rules, [feature_id th])
            end
        end
    end
    best_rule = rules[argmin(gains), :]
    best_gini = minimum(gains)
    feature_id = Int(best_rule[1])
    threshold = best_rule[2]
    left = findall(x[:, feature_id] .< threshold)
    right = findall(x[:, feature_id] .>= threshold)
    return x[left, :], t[left], x[right, :], t[right], feature_id, threshold, best_gini
end

function grow(x, t, classes, gini, alpha, branch_count)
    check_size(x, t)
    uniques, counts = count_unique(t)
    counter = Dict(zip(uniques, counts))
    class_count = [(c in keys(counter)) ? counter[c] : 0 for c in classes]
    this = Dict("class_count" => class_count, "feature_id" => nothing, "threshold" => nothing, "left" => nothing, "right" => nothing)
    if length(t) == 1
        return this
    elseif length(uniques) == 1
        return this
    end
    a = true
    for i in 1:size(x, 2)
        a = a && length(unique(x[:, i])) == 1
    end
    a && return this
    left_x, left_t, right_x, right_t, feature_id, threshold, best_gini = branch(x, t, alpha, branch_count)
    if best_gini < gini
        branch_count += 1
        left = grow(left_x, left_t, classes, best_gini, alpha, branch_count)
        right = grow(right_x, right_t, classes, best_gini, alpha, branch_count)
        return Dict("class_count" => class_count, "feature_id" => feature_id, "threshold" => threshold, "left" => left, "right" => right)
    else
        return this
    end
end

"""
    DecisionTree(; alpha = 0.01)
Normal DecisionTree. `alpha` specify the complexity of the model. If it's small, it's complicated, and if it's big, it's simple.

# Example
```jldoctest classification
julia> tree = DecisionTree()
DecisionTree(0.01, Dict{Any, Any}(), Any[])

julia> fit!(tree, x, t)
Dict{String, Any} with 5 entries:
  "left"        => Dict{String, Any}("left"=>Dict{String, Union{Nothing, Vector…
  "class_count" => [8, 13, 16]
  "threshold"   => 5.7
  "right"       => Dict{String, Any}("left"=>Dict{String, Union{Nothing, Vector…
  "feature_id"  => 1

julia> println(predict(tree, x))
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 1, 2, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2]
```
"""
mutable struct DecisionTree
    α::Float64
    tree::Dict
    classes::Array{Any, 1}
    DecisionTree(; alpha = 0.01) = new(alpha, Dict(), Array{Any}(undef, 0))
end

function fit!(model::DecisionTree, x, t)
    model.classes = unique(t)
    model.tree = grow(x, t, model.classes, Inf, model.α, 1)
end

function predict(model::DecisionTree, x)
    function predict_one(x, tree)
        while tree["feature_id"] != nothing
            tree = (x[tree["feature_id"]] < tree["threshold"]) ? tree["left"] : tree["right"]
        end
        return argmax(tree["class_count"])
    end
    predicts = [predict_one(x[i, :], model.tree) for i in 1:size(x, 1)]
    d = Dict(zip(collect(1:length(model.classes)), model.classes))
    return map(x->d[x], predicts)
end


@symbol_func function K_BR(cur_reactor::AbstractReactor)
  cur_nu_n = cur_reactor.nu_n

  cur_nu_T = cur_reactor.nu_T

  cur_K = 0.1056

  cur_K *= ( 1 + cur_nu_n ) ^ 2

  cur_K *= sqrt( 1 + cur_nu_T )

  cur_K /= 1 + 2 * cur_nu_n + cur_nu_T / 2

  cur_K *= cur_reactor.Z_eff

  cur_K *= cur_reactor.epsilon ^ 2

  cur_K *= cur_reactor.kappa_95

  cur_K *= g(cur_reactor)

  cur_K
end


function k3p_cf(f, η1, η2, η3, ξ)

	(ξ^3)*((η1)^2)*(η2)*(

	f((1-ξ, ξ-ξ*η1+ξ*η1*η2), (1-(ξ-ξ*η1*η2*η3), ξ-ξ*η1)) +

	f((1-(ξ-ξ*η1*η2*η3), ξ-ξ*η1), (1-ξ, ξ-ξ*η1+ξ*η1*η2)) +

	f((1-ξ, ξ*η1*(1-η2+η2*η3)), (1-(ξ-ξ*η1*η2), ξ*η1*(1-η2))) +

	f((1-(ξ-ξ*η1*η2), ξ*η1*(1-η2)), (1-ξ, ξ*η1*(1-η2+η2*η3))) +

	f((1-(ξ-ξ*η1*η2*η3), ξ*η1*(1-η2*η3)), (1-ξ, ξ*η1*(1-η2))) +

	f((1-ξ, ξ*η1*(1-η2)), (1-(ξ-ξ*η1*η2*η3), ξ*η1*(1-η2*η3)))

	)
end

function k3p_ce(f, η1, η2, η3, ξ)

	(ξ^3)*((η1)^2) * f((1-ξ, ξ*η1*η3), (1-ξ*(1-η1*η2), ξ*η1*(1-η2))) +

	(ξ^3)*((η1)^2)*(η2)*(

	f((1-ξ, ξ*η1), (1-ξ*(1-η1*η2*η3), ξ*η1*η2*(1-η3))) +

	f((1-ξ*(1-η1*η2), ξ*η1*(1-η2)), (1-ξ, ξ*η1*η2*η3)) +

	f((1-ξ*(1-η1*η2*η3), ξ*η1*η2*(1-η3)), (1-ξ, ξ*η1)) +

	f((1-ξ*(1-η1*η2*η3), ξ*η1*(1-η2*η3)), (1-ξ, ξ*η1*η2))

	)
end

function k3p_cv(f,η1, η2, η3, ξ)

	(ξ^3)*η2*(

	f((1-ξ, ξ*η1), (1-ξ*η2, ξ*η2*η3)) +  f((1-ξ*η2, ξ*η2*η3), (1-ξ, ξ*η1))

	)
end

function k3p_pd(f, η1, η2, η3, ξ)

	u = [η1*(1-η2), η2]		#jacobian of this transformation is (1-η2)
	v = [η3*(1-ξ), ξ]		#jacobian of this transformation is (1-ξ)

	return(f(u,v)*(1-η2)*(1-ξ))
end


#! /usr/bin/julia

# Rosetta Code, Stem-and-leaf plot

function stemleaf{T<:Real}(a::Array{T,1}, leafsize=1)
    ls = 10^int(log10(leafsize))
    (stem, leaf) = divrem(sort(int(a/ls)), 10)
    leaf[sign(stem) .== -1] *= -1
    negzero = leaf .< 0
    if any(negzero)
        leaf[negzero] *= -1
        nz = @sprintf "%10s | " "-0"
        nz *= join(map(string, leaf[negzero]), " ")
        nz *= "\n"
        stem = stem[!negzero]
        leaf = leaf[!negzero]
    else
        nz = ""
    end
    slp = ""
    for i in stem[1]:stem[end]
        i != 0 || (slp *= nz)
        slp *= @sprintf "%10d | " i
        slp *= join(map(string, leaf[stem .== i]), " ")
        slp *= "\n"
    end
    slp *= " Leaf Unit = " * string(convert(T, ls)) * "\n"
    return slp
end

println("Using the Task's Test Data")
test = """12 127 28 42 39 113 42 18 44 118 44 37 113 124 37 48 127 36 29
   31 125 139 131 115 105 132 104 123 35 113 122 42 117 119 58 109 23 105
   63 27 44 105 99 41 128 121 116 125 32 61 37 127 29 113 121 58 114 126
   53 114 96 25 109 7 31 141 46 13 27 43 117 116 27 7 68 40 31 115 124 42
   128 52 71 118 117 38 27 106 33 117 116 111 40 119 47 105 57 122 109
   124 115 43 120 43 27 27 18 28 48 125 107 114 34 133 45 120 30 127 31
   116 146"""
test = map(parseint, split(test, r"\s"))
println(stemleaf(test))

println("Test with Reals and Negative Zero Stem")
test = [-23.678758, -12.45, -3.4, 4.43, 5.5, 5.678, 16.87, 24.7, 56.8]
println(stemleaf(test))

println("Test with Leaf Size Scaling")
test = int(500*randn(20))
println("Using:  ", test)
println(stemleaf(test, 10))


### A Pluto.jl notebook ###
# v0.12.18

using Markdown
using InteractiveUtils

# This Pluto notebook uses @bind for interactivity. When running this notebook outside of Pluto, the following 'mock version' of @bind gives bound variables a default value (instead of an error).
macro bind(def, element)
    quote
        local el = $(esc(element))
        global $(esc(def)) = Core.applicable(Base.get, el) ? Base.get(el) : missing
        el
    end
end

# ╔═╡ 9c0abdb4-4703-11eb-2bcf-6387d2445b8a
using Plots

# ╔═╡ 22dc1d68-4709-11eb-1112-f31e0854356f
using PlutoUI, LaTeXStrings

# ╔═╡ ae386df6-4703-11eb-200c-e9d7e818f15e
md"""
## Week 1: Circles and Lines

**Author:** Marlin Figgins.
**Date:** Jan 5, 2021.

The goal of this notebook is to review the tools needed to be able to complete worksheet 1. We'll begin with some review of lines, slopes, and circles. Then we'll use a bit of geometry to show how to find the tangent line to a circle.
"""

# ╔═╡ becfe73e-4703-11eb-1def-a357f306b5ff
md"""
## Review: Lines

Lines (and linear functions) are a building block in most areas of mathematics and its applications. They are essentially one of the simplest geometric objects and form the basis for the techniques we use when analyzing more complicated shapes.
"""

# ╔═╡ f5d9047a-4af2-11eb-2339-2b7b83990bbe
begin
	n_lines = 3
	random_m = 2 .* rand(n_lines) .- 1
	random_b = 2 .* rand(n_lines) .- 1
	x_coord = -1:1

	fig1 = plot(x_coord,  random_m[1] .* x_coord .+ random_b[1],
		legend = false,
		xticks = false,
		yticks = false)
	for i in 2:n_lines
		fig1 = plot!(x_coord,  random_m[i] .* x_coord .+ random_b[i])
	end

	fig1
end

# ╔═╡ 94fd0c3e-4af5-11eb-3968-1316069a9e8d
md"""
In earlier mathematics classes, you've likely learned how to find lines using several different but equivalent representations. We'll review how to solve for a line with these different representations.

First, we'll use the two-points representation. This is probably the one all of us are most familar with on a intuitive level! This is just the statement that any two points has a unique straight line between them. In mathematical language, we say that if we have two points $(x_{0}, y_{0})$ and $(x_{1}, y_{1})$, then the line has formula

```math
y - y_{0} = \frac{y_1 - y_0}{x_1 - x_0}  (x - x_0)
```
"""

# ╔═╡ 5533ef12-4af7-11eb-110e-23ea2ff268fb
begin
	fig2 = deepcopy(fig1)

	rx_points =  2 .* rand(2) .- 1
	fig2 = scatter!(rx_points,  random_m[1] .* rx_points .+ random_b[1],
		color = "white",
		legend = false,
		xticks = false,
		yticks = false)

	for i in 2:n_lines
		rx_points =  2 .* rand(2) .- 1
		fig2 = scatter!(rx_points,  random_m[i] .* rx_points .+ random_b[i],
				color = "white")
	end

	fig2
end

# ╔═╡ 4fdb0a00-4af7-11eb-3fa2-bb120202054d
md"""
Next up, is the point-slope representation. That is, we can uniquely define any of the lines above by specifying a single point $(x_{0}, y_{0})$ on the line and its slope $m$:

```math
y - y_{0} = m (x - x_{0}).
```

Notice, that this is the same as the two point representation except we use $m$ to signify the slope of the line which can be computed using any two points as

```math
m = \frac{y_{1} - y_{0}}{x_{1} - x_{0}}.
```
"""

# ╔═╡ e6c5c248-4af9-11eb-1e09-455232f6e2ce
begin
	fig3 = plot(x_coord,  random_m[1] .* x_coord .+ random_b[1],
		legend = false,
		xticks = false,
		yticks = false)
	for i in 2:n_lines
		fig3 = plot!(x_coord,  random_m[i] .* x_coord .+ random_b[i])
	end

	fig3 = scatter!([rx_points[1]],  [random_m[1] * rx_points[1] + random_b[1]],
		color = "white",
		legend = false,
		xticks = false,
		yticks = false)

	for i in 2:n_lines
		rx_points =  2 .* rand(1) .- 1
		fig3 = scatter!([rx_points[1]],  [random_m[i] * rx_points[1] + random_b[i]],
				color = "white")
	end

	fig3
end

# ╔═╡ 7ed2de82-4af7-11eb-1439-8d3b5cee0ae0
md"""
Lastly, we have slope-intercept form. This is when we write the line in the form

\begin{equation}
y = m x + b.
\end{equation}

Here, $m$ is the slope as before and $b$ is the $y$-intercept. The $y$-intercept $b$ is just the value of the line when $x = 0$. You can see this since

\begin{equation}
y = m \cdot 0 + b = b.
\end{equation}

This allows us to uniquely define a line by its value at $x = 0$ and the slope of the line itself.
"""

# ╔═╡ d40d36f6-4afa-11eb-003b-a7e088f40e15
begin
	fig4 = plot(x_coord,  random_m[1] .* x_coord .+ random_b[1],
		legend = false,
		xticks = false,
		yticks = false)
	for i in 2:n_lines
		fig4 = plot!(x_coord,  random_m[i] .* x_coord .+ random_b[i])
	end

	fig4 = scatter!([rx_points[1]],  [random_m[1] * rx_points[1] + random_b[1]],
		color = "white",
		legend = false,
		xticks = false,
		yticks = false)

	for i in 2:n_lines
		fig4 = scatter!([rx_points[1]],  [random_m[i] * rx_points[1] + random_b[i]],
				color = "white")
	end

	fig4
end

# ╔═╡ e0abe984-4703-11eb-1d47-a945691991cb
md"""
## Tangent Lines

In calculus, we're often interested in finding the tangent line to shapes of interest. For our purposes, this is the same as finding the point which touches a curve at one point and has the same direction of the curve at the point of contact. This point of contact is called the *point of tangency*.
"""

# ╔═╡ 814a9bb0-4afb-11eb-287d-8537b497853d
begin
	a = -0.1
	example_func(x) = x^3 - x^2 - x - 1
	example_deriv(x) = 3*x^2 - 2*x - 1
	example_tangent(x) = example_deriv(a) * (x - a) + example_func(a)

	plot(example_func, -1, 1,
		linewidth = 1.5,
		label = "Original Function",
		xticks = false,
		yticks = false)

	plot!(example_tangent, -1, 1,
		linewidth = 1.5,
		label = "Tangent line")

	scatter!([a], [example_func(a)],
			label = "Point of tangency",
			color = "white")
end

# ╔═╡ ec0bd9e4-4703-11eb-26e3-834539a39d83
md"""
### Circles
A circle has the following equation

```math
(x - h)^2 + (y - k)^2 = r^2,
```
where $(h,k)$ is the center of the circle and $r$ is its radius. In what follows, we'll use the shorthand $P = (h,k)$ for the center of the circle. You might also write the circle as a parametric equation with $x$ and $y$ functions

```math
x(t) = r \cos(t) + h
```
```math
y(t) = r \sin(t) + k,
```
but we'll primarily work with the first characterization in what follows.
"""

# ╔═╡ 12af1234-4705-11eb-06f5-970c779f5b1a
begin
	function circle(h, k, r)
		t = 0:0.01:2π
		return h .+ r .* cos.(t), k .+ r .* sin.(t);
	end
end

# ╔═╡ 65b256c2-4704-11eb-06a9-ffdcd2545045
md"""
### Tangent line of a circle

In the case of the circle, we don't actually need any tools of calculus to solve for the tangent line if we've given the point of tangency. We'll rely on some fun facts of geometry. In the case of circles, we know that the tangent line intersects the circle at exactly one place.
"""

# ╔═╡ 42233d1e-4afd-11eb-3af1-71cc1b4e9829
md"""
We can solve for the tangent line using the fact that the tangent line at a point $Q$ is going to be perpendicular to the line connecting the center of the circle $P$ to the point $Q$. We'll begin by just graphing the points $P$ and $Q$ and the line connecting them on the circle.
"""

# ╔═╡ 7aa4a83c-4709-11eb-3ec5-8784f5748b62
begin
	h = 0;
	k = 0;
	t = 51;

	#(0:0.01:2π)[t]
	#@bind t Slider(1:10:length(0:0.01:2π))

	println("Empty Line")
end

# ╔═╡ 736a0656-4705-11eb-229a-6b60032aa346
begin
	circle_xs, circle_ys = circle(h,k,1)
	plot(circle_xs, circle_ys,
		size = (500,500),
		linewidth = 2,
		color = "black",
		label = false,
		xlim = (-1.1, 1.1),
		ylim = (-1.1,1.1),
		xticks = false,
		yticks = false)

	plot!([h, circle_xs[t]], [k, circle_ys[t]],
		  label = false,
		  linewidth = 1.5,
		  linestyle = :dash,
		  color = "black")

	scatter!([h], [k], markersize = 5, label = "P")
	scatter!([circle_xs[t]], [circle_ys[t]], markersize = 5, label = "Q")
end

# ╔═╡ ff1f5862-4afd-11eb-1aa1-61f93e244a87
md"""
As you can see below, the tangent line that touches the circle at point $Q$ forms a right angle with the radial line we graphed in the last plot. In short, the two lines are perpendicular.
"""

# ╔═╡ f617bf5c-4706-11eb-00d0-35f62229ff28
begin
	m = (circle_ys[t] - k) / (circle_xs[t] - h)
	tangent_circle(x) = (-1/m)*(x-circle_xs[t]) + circle_ys[t]

	plot!(tangent_circle, -1, 1,
		label = false,
		linewidth = 1.5,
		color = "black")
end

# ╔═╡ c1a9822c-4afe-11eb-35ee-4d9b576c273d
md"""
One fact of geometry is that lines which are perpendicular have slopes which are negative reciprocals of one another. That is, if the line connecting $P$ and $Q$ has slope $m_{PQ}$, then the tangent line has slope

```math
m_{\tan Q} = - \frac{1}{m_{PQ}}.
```
"""

# ╔═╡ 88c3e17c-4aff-11eb-2611-b5549aaeaa54
md"""
This allows us to solve for the tangent line to the circle at point $Q$ with just a bit of algebra since we have the slope of the tangent line and the point of tangency $Q$. We can write these steps clearly as:

**Finding the tangent line to a circle at a point $Q$**

1. Compute the slope $m_{PQ}$ of the radial line connecting the center $P$ and $Q$.
2. Compute the slope of the tangent line at point $Q$ using $m_{\tan Q} = - \frac{1}{m_{PQ}}$.
3. Solve for the tangent line using the point $Q$ and the slope $m_{\tan Q}$.
"""

# ╔═╡ 7ad2d2ce-4fbb-11eb-2566-0d1ecf72b897
md""" ## Application: Intersection of tangent lines.

Consider the circle with radius 2 centered at $C = (0, 0)$. For the following sets of points $P$ and $Q$, find the point(s) where the lines tangent to the circle at $P$ and $Q$ intersect if they exist.

(a) $P = (-\sqrt{2}, \sqrt{2})$, $Q = (\sqrt{2}, \sqrt{2})$

(b) $P = (-1, \sqrt{3})$, $Q = (1, -\sqrt{3})$

(c) $P = (\sqrt{2}, \sqrt{2})$, $Q = (1, \sqrt{3})$

"""

# ╔═╡ eab8e50e-4fc2-11eb-25d0-05d133782270
md""" #### Part (a)

We'll begin by graphing the the points and the two lines.
"""

# ╔═╡ c6661a46-4fc9-11eb-0d58-4d38df0118ee
md"""
From the plot above, we can see that there is a clear point of intersection, but how do we solve for it?

##### Finding the tangent line to the circle at $P$

Our first step is to find the equation for the two tangent lines of interest with the theory we developed earlier. First, let's find the equation of the tangent line at point $P$. This means finding the slope of the radial line connecting the center of the circle $C = (0,0)$ to the point $P = (-\sqrt{2}, \sqrt{2})$. We can find this slope using the familiar rise-over-run formula:

```math
m_{CP} = \frac{\Delta y}{\Delta x} = \frac{\sqrt{2} - 0}{-\sqrt{2} - 0} = -1.
```

Since the radial line from $C$ to $P$ is perpendicular to the tangent line to the circle at point $P$, we have that the slope of the tangent line at $P$ is

```math
m_{\tan P} = -\frac{1}{m_{CP}} = 1.
```

Additionally, we know that the tangent line at $P$ must pass through the point $P$, so we can write the equation of the tangent line using the point-slope formula as:

```math
y - \sqrt{2} = 1 (x - ( - \sqrt{2})) = x + \sqrt{2}
```

or alternatively after a bit of algebra
```math
y = x + 2\sqrt{2}.
```

##### Finding the tangent line to the circle at $Q$
We can repeat this same process but instead using the point $Q = (\sqrt{2}, \sqrt{2})$. Solving for the slope

```math
m_{CQ} = \frac{\Delta y}{\Delta x} = \frac{\sqrt{2} - 0}{\sqrt{2} - 0} = 1.
```

Once again, since the radial line must be perpendicular to the tangent line, we have that

```math
m_{\tan Q} = -\frac{1}{m_{CQ}} = -1.
```

Using point-slope form, this is

```math
y - \sqrt{2} = -1 (x -  \sqrt{2})
```

or after some algebra

```math
y = -x + 2\sqrt{2}.
```

##### Finding the intersection of the tangent lines

Now that we have the equations of the two tangent lines, we can solve for their intersection as follows. We know that the point we're interested in solving for is $(x,y)$ which is on both of the tangent lines we solved for. Therefore, it is the solution to both of the equations we found simulataneously i.e.

```math
y = x + 2\sqrt{2} \text{ AND }
 y = -x + 2\sqrt{2}.
```
Setting the two equations equal to one another, we see that

```math
x + 2 \sqrt{2} = -x + 2\sqrt{2}
```
Substracting over the $2\sqrt{2}$, we see that $x = -x$, so our solution has $x = 0$. We solve for the corresponding $y$ using either equations which shows that $y = 2\sqrt{2}$. Therefore our final solution is $(0, 2\sqrt{2})$, which you can see plotted on the graph below.

**Exercise:** Based only on the equation of the tangent lines, what is the angle between the two tangent lines?
"""

# ╔═╡ bae2a594-4fc5-11eb-0509-f920c67af600
md"""
#### Part (b)

We can simply follow the approach of part (a) here.

##### Finding the tangent line at point $P = (-1, \sqrt{3})$

Starting with the slope of the radial line, we compute

```math
m_{CP} = \frac{\sqrt{3} - 0}{ - 1 } = - \sqrt{3}
```
Therefore, the tangent line has slope

```math
m_{\tan P} = - 1 / m_{CP} = \frac{1}{\sqrt{3}},
```

which shows that our tangent line has equation

```math
y  - \sqrt{3} = \frac{1}{\sqrt{3}} (x + 1).
```

##### Finding the tangent line at point $Q = (1, -\sqrt{3})$

Repeating this process for point $Q$, we see

```math
m_{CQ} = \frac{-\sqrt{3} - 0}{1 - 0} = -\sqrt{3}.
```

Next, we find the slope of our tangent line

```math
m_{\tan Q} = - 1 / m_{CQ} = \frac{1}{\sqrt{3}}.
```

We can finish by using point-slope to find the equation of our tangent line.
```math
y + \sqrt{3} = \frac{1}{\sqrt{3}} ( x - 1).
```

##### Point of intersection?

As before we can try and solve for the point of intersection using the two equations by setting them equal to one another, but what we'll quickly notice is that there is no solution here. This is because the two tangent lines have equal slopes are parralel and by definition have no point of intersection. We can see this more clearly by graphing the circle below.
"""

# ╔═╡ 8be12e48-4fc8-11eb-27bc-6198c13f2c6e
md"""

### Part (c)

Try setting up and finishing part (c) on your own.

"""

# ╔═╡ 242d7740-4fc4-11eb-2db7-11cfca6413fe
begin
	## Helper Functions

		function add_points(P, name)
				scatter!([P[1]], [P[2]],
				label = name)
		end

		function add_lines(P, m)
			plot!(x -> m*(x-P[1]) + P[2], -3, 3,
				label = false,
				linewidth = 1.5,
				color = "grey")
		end

		function find_tangent_slope(P, C)
			m =  - (P[1] - C[1] ) / (P[2] - C[2])
		end
	println("Empty Line because Pluto is weird")
end

# ╔═╡ 7938440e-4fbf-11eb-30f5-cde38c57d1cc
begin
		Pa = (-sqrt(2), sqrt(2))
		Qa = (sqrt(2), sqrt(2))

		ex_xs, ex_ys = circle(0,0,2)
		Ex1 = plot(ex_xs, ex_ys,
			size = (500,500),
			xlim = (-3, 3),
			ylim = (-3, 3),
			linewidth = 2,
			color = "black",
			aspect_ratio = :equal,
			label = false)
		Ex1 = scatter!([0], [0], label = "C")

		Ex1 = add_points(Pa, "P")
		Ex1 = add_points(Qa, "Q")

		Ex1 = add_lines(Pa,
				 find_tangent_slope(Pa, (0,0)))
		Ex1 = add_lines(Qa,
				find_tangent_slope(Qa, (0,0)))
end

# ╔═╡ 7cc3dd0a-4fcd-11eb-3644-5308c93514c7
scatter!(Ex1, [0], [2*sqrt(2)],
		label = L"(0, 2 \sqrt{2})",
		markersize = 6,
		color = "Yellow")

# ╔═╡ 8a7cba3c-4fc5-11eb-32af-f5fae59dba6e
begin
			Pb = (-1, sqrt(3))
			Qb = (1, -sqrt(3))

			plot(ex_xs, ex_ys,
				size = (500,500),
				linewidth = 2,
				color = "black",
				aspect_ratio = :equal,
				label = false)
			scatter!([0], [0], label = "C")

			add_points(Pb, "P")
			add_points(Qb, "Q")

			add_lines(Pb,
					 find_tangent_slope(Pb, (0,0)))
			add_lines(Qb,
					find_tangent_slope(Qb, (0,0)))
end

# ╔═╡ Cell order:
# ╟─ae386df6-4703-11eb-200c-e9d7e818f15e
# ╟─becfe73e-4703-11eb-1def-a357f306b5ff
# ╟─f5d9047a-4af2-11eb-2339-2b7b83990bbe
# ╟─94fd0c3e-4af5-11eb-3968-1316069a9e8d
# ╟─5533ef12-4af7-11eb-110e-23ea2ff268fb
# ╟─4fdb0a00-4af7-11eb-3fa2-bb120202054d
# ╟─e6c5c248-4af9-11eb-1e09-455232f6e2ce
# ╟─7ed2de82-4af7-11eb-1439-8d3b5cee0ae0
# ╟─d40d36f6-4afa-11eb-003b-a7e088f40e15
# ╟─e0abe984-4703-11eb-1d47-a945691991cb
# ╟─814a9bb0-4afb-11eb-287d-8537b497853d
# ╟─ec0bd9e4-4703-11eb-26e3-834539a39d83
# ╟─12af1234-4705-11eb-06f5-970c779f5b1a
# ╟─65b256c2-4704-11eb-06a9-ffdcd2545045
# ╟─42233d1e-4afd-11eb-3af1-71cc1b4e9829
# ╟─7aa4a83c-4709-11eb-3ec5-8784f5748b62
# ╟─736a0656-4705-11eb-229a-6b60032aa346
# ╟─ff1f5862-4afd-11eb-1aa1-61f93e244a87
# ╟─f617bf5c-4706-11eb-00d0-35f62229ff28
# ╟─c1a9822c-4afe-11eb-35ee-4d9b576c273d
# ╟─88c3e17c-4aff-11eb-2611-b5549aaeaa54
# ╟─9c0abdb4-4703-11eb-2bcf-6387d2445b8a
# ╟─7ad2d2ce-4fbb-11eb-2566-0d1ecf72b897
# ╟─eab8e50e-4fc2-11eb-25d0-05d133782270
# ╟─7938440e-4fbf-11eb-30f5-cde38c57d1cc
# ╟─c6661a46-4fc9-11eb-0d58-4d38df0118ee
# ╟─7cc3dd0a-4fcd-11eb-3644-5308c93514c7
# ╟─bae2a594-4fc5-11eb-0509-f920c67af600
# ╟─8a7cba3c-4fc5-11eb-32af-f5fae59dba6e
# ╟─8be12e48-4fc8-11eb-27bc-6198c13f2c6e
# ╟─242d7740-4fc4-11eb-2db7-11cfca6413fe
# ╟─22dc1d68-4709-11eb-1112-f31e0854356f


using LazySets, MathematicalPredicates, SparseArrays

function building_specification()
    # initial set:
    # - xᵢ ∈ [0.0002, 0.00025] for i ∈ [1, 10]
    # - xᵢ ∈ [-0.0001, 0.0001] for i = 25
    # - xᵢ = 0 for all other i
    X0 = Hyperrectangle(low=[fill(0.0002, 10); zeros(14); -0.0001; zeros(23)],
                        high=[fill(0.00025, 10); zeros(14); 0.0001; zeros(23)])

    # safety property: x25 ≤ 6e-3
    property = is_contained_in(HalfSpace(sparsevec([25], [1.0], 48), 6e-3))

    # time horizon: 20 time units
    time_horizon = 20.0

    # specification
    O = Dict(:T => time_horizon, :property => property)

    return X0, O
end


# Explicit 1D heat equation
using Checkpointing
using Plots
using Zygote

mutable struct Heat
    Tnext::Vector{Float64}
    Tlast::Vector{Float64}
    n::Int
    λ::Float64
    tsteps::Int
end

function advance(heat)
    next = heat.Tnext
    last = heat.Tlast
    λ = heat.λ
    n = heat.n
    for i in 2:(n-1)
        next[i] = last[i] + λ*(last[i-1]-2*last[i]+last[i+1])
    end
    return nothing
end

function sumheat(heat::Heat)
    # AD: Create shadow copy for derivatives
    shadowheat = Heat(zeros(n), zeros(n), 0, 0.0, 0)
    @checkpoint_struct revolve heat shadowheat for i in 1:tsteps
        heat.Tlast .= heat.Tnext
        advance(heat)
    end
    return reduce(+, heat.Tnext)
end

n = 100
Δx=0.1
Δt=0.001
# Select μ such that λ ≤ 0.5 for stability with μ = (λ*Δt)/Δx^2
λ = 0.5
# time steps
tsteps = 500

# Create object from struct
heat = Heat(zeros(n), zeros(n), n, λ, tsteps)

# Boundary conditions
heat.Tnext[1]   = 20.0
heat.Tnext[end] = 0

# Set up AD
# Number of available snapshots
snaps = 4
verbose = 0
revolve = Revolve(tsteps, snaps; verbose=verbose)

# Compute gradient
g = Zygote.gradient(sumheat,heat)

# Plot function values
plot(heat.Tnext)
# Plot gradient with respect to sum(T).
plot(g[1].Tnext[2:end-1])


@testset "Kronecker powers" begin
    A = [0.1 0.4; 0.6 2.1]
    B = [1 2 3 4; 5 6 7 8]

    K1 = kronecker(A, 3)
    K2 = kronecker(B, 3)

    K1dense = kron(A, A, A)
    K2dense = kron(B, B, B)

    C = [1 2; 3 4]
    KC = kronecker(C, 3)
    KCdense = kron(C, C, C)

    @testset "Types and basic properties" begin

        @test K1 isa AbstractKroneckerProduct
        @test K1 isa KroneckerPower

        @test issquare(K1)
        @test !issquare(K2)

        @test eltype(K1) <: Real
        @test eltype(K2) <: Integer

        @test order(K1) == 3

        @test getmatrices(K1)[1] isa KroneckerPower
        @test getmatrices(K1)[2] === A
        @test order(getmatrices(K1)[1]) == 2

        # scaling
        @test 3K1 ≈ 3K1dense ≈ K1 * 3
        @test 9.2 * K2 ≈ 9.2K2dense ≈ K2 * 9.2

        @test !issymmetric(K1)

        @test collect(K1) ≈ K1dense
        @test collect!(similar(K2dense), K2) ≈ K2dense

        @test sum(K1) ≈ sum(K1dense)

    end

    @testset "Inplace scaling" begin
        using LinearAlgebra: lmul!
        K1copy = kronecker(copy(A), 3)
        K2copy = kronecker(copy(B), 3)

        lmul!(2.7, K1copy)
        @test_throws InexactError lmul!(2.3, K2copy)

        @test K1copy ≈ 2.7K1dense

    end

    @testset "Linear algebra" begin
        @test tr(K1) ≈ tr(K1dense)
        @test !isposdef(K1)
        @test transpose(K1) ≈ transpose(K1dense)
        @test conj(K1) ≈ conj(K1dense)
        @test K1' ≈ K1dense'
        @test inv(K1) ≈ inv(K1dense)

        @test diag(K1) ≈ diag(K1dense)
        @test diag(K2) == diag(K2dense)
        @test diag(KC) == diag(KCdense)

        # test on pos def functions
        As = A' * A
        @test det(⊗(As, 2)) ≈ det(kron(As, As))
        @test logdet(⊗(As, 2)) ≈ log(det(kron(As, As)))
    end

    @testset "Inference in indexing" begin
        @test (@inferred K2[1,1]) == K2dense[1,1]
    end

    @testset "Arithmetic" begin
        @test K2 + K2 == 2 * K2 == 2 * K2dense
        @test K2 + K2 + K2 == 3 * K2 == 3 * K2dense
        @test K2 - K2 == 0 * K2 == 0 * K2dense
        @test K2 - K2 - K2 == -K2 == -K2dense
    end

    @testset "Broadcasting" begin
        @test K2 .+ K2 == 2 .* K2 == 2 .* K2dense
        @test K2 .- K2 == 0 .* K2 == 0 .* K2dense
        @test sin.(K2) == sin.(K2dense)
    end

    @testset "Multiplication" begin
        # product between two Kronecker powers
        @test K1 * K2 ≈ K1dense * K2dense
        # power
        @test K1^4 ≈ K1dense^4
        # mixed product
        @test K1 * (A ⊗ B ⊗ A) ≈ (A ⊗ A ⊗ A) * (A ⊗ B ⊗ A)
        @test (A ⊗ B' ⊗ A) * K1 ≈ (A ⊗ B' ⊗ A) * (A ⊗ A ⊗ A)
    end

@testset "Mixed product" begin

    @test K1 * K2 ≈ K1dense * K2dense
end

end


using Plots, Printf, LinearAlgebra

# enable plotting by default
if !@isdefined do_visu; do_visu = true end

# finite-difference support functions
@views av_xi(A) = 0.5*(A[1:end-1,2:end-1].+A[2:end,2:end-1])
@views av_yi(A) = 0.5*(A[2:end-1,1:end-1].+A[2:end-1,2:end])
@views   inn(A) = A[2:end-1,2:end-1]

@views function diffusion_2D_expl(; do_visu=true, save_fig=false)
    # Physics
    lx, ly = 10.0, 10.0   # domain size
    npow   = 3            # power-law exponent
    ttot   = 1.0          # total simulation time
    # Numerics
    nx, ny = 128, 128     # numerical grid resolution
    # Derived numerics
    dx, dy = lx/nx, ly/ny # grid size
    xc, yc = LinRange(dx/2, lx-dx/2, nx), LinRange(dy/2, ly-dy/2, ny)
    # Array allocation
    qHx    = zeros(nx-1, ny-2) # on staggered grid
    qHy    = zeros(nx-2, ny-1) # on staggered grid
    dHdt   = zeros(nx-2, ny-2) # normal grid, without boundary points
    # Initial condition
    H      = exp.(.-(xc.-lx/2).^2 .-(yc.-ly/2)'.^2)
    dt     = minimum(min(dx, dy)^2 ./inn(H).^npow./4.1)  # time step (obeys ~CFL condition)
    t = 0.0; it = 0
    # Physical time loop
    while t<ttot
        qHx    .= -av_xi(H).^npow.*diff(H[:,2:end-1], dims=1)/dx  # flux
        qHy    .= -av_yi(H).^npow.*diff(H[2:end-1,:], dims=2)/dy  # flux
        dHdt   .= -diff(qHx, dims=1)/dx -diff(qHy, dims=2)/dy     # rate of change
        H[2:end-1,2:end-1] .= inn(H) .+ dt.*dHdt      # update rule, sets the BC as H[1]=H[end]=0
        t += dt; it += 1
    end
    @printf("Total time = %1.2f, time steps = %d \n", round(ttot, sigdigits=2), it)
    # Visualize
    if do_visu
        fontsize = 12
        opts = (aspect_ratio=1, yaxis=font(fontsize, "Courier"), xaxis=font(fontsize, "Courier"),
                ticks=nothing, framestyle=:box, titlefontsize=fontsize, titlefont="Courier", colorbar_title="",
                xlabel="Lx", ylabel="Ly", xlims=(xc[1],xc[end]), ylims=(yc[1],yc[end]), clims=(0.,1.))
        display(heatmap(xc, yc, H'; c=:davos, title="explicit diffusion (nt=$it)", opts...))
        if save_fig savefig("diff2D_expl.png") end
    end
    return xc, yc, H
end

diffusion_2D_expl(; do_visu=do_visu);