@testset "Small special tests" begin
    @testset "Fix variable" begin
        m = Model(CSJuMPTestOptimizer())
        @variable(m, 1 <= x <= 9, Int)
        @variable(m, y == 2, Int)
        # should just return optimal with any 1-9 for x and y is fixed
        optimize!(m)

        @test JuMP.termination_status(m) == MOI.OPTIMAL
        @test 1 <= JuMP.value(x) <= 9 && length(CS.values(m, x)) == 1
        @test JuMP.value(y) == 2

        m = Model(CSJuMPTestOptimizer())
        @variable(m, 1 <= x <= 9, Int)
        @variable(m, y == 2, Int)
        @constraint(m, x + y == 10)
        optimize!(m)

        @test JuMP.termination_status(m) == MOI.OPTIMAL
        @test JuMP.value(x) == 8
        @test JuMP.value(y) == 2
    end

    @testset "LessThan constraints JuMP" begin
        m = Model(CSJuMPTestOptimizer(; branch_strategy = :ABS))
        @variable(m, 1 <= x[1:5] <= 9, Int)
        @constraint(m, sum(x) <= 25)
        @constraint(m, sum(x) >= 20)
        weights = [1, 2, 3, 4, 5]
        @objective(m, Max, sum(weights .* x))
        optimize!(m)

        @test JuMP.termination_status(m) == MOI.OPTIMAL
        @test JuMP.value.(x) == [1, 1, 5, 9, 9]
        @test JuMP.objective_value(m) == 99

        # minimize
        m = Model(CSJuMPTestOptimizer())
        @variable(m, 1 <= x[1:5] <= 9, Int)
        @constraint(m, sum(x) <= 25)
        @constraint(m, sum(x) >= 20)
        weights = [1, 2, 3, 4, 5]
        @objective(m, Min, sum(weights .* x))
        optimize!(m)

        @test JuMP.termination_status(m) == MOI.OPTIMAL
        @test JuMP.value.(x) == [9, 8, 1, 1, 1]
        @test JuMP.objective_value(m) == 37

        # minimize with negative and positive real weights
        m = Model(CSJuMPTestOptimizer(; branch_strategy = :ABS))
        @variable(m, 1 <= x[1:5] <= 9, Int)
        @constraint(m, sum(x) <= 25)
        weights = [-0.1, 0.2, -0.3, 0.4, 0.5]
        @constraint(m, sum(x[i] for i = 1:5 if weights[i] > 0) >= 15)
        @objective(m, Min, sum(weights .* x))
        optimize!(m)

        @test JuMP.termination_status(m) == MOI.OPTIMAL
        @test JuMP.value.(x) == [1, 9, 9, 5, 1]
        @test JuMP.objective_value(m) ≈ 1.5
    end

    @testset "Knapsack problems" begin
        m = Model(CSJuMPTestOptimizer())

        @variable(m, 1 <= x[1:5] <= 9, Int)
        @constraint(m, sum(x) <= 25)
        @constraint(m, x[2] + 1.2 * x[4] - x[5] <= 12)
        weights = [1.2, 3.0, -0.3, -5.2, 2.7]
        @objective(m, Max, dot(weights, x))

        optimize!(m)

        @test JuMP.termination_status(m) == MOI.OPTIMAL
        x_vals = JuMP.value.(x)
        @test x_vals[1] ≈ 5
        @test x_vals[2] ≈ 9
        @test x_vals[3] ≈ 1
        @test x_vals[4] ≈ 1
        @test x_vals[5] ≈ 9
        @test JuMP.objective_value(m) ≈ 51.8

        # less variables in the objective
        m = Model(CSJuMPTestOptimizer())

        @variable(m, 1 <= x[1:5] <= 9, Int)
        @constraint(m, sum(x) <= 25)
        @constraint(m, x[2] + 1.2 * x[4] - x[5] <= 12)
        @constraint(m, -x[3] - 1.2 * x[4] + x[5] <= 12)
        weights = [1.2, 3.0, -0.3, -5.2, 2.7]
        @objective(m, Max, x[3] + 2.7 * x[4] - x[1])

        optimize!(m)

        @test JuMP.termination_status(m) == MOI.OPTIMAL
        x_vals = JuMP.value.(x)
        @test sum(x_vals) <= 25
        @test x_vals[2] + 1.2 * x_vals[4] - x_vals[5] <= 12
        @test JuMP.objective_value(m) ≈ 32.3

        # minimize
        m = Model(CSJuMPTestOptimizer())

        @variable(m, 1 <= x[1:5] <= 9, Int)
        @constraint(m, sum(x) >= 25)
        @constraint(m, x[2] + 1.2 * x[4] >= 12)
        weights = [1.2, 3.0, 0.3, 5.2, 2.7]
        @objective(m, Min, dot(weights, x))

        optimize!(m)

        @test JuMP.termination_status(m) == MOI.OPTIMAL
        x_vals = JuMP.value.(x)
        @test x_vals[1] ≈ 3
        @test x_vals[2] ≈ 9
        @test x_vals[3] ≈ 9
        @test x_vals[4] ≈ 3
        @test x_vals[5] ≈ 1
        @test JuMP.objective_value(m) ≈ 51.6

        # minimize only part of the weights and some are negative
        m = Model(CSJuMPTestOptimizer())

        @variable(m, 1 <= x[1:5] <= 9, Int)
        @constraint(m, sum(x) >= 25)
        @constraint(m, x[2] + 1.2 * x[4] - x[1] >= 12)
        @constraint(m, x[5] <= 7)
        @objective(m, Min, 3 * x[2] + 5 * x[1] - 2 * x[3])

        optimize!(m)

        @test JuMP.termination_status(m) == MOI.OPTIMAL
        x_vals = JuMP.value.(x)
        @test x_vals[1] ≈ 1
        @test x_vals[2] ≈ 3
        @test x_vals[3] ≈ 9
        @test x_vals[4] ≈ 9
        @test sum(x_vals) >= 25
        @test JuMP.objective_value(m) ≈ -4

        m = Model(CSJuMPTestOptimizer())
        @variable(m, 1 <= x[1:5] <= 9, Int)
        @constraint(m, sum(x) <= 25)
        @constraint(m, -x[1] - x[2] - x[3] + x[4] + x[5] >= 5)
        weights = [-1, 2, 3, 4, 5]
        @objective(m, Min, sum(weights[1:3] .* x[1:3]))

        optimize!(m)

        x_vals = JuMP.value.(x)
        @test sum(x_vals) <= 25
        @test -x_vals[1] - x_vals[2] - x_vals[3] + x_vals[4] + x_vals[5] >= 5
        @test JuMP.objective_value(m) ≈ -3
    end

    @testset "Not supported constraints" begin
        m = Model(CSJuMPTestOptimizer())
        # must be an Integer upper bound
        @variable(m, 1 <= x[1:5] <= NaN, Int)
        @test_throws ErrorException optimize!(m)

        m = Model(CSJuMPTestOptimizer())
        # must be an Integer lower bound
        @variable(m, NaN <= x[1:5] <= 2, Int)
        @test_throws ErrorException optimize!(m)

        m = Model(CSJuMPTestOptimizer())
        @variable(m, 1 <= x[1:5] <= 2, Int)

        m = Model(CSJuMPTestOptimizer())
        @variable(m, 1 <= x[1:5] <= 2, Int)

        m = Model(CSJuMPTestOptimizer())
        @variable(m, 1 <= x[1:5] <= 2, Int)
    end

    @testset "Bipartite matching" begin
        match = CS.bipartite_cardinality_matching([2, 1, 3], [1, 2, 3], 3, 3)
        @test match.weight == 3
        @test match.match == [2, 1, 3]

        # no perfect matching
        match = CS.bipartite_cardinality_matching(
            [1, 2, 3, 4, 1, 2, 3, 3],
            [1, 1, 2, 2, 2, 2, 3, 4],
            4,
            4,
        )
        @test match.weight == 3
        # 4 is zero and the rest should be different
        @test match.match[4] == 0
        @test allunique(match.match)

        # more values than indices
        match = CS.bipartite_cardinality_matching(
            [1, 2, 3, 4, 1, 2, 3, 3, 2, 1, 2],
            [1, 1, 2, 2, 2, 2, 3, 4, 5, 5, 6],
            4,
            6,
        )
        @test match.weight == 4
        # all should be matched to different values
        @test allunique(match.match)
        # no unmatched vertex
        @test count(i -> i == 0, match.match) == 0

        # more values than indices with matching_init
        m = 4
        n = 6
        l = [1, 2, 3, 4, 1, 2, 3, 3, 2, 1, 2, 0, 0]
        r = [1, 1, 2, 2, 2, 2, 3, 4, 5, 5, 6, 0, 0]
        # don't use the zeros
        l_len = length(l) - 2
        matching_init = CS.MatchingInit(
            l_len,
            zeros(Int, m),
            zeros(Int, n),
            zeros(Int, m + 1),
            zeros(Int, m + n),
            zeros(Int, m + n),
            zeros(Int, m + n),
            zeros(Bool, m),
            zeros(Bool, n),
        )
        match = CS.bipartite_cardinality_matching(l, r, m, n; matching_init = matching_init)
        @test match.weight == 4
        # all should be matched to different values
        @test allunique(match.match)
        # no unmatched vertex
        @test count(i -> i == 0, match.match) == 0
    end

    @testset "Not equal" begin
        m = Model(CSJuMPTestOptimizer())

        @variable(m, 1 <= x <= 10, Int)
        @variable(m, 1 <= y <= 1, Int)
        @variable(m, 1 <= z <= 10, Int)
        @constraint(m, x != 2 - 1) # != 1
        @constraint(m, 2x != 4) # != 2
        @constraint(m, π / 3 * x != π) # != 3
        @constraint(m, 2.2x != 8.8) # != 4
        @constraint(m, 4x + 5y != 25) # != 5
        @constraint(m, 4x + π * y != 10) # just some random stuff
        @constraint(m, x + y + z - π != 10)
        @constraint(m, x + y + z + 2 != 10)
        @objective(m, Min, x)
        optimize!(m)

        @test JuMP.objective_value(m) == 6
        @test JuMP.termination_status(m) == MOI.OPTIMAL
        @test JuMP.value(x) == 6
        @test JuMP.value(y) == 1
        # the values should be fixed
        @test length(CS.values(m, x)) == 1
        @test length(CS.values(m, y)) == 1
        @test length(CS.values(m, z)) == 1
        @test JuMP.value(x) + JuMP.value(y) + JuMP.value(z) + 2 != 10
    end

    @testset "Integers basic" begin
        m = Model(CSJuMPTestOptimizer())
        @variable(m, x, CS.Integers([1, 2, 4]))
        @variable(m, y, CS.Integers([2, 3, 5, 6]))
        @constraint(m, x == y)
        @objective(m, Max, x)
        optimize!(m)
        @test JuMP.value(x) ≈ 2
        @test JuMP.value(y) ≈ 2
        @test JuMP.objective_value(m) ≈ 2

        m = Model(optimizer_with_attributes(CS.Optimizer, "backtrack" => false))
        @variable(m, x, CS.Integers([1, 2, 4]))
        optimize!(m)
        com = CS.get_inner_model(m)
        @test !CS.has(com.search_space[1], 3)
        @test sort(CS.values(com.search_space[1])) == [1, 2, 4]

        m = Model(optimizer_with_attributes(CS.Optimizer, "backtrack" => false))
        @variable(m, y, CS.Integers([2, 5, 6, 3]))
        optimize!(m)
        com = CS.get_inner_model(m)
        @test !CS.has(com.search_space[1], 1)
        @test !CS.has(com.search_space[1], 4)
        @test sort(CS.values(com.search_space[1])) == [2, 3, 5, 6]
    end

    @testset "Biggest cube square number up to 100" begin
        m = Model(CSJuMPTestOptimizer())
        @variable(m, x, CS.Integers([i^2 for i = 1:20 if i^2 < 100]))
        @variable(m, y, CS.Integers([i^3 for i = 1:20 if i^3 < 100]))
        @constraint(m, x == y)
        @objective(m, Max, x)
        optimize!(m)
        @test JuMP.value(x) ≈ 64
        @test JuMP.value(y) ≈ 64
        @test JuMP.objective_value(m) ≈ 64
    end

    @testset "Pythagorean triples" begin
        m = Model(optimizer_with_attributes(
            CS.Optimizer,
            "all_solutions" => true,
            "branch_strategy" => :ABS,
            "activity.decay" => 0.999,
            "activity.max_probes" => 20,
            "logging" => [],
        ))
        @variable(m, x[1:3], CS.Integers([i^2 for i in 1:50]))
        @constraint(m, x[1] + x[2] == x[3])
        @constraint(m, x[1] <= x[2])
        optimize!(m)
        com = CS.get_inner_model(m)
        @test is_solved(com)
        @test MOI.get(m, MOI.ResultCount()) == 20
    end

    @testset "Infeasible by fixing variable to outside domain" begin
        m = Model(CSJuMPTestOptimizer())
        @variable(m, x, CS.Integers([1, 2, 4]))
        @constraint(m, x == 3)
        optimize!(m)
        @test JuMP.termination_status(m) == MOI.INFEASIBLE
    end

    @testset "Infeasible by fixing variable to two values" begin
        m = Model(CSJuMPTestOptimizer())
        @variable(m, x, CS.Integers([1, 2, 4]))
        @constraint(m, x == 1)
        @constraint(m, x == 2)
        optimize!(m)
        @test JuMP.termination_status(m) == MOI.INFEASIBLE
    end

    @testset "5 variables all equal" begin
        m = Model(optimizer_with_attributes(
            CS.Optimizer,
            "all_solutions" => true,
            "logging" => [],
        ))

        @variable(m, 5 <= x <= 10, Int)
        @variable(m, 2 <= y <= 15, Int)
        @variable(m, 1 <= z <= 7, Int)
        @variable(m, 2 <= a <= 9, Int)
        @variable(m, 6 <= b <= 10, Int)
        @constraint(m, x == y)
        # should not result in linking to x -> y -> x ...
        @constraint(m, y == x)
        @constraint(m, x == y)

        @constraint(m, y == z)
        @constraint(m, a == z)
        @constraint(m, b == y)
        optimize!(m)

        @test JuMP.termination_status(m) == MOI.OPTIMAL
        @test JuMP.value(x) ==
              JuMP.value(y) ==
              JuMP.value(z) ==
              JuMP.value(a) ==
              JuMP.value(b)
        @test JuMP.value(x; result = 2) ==
              JuMP.value(y; result = 2) ==
              JuMP.value(z; result = 2) ==
              JuMP.value(a; result = 2) ==
              JuMP.value(b; result = 2)
        @test JuMP.value(x) == 6 || JuMP.value(x) == 7
        @test JuMP.value(x; result = 2) == 6 || JuMP.value(x; result = 2) == 7
        @test JuMP.value(x) != JuMP.value(x; result = 2)
    end

    @testset "x[1] <= x[1]" begin
        model = Model(CSJuMPTestOptimizer())
        n = 4
        @variable(model, 1 <= x[1:n] <= n, Int)
        for i in 1:n
            @constraint(model, x[1] <= x[i])
        end

        optimize!(model)
        status = JuMP.termination_status(model)
        @test status == MOI.OPTIMAL
        com = CS.get_inner_model(model)
        @test is_solved(com)
    end

    @testset "x[1] <= x[1] - 1 " begin
        model = Model(CSJuMPTestOptimizer())
        n = 4
        @variable(model, 1 <= x[1:n] <= n, Int)
        for i in 1:n
            @constraint(model, x[1] <= x[i] - 1)
        end

        optimize!(model)
        status = JuMP.termination_status(model)
        @test status == MOI.INFEASIBLE
    end

    @testset "x[1] == x[1]" begin
        model = Model(CSJuMPTestOptimizer())
        n = 4
        @variable(model, 1 <= x[1:n] <= n, Int)
        for i in 1:n
            @constraint(model, x[1] == x[i])
        end

        optimize!(model)
        status = JuMP.termination_status(model)
        @test status == MOI.OPTIMAL
        com = CS.get_inner_model(model)
        @test is_solved(com)
    end

    @testset "x[1] == x[1] - 1 " begin
        model = Model(CSJuMPTestOptimizer())
        n = 4
        @variable(model, 1 <= x[1:n] <= n, Int)
        for i in 1:n
            @constraint(model, x[1] == x[i] - 1)
        end

        optimize!(model)
        status = JuMP.termination_status(model)
        @test status == MOI.INFEASIBLE
    end

    @testset "x[1] == x[1] - 1 in reified" begin
        model = Model(CSCbcJuMPTestOptimizer())
        n = 4
        @variable(model, 1 <= x[1:n] <= n, Int)
        @variable(model, b[1:n], Bin)
        for i in 1:n
            @constraint(model, b[i] := {x[1] == x[i] - 1})
        end
        @objective(model, Max, sum(b))
        optimize!(model)
        status = JuMP.termination_status(model)
        @test status == MOI.OPTIMAL
        @test JuMP.objective_value(model) ≈ 3
        @test JuMP.value(b[1]) ≈ 0
    end

    @testset "x[1] == x[1] - 1 in indicator" begin
        model = Model(CSCbcJuMPTestOptimizer())
        n = 4
        @variable(model, 1 <= x[1:n] <= n, Int)
        @variable(model, b[1:n], Bin)
        for i in 1:n
            @constraint(model, b[i] => {x[1] == x[i] - 1})
        end
        @objective(model, Max, sum(b))
        optimize!(model)
        status = JuMP.termination_status(model)
        @test status == MOI.OPTIMAL
        @test JuMP.objective_value(model) ≈ 3
        @test JuMP.value(b[1]) ≈ 0
    end

    @testset "Infeasible all different in indicator" begin
        model = Model(CSCbcJuMPTestOptimizer(; branch_strategy = :ABS))
        n = 2
        @variable(model, 1 <= x[1:n] <= n - 1, Int)
        @variable(model, b, Bin)
        @constraint(model, b => {x in CS.AllDifferent()})
        @objective(model, Max, b)
        optimize!(model)
        status = JuMP.termination_status(model)
        @test status == MOI.OPTIMAL
        @test JuMP.objective_value(model) ≈ 0
        @test JuMP.value(b) ≈ 0
    end

    @testset "Infeasible all different in reified" begin
        model = Model(CSJuMPTestOptimizer())
        n = 4
        @variable(model, 1 <= x[1:n] <= n - 1, Int)
        @variable(model, b, Bin)
        @constraint(model, b := {x in CS.AllDifferent()})
        @objective(model, Max, b)
        optimize!(model)
        status = JuMP.termination_status(model)
        @test status == MOI.OPTIMAL
        @test JuMP.objective_value(model) ≈ 0
        @test JuMP.value(b) ≈ 0
    end

    @testset "Table dim not matching" begin
        model = Model(CSJuMPTestOptimizer())
        n = 4
        @variable(model, 1 <= x[1:n] <= n - 1, Int)
        @variable(model, b, Bin)
        @test_throws ArgumentError @constraint(model, b => {x in CS.TableSet([
            10 20
            11 5
        ])})
    end

    @testset "Infeasible table in indicator" begin
        model = Model(CSJuMPTestOptimizer())
        n = 2
        @variable(model, 1 <= x[1:n] <= n - 1, Int)
        @variable(model, b, Bin)
        @constraint(model, b => {x in CS.TableSet([
            10 20
            11 5
        ])})
        @objective(model, Max, b)
        optimize!(model)
        status = JuMP.termination_status(model)
        @test status == MOI.OPTIMAL
        @test JuMP.objective_value(model) ≈ 0
        @test JuMP.value(b) ≈ 0
    end

    @testset "Infeasible table in reified" begin
        model = Model(CSJuMPTestOptimizer())
        n = 2
        @variable(model, 1 <= x[1:n] <= n - 1, Int)
        @variable(model, b, Bin)
        @constraint(model, b := {x in CS.TableSet([
            10 20
            11 5
        ])})
        @objective(model, Max, b)
        optimize!(model)
        status = JuMP.termination_status(model)
        @test status == MOI.OPTIMAL
        @test JuMP.objective_value(model) ≈ 0
        @test JuMP.value(b) ≈ 0
    end

    @testset "Magic Square 5x5" begin
        n = 5
        model = Model(optimizer_with_attributes(
            CS.Optimizer,
            "traverse_strategy" => :BFS,
            "logging" => [],
            "branch_split" => :InHalf,
            "time_limit" => 3,
        ))

        # The total for each row, column, and the two main diaginals
        s = round(Int, n * (n^2 + 1) / 2)
        @variable(model, 1 <= x[1:n, 1:n] <= n^2, Int)
        @constraint(model, x[:] in CS.AllDifferent())

        for i in 1:n
            # Rows
            @constraint(model, sum(x[i, :]) == s)

            # Columns
            @constraint(model, sum(x[:, i]) == s)
        end

        # diagonals
        @constraint(model, sum([x[i, i] for i in 1:n]) == s)
        @constraint(model, s == sum([x[i, n - i + 1] for i in 1:n]))

        optimize!(model)
        status = JuMP.termination_status(model)
        @test status == MOI.OPTIMAL
        sol = convert.(Int, JuMP.value.(x))
        for i in 1:n
            @test sum(sol[i, :]) == 65
            @test sum(sol[:, i]) == 65
        end
        @test sum([sol[i, i] for i in 1:n]) == 65
        @test sum([sol[i, n - i + 1] for i in 1:n]) == 65
        @test allunique(sol)
    end

    @testset "Indicator with > and active" begin
        cbc_optimizer = optimizer_with_attributes(Cbc.Optimizer, "logLevel" => 0)
        model = Model(optimizer_with_attributes(
            CS.Optimizer,
            "lp_optimizer" => cbc_optimizer,
            "logging" => []
        ))
        @variable(model, b >= 1, Bin)
        @variable(model, 0 <= x[1:4] <= 5, Int)
        @constraint(model, b => {sum([0.4,0.5,0.7,0.8] .* x) > 9})
        @objective(model, Min, sum([0.4,0.5,0.7,0.8] .* x))
        optimize!(model)
        CS.get_inner_model(model)
        @test JuMP.termination_status(model) == MOI.OPTIMAL
        @test JuMP.objective_value(model) ≈ 9.1
        @test JuMP.value(x[1]) ≈ 2.0
        @test JuMP.value(x[2]) ≈ 3.0
        @test JuMP.value(x[3]) ≈ 4.0
        @test JuMP.value(x[4]) ≈ 5.0
    end

    @testset "AllDifferent except 0" begin
        m = Model(optimizer_with_attributes(
            CS.Optimizer,
            "logging" => []
        ))
        @variable(m, 0 <= x[1:5] <= 3, Int)
        len = length(x)
        for i in 2:len, j in 1:i-1
            b = @variable(m, binary=true)
            @constraint(m, b := {x[i] != 0 && x[j] != 0})
            @constraint(m, b => {x[i] != x[j]} )
        end

        @objective(m, Min, sum(x))
        optimize!(m)
        CS.get_inner_model(m)
        @test JuMP.termination_status(m) == MOI.OPTIMAL
        @test JuMP.objective_value(m) ≈ 0
        @test JuMP.value(x[1]) ≈ 0
        @test JuMP.value(x[2]) ≈ 0
        @test JuMP.value(x[3]) ≈ 0
        @test JuMP.value(x[4]) ≈ 0
        @test JuMP.value(x[5]) ≈ 0

        # maximize
        m = Model(optimizer_with_attributes(
            CS.Optimizer,
            "logging" => []
        ))
        @variable(m, 0 <= x[1:5] <= 3, Int)
        len = length(x)
        for i in 2:len, j in 1:i-1
            b = @variable(m, binary=true)
            @constraint(m, b := {x[i] != 0 && x[j] != 0})
            @constraint(m, b => {x[i] != x[j]} )
        end

        @objective(m, Max, sum(x))
        optimize!(m)
        CS.get_inner_model(m)
        @test JuMP.termination_status(m) == MOI.OPTIMAL
        @test JuMP.objective_value(m) ≈ 3+2+1+0+0
    end

    @testset "Try to fulfill b with several conditions" begin
        # inner part is infeasible
        m = Model(optimizer_with_attributes(
            CS.Optimizer,
            "logging" => []
        ))
        @variable(m, 0 <= x[1:5] <= 3, Int)
        @variable(m, b, Bin)
        @constraint(m, b => {x[1] > x[2] && x[2] > x[3] && x[3] > x[4] && x[4] > x[5]})

        @objective(m, Max, b)
        optimize!(m)
        CS.get_inner_model(m)
        @test JuMP.termination_status(m) == MOI.OPTIMAL
        @test JuMP.objective_value(m) ≈ 0
        @test JuMP.value(b) ≈ 0

        # inner part is feasible
        m = Model(optimizer_with_attributes(
            CS.Optimizer,
            "logging" => []
        ))
        @variable(m, 0 <= x[1:5] <= 4, Int)
        @variable(m, b, Bin)
        @constraint(m, b => {x[1] > x[2] && x[2] > x[3] && x[3] > x[4] && x[4] > x[5]})

        @objective(m, Max, b)
        optimize!(m)
        CS.get_inner_model(m)
        @test JuMP.termination_status(m) == MOI.OPTIMAL
        @test JuMP.objective_value(m) ≈ 1
        @test JuMP.value(b) ≈ 1
        vx = JuMP.value.(x)
        @test vx[1] > vx[2] && vx[2] > vx[3] && vx[3] > vx[4] && vx[4] > vx[5]

        # inner part is feasible
        m = Model(optimizer_with_attributes(
            CS.Optimizer,
            "logging" => []
        ))
        @variable(m, 0 <= x[1:5] <= 4, Int)
        @variable(m, b, Bin)
        @constraint(m, b => {x[1] >= x[2] && (x[3] <= x[2] && (x[3] >= x[4] && x[4] > x[5])) && x in CS.AllDifferent()})

        @objective(m, Max, b)
        optimize!(m)
        CS.get_inner_model(m)
        @test JuMP.termination_status(m) == MOI.OPTIMAL
        @test JuMP.objective_value(m) ≈ 1
        @test JuMP.value(b) ≈ 1
        vx = JuMP.value.(x)
        @test vx[1] > vx[2] && vx[2] > vx[3] && vx[3] > vx[4] && vx[4] > vx[5]

        # inner part is feasible have AndSet && AndSet
        m = Model(optimizer_with_attributes(
            CS.Optimizer,
            "logging" => []
        ))
        @variable(m, 0 <= x[1:5] <= 4, Int)
        @variable(m, b, Bin)
        @constraint(m, b => {(x[1] >= x[2] && x[3] <= x[2]) && (x[3] >= x[4] && x[4] > x[5] && x in CS.AllDifferent())})

        @objective(m, Max, b)
        optimize!(m)
        CS.get_inner_model(m)
        @test JuMP.termination_status(m) == MOI.OPTIMAL
        @test JuMP.objective_value(m) ≈ 1
        @test JuMP.value(b) ≈ 1
        vx = JuMP.value.(x)
        @test vx[1] > vx[2] && vx[2] > vx[3] && vx[3] > vx[4] && vx[4] > vx[5]
    end

    @testset "Table in reified where setting activator to false" begin
        m = Model(optimizer_with_attributes(
            CS.Optimizer,
            "logging" => []
        ))
        @variable(m, 1 <= x[1:5] <= 4, Int)
        @variable(m, b, Bin)
        @constraint(m, b => {x[1] > x[2] && [x[1],x[2]] in CS.TableSet([1 2; 1 3])})

        @objective(m, Max, 10b+sum(x))
        optimize!(m)

        @test JuMP.termination_status(m) == MOI.OPTIMAL
        @test JuMP.objective_value(m) ≈ 20
        @test JuMP.value(b) ≈ 0
    end

    @testset "Small test case for || in reified" begin
        m = Model(optimizer_with_attributes(
            CS.Optimizer,
            "logging" => []
        ))
        @variable(m, 1 <= x[1:5] <= 4, Int)
        @variable(m, b, Bin)
        @constraint(m, b := {(x[1] > x[2] || x in CS.AllDifferent()) })

        @objective(m, Max, 100b+sum(x))
        optimize!(m)

        @test JuMP.termination_status(m) == MOI.OPTIMAL

        @test JuMP.value(b) ≈ 1.0
        @test JuMP.value.(x) ≈ [4,3,4,4,4]
        @test JuMP.objective_value(m) ≈ sum([4,3,4,4,4]) + 100
    end

    @testset "Small test case for || and && in reified" begin
        m = Model(optimizer_with_attributes(
            CS.Optimizer,
            "logging" => []
        ))
        @variable(m, 1 <= x[1:5] <= 4, Int)
        @variable(m, b, Bin)
        table = [
            4 0;
            3 2;
        ]
        @constraint(m, b := {(x[1] > x[2] || x in CS.AllDifferent()) && x[4:5] in CS.TableSet(table) })

        @objective(m, Max, 100b+sum(x))
        optimize!(m)

        @test JuMP.termination_status(m) == MOI.OPTIMAL

        @test JuMP.value(b) ≈ 1.0
        @test JuMP.value.(x) ≈ [4,3,4,3,2]
        @test JuMP.objective_value(m) ≈ sum([4,3,4,3,2]) + 100
    end

    @testset "Small test case for || and &&" begin
        m = Model(optimizer_with_attributes(
            CS.Optimizer,
            "logging" => []
        ))
        @variable(m, 1 <= x[1:5] <= 4, Int)
        table = [
            4 0;
            3 2;
        ]
        @constraint(m, (x[1] > x[2] || x in CS.AllDifferent()) && x[4:5] in CS.TableSet(table))

        @objective(m, Max, sum(x))
        optimize!(m)

        @test JuMP.termination_status(m) == MOI.OPTIMAL

        @test JuMP.value.(x) ≈ [4,3,4,3,2]
        @test JuMP.objective_value(m) ≈ sum([4,3,4,3,2])
    end

    @testset "Small test case for && and TableSet" begin
        m = Model(optimizer_with_attributes(
            CS.Optimizer,
            "logging" => [],
            "all_solutions" => true
        ))
        @variable(m, x[1:2], Bin)
        @variable(m, y[1:2], Bin)
        xor = [
            0 1;
            1 0;
        ]
        and = [
            1 1;
        ]
        @constraint(m, (x in CS.TableSet(xor) || y in CS.TableSet(and)))

        optimize!(m)

        @test JuMP.termination_status(m) == MOI.OPTIMAL

        # x = 0 1 | y = 00 / 01 / 10 / 11
        # x = 1 0 | y = 00 / 01 / 10 / 11
        # y = 1 1 | x = 00 / 11 <- without double counting
        num_sols = MOI.get(m, MOI.ResultCount())
        @test num_sols == 10

        sets = [
            [0, 1, 0, 0],
            [0, 1, 0, 1],
            [0, 1, 1, 0],
            [0, 1, 1, 1],
            [1, 0, 0, 0],
            [1, 0, 0, 1],
            [1, 0, 1, 0],
            [1, 0, 1, 1],
            [0, 0, 1, 1],
            [1, 1, 1, 1]
        ]
        for i in 1:10
            found = false
            for j in 1:10
                if JuMP.value.([x...,y...]; result=j) == sets[i]
                    found = true
                    break
                end
            end
            @test found
        end
    end

    @testset "Monks and Doors" begin
        model = Model(optimizer_with_attributes(CS.Optimizer,   "all_solutions"=> true,
                "logging"=>[],
                "traverse_strategy"=>:BFS,
                "branch_split"=>:InHalf,
                "branch_strategy" => :ABS,
        ))

        num_doors = 4
        num_monks = 8
        @variable(model, doors[1:num_doors], Bin)
        da,db,dc,dd = doors
        door_names = ["A","B","C","D"]

        @variable(model, monks[1:num_monks], Bin)
        m1,m2,m3,m4,m5,m6,m7,m8 = monks

        # Monk 1: Door A is the exit.
        # M1 #= A (Picat constraint)
        @constraint(model, m1 == da)

        #  Monk 2: At least one of the doors B and C is the exit.
        # M2 #= 1 #<=> (B #\/ C)
        # @constraint(model, m2 := { db + dc >= 1})
        @constraint(model, m2 := { [db, dc] in CS.TableSet([
            1 0;
        ]) || dc == 1})

        #  Monk 3: Monk 1 and Monk 2 are telling the truth.
        # M3 #= 1 #<=> (M1 #/\ M2)
        # @constraint(model, m3 := { m1 + m2 == 2})
        # @constraint(model, m3 := { m1 == 1 && m2 == 1})
        @constraint(model, m3 := { m1 && m2})

        #  Monk 4: Doors A and B are both exits.
        # M4 #= 1 #<=> (A #/\ B)
        # @constraint(model, m4 := { da + db == 2})
        @constraint(model, m4 := { da && db })

        #  Monk 5: Doors A and C are both exits.
        # M5 #= 1 #<=> (A #/\ C)
        # @constraint(model, m5 := { da + dc == 2})
        @constraint(model, m5 := { da && dc })

        #  Monk 6: Either Monk 4 or Monk 5 is telling the truth.
        # M6 #= 1 #<=> (M4 #\/ M5)
        # @constraint(model, m6 := { m4 + m5 == 1})
        @constraint(model, m6 := { m4 || m5 })

        #  Monk 7: If Monk 3 is telling the truth, so is Monk 6.
        # M7 #= 1 #<=> (M3 #=> M6)
        @constraint(model, m7 := { [m3, m6] in CS.TableSet([1 1; 0 0; 0 1;]) && [m3, m6] in CS.TableSet([1 1; 0 0; 0 1;]) })
        # @constraint(model, m7 := { m3 => m6})  # This don't work!
        # @constraint(model, m7 := { m3 => {m6 == 1}}) # This don't work either.

        #  Monk 8: If Monk 7 and Monk 8 are telling the truth, so is Monk 1.
        # M8 #= 1 #<=> ((M7 #/\ M8) #=> (M1))
        b1 = @variable(model, binary=true)
        # @constraint(model, b1 := {m7 + m8 == 2})
        @constraint(model, b1 := {m7 && [m7,m8] in CS.TableSet([1 1; 2 2; 3 3;])})
        @constraint(model, m8 := {b1 <= m1})

        # Exactly one door is an exit.
        # (A + B + C + D) #= 1
        @constraint(model, da + db + dc + dd == 1)

        optimize!(model)

        status = JuMP.termination_status(model)
        @test JuMP.termination_status(model) == MOI.OPTIMAL

        @test MOI.get(model, MOI.ResultCount()) == 1
        doors_val = convert.(Integer,JuMP.value.(doors))
        monks_val = convert.(Integer,JuMP.value.(monks))
        @test doors_val == [1, 0, 0, 0]
        @test monks_val == [1, 0, 0, 0, 0, 0, 1, 1]

        com = CS.get_inner_model(model)
        for var in com.search_space
            @test issorted(var.init_vals)
        end
    end
end


a = [1,2,3+1im,8,5]
V=Vandermonde(a)

# Test element
@test V[4,5] == a[4]^4

# Test full matrix conversions
M = Matrix(V)
@test Matrix(V') == M'
@test Matrix(transpose(V)) == transpose(M)

# Test solving with vector and matrix rhs
y = [1im,1,5,0,2]
Y = [y 2*y]
for rhs=[y, Y]
    # Test solution
    @test isapprox(V\rhs, M\rhs)
    @test isapprox(V'\rhs, M'\rhs)
    @test isapprox(rhs'/V, rhs'/M)
    @test isapprox(transpose(V)\rhs, transpose(M)\rhs)
    # Check that overloading works
    x = zero(M\rhs)
    copyto!(x, rhs)
    SpecialMatrices.dvand!(a, x)
    @test V\rhs==x
    copyto!(x, rhs)
    SpecialMatrices.pvand!(a', x)
    @test V'\rhs==x
    @test rhs'/V==x'
    copyto!(x, rhs)
    SpecialMatrices.pvand!(a, x)
    @test transpose(V)\rhs==x
end

# Test dimension errors
rhs = zeros(2,2)
try
    V\rhs
catch e
    @test typeof(e)==DimensionMismatch
end
try
    V'\rhs
catch e
    @test typeof(e)==DimensionMismatch
end


@testset "test_matrix_comps" begin
A = [-0.21 0.2; 0.2 -0.21]
B = 0.01*[1 0; 0 1]
C = [1 0; 0 1]
D = 0
sys = ss(A,B,C,D)
sysr, G = balreal(sys)

@test gram(sysr, :c) ≈ G
@test gram(sysr, :o) ≈ G
@test sort(pole(sysr)) ≈ sort(pole(sys))

sysb,T = ControlSystems.balance_statespace(sys)
Ab,Bb,Cb,T = ControlSystems.balance_statespace(A,B,C)

@test Ab*T ≈ T*A
@test Bb ≈ T*B
@test Cb*T ≈ C

@test sysb.A ≈ Ab

@test ControlSystems.balance_transform(A,B,C) ≈ ControlSystems.balance_transform(sys)

W = [1 0; 0 1]
@test covar(sys, W) ≈ [0.002560975609756 0.002439024390244; 0.002439024390244 0.002560975609756]
D2 = [1 0; 0 1]
@test covar(ss(A,B,C,D2, 1), W) ≈ [1.000110108378310 -0.000010098377310; -0.000010098377310 1.000110108378310]
# Direct term means infinite covariance
@test covar(ss(A,B,C,D2), W) ≈ [Inf Inf; Inf Inf]

# No noise on second output should give finite variance
@test covar(ss(A,B,C,[1 0; 0 0]), W) ≈ [Inf Inf; Inf 0.002560975609756]

# Unstable system has inf covar
@test covar(ss([1 0; 0 1],B,C,0), W) == [Inf Inf; Inf Inf]

# Discrete system can have direct term
@test covar(ss(A,B,C,D2,0.1),W) ≈ [1.00011010837831 -1.0098377309782909e-5; -1.0098377309782909e-5 1.00011010837831]

# TODO test in Julia 0.7 to see if supported
# # Test special matrices
As = sparse(A)
Bs = sparse(B)
Cs = sparse(C)
Asb,Bsb,Csb,Ts = ControlSystems.balance_statespace(As,Bs,Cs) #Error no LAPACK function
#
# @test Abs*Ts ≈ Ts*As
# @test Bbs ≈ Ts*Bs
# @test Cbs*Ts ≈ Cs

# Test special values
Ar = rationalize.(A)
Br = rationalize.(B)
Cr = rationalize.(Float64.(C))    # When did ever rationalize work on Int?
Arb,Brb,Crb,Tr = ControlSystems.balance_statespace(Ar,Br,Cr)

@test Arb*Tr ≈ Tr*Ar
@test Brb ≈ Tr*Br
@test Crb*Tr ≈ Cr

Tr = randn(2,2)
syst = similarity_transform(sys, Tr)
@test sys.A*Tr ≈ Tr*syst.A
@test sys.B ≈ Tr*syst.B
@test sys.C*Tr ≈ syst.C

sys = ss([1 0.1; 0 1], ones(2), [1. 0], 0)
sysi = ControlSystems.innovation_form(sys, I, I)
@test sysi.A ≈ sysi.A
@test sysi.B ≈ [4.415675759647131
 48.334204475215365]

 sysi = innovation_form(sys)
 @test sysi.B ≈ [4.415675759647131
 48.334204475215365]

 sysi = innovation_form(sys, R2 = 2I)
 @test sysi.B ≈ [4.225661436353894
 44.52445850991302]

 sysi = innovation_form(sys, R1 = 2I)
 @test sysi.B ≈ [4.734159731874057
 54.719744515739514]

 sysi = ControlSystems.innovation_form(sys, R1=2I, R2=2I)
 @test sysi.B ≈ [4.415675759647131
 48.334204475215365]

# Test with noise filters
sysw = ss([0.5 0.1; 0 0.5], [0,1], eye_(2), 0, 1)
sysi = ControlSystems.innovation_form(sys, sysw=sysw)
@test sysi.A ≈ sys.A
@test sysi.B ≈ [4.01361818808572
 40.26132476965486]

end


using DynamicProgrammingFibonacci.Dp

@test fibonacci(0) == 0
@test fibonacci(1) == 1
@test fibonacci(2) == 1
@test fibonacci(4) == 3
@test fibonacci(100) == 354224848179261915075
@test fibonacci(140) == 81055900096023504197206408605
@test fibonacci(300) == 222232244629420445529739893461909967206666939096499764990979600


# Functions to parse strings to intervals

"""
    parse{T}(DecoratedInterval{T}, s::AbstractString)

Parse a string of the form `"[a, b]_dec"` as a `DecoratedInterval`
with decoration `dec`.
"""
function parse(::Type{DecoratedInterval{T}}, s::AbstractString) where T
    m = match(r"(\[.*\])(\_.*)?", s)

    if m == nothing  # matched
        throw(ArgumentError("Unable to process string $x as decorated interval"))

    end

    interval_string, decoration_string = m.captures
    interval = parse(Interval{T}, interval_string)

    # type unstable:
    if decoration_string == nothing
        decoration_string = "_com"
    end

    decoration_symbol = Symbol(decoration_string[2:end])
    decoration = getfield(IntervalArithmetic, decoration_symbol)

    return DecoratedInterval(interval, decoration)

end

"""
    parse{T}(Interval{T}, s::AbstractString)

Parse a string as an interval. Formats allowed include:
- "1"
- "[1]"
- "[3.5, 7.2]"
- "[-0x1.3p-1, 2/3]"  # use numerical expressions
"""
function parse(::Type{Interval{T}}, s::AbstractString) where T

    # Check version!
    if !(occursin("[", s))  # string like "3.1"

        m = match(r"(.*)±(.*)", s)
        if m != nothing
            a = parse(T, strip(m.captures[1]))
            b = parse(T, strip(m.captures[2]))

            return a ± b
        end

        a = parse(T, s, RoundDown)
        b = parse(T, s, RoundUp)

        return Interval(a, b)

    end

    # match string of form [a, b]_dec:
    m = match(r"\[(.*),(.*)\]", s)

    if m != nothing  # matched
        lo, hi = m.captures

    else

        m = match(r"\[(.*)\]", s)  # string like "[1]"

        if m == nothing
            throw(ArgumentError("Unable to process string $s as interval"))
        end

        if m.captures[1] == "Empty"
            return emptyinterval(T)
        end

        lo = m.captures[1]
        hi = lo

    end

    expr1 = Meta.parse(lo)
    expr2 = Meta.parse(hi)

    interval = eval(make_interval(T, expr1, [expr2]))

    return interval

end


using AbstractPlotting
using AbstractPlotting.MakieLayout
using GLMakie

using Roots

## Assumes f(a), f(b) are zero
## only 1 or 2 solutions for f(x) = f(c) for any c in [a,b]
f(x) = 1 - x^4
a = -1
b = 1

    descr = """
Adjust the point (c, f(c)) with c > 0 to find the inscribed rectangle with maximal area
"""

function _inscribed_area(c)
    zs = fzeros(u -> f(u) - f(c), a, b)
    length(zs) <= 1 ? 0 : abs(zs[1] - zs[2]) * f(c)
end
D(f) = c -> (f(c + 1e-4) - f(c))/1e-4
function answer()
    h = 1e-4
    zs = fzeros(D(_inscribed_area), 0, b-h)
    a,i = findmax(_inscribed_area.(zs))
    a
end

Answer = answer()

## From http://juliaplots.org/MakieReferenceImages/gallery//edit_polygon/index.html:
function add_move!(scene, points, pplot)
    idx = Ref(0); dragstart = Ref(false); startpos = Base.RefValue(Point2f0(0))
    on(events(scene).mousedrag) do drag
        if ispressed(scene, Mouse.left)
            if drag == Mouse.down
                plot, _idx = mouse_selection(scene)
                if plot == pplot
                    idx[] = _idx; dragstart[] = true
                    startpos[] = to_world(scene, Point2f0(scene.events.mouseposition[]))
                end
            elseif drag == Mouse.pressed && dragstart[] && checkbounds(Bool, points[], idx[])

                if idx[] == 3
                    pos = to_world(scene, Point2f0(scene.events.mouseposition[]))

                    # we work with components not vector
                    x,y = pos
                    c = clamp(x, a, b)
                    zs = fzeros(u -> f(u) - f(c), a , b)

                    if length(zs) == 1
                        c′ = c = first(zs)
                    else
                        c′, c = zs
                    end

                    points[][1] = [c′, 0]
                    points[][2] = [c′, f(c′)]
                    points[][3] = [c, f(c)]
                    points[][4] = [c, 0]
                    points[] = points[]
                end
            end
        else
            dragstart[] = false
        end
        return
    end
end

c = b/2
c′ = first(fzeros(u -> f(u) - f(c), a , b))
area = round(abs(c - c′) * f(c), digits=4)

points = Node(Point2f0[(c′,0), (c′, f(c′)), (c, f(c)), (c, 0)])

# where we lay our scene:
scene, layout = layoutscene()
layout.halign = :left
layout.valign = :top

p = layout[0,0] = LScene(scene)

colsize!(layout, 1, Auto(1))
rowsize!(layout, 1, Auto(1))

label = layout[end, 1] = LText(scene, "Area = $area")
layout[end+1,1] = LText(scene, chomp(descr))

polycolor = lift(points) do pts
    c′ = pts[1][1]
    c = pts[3][1]
    area = round(abs(c - c′)*f(c), digits=4)

    lbl = "Area = $area"
    label.text[] = lbl

    if abs(area - Answer) <= 1e-4
        :green
    else
        :gray75
    end

end

lines!(p.scene, a..b, f, strokecolor=:red, strokewidth=15)
lines!(p.scene, a..b, zero, strokecolor=:black, strokewidth=10)
poly!(p.scene, points, color = polycolor)
scatter!(p.scene, points, color = :white, strokewidth = 10, markersize = 0.05, strokecolor = :black, raw = true)
xlims!(p.scene, (a, b))

add_move!(p.scene, points, p.scene[end])

scene


using LinearSolve, LinearAlgebra, SparseArrays
using Test
import Random

n = 8
A = Matrix(I,n,n)
b = ones(n)
A1 = A/1; b1 = rand(n); x1 = zero(b)
A2 = A/2; b2 = rand(n); x2 = zero(b)

prob1 = LinearProblem(A1, b1; u0=x1)
prob2 = LinearProblem(A2, b2; u0=x2)

cache_kwargs = (;verbose=true, abstol=1e-8, reltol=1e-8, maxiter=30,)

function test_interface(alg, prob1, prob2)
    A1 = prob1.A; b1 = prob1.b; x1 = prob1.u0
    A2 = prob2.A; b2 = prob2.b; x2 = prob2.u0

    y = solve(prob1, alg; cache_kwargs...)
    @test A1 *  y  ≈ b1

    cache = SciMLBase.init(prob1,alg; cache_kwargs...) # initialize cache
    y = solve(cache)
    @test A1 *  y  ≈ b1

    cache = LinearSolve.set_A(cache,copy(A2))
    y = solve(cache)
    @test A2 *  y  ≈ b1

    cache = LinearSolve.set_b(cache,b2)
    y = solve(cache)
    @test A2 *  y  ≈ b2

    return
end

@testset "LinearSolve" begin

@testset "Default Linear Solver" begin
    test_interface(nothing, prob1, prob2)

    A1 = prob1.A; b1 = prob1.b; x1 = prob1.u0
    y = solve(prob1)
    @test A1 *  y  ≈ b1

    _prob = LinearProblem(Diagonal(A1), b1; u0=x1)
    y = solve(_prob)
    @test A1 *  y  ≈ b1

    _prob = LinearProblem(SymTridiagonal(A1), b1; u0=x1)
    y = solve(_prob)
    @test A1 *  y  ≈ b1

    _prob = LinearProblem(Tridiagonal(A1), b1; u0=x1)
    y = solve(_prob)
    @test A1 *  y  ≈ b1

    _prob = LinearProblem(Symmetric(A1), b1; u0=x1)
    y = solve(_prob)
    @test A1 *  y  ≈ b1

    _prob = LinearProblem(Hermitian(A1), b1; u0=x1)
    y = solve(_prob)
    @test A1 *  y  ≈ b1

    _prob = LinearProblem(sparse(A1), b1; u0=x1)
    y = solve(_prob)
    @test A1 *  y  ≈ b1
end

@testset "UMFPACK Factorization" begin
    A1 = A/1; b1 = rand(n); x1 = zero(b)
    A2 = A/2; b2 = rand(n); x2 = zero(b)

    prob1 = LinearProblem(sparse(A1), b1; u0=x1)
    prob2 = LinearProblem(sparse(A2), b2; u0=x2)
    test_interface(UMFPACKFactorization(), prob1, prob2)

    # Test that refactoring wrong throws.
    cache = SciMLBase.init(prob1,UMFPACKFactorization(); cache_kwargs...) # initialize cache
    y = solve(cache)
    cache = LinearSolve.set_A(cache,sprand(n, n, 0.8))
    @test_throws ArgumentError solve(cache)
end

@testset "KLU Factorization" begin
    A1 = A/1; b1 = rand(n); x1 = zero(b)
    A2 = A/2; b2 = rand(n); x2 = zero(b)

    prob1 = LinearProblem(sparse(A1), b1; u0=x1)
    prob2 = LinearProblem(sparse(A2), b2; u0=x2)
    test_interface(KLUFactorization(), prob1, prob2)

    # Test that refactoring wrong throws.
    cache = SciMLBase.init(prob1,KLUFactorization(); cache_kwargs...) # initialize cache
    y = solve(cache)
    X = copy(A1)
    X[8,8] = 0.0
    X[7,8] = 1.0
    cache = LinearSolve.set_A(cache,sparse(X))
    @test_throws ArgumentError solve(cache)
end

@testset "Concrete Factorizations" begin
    for alg in (
                LUFactorization(),
                QRFactorization(),
                SVDFactorization(),
                RFLUFactorization()
               )
        @testset "$alg" begin
            test_interface(alg, prob1, prob2)
        end
    end
end

@testset "Generic Factorizations" begin
    for fact_alg in (
                     lu, lu!,
                     qr, qr!,
                     cholesky,
                     #cholesky!,
    #                ldlt, ldlt!,
                     bunchkaufman, bunchkaufman!,
                     lq, lq!,
                     svd, svd!,
                     LinearAlgebra.factorize,
                    )
        @testset "fact_alg = $fact_alg" begin
            alg = GenericFactorization(fact_alg=fact_alg)
            test_interface(alg, prob1, prob2)
        end
    end
end

@testset "KrylovJL" begin
    kwargs = (;gmres_restart=5,)
    for alg in (
                ("Default",KrylovJL(kwargs...)),
                ("CG",KrylovJL_CG(kwargs...)),
                ("GMRES",KrylovJL_GMRES(kwargs...)),
    #           ("BICGSTAB",KrylovJL_BICGSTAB(kwargs...)),
                ("MINRES",KrylovJL_MINRES(kwargs...)),
               )
        @testset "$(alg[1])" begin
            test_interface(alg[2], prob1, prob2)
        end
    end
end

@testset "IterativeSolversJL" begin
    kwargs = (;gmres_restart=5,)
    for alg in (
                ("Default", IterativeSolversJL(kwargs...)),
                ("CG", IterativeSolversJL_CG(kwargs...)),
                ("GMRES",IterativeSolversJL_GMRES(kwargs...)),
    #           ("BICGSTAB",IterativeSolversJL_BICGSTAB(kwargs...)),
    #            ("MINRES",IterativeSolversJL_MINRES(kwargs...)),
               )
        @testset "$(alg[1])" begin
            test_interface(alg[2], prob1, prob2)
        end
    end
end

@testset "KrylovKit" begin
    kwargs = (;gmres_restart=5,)
    for alg in (
                ("Default", KrylovKitJL(kwargs...)),
                ("CG", KrylovKitJL_CG(kwargs...)),
                ("GMRES",KrylovKitJL_GMRES(kwargs...)),
               )
        @testset "$(alg[1])" begin
            test_interface(alg[2], prob1, prob2)
        end
    end
end

@testset "PardisoJL" begin
    @test_throws UndefVarError alg = PardisoJL()

    using Pardiso, SparseArrays

    A1 = sparse([ 1. 0 -2  3
                 0  5  1  2
                -2  1  4 -7
                 3  2 -7  5 ])
    b1 = rand(4)
    prob1 = LinearProblem(A1, b1)

    lambda = 3
    e = ones(n)
    e2 = ones(n-1)
    A2 = spdiagm(-1 => im*e2, 0 => lambda*e, 1 => -im*e2)
    b2 = rand(n) + im * zeros(n)

    prob2 = LinearProblem(A2, b2)

    for alg in (
                PardisoJL(),
                MKLPardisoFactorize(),
                MKLPardisoIterate(),
               )

        u = solve(prob1, alg; cache_kwargs...).u
        @test A1 * u ≈ b1

        u = solve(prob2, alg; cache_kwargs...).u
        @test eltype(u) <: Complex
        @test_broken A2 * u ≈ b2
    end

end

@testset "Preconditioners" begin
    @testset "Vector Diagonal Preconditioner" begin
        s = rand(n)
        Pl, Pr = Diagonal(s),LinearSolve.InvPreconditioner(Diagonal(s))

        x = rand(n,n)
        y = rand(n,n)

        mul!(y, Pl, x); @test y ≈ s .* x
        mul!(y, Pr, x); @test y ≈ s .\ x

        y .= x; ldiv!(Pl, x); @test x ≈ s .\ y
        y .= x; ldiv!(Pr, x); @test x ≈ s .* y

        ldiv!(y, Pl, x); @test y ≈ s .\ x
        ldiv!(y, Pr, x); @test y ≈ s .* x
    end

    @testset "ComposePreconditioenr" begin
        s1 = rand(n)
        s2 = rand(n)

        x = rand(n,n)
        y = rand(n,n)

        P1 = Diagonal(s1)
        P2 = Diagonal(s2)

        P  = LinearSolve.ComposePreconditioner(P1,P2)

        # ComposePreconditioner
        ldiv!(y, P, x);      @test y ≈ ldiv!(P2, ldiv!(P1, x))
        y .= x; ldiv!(P, x); @test x ≈ ldiv!(P2, ldiv!(P1, y))
    end
end

@testset "Sparse Precaching" begin
    n = 4
    Random.seed!(10)
    A = sprand(n,n,0.8); A2 = 2.0 .* A
    b1 = rand(n); b2 = rand(n)

    prob = LinearProblem(copy(A), copy(b1))
    linsolve = init(prob,UMFPACKFactorization())
    sol11 = solve(linsolve)
    linsolve = LinearSolve.set_b(sol11.cache,copy(b2))
    sol12 = solve(linsolve)
    linsolve = LinearSolve.set_A(sol12.cache,copy(A2))
    sol13 = solve(linsolve)

    prob = LinearProblem(copy(A), copy(b1))
    linsolve = init(prob,KLUFactorization())
    sol21 = solve(linsolve)
    linsolve = LinearSolve.set_b(sol21.cache,copy(b2))
    sol22 = solve(linsolve)
    linsolve = LinearSolve.set_A(sol22.cache,copy(A2))
    sol23 = solve(linsolve)

    linsolve = init(prob,MKLPardisoFactorize())
    sol31 = solve(linsolve)
    linsolve = LinearSolve.set_b(sol31.cache,copy(b2))
    sol32 = solve(linsolve)
    linsolve = LinearSolve.set_A(sol32.cache,copy(A2))
    sol33 = solve(linsolve)

    @test sol11.u ≈ sol21.u
    @test sol11.u ≈ sol31.u
    @test sol12.u ≈ sol22.u
    @test sol12.u ≈ sol32.u
    @test sol13.u ≈ sol23.u
    @test sol13.u ≈ sol33.u
end

@testset "Solve Function" begin

    A1 = rand(n) |> Diagonal; b1 = rand(n); x1 = zero(b1)
    A2 = rand(n) |> Diagonal; b2 = rand(n); x2 = zero(b1)

    function sol_func(A,b,u,p,newA,Pl,Pr,solverdata;verbose=true, kwargs...)
        if verbose == true
            println("out-of-place solve")
        end
        u = A \ b
    end

    function sol_func!(A,b,u,p,newA,Pl,Pr,solverdata;verbose=true, kwargs...)
        if verbose == true
            println("in-place solve")
        end
        ldiv!(u,A,b)
    end

    prob1 = LinearProblem(A1, b1; u0=x1)
    prob2 = LinearProblem(A1, b1; u0=x1)

    for alg in (
                LinearSolveFunction(sol_func),
                LinearSolveFunction(sol_func!),
               )

        test_interface(alg, prob1, prob2)
    end
end

end # testset


"""
```
corners = imcorner(img; [method])
corners = imcorner(img, threshold, percentile; [method])
```

Performs corner detection using one of the following methods -

    1. harris
    2. shi_tomasi
    3. kitchen_rosenfeld

The parameters of the individual methods are described in their documentation. The
maxima values of the resultant responses are taken as corners. If a threshold is
specified, the values of the responses are thresholded to give the corner pixels.
The threshold is assumed to be a percentile value unless `percentile` is set to false.
"""
function imcorner(img::AbstractArray; method::Function = harris, args...)
    img_gray = convert(Array{Gray}, img)
    responses = method(img_gray; args...)
    corners = falses(size(img))
    maxima = map(CartesianIndex{2}, findlocalmaxima(responses))
    for m in maxima corners[m] = true end
    corners
end

function imcorner(img::AbstractArray, threshold, percentile; method::Function = harris, args...)
    img_gray = convert(Array{Gray}, img)
    responses = method(img_gray; args...)

    if percentile == true
        threshold = StatsBase.percentile(responses[:], threshold * 100)
    end

    corners = map(i -> i > threshold, responses)
    corners
end

"""
```
harris_response = harris(img; [k], [border], [weights])
```

Performs Harris corner detection. The covariances can be taken using either a mean
weighted filter or a gamma kernel.
"""
function harris(img::AbstractArray; k::Float64 = 0.04, args...)
    cov_xx, cov_xy, cov_yy = gradcovs(img, args...)
    corner = map((xx, yy, xy) -> xx * yy - xy ^ 2 - k * (xx + yy) ^ 2, cov_xx, cov_yy, cov_xy)
    corner
end

"""
```
shi_tomasi_response = shi_tomasi(img; [border], [weights])
```

Performs Shi Tomasi corner detection. The covariances can be taken using either a mean
weighted filter or a gamma kernel.
"""
function shi_tomasi(img::AbstractArray; border::AbstractString = "replicate", args...)
    cov_xx, cov_xy, cov_yy = gradcovs(img, border; args...)
    corner = map((xx, yy, xy) -> ((xx + yy) - (sqrt((xx - yy) ^ 2 + 4 * xy ^ 2))) / 2, cov_xx, cov_yy, cov_xy)
    corner
end

"""
```
kitchen_rosenfeld_response = kitchen_rosenfeld(img; [border])
```

Performs Kitchen Rosenfeld corner detection. The covariances can be taken using either a mean
weighted filter or a gamma kernel.
"""
function kitchen_rosenfeld(img::AbstractArray; border::AbstractString = "replicate")
    (grad_x, grad_y) = imgradients(img, "sobel", border)
    (grad_xx, grad_xy) = imgradients(grad_x, "sobel", border)
    (grad_yx, grad_yy) = imgradients(grad_y, "sobel", border)
    numerator = map((x, y, xx, xy, yy) -> xx * (y ^ 2) + yy * (x ^ 2) - 2 * xy * x * y, grad_x, grad_y, grad_xx, grad_xy, grad_yy)
    denominator = map((x, y) -> x ^ 2 + y ^ 2, grad_x, grad_y)
    corner = map((n, d) -> d == 0.0 ? 0.0 : -n / d, numerator, denominator)
    corner
end

"""
```
corners = fastcorners(img, n, threshold)
```

Performs FAST Corner Detection. `n` is the number of contiguous pixels
which need to be greater (lesser) than intensity + threshold (intensity - threshold)
for a pixel to be marked as a corner. The default value for n is 12.
"""
function fastcorners{T}(img::AbstractArray{T}, n::Int = 12, threshold::Float64 = 0.15)
    img_padded = padarray(img, [3,3], [3,3], "value", 0)
    corner = falses(size(img))
    h, w = size(img)
    R = CartesianRange(CartesianIndex((4, 4)), CartesianIndex((h + 3, w + 3)))
    idx = map(CartesianIndex{2}, [(0, 3), (1, 3), (2, 2), (3, 1), (3, 0), (3, -1), (2, -2), (1, -3),
            (0, -3), (-1, -3), (-2, -2), (-3, -1), (-3, 0), (-3, 1), (-2, 2), (-1, 3)])

    idxidx = [1, 5, 9, 13]
    for I in R
        bright_threshold = img_padded[I] + threshold
        dark_threshold = img_padded[I] - threshold
        if n >= 12
            sum_bright = 0
            sum_dark = 0
            for k in idxidx
                pixel = img_padded[I + idx[k]]
                if pixel > bright_threshold
                    sum_bright += 1
                elseif pixel < dark_threshold
                    sum_dark += 1
                end
            end
            if sum_bright < 3 && sum_dark < 3
                continue
            end
        end
        consecutive_bright = 0
        consecutive_dark = 0

        for i in 1:15 + n
            k = mod1(i, 16)
            pixel = img_padded[I + idx[k]]
            if pixel > bright_threshold
                consecutive_dark = 0
                consecutive_bright += 1
            elseif pixel < dark_threshold
                consecutive_bright = 0
                consecutive_dark += 1
            end

            if consecutive_dark == n || consecutive_bright == n
                corner[I - CartesianIndex((3, 3))] = true
                break
            end
        end
    end
    corner
end

function gradcovs(img::AbstractArray, border::AbstractString = "replicate"; weights::Function = meancovs, args...)
    (grad_x, grad_y) = imgradients(img, "sobel", border)

    cov_xx = grad_x .* grad_x
    cov_xy = grad_x .* grad_y
    cov_yy = grad_y .* grad_y

    weights(cov_xx, cov_xy, cov_yy, args...)
end

function meancovs(cov_xx, cov_xy, cov_yy, blockSize::Int = 3)

    box_filter_kernel = (1 / (blockSize * blockSize)) * ones(blockSize, blockSize)

    filt_cov_xx = imfilter(cov_xx, box_filter_kernel)
    filt_cov_xy = imfilter(cov_xy, box_filter_kernel)
    filt_cov_yy = imfilter(cov_yy, box_filter_kernel)

    filt_cov_xx, filt_cov_xy, filt_cov_yy
end

function gammacovs(cov_xx, cov_xy, cov_yy, gamma::Float64 = 1.4)

    filt_cov_xx = imfilter_gaussian(cov_xx, [gamma, gamma])
    filt_cov_xy = imfilter_gaussian(cov_xy, [gamma, gamma])
    filt_cov_yy = imfilter_gaussian(cov_yy, [gamma, gamma])

    filt_cov_xx, filt_cov_xy, filt_cov_yy
end


function quadratic1(a, b, c)
    t(s) = (-b + s*sqrt(Δ))/(2a)
    a == 0 && error("a must be different than zero")
    Δ = Complex(b^2-4*a*c)
    t(1), t(-1)
end

function quadratic2(a, b, c)
    Δ = Complex(b^2-4*a*c)
    t(s) = (-b + s*sqrt(Δ))/(2a)
    a == 0 && error("a must be different than zero")
    t(1), t(-1)
end

Chapter06/08. Ensuring type stability of your code/quad.jl


"""
Calculates vertical functional principal component analysis on aligned data

    vert_fPCA(fn, timet, qn; no=1)
    :param fn: array of shape (M,N) of N aligned functions with M samples
    :param timet: vector of size M describing the sample points
    :param qn: array of shape (M,N) of N aligned SRSF with M samples
    :param no: number of components to extract (default = 1)

    Returns Dict containing
    :return q_pca: srsf principal directions
    :return f_pca: functional principal directions
    :return latent: latent values
    :return coef: scores
    :return U: eigenvectors
"""
function vert_fPCA(fn, timet, qn; no=1)
    coef = collect(-2:3);
    Nstd = length(coef);

    # fPCA
    mq_new = mean(qn, 2);
    N = length(mq_new);
    mididx = round(Integer, length(timet)/2);
    m_new = sign(fn[mididx, :]) .* sqrt(abs(fn[mididx, :]));
    mqn = [mq_new; mean(m_new)];
    qn2 = vcat(qn, m_new');
    K = Base.covm(qn2, mean(qn2,2), 2);

    U, s, V = svd(K);
    stdS = sqrt(s);

    # compute the PCA in the q domain
    q_pca = Array(Float64, N+1, Nstd, no);
    for k in 1:no
        for l in 1:Nstd
            q_pca[:, l, k] = mqn + coef[l] * stdS[k] * U[:, k];
        end
    end

    # compute the correspondence in the f domain
    f_pca = Array(Float64, N, Nstd, no);
    for k in 1:no
        for l in 1:Nstd
            f_pca[:, l, k] = cumtrapzmid(timet, q_pca[1:N,l,k].*
                                         abs(q_pca[1:N,l,k]),
                                         sign(q_pca[N+1,l,k]*q_pca[N,l,k]^2));
        end
    end

    N2 = size(qn,2);
    c = zeros(N2, no);
    for k in 1:no
        for l in 1:N2
            c[l,k] = sum(vec((qn2[:,l] - mqn).* U[:,k]));
        end
    end

    out = Dict("q_pca" => q_pca, "f_pca" => f_pca, "latent" => s,
               "coef" => c, "U" => U);
    return out
end

"""
Calculates horizontal functional principal component analysis on aligned data

    horiz_fPCA(gam, timet; no=1)
    :param gam: array of shape (M,N) of N warping functions with M samples
    :param timet: vector of size M describing the sample points
    :param no: number of components to extract (default = 1)

    Returns Dict containing
    :return gam_pca: warping principal directions
    :return psi_pca: srsf functional principal directions
    :return latent: latent values
    :return U: eigenvectors
    :return coef: scores
    :return gam_mu: mean warping function
    :return vec1: shooting vectors
"""
function horiz_fPCA(gam, timet; no=1)
    mu, gam_mu, psi, vec1 = sqrt_mean(gam);
    tau = collect(1:6);
    TT = length(timet);
    n = length(tau);
    m = length(mu);

    # TFPCA
    K = Base.covm(vec1, mean(vec1,2), 2);

    U, s, V = svd(K);
    vm = mean(vec1, 2);

    gam_pca = Array(Float64, n, m+1, no);
    psi_pca = Array(Float64, n, m, no);
    for j in 1:no
        for k in tau
            v = (k-3)*sqrt(s[j])*U[:,j];
            vn = norm(v) / sqrt(TT);
            if vn < 0.0001
                psi_pca[k, :, j] = mu;
            else
                psi_pca[k, :, j] = cos(vn).*mu + sin(vn).*v/vn;
            end

            tmp = zeros(TT);
            tmp[2:TT] = cumsum(psi_pca[k,:,j] .* psi_pca[k,:,j],2);
            gam_pca[k,:,j] = (tmp - tmp[1]) ./ (tmp[end] - tmp[1]);
        end
    end

    N2 = size(gam,2);
    c = zeros(N2, no);
    for k in 1:no
        for l in 1:N2
            c[l,k] = sum((vec1[:,l]-vm).* U[:,k]);
        end
    end

    out = Dict("gam_pca" => gam_pca, "psi_pca" => psi_pca, "latent" => s,
               "U" => U, "coef" => c, "gam_mu" => gam_mu, "vec1" => vec1);
    return out
end


function TriMesh_PromoteL2Q(p,eConn)
#=
  TriMesh_PromoteL2Q
   Interpolate values at the midside nodes from the vertices in a
   triangular mesh.  Used to promote a linear to a quadratic representation
   of a finite element function.

  Author: Jeff Borggaard, Virginia Tech
          part of FEMfunctions.jl

  Licensing:
     This code is distributed under the MIT license.
=#

  nElements = size(eConn,1)

  for nEl=1:nElements
    localNodes = eConn[nEl,:]
    p[ localNodes[4] ] = ( p[localNodes[1]] + p[localNodes[2]] )/2
    p[ localNodes[5] ] = ( p[localNodes[2]] + p[localNodes[3]] )/2
    p[ localNodes[6] ] = ( p[localNodes[3]] + p[localNodes[1]] )/2
  end

  return p
end


using OMEinsum
using LinearAlgebra

function boundary_vector(A, O)
    @assert size(A, 2)^4 == size(O, 1)
    X = kron(A, A, A, A)
    return vec(X * O * X')
end

A = rand(3, 2)
O = rand(2^4, 2^4)
V = ein"ab,cd,ef,gh,ij,kl,mn,op,jlnpbdfh->acegikmo"(A,A,A,A,A,A,A,A,reshape(O, ntuple(_->2, 8)));
dot(V, V)

st = vec(transpose(A) * A)
psi = kron(st, st, st, st)
psi = vec(permutedims(reshape(psi, ntuple(_->2, 8)), (1,3,5,7,2,4,6,8)))
psi' * kron(O, O) * psi

X = kron(A, A, A, A)
V = X * O * X'
dot(V, V)
(X * O * transpose(X))[]
st
dot(A[:, 1], A[:, 1])
dot(A[:, 1], A[:, 2])
dot(A[:, 2], A[:, 1])
dot(A[:, 2], A[:, 2])


using Test
using Unitful

@testset "Test thermodynamic functions" begin
#Test NASA
@testset "Test thermodyamics from NASA polynomial" begin
nasap1 = NASApolynomial(coefs=[4.03055,-0.00214171,4.90611e-05,-5.99027e-08,2.38945e-11,-11257.6,3.5613],Tmin=300.0,Tmax=650.73)
nasap2 = NASApolynomial(coefs=[-0.307954,0.0245269,-1.2413e-05,3.07724e-09,-3.01467e-13,-10693,22.628],Tmin=650.73,Tmax=3000.0)
nasa = NASA(polys=[nasap1,nasap2])
Tlist= 400.0:200:2000.0

#test Cp
@testset "Test get heat capacity function" begin
Cpexplist = [7.80157, 10.5653, 12.8213, 14.5817, 15.9420, 16.9861, 17.78645, 18.4041, 18.8883] * R
Cps = [getHeatCapacity(nasa,T) for T in Tlist]
@test Cps ≈ Cpexplist rtol=1e-3
end;

#test S
@testset "Test get entropy function" begin
Sexplist = [29.6534, 33.3516, 36.7131, 39.7715, 42.5557, 45.0952, 47.4179, 49.5501, 51.5152]*R
Ss = [getEntropy(nasa,T) for T in Tlist]
@test Ss ≈ Sexplist rtol=1e-3
end;

#test H
@testset "Test get enthalpy function" begin
Hexplist = [-22.7613, -12.1027, -6.14236, -2.16615, 0.743456, 2.99256, 4.79397, 6.27334, 7.51156].*Tlist*R
Hs = [getEnthalpy(nasa,T) for T in Tlist]
@test Hs ≈ Hexplist rtol=1e-3
end;
end;

#test Wilhoit
@testset "Test thermodynamics from Wilhoit" begin
w = Wilhoit(Cp0=4.0*R,Cpinf=21.5*R,coefs=[0.0977518,-16.3067,26.2524,-12.6785],B=1068.68,H0=-94088.0*R,S0=-118.46*R)
Tlist = 200.0:200:2000.0

#test Cp
@testset "Test get heat capacity function" begin
Cpexplist = [5.12003, 7.80327, 10.5528, 12.8323, 14.6013, 15.9511, 16.9842, 17.7837, 18.4114, 18.9117]*R
Cps = [getHeatCapacity(w,T) for T in Tlist]
@test Cps ≈ Cpexplist rtol=1e-3
end;

#test S
@testset "Test get entropy function" begin
Sexplist = [25.3095, 29.6445, 33.3398, 36.7006, 39.7629, 42.5499, 45.0898, 47.4122, 49.5445, 51.5112]*R
Ss = [getEntropy(w,T) for T in Tlist]
@test Ss ≈ Sexplist rtol=1e-3
end;

#test H
@testset "Test get enthalpy function" begin
Hexplist = [-51.9303, -22.7609, -12.1050, -6.14444, -2.16433, 0.747500, 2.99646, 4.79698, 6.27618, 7.51564].*Tlist*R
Hs = [getEnthalpy(w,T) for T in Tlist]
@test Hs ≈ Hexplist rtol=1e-3
end;
end;
end;


using LinearAlgebra
using SparseArrays

"""
Compute the thermal state `exp(-βH) / Z`, with `Z` the partition function.
"""
function thermal_state(βH::AbstractMatrix)
    σ = exp(-Matrix(βH))
    return σ / tr(σ)
end


@testset "$(rpad("Gauss Tableaus",80))" begin

    function _getTableauGLRK1(T=Float64)
        a = reshape([1//2], (1,1))
        b = [1//1]
        c = [1//2]
        o = 2

        Tableau{T}(:Gauss1, o, a, b, c)
    end

    function _getTableauGLRK2(T=Float64)
        a = [
                [1/4       1/4-√3/6]
                [1/4+√3/6  1/4     ]
            ]
        b = [1/2,      1/2     ]
        c = [1/2-√3/6, 1/2+√3/6]
        o = 4

        Tableau{T}(:Gauss2, o, a, b, c)
    end

    function _getTableauGLRK3(T=Float64)
        a = [
                [5/36         2/9-√15/15  5/36-√15/30]
                [5/36+√15/24  2/9         5/36-√15/24]
                [5/36+√15/30  2/9+√15/15  5/36       ]
            ]
        b = [5/18,        4/9,        5/18       ]
        c = [1/2-√15/10,  1/2,        1/2+√15/10 ]
        o = 6

        Tableau{T}(:Gauss3, o, a, b, c)
    end

    function _getTableauGLRK4(T=Float64)
        a = reshape([
                [(-√30/144 + 1/8)
                 (-13*√42/336 - √10/16 - √3/12 + √(-60*√30 + 450)/144 + √35/28 + √(-2*√30 + 15)/8)/√(-2*√30 + 15)
                 (-√35/28 - √10/16 - √3/12 + √(-60*√30 + 450)/144 + 13*√42/336 + √(-2*√30 + 15)/8)/√(-2*√30 + 15)
                 (-√35/14 - √(60*√30 + 450)/144 - √42/168 + √(2*√30 + 15)/8)/√(2*√30 + 15)];
                [(-√(60*√30 + 450)/144 - √3/12 + √10/16 + √35/28 + 13*√42/336 + √(2*√30 + 15)/8)/√(2*√30 + 15)
                 (√30/144 + 1/8)
                 (-√35/14 + √42/168 + √(-60*√30 + 450)/144 + √(-2*√30 + 15)/8)/√(-2*√30 + 15)
                 (-13*√42/336 - √35/28 - √(60*√30 + 450)/144 - √3/12 + √10/16 + √(2*√30 + 15)/8)/√(2*√30 + 15)];
                [(-√10/16 - √(60*√30 + 450)/144 + √3/12 + √35/28 + 13*√42/336 + √(2*√30 + 15)/8)/√(2*√30 + 15)
                 (-√42/168 + √(-60*√30 + 450)/144 + √(-2*√30 + 15)/8 + √35/14)/√(-2*√30 + 15)
                 (√30/144 + 1/8)
                 (-13*√42/336 - √35/28 - √10/16 - √(60*√30 + 450)/144 + √3/12 + √(2*√30 + 15)/8)/√(2*√30 + 15)];
                [(-√(60*√30 + 450)/144 + √42/168 + √35/14 + √(2*√30 + 15)/8)/√(2*√30 + 15)
                 (-13*√42/336 + √(-60*√30 + 450)/144 + √3/12 + √10/16 + √35/28 + √(-2*√30 + 15)/8)/√(-2*√30 + 15)
                 (-√35/28 + √(-60*√30 + 450)/144 + √3/12 + √10/16 + 13*√42/336 + √(-2*√30 + 15)/8)/√(-2*√30 + 15)
                 (-√30/144 + 1/8)]
                ], (4,4))'

        b = [
                49/(216+12*√30),
                49/(216-12*√30),
                49/(216-12*√30),
                49/(216+12*√30)
            ]

        c = [
                1/2-√(3/7+2*√30/35)/2,
                1/2-√(3/7-2*√30/35)/2,
                1/2+√(3/7-2*√30/35)/2,
                1/2+√(3/7+2*√30/35)/2
            ]

        o = 8

        Tableau{T}(:Gauss4, o, a, b, c)
    end

    function _getTableauGLRK5(T=Float64)
        a = reshape([
                [-13*√70/3600 + 161/1800
                 -59*√(20*√70 + 350)/19440 - 113*√(14*√70 + 245)/34020 - 11*√(-20*√70 + 350)/6480 + 4*√(-14*√70 + 245)/2835 + 13*√70/3600 + 161/1800
                 -92*√(14*√70 + 245)/8505 + 4*√(20*√70 + 350)/1215 + 32/225
                 -59*√(20*√70 + 350)/19440 - 113*√(14*√70 + 245)/34020 - 4*√(-14*√70 + 245)/2835 + 11*√(-20*√70 + 350)/6480 + 13*√70/3600 + 161/1800
                 -2*√(14*√70 + 245)/315 - 13*√70/3600 + √(20*√70 + 350)/360 + 161/1800];
                [-113*√(-14*√70 + 245)/34020 - 13*√70/3600 + 4*√(14*√70 + 245)/2835 + 11*√(20*√70 + 350)/6480 + 59*√(-20*√70 + 350)/19440 + 161/1800
                 13*√70/3600 + 161/1800
                 -92*√(-14*√70 + 245)/8505 - 4*√(-20*√70 + 350)/1215 + 32/225
                 -2*√(-14*√70 + 245)/315 - √(-20*√70 + 350)/360 + 13*√70/3600 + 161/1800
                 -11*√(20*√70 + 350)/6480 - 113*√(-14*√70 + 245)/34020 - 13*√70/3600 - 4*√(14*√70 + 245)/2835 + 59*√(-20*√70 + 350)/19440 + 161/1800];
                [-23*√(20*√70 + 350)/5760 - 13*√70/3600 + 161/1800 + 11*√(14*√70 + 245)/1440
                 13*√70/3600 + 23*√(-20*√70 + 350)/5760 + 11*√(-14*√70 + 245)/1440 + 161/1800
                 32/225
                 -11*√(-14*√70 + 245)/1440 - 23*√(-20*√70 + 350)/5760 + 13*√70/3600 + 161/1800
                 -11*√(14*√70 + 245)/1440 - 13*√70/3600 + 161/1800 + 23*√(20*√70 + 350)/5760];
                [-59*√(-20*√70 + 350)/19440 - 13*√70/3600 + 4*√(14*√70 + 245)/2835 + 113*√(-14*√70 + 245)/34020 + 11*√(20*√70 + 350)/6480 + 161/1800
                 13*√70/3600 + √(-20*√70 + 350)/360 + 2*√(-14*√70 + 245)/315 + 161/1800
                 4*√(-20*√70 + 350)/1215 + 92*√(-14*√70 + 245)/8505 + 32/225
                 13*√70/3600 + 161/1800
                 -59*√(-20*√70 + 350)/19440 - 11*√(20*√70 + 350)/6480 - 13*√70/3600 - 4*√(14*√70 + 245)/2835 + 113*√(-14*√70 + 245)/34020 + 161/1800];
                [-√(20*√70 + 350)/360 - 13*√70/3600 + 161/1800 + 2*√(14*√70 + 245)/315
                 -11*√(-20*√70 + 350)/6480 + 4*√(-14*√70 + 245)/2835 + 13*√70/3600 + 113*√(14*√70 + 245)/34020 + 59*√(20*√70 + 350)/19440 + 161/1800
                 -4*√(20*√70 + 350)/1215 + 32/225 + 92*√(14*√70 + 245)/8505
                 -4*√(-14*√70 + 245)/2835 + 11*√(-20*√70 + 350)/6480 + 13*√70/3600 + 113*√(14*√70 + 245)/34020 + 59*√(20*√70 + 350)/19440 + 161/1800
                 -13*√70/3600 + 161/1800]
            ], (5,5))'

        b = [
                1/(4600/729+1300*√70/5103),
                1/(4600/729-1300*√70/5103),
                64/225,
                1/(4600/729-1300*√70/5103),
                1/(4600/729+1300*√70/5103)
            ]

        c = [
                1/2-√(5/9+2*√70/63)/2,
                1/2-√(5/9-2*√70/63)/2,
                1/2,
                1/2+√(5/9-2*√70/63)/2,
                1/2+√(5/9+2*√70/63)/2
            ]

        o = 10

        Tableau{T}(:Gauss5, o, a, b, c)
    end

    function _getTableauGLRK6(T=Float64)
        a = reshape([
                [+0.0428311230947925862600740355431832233817056253710109955996588599497364402,
                 -0.0147637259971974124753725910605206514420001878003408840631515394063317448,
                 +0.00932505070647775119143888450800314858828816322281640330116548464376180630,
                 -0.00566885804948351190092125641621650656214351346713436514621339322133564885,
                 +0.00285443331509933513092928583011602153367099528247654439080898011903375405,
                 -0.000812780171264762112299135651562540066903951464973606518543200348171357290];
                [+0.0926734914303788631865122917633203161433485534502333679517368248345204102,
                 +0.0901903932620346518924583784594290279153804731866863705724348100592850095,
                 -0.0203001022932395859524940805242724601067259796418104502230488160090068057,
                 +0.0103631562402464237307199458065599778725040265335003155034035217850387283,
                 -0.00488719292803767146341420376578964407137592740187386736001668777722125914,
                 +0.00135556105548506177551787075080010874364457167567922814822699418129216854];
                [+0.0822479226128438738077716511411289215554396236655129640997987178532804788,
                 +0.196032162333245006055759781563801382788760315019545751706035725357898777,
                 +0.116978483643172761847467585997387748702913901442302633827906329990978550,
                 -0.0204825277456560976298590118654006438219872235078237396303198827234514190,
                 +0.00798999189966233579720442148030827079362758194981571971201211474213850758,
                 -0.00207562578486633419359528915758164772805951363936400480613058750798236207];
                [+0.0877378719744515067137433602439480944914707643813859960054483074074552416,
                 +0.172390794624406967987712335438549785037133364423557021432857505376431512,
                 +0.254439495032001621324794183860176141227815026392429007286132542705408521,
                 +0.116978483643172761847467585997387748702913901442302633827906329990978551,
                 -0.0156513758091757022708430246449433269579993686461730105611661052393287579,
                 +0.00341432357674129871237641994523752520797162707650902709951900204619240137];
                [+0.0843066851341001107446302003355663380197666790663427630510907257181807085,
                 +0.185267979452106975248330960684647699902136873775246608504886307895791278,
                 +0.223593811046099099964215226188215519533323776351104952152409138196918370,
                 +0.254257069579585109647429252519047957512553782526415717878861475990963909,
                 +0.0901903932620346518924583784594290279153804731866863705724348100592850092,
                 -0.00701124524079369066636422067695386937993730270821137675241910493504753033];
                [+0.0864750263608499346324472067379289868303152022069955977178609202476442335,
                 +0.177526353208969968653987471088742034297089951090896196754060639999536273,
                 +0.239625825335829035595856428410992003967971316351739632802026053203292742,
                 +0.224631916579867772503496287486772348817539639661788864354647175338195299,
                 +0.195144512521266716260289347979378707272761134173713625208021159524901768,
                 +0.0428311230947925862600740355431832233817056253710109955996588599497364397]
            ], (6,6))'

        b = [
                0.0856622461895851725201480710863664467634112507420219911993177198994728803,
                0.180380786524069303784916756918858055830760946373372741144869620118570019,
                0.233956967286345523694935171994775497405827802884605267655812659981957101,
                0.233956967286345523694935171994775497405827802884605267655812659981957101,
                0.180380786524069303784916756918858055830760946373372741144869620118570019,
                0.0856622461895851725201480710863664467634112507420219911993177198994728804
            ]

        c = [
                0.0337652428984239860938492227530026954326171311438550875637251917366932495891,
                0.169395306766867743169300202490047326496775717802414964592736647073908251704,
                0.380690406958401545684749139159644032290694684929989324909302417712862532843,
                0.619309593041598454315250860840355967709305315070010675090697582287137467157,
                0.830604693233132256830699797509952673503224282197585035407263352926091748261,
                0.966234757101576013906150777246997304567382868856144912436274808263306750376
            ]

        o = 12

        Tableau{T}(:Gauss6, o, a, b, c)
    end

    @test TableauGauss(1) ≈ _getTableauGLRK1()
    @test TableauGauss(2) ≈ _getTableauGLRK2()
    @test TableauGauss(3) ≈ _getTableauGLRK3()
    @test TableauGauss(4) ≈ _getTableauGLRK4()
    @test TableauGauss(5) ≈ _getTableauGLRK5()
    @test TableauGauss(6) ≈ _getTableauGLRK6()

end


# ------------------------------------------------------------------------------------------
# ## Learning with a single neuron
#
# In this notebook, we'll build a neuron that classifies an image as an apple or as a banana
# using multiple features from the image. We'll **train** our neuron using data from many
# images that have already been correctly categorised; our neuron will thereby **learn**
# what parameters to use, by minimizing a loss function using gradient descent.
#
# We'll do this with almost the simplest possible neuron, namely one that takes just two
# inputs:
# ------------------------------------------------------------------------------------------

include("draw_neural_net.jl")
number_inputs, number_neurons = 2, 1
draw_network([number_inputs, number_neurons])

# ------------------------------------------------------------------------------------------
# To do this, we need to work with and *clean* some real data. Let's get started!
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# ## Loading in some data
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# Let's load in some real data! We'll use data that we have prepared from photos of apples
# and bananas; it turns out to be stored on disk in data files as "tab-separated values". We
# can read this data in with the `CSV.jl` package, as follows.
# ------------------------------------------------------------------------------------------

;head data/Apple_Golden_1.dat

;head data/bananas.dat

using CSV
using TextParse

applecols, applecolnames = TextParse.csvread("data/Apple_Golden_1.dat", '\t')
bananacols, bananacolnames = TextParse.csvread("data/bananas.dat", '\t');

# ------------------------------------------------------------------------------------------
# Next, we want to use `DataFrames` to store the information from our CSV files.
# ------------------------------------------------------------------------------------------

using DataFrames

# ------------------------------------------------------------------------------------------
# One way we can create a `DataFrame` is to pass a dictionary that contain arrays as values
# and descriptive names for those arrays as keys to the `DataFrame` function:
# ------------------------------------------------------------------------------------------

apples =  DataFrame(Dict(strip(name) => col for (name, col) in zip(applecolnames, applecols)))
bananas = DataFrame(Dict(strip(name) => col for (name, col) in zip(bananacolnames, bananacols)))

# ------------------------------------------------------------------------------------------
# Above, we used a "dictionary comprehension" to create each `DataFrame`. Here is some code
# to create a dictionary, `appledict`, via a dictionary comprehension:
#
# ```julia
# appledict = Dict(strip(name)=>col for (name, col) in zip(applecolnames, applecols))
# ```
# For now, don't worry about the exact syntax used here. Just note that the dictionary
# created associates names with arrays, and that we've used a dictionary like this to create
# a `DataFrame` with named columns!
#
# So for now, each of the two data sets is stored in a `DataFrame` (from the `DataFrames.jl`
# package).
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# ### Roadmap
#
# To use a neuron with two inputs, we will use just two of the data (numbers) for each
# image, say columns 3 and 4, the mean amount of red and the mean amount of green; each data
# point will then be a 2-dimensional vector, and the data points lie on a two-dimensional
# plane. We will have many input data, labelled with an index $i$. We will denote the $i$th
# data point as  $\mathbf{x}^{(i)}$.
#
# The goal is that our neuron will take a single point on the two-dimensional plane as its
# input, and should return a single output that **classifies** it as an apple ($0$) or a
# banana ($1$).
#
# To do so, it must "**learn**" the correct values of its parameters $\mathbf{w}$ and $b$.
# For this learning to take place, we'll need **labels** for each of the input data, which
# identify them as either an apple (0) or as a banana (1).
#
# These labels will, in turn, allow us to create a loss function, which will allow our
# algorithm to learn to determine if a given choice of parameters does a good or a poor job
# of classifying our fruit images.
#
# **So what do we have left to do?**
#
# The above might sound complex, but luckily we can break it down into a series of
# actionable steps:
#
# 1. Clean our input data (amounts of red and green) to get it into a usable format;
# 2. Create a sequence of labels that we can use to identify correct and incorrect
# classifications;
# 3. Define a loss function that contains parameters;
# 4. Implement an algorithm to pick parameters for our neuron by minimizing the loss
# function with respect to the parameters;
# 5. Use all of the above to train our neuron how to classify images as apples or bananas!
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# #### Note:
#
# Note that *in general we cannot expect that a single neuron will be adequate for
# classification.*
#
# If a single neuron struggles to classify our images, we may need to use a more complicated
# neural network structure (which corresponds to using a more complicated function).
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# ## Cleaning the data
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# Usually it will be necessary to "clean" the data in some way, i.e. pre-process it, before
# it can be used for whichever task you are interested in.
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# Our next *meta*-exercise will be to collect all the data from columns 3 and 4 into a
# *single* Julia vector `x` (of which each entry is itself a vector), and the labels into a
# single vector `y`. Let's do this in a series of steps!
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# #### Exercise 1
#
# First, let's practice grabbing a single column of a `DataFrame`. To grab a column, you can
# index into the `DataFrame` with the name of the column you want, passed as a symbol. In
# Julia, symbols are names preceded by a `:`. For example, we could grab the "height" column
# from `apples` by indexing into `apples` with the symbol, `:height`:
#
# ```julia
# apples[:height]
# ```
#
# Index into `apples` to get the "red" column. What is the type of the object returned? How
# many entries does it have?
#
# A) Array, 5

# B) DataArray, 5

# C) Array, 64

# D) DataArray, 64

# E) Array, 492

# F) DataArray, 492
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# #### Exercise 2
#
# We can grab an individual entry of a `DataFrame` by specifying the row index of the entry
# and the column symbol. For example, to access the height of the 4th image of an apple, we
# would execute
#
# ```julia
# apples[4, :height]
# ```
#
# How much red is there in the 63rd image of a banana?
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# #### Exercise 3
#
# We want to reorganize data from the 3rd and 4th columns of `apples` and `bananas` to put
# that data in a single array. Let's start by organizing the data from the 3rd and 4th
# columns of `apples` into a single array, `x_apples`. Create `x_apples` such that there is
# a single element in `x_apples` for each image in `apples`. The $i^\mathrm{th}$ element in
# `x_apples` should be a `Vector`, i.e. a 1D `Array`, with two elements - the amount of red
# and the amount of blue in the $i^\mathrm{th}$ image from `apples`.
#
# Similarly create the `Array` `x_bananas` using data from `bananas`.
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# #### Exercise 4
#
# Next we want to combine the elements of `x_apples` and `x_bananas` into a single array,
# `xs`. `xs` should contain, first, all the elements of `x_apples`, and then all the
# elements of `x_bananas`. Use the `vcat` function to create `xs`.
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# #### Exercise 5
#
# If you've gotten this far, our data is in the format we want for learning. Now we need
# labels! We want to store a label (either a `0` or a `1` for every apple or banana image in
# our data set in an array called `ys`. Recall that "0" refers to an apple and "1" refers to
# a banana.
#
# Create an array `ys` where the $i^\mathrm{th}$ element of `ys` is a `0` if the
# $i^\mathrm{th}$ element of `xs` is an apple, and where the $i^\mathrm{th}$ element of `ys`
# is a `1` if the $i^\mathrm{th}$ element of `xs` is a banana.
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# #### Exercise 6
#
# Add data points for all apple and banana images in our data sets to a plot using
# `scatter`. Plot data points for apples in one color and use a different color for banana
# data points.
#
# Hint: You may want to use the `first` and `last` functions.
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# ## "Learning" by hand
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# Intuitively, looking at the plot of the data, we see that it should be "easy" to find a
# function that separates the data into bananas on one side and apples on the other: we just
# need to draw a straight line that divides the two clouds of data. We can do this "by
# hand", as follows.
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# In the code below, the neuron will learn a function of the form $\sigma(\mathbf{w} \cdot
# \mathbf{x} + b)$. Since $\sigma$ looks like a smooth version of a step function, we can
# think of $\sigma$ classifying based on whether the value of its output argument is less
# than `0.5` or greater than `0.5`.
#
# **Game**: Use the interactive visualization to find suitable values of $\mathbf{w}$ and
# $b$ such that the hyperplane $\sigma(w_1 x_1 + w_2 x_2 + b) = 0.5$ divides the data. This
# is the same as the hyperplane for which $w_1 x_1 + w_2 x_2 + b = 0$ ! (Note that there are
# many such values!)
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# We can solve for $x_2$ via
#
# $$x_2 = -(w_1 x_1 + b) / w_2,$$
#
# and use this to draw the corresponding hyperplane.
# ------------------------------------------------------------------------------------------

using Interact

@manipulate for w1 in -2:0.01:3, w2 in -2:0.01:3, b in -2:0.01:3

    scatter(first.(x_apples), last.(x_apples), m=:cross, label="apples")
    scatter!(first.(x_bananas), last.(x_bananas), label="bananas")

    ylims!(0.3, 0.66)
    xlims!(0.45, 0.75)

    plot!(x -> -(w1*x + b) / w2)
end

# ------------------------------------------------------------------------------------------
# ## How can the neuron *learn* to classify the data?
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# We are now ready for our first experience of **machine learning**: we will let the neuron
# learn automatically by processing data and tuning model parameters accordingly (the
# process we call "learning")!
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# For given values of the parameters $w_1$, $w_2$ and $b$, the function $f_{\mathbf{w}, b}$
# maps a vector of length $2$ to a number between $0$ and $1$ (due to the definition of
# $\sigma$). Now we want to have a neuron *learn* suitable values of these parameters.
#
# We want to discover (learn!) the parameters such that $f$ models the relationship between
# the data we explored above about the fruit images, and outputs a classification of the
# fruit: $0$ if it corresponds to an apple, and $1$ if it is a banana.
#
# So the neuron's input will be a vector of 2 pieces of information about an image; let's
# call the data about the $i$th image $\mathbf{x}^{(i)}$.
# We also are given the label that says which type of fruit it is: $0$ for an apple, and $1$
# for a banana; let's call this *desired* output number $y^{(i)}$. When we feed in the $i$th
# data, $\mathbf{x}^{(i)}$, we want the neuron to give an output that is *as close as
# possible* to the desired output $y^{(i)}$; i.e. it should **minimize** the mean-squared
# distance
#
# $$L_i = [f_{\mathbf{w}, b}(\mathbf{x}^{(i)}) - y^{(i)} ]^2.$$
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# However, now we see a key difference from what we did previously: the neuron should vary
# its parameters in such a way that it manages to minimize this distance for *all* of the
# input data, simultaneously!
#
# How can we express this mathematically? We once again define a loss function,
# $L(\mathbf{w}, b)$, which tells us "how wrong" we are when the parameters take on the
# given values, and then **minimize** this loss function with respect to all of its
# parameters.
#
# One way to take account of all the data at once is to use the "mean-squared error" loss
# function, which is the mean (squared) over all the differences between the output of the
# network, $f_{\mathbf{w}, b}(\mathbf{x}^{(i)})$ on the $i$th data, and the desired output
# $y^{(i)}$:
#
# $$L_\mathrm{total}(\mathbf{w}, b) = \frac{1}{N} \sum_i L_i = \frac{1}{N} \sum_i
# [f_{\mathbf{w}, b}(\mathbf{x}^{(i)}) - y^{(i)} ]^2,$$
#
# where $N$ is the total number of data in the training set.
#
# Why do we choose this particular loss function? Because the minimum possible value of this
# loss function is $0$ (since it is a sum of squares), and this is reached only when the
# neural network perfectly predicts the output. If we can find a way to minimize this loss
# function, we will get as close as possible to this perfect prediction. (In general,
# though, we won't be able to get an exact prediction.)
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# ## Minimizing the loss function: *stochastic* gradient descent
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# We already know how to minimise loss functions on a computer: we just calculate the
# gradient, and do gradient descent! But here we hit a problem: the function
# $L_\mathrm{total}$ usually has a *lot* of terms, and so calculating the gradient of that
# function will be very time-consuming.
#
# Instead, we will use a variant of gradient descent, called *stochastic* gradient descent.
# Here, the idea is that we will not use the complete loss function; instead, at each step
# we will choose a random data point, number $i$, and do a step of gradient descent for the
# partial loss function $L_i$ *corresponding to only that data point*.
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# **Exercise 7:**
#
# Write functions for the partial loss function `L(w, b, x, y)`.
#
# To do this, recall
#
# $$
# \mathbf{x} = \begin{pmatrix} x_1 \\ x_2 \end{pmatrix};
# \qquad
# \mathbf{w} = \begin{pmatrix} w_1 \\ w_2 \end{pmatrix};
# \qquad
# f_{\mathbf{w}, b}(\mathbf{x}) = \sigma(\mathbf{w} \cdot \mathbf{x} + b),$$
#
# and declare `f(x, w, b)` as in notebook 8.
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# #### Exercise 8
#
# Write a function for the gradient of `L`, i.e. `∇L(w, b, x, y)`, with respect to the
# parameters $(w_1, w_2, b)$, using finite differences. $∇L$ will be a vector with one
# component per parameter:
#
# $$∇L = \left( \frac{\partial L}{\partial w_1}, \frac{\partial L}{\partial w_2},
# \frac{\partial L}{\partial b} \right).$$
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# #### Exercise 9
#
# Implement stochastic gradient descent in the function `stochastic_gradient_descent(L, w,
# b, xs, ys, N = 1000)`. Use this function to minimize the function $L_\mathrm{total}$.
#
# The algorithm: For each of `N` steps, randomly select an index $i$ into the vector `xs`
# storing your image data. Calculate the gradient of the cost function, $L_i$, for this
# image and update each of the parameters, $p_j$, of $L_i$ according to
#
# $$p_j = p_j - 0.01 * ∇L_j$$
#
# (Here, $j$ signifies the $j^{th}$ parameter of $L$ and similarly the $j^{th}$ component of
# $∇L$.)
#
# `stochastic_gradient_descent` should return the updated values for vector $\mathbf{w}$ and
# scalar $b$.
#
# Optional: Keep track of the value of $L_\mathrm{total}$ over time if you want to visualize
# the learning process.
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# #### Exercise 10
#
# Use the values of `w` and `b` from the last exercise to see how `f` is classifying a
# couple of the images in out dataset.
#
# In particular, calculate `f` using the 1st and the 900th image in `xs`. For which image is
# the output of `f` closer to the value of its label?
#
# A) The output of `f` for the 1st image in `xs` is closer to its label
#
# B) The output of `f` for the 900th image in `xs` is closer to its label.
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# #### Exercise 11
#
# Use the `maximum` function to determine the maximum squared distance of the prediction
# from the true value. (For each image, this formula is $y_i - f_{w, b}(x_i)$.)
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# #### Exercise 12
#
# Use `w` and `b` from stochastic gradient descent to draw the function that the network has
# learned, as before, as the hyperplane $w_1 x + w_2 y + b = 0$. Overlay this with the data.
#
#
# Does this hyperplane correctly separate the data? (I.e., is the data for all apples on one
# side of the line, and is the data for all bananas on the other side of the line?)
#
# A) Yes
# B) No
# ------------------------------------------------------------------------------------------

.nbexports/introductory-tutorials/broader-topics-and-ecosystem/intro-to-ml/12. Tools - Learning with a single neuron.jl


# read the ECCOv4r4 lat-lon-cap (LLC) grid, return grid in γ

using Revise
using ECCOtour, MeshArrays, MITgcmTools

# other useful packages in Reemergence project
# using Statistics, PyPlot, Distributions, FFTW, LinearAlgebra, StatsBase

workdir = pwd()
push!(LOAD_PATH, workdir)
cd(workdir)

pth="../inputs/GRID_LLC90/"

http="https://github.com/gaelforget/GRID_LLC90"
!isdir(pth) ? run(`git clone $http $pth`) : nothing;

γ=GridSpec("LatLonCap",pth)

D=γ.read(γ.path*"Depth.data",MeshArray(γ,Float64))
tmp1=write(D); tmp2=read(tmp1,D)
show(D)

LAT=γ.read(γ.path*"YC.data",MeshArray(γ,Float64))
tmp1=write(LAT); tmp2=read(tmp1,LAT)
show(LAT)

LON=γ.read(γ.path*"XC.data",MeshArray(γ,Float64))
tmp1=write(LON); tmp2=read(tmp1,LON)
show(LON)


"""
    count_added_edges_elim(g, v)
Computes the number of edges that will be added to the graph if we choose to
eliminate this vertex.

# Arguments
- `g::SimpleGraph{Int64}` the graph to consider.
- `v::Int64` the vertex to eliminate.

# Return
- `count::Int64` number of edges to add in the graph g if v is eliminated.
"""
function count_added_edges_elim(g::SimpleGraph{Int64}, v::Int64)::Int64
    count = 0
    ns = neighbors(g, v)
    len = length(ns)
    for i = 1:(len-1)
        for j = (i+1):len
            if !has_edge(g, ns[i], ns[j])
                count += 1
            end
        end
    end
    return count
end

"""
    minfill!(g)
Computes an approximation of the upper bound of the treewidth of the graph G and
an order for elimination using the min-fill heuristic. Computation is done by
elimating vertices in g so the graph is empty after this function.

# Arguments
- `g::SimpleGraph{Int64}`: the graph to analyse.

# Return
- ordering::`Vector{Int64}` an array of vertices index.
- result::`Int64`  of the approximation.

# Examples
```jlrepl
julia> G = smallgraph("house")

julia> minfill!(G)
([5, 1, 4, 3, 2], 2)
```
"""
function minfill!(g::SimpleGraph{Int64})
    nvertices = nv(g)
    order = zeros(Int64, nvertices)
    labels = collect(1:nvertices)
    treewidth = 0
    for i = 1:nvertices
        # find the vertex which add the less edges after elimination
        toremove = argmin(map(v -> count_added_edges_elim(g, v), vertices(g)))
        order[i] = labels[toremove]

        # compute the treewidth of the current decomposition
        treewidth = max(treewidth, degree(g, toremove))

        # remove the vertex by making it simplicial
        eliminate!(g, toremove, labels)
    end
    return (order, treewidth)
end

"""
    minwidth!(g)
Computes an approximation of the upper bound of the treewidth of the graph G and
an order for elimination using the min-width heuristic. Computation is done by
elimating vertices in g so the graph is empty after this function.

# Arguments
- `g::SimpleGraph{Int64}`: the graph to analyse.

# Return
- ordering::`Vector{Int64}` an array of vertices index.
- result::`Int64`  of the approximation.

# Examples
```jlrepl
julia> G = smallgraph("house")

julia> minwidth!(G)
([1, 5, 4, 3, 2], 2)
```
"""
function minwidth!(g::SimpleGraph{Int64})
    nvertices = nv(g)
    order = zeros(Int64, nvertices)
    labels = collect(1:nvertices)
    treewidth = 0
    for i = 1:nvertices
        # find the vertex whith the minimum degree
        toremove = argmin(degree(g))
        order[i] = labels[toremove]

        # compute the treewidth of the current decomposition
        treewidth = max(treewidth, degree(g, toremove))

        # remove the vertex by making it simplicial
        eliminate!(g, toremove, labels)
    end
    return (order, treewidth)
end