# Advent of Code 2021, Day 15
#
# Objective is to start at the top right, and end up at
# the bottom right, adding up any cells you enter, so as to
# minimize total cost.
#
# First attempt depth-first search with maximum look-ahead,
# but did not succeed on full data set. New solution uses
# Dijkstra's algorithm, adapted from:
# https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm
# Note that the numbered steps in the comments are copied
# from this Wikipedia article.
#
# AK, 15/12/2021

# Name of input file
f = "sample.txt"
f = "input.txt"

# Read data into a matrix
d = split(strip(read(f, String)), "\n")
l = map(r -> [x - '0' for x in r], d)
m = Matrix(transpose(reshape(vcat(l...), (:, length(l)))))
#display(m)

# Use this value to represent extremely high cost
PENALTY = 99999999

# Find the lowest total cost path from 1,1 to m,n of a matrix
function solve(M::Matrix{Int64})

    # 1. Mark all nodes as unvisited
    nr, nc = size(M)
    visited = zeros(nr, nc)

    # 2. Assign to every node a tentative distance value: set it to zero for our
    # initial node and to infinity for all other nodes. The tentative distance of a
    # node v is the length of the shortest path discovered so far between the node
    # v and the starting node. Since initially no path is known to any other vertex
    # than the source itself (which is a path of length zero), all other tentative
    # distances are initially set to infinity. Set the initial node as current.[15]
    distances = fill(PENALTY, nr, nc)
    distances[1,1] = 0

    # Start algoritm iterations, will stop when reach bottom right
    iter = 0
    r, c = 1, 1     # Initial node: top left
    while ! (r == nr && c == nc)

        # 3. For the current node, consider all of its unvisited neighbors and
        # calculate their tentative distances through the current node. Compare the
        # newly calculated tentative distance to the current assigned value and assign
        # the smaller one. For example, if the current node A is marked with a distance
        # of 6, and the edge connecting it with a neighbor B has length 2, then the
        # distance to B through A will be 6 + 2 = 8. If B was previously marked with a
        # distance greater than 8 then change it to 8. Otherwise, the current value
        # will be kept.
        for p in [[r, c-1], [r, c+1], [r-1,c], [r+1,c]] # left, right, up, down
            y, x = p
            if y >= 1 && y <= nr && x >= 1 && x <= nc && visited[y, x] == 0
                c1 = M[y, x] + distances[r, c]
                if c1 < distances[y, x]
                    distances[y, x] = c1
                end
            end
        end

        # 4. When we are done considering all of the unvisited neighbors of the current
        # node, mark the current node as visited and remove it from the unvisited set.
        # A visited node will never be checked again.
        visited[r, c] = 1

        # 5. If the destination node has been marked visited (when planning a route
        # between two specific nodes) or if the smallest tentative distance among the
        # nodes in the unvisited set is infinity (when planning a complete traversal;
        # occurs when there is no connection between the initial node and remaining
        # unvisited nodes), then stop. The algorithm has finished.
        #
        # 6. Otherwise, select the unvisited node that is marked with the smallest
        # tentative distance, set it as the new current node, and go back to step 3.
        #
        # When planning a route, it is actually not necessary to wait until the
        # destination node is "visited" as above: the algorithm can stop once the
        # destination node has the smallest tentative distance among all "unvisited"
        # nodes (and thus could be selected as the next "current").
        #
        # In this case, get the coordinates of the minimum value in the matrix, first
        # changing visited cells to high-cost penalty so they are not considered.
        nextCosts = distances .+ visited * PENALTY
        r, c = argmin(nextCosts).I   # coords of min cost (next iter starts there)

        # Report progress
        iter += 1
        if iter % 1000 == 0
            print("\rIteration $iter: now at $r,$c   ")
        end

    end

    # Return solution, cost set in the distances matrix for bottom right point
    return distances[nr, nc]

end

# For part 2, make a 5x5 copy of the matrix, each row/col being one higher
# than the previous, with 9s wrapped back to 1
function enlarge(x, n)

    # To grow 5x, add 4 copies either direction
    n -= 1

    # Grow horizontally
    m1 = copy(x)
    for i in 1:n
        m1 = hcat(m1, x .+ i)
    end

    # Grow vertically
    m2 = copy(m1)
    for i in 1:n
        m2 = vcat(m2, m1 .+ i)
    end

    # Adjust cells that are bigger than 9
    nr, nc = size(m2)
    for r in 1:nr
        for c in 1:nc
            while m2[r,c] > 9
                m2[r,c] -= 9
            end
        end
    end

    # Return result
    return m2
end

# Part 1 solution
s1 = solve(m)
println("\nPart 1: ", s1)

# Part 2, same but on enlarged matrix
mbig = enlarge(m, 5)
s2 = solve(mbig)
println("\nPart 2: ", s2)


##
using Plots
backend(:plotly)
theme(:juno)

##
function finklet(j, l)
    # -------- set filters
    jrad = 7-j
    dθ = π/8        # 8 angular bins hardwired
    θ_l = dθ*l
    # -------- define coordinates
    nx = 256
    xbox = LinRange(-nx/2, nx/2-1 , nx)
    # make a 256x256 grid of X
    sx = xbox' .* ones(nx)
    sy = ones(nx)' .* xbox
    r  = sqrt.((sx).^2 + (sy).^2)
    θ  = mod.(atan.(sy, sx).+π .-θ_l,2*π)
    nozeros = r .> 0
    logr = log2.(r[nozeros])
    r[nozeros] = logr
    # -------- in Fourier plane, envelope of psi_j,l
    mask = (abs.(θ.-π).<= dθ) .& (abs.(r.-jrad) .<= 1)
    # -------- angular part
    ang = cos.((θ.-π).*4)
    # -------- radial part
    rad = cos.((r.-jrad).*π./2)
    psi = mask.*ang.*rad             #mask times angular part times radial part
    return psi
end

##
temp = finklet(1,0)
Plots.heatmap(temp, aspectratio=1)

##
using FFTW

##
J = 8
L = 16

fink_filter = Array{Float64, 4}(undef, J, L, 256, 256)
for j = 1:J
    for l = 1:L
        fink_filter[j,l,:,:]=fftshift(finklet(j-1,l-1))
    end
end

Plots.heatmap(fftshift(fink_filter[3,16,:,:]), aspectratio=1)

fink_filter_sum = zeros(256, 256)
for j = 1:J
    for l = 1:L
        fink_filter_sum+=(finklet(j-1,l-1)).^2
    end
end

Plots.heatmap(fink_filter_sum, aspectratio=1)

fink_filter_sum[127:129,127:129]

##
Plots.heatmap(fftshift(fink_filter[8,4,:,:]), aspectratio=1)

(fftshift(fink_filter[8,4,:,:]))[126:130,126:130]

fink_filter_1 = Array{Float64, 4}(undef, J, L, 256, 256)
for j = 1:J
    for l = 1:L
        fink_filter_1[j,l,:,:]=finklet(j-1,l-1)
    end
end

fink_filter_1[8,3,127:131,127:131]

##
temp2 = [1 2; 3 4]
fftshift(temp2)

blarg = rand(4,4)
fftshift(blarg)

fink_filter_sum = zeros(256, 256)
for j = J
    for l = 1:L
        fink_filter_sum+=(fink_filter_1[j,l,:,:]).^2
    end
end

##
Plots.heatmap(fink_filter_sum, aspectratio=1)

fink_filter_sum[127:131,127:131]

phi = sqrt.(fink_filter_sum)

##
phi_real = ifft(fftshift(phi))

Plots.heatmap(phi, aspectratio=1)

Plots.heatmap(abs.(phi_real), aspectratio=1)

Plots.heatmap(real.(phi_real), aspectratio=1)

Plots.heatmap(imag.(phi_real), aspectratio=1)

function fink_filter_bank(J,L)
    fink_filter = Array{Float64, 4}(undef, J, L, 256, 256)
    for j = 1:J-1
        for l = 1:L
            fink_filter[j,l,:,:]=fftshift(finklet(j-1,l-1))
        end
    end
end

@time fink_filter_bank(8,16)

@time fink_filter_bank(8,16)

##
using ColorSchemes

Plots.heatmap(abs.(phi_real), aspectratio=1,c=:cubehelix)

colorschemes[:cubehelix]

findcolorscheme("cube")

##

Plots.heatmap(abs.(phi_real),
    xlims=(1,256),
    ylims=(1,256),
    aspectratio=1,
    axis=nothing,
    border=:none,
    c=:cubehelix)

using Profile

##
Profile.clear()
@profile fink_filter_bank(8,16)

Juno.profiler()

Profile.print()

@time fink_filter_bank(8,16)


using Test, Random, LSHFunctions, QuadGK
using LinearAlgebra: dot, norm

include("utils.jl")

#==================
Tests
==================#

@testset "ℓ^p distance and norm tests" begin
    Random.seed!(RANDOM_SEED)

    @testset "Compute ℓ^1 distance and norm" begin
        x = 2.0 * ones(10)
        y = zero(x)

        @test ℓ1_norm(x) == ℓ1(x, zero(x)) == 20
        @test ℓ1_norm(y) == ℓ1(y, zero(y)) == 0

        x = [1, 2, 3, 4]
        y = [1, 2, 0, 0]

        @test ℓ1_norm(x) == 1 + 2 + 3 + 4
        @test ℓ1_norm(y) == 1 + 2
        @test ℓ1(x,y) == ℓ1_norm(x - y) == 3 + 4

        x = randn(128)
        y = randn(128)

        @test ℓ1_norm(x) ≈ x     .|> abs |> sum
        @test ℓ1_norm(y) ≈ y     .|> abs |> sum
        @test ℓ1(x,y)    ≈ x - y .|> abs |> sum

        @test_throws DimensionMismatch ℓ1(zeros(5), zeros(6))
    end

    @testset "Compute ℓ^2 distance and norm" begin
        x = 2.0 * ones(10)
        y = zero(x)

        @test ℓ2_norm(x) == ℓ2(x, zero(x)) == √40
        @test ℓ2_norm(y) == ℓ2(y, zero(y)) == 0

        x = [1, 2, 3, 4]
        y = [1, 2, 0, 0]

        @test ℓ2_norm(x) == √(1^2 + 2^2 + 3^2 + 4^2)
        @test ℓ2_norm(y) == √(1^2 + 2^2)
        @test ℓ2(x,y) == ℓ2_norm(x - y) == √(3^2 + 4^2)

        x = randn(128)
        y = randn(128)

        @test ℓ2_norm(x) ≈ x     .|> abs2 |> sum |> √
        @test ℓ2_norm(y) ≈ y     .|> abs2 |> sum |> √
        @test ℓ2(x,y)    ≈ x - y .|> abs2 |> sum |> √

        @test_throws DimensionMismatch ℓ2(zeros(5), zeros(6))
    end

    @testset "Compute ℓ^p distance and norm" begin
        x = [1, 2, 3, 4]
        y = [1, 2, 0, 0]

        @test ℓp_norm(x, 3) ≈ (1^3 + 2^3 + 3^3 + 4^3)^(1/3)
        @test ℓp_norm(y, 3) ≈ (1^3 + 2^3)^(1/3)
        @test ℓp(x, y, 3) ≈ ℓp_norm(x - y, 3) ≈ (3^3 + 4^3)^(1/3)

        x = randn(128)
        y = randn(128)

        @test ℓp_norm(x, 1) ≈ ℓ1_norm(x)
        @test ℓp_norm(x, 2) ≈ ℓ2_norm(x)
        @test ℓp_norm(x, 3) ≈ mapreduce(u -> abs(u)^3, +, x)^(1/3)
        @test ℓp(x, y, 1) ≈ ℓ1(x, y)
        @test ℓp(x, y, 2) ≈ ℓ2(x, y)
        @test ℓp(x, y, 3) ≈ mapreduce(u -> abs(u)^3, +, x - y)^(1/3)

        p = rand() + 1

        @test ℓp_norm(x, p) ≈ mapreduce(u -> abs(u)^p, +, x)^(1/p)
        @test ℓp(x, y, p) ≈ mapreduce(u -> abs(u)^p, +, x - y)^(1/p)
    end
end

@testset "Function space L^p distance and norm tests" begin
    Random.seed!(RANDOM_SEED)

    @testset "Compute L^1 distance and norm" begin
        interval = LSHFunctions.@interval(-π ≤ x ≤ π)
        f(x) = 0
        g(x) = 2

        @test L1_norm(g, interval) ≈ L1(f, g, interval) ≈ 4π

        g(x) = x

        @test L1_norm(g, interval) ≈ L1(f, g, interval) ≈ π^2

        f(x) = x
        g(x) = 2x.^2

        @test L1(f, g, interval) ≈ L1_norm(x -> f(x) - g(x), interval)
        @test L1(f, g, interval) ≈ quadgk(x -> abs(f(x) - g(x)), -π, π)[1]
    end

    @testset "Compute L^2 distance and norm" begin
        interval = LSHFunctions.@interval(-π ≤ x ≤ π)
        f(x) = 0
        g(x) = 2

        @test L2_norm(g, interval) ≈ L2(f, g, interval) ≈ √(8π)

        g(x) = x

        @test L2_norm(g, interval) ≈ L2(f, g, interval) ≈ √(2π^3 / 3)

        f(x) = x
        g(x) = 2x.^2

        @test L2(f, g, interval) ≈ L2_norm(x -> f(x) - g(x), interval)
        @test L2(f, g, interval) ≈ √quadgk(x -> abs2(f(x) - g(x)), -π, π)[1]
    end

    @testset "Compute L^p distance and norm" begin
        interval = LSHFunctions.@interval(-π ≤ x ≤ π)
        f(x) = 0
        g(x) = 2

        @test Lp_norm(g, interval, 1) ≈ Lp(f, g, interval, 1) ≈ L1(f, g, interval)
        @test Lp_norm(g, interval, 2) ≈ Lp(f, g, interval, 2) ≈ L2(f, g, interval)
        @test Lp_norm(g, interval, 3) ≈ Lp(f, g, interval, 3) ≈ (16π)^(1/3)

        g(x) = x

        @test Lp_norm(g, interval, 1) ≈ Lp(f, g, interval, 1) ≈ L1(f, g, interval)
        @test Lp_norm(g, interval, 2) ≈ Lp(f, g, interval, 2) ≈ L2(f, g, interval)
        @test Lp_norm(g, interval, 3) ≈ Lp(f, g, interval, 3) ≈ (π^4/2)^(1/3)

        f(x) = x
        g(x) = 2x.^2
        p = rand() + 1

        @test Lp(f, g, interval, p) ≈ Lp_norm(x -> f(x) - g(x), interval, p)
        @test Lp(f, g, interval, p) ≈
              quadgk(x -> abs(f(x) - g(x))^p, -π, π)[1]^(1/p)
    end
end

@testset "Cosine similarity tests" begin
    Random.seed!(RANDOM_SEED)

    @testset "Compute cosine similarity between Vectors" begin
        x = [1, 0, 1, 0]
        y = [0, 1, 0, 1]

        @test cossim(x, y) == 0

        y = [1, 0, 0, 0]

        @test cossim(x, y) == 1.0 / √2

        x = randn(20)

        @test cossim(x,   x) ≈  1
        @test cossim(x,  2x) ≈  1
        @test cossim(x,  -x) ≈ -1
        @test cossim(x, -2x) ≈ -1

        @test_throws ErrorException cossim(x, zero(x))
    end

    @testset "Compute cosine similarity between functions" begin
        interval = LSHFunctions.@interval(0 ≤ x ≤ 1)
        f(x) = (x ≤ 0.5) ? 1.0 : 0.0
        g(x) = (x ≤ 0.5) ? 0.0 : 1.0

        @test cossim(f, g, interval) ≈ 0
        @test cossim(f, f, interval) ≈ cossim(g, g, interval) ≈ 1
        @test cossim(f, x -> 2f(x), interval) ≈
              cossim(g, x -> 2g(x), interval) ≈ 1
        @test cossim(f, x -> -f(x), interval) ≈
              cossim(g, x -> -g(x), interval) ≈ -1
        @test_throws ErrorException cossim(f, x -> 0.0, interval)

        f(x) = x
        g(x) = x.^2

        @test L2_norm(f, interval) ≈ 1/√3
        @test L2_norm(g, interval) ≈ 1/√5
        @test inner_prod(f, g, interval) ≈ 1/4
        @test cossim(f, g, interval) ≈ 1/4 / (1/√3 * 1/√5)

        f, f_steps = create_step_function(10)
        g, g_steps = create_step_function(10)

        @test cossim(f, g, LSHFunctions.@interval(0 ≤ x ≤ 10)) ≈ cossim(f_steps, g_steps)
    end
end

@testset "Jaccard similarity tests" begin
    Random.seed!(RANDOM_SEED)

    @testset "Compute Jaccard similarity with Int64 sets" begin
        A = Set([1, 2, 3])
        B = Set([2, 3, 4])

        @test jaccard(A, A) == jaccard(B, B) == 1
        @test jaccard(A, B) == jaccard(B, A) == 2 / 4

        @test jaccard(A, Set()) == jaccard(B, Set()) == 0

        @test jaccard(A, Set([2])) == jaccard(B, Set([2])) == 1 / 3
        @test jaccard(A, Set([5])) == jaccard(B, Set([5])) == 0

        # Convention used in this module
        @test jaccard(Set(), Set()) == 0
    end

    @testset "Compute Jaccard similarity with String sets" begin
        A = Set(["a", "b", "c"])
        B = Set(["b", "c", "d"])

        @test jaccard(A, A) == jaccard(B, B) == 1
        @test jaccard(A, B) == jaccard(B, A) == 2 / 4

        @test jaccard(A, Set()) == jaccard(B, Set()) == 0

        @test jaccard(A, Set(["b"])) == jaccard(B, Set(["b"])) == 1 / 3
        @test jaccard(A, Set(["e"])) == jaccard(B, Set(["e"])) == 0

        # Convention used in this module
        @test jaccard(Set(), Set()) == 0
    end

    @testset "Compute Jaccard similarity between binary vectors" begin
        x = BitArray([true, false, true, true, false])
        y = BitArray([false, false, true, true, true])

        @test jaccard(x, y) == jaccard(y, x) == 2 / 4

        # When x and y are both full of zero bits, we define the
        # Jaccard similarity between them to be zero.
        x = falses(5)
        y = falses(5)
        @test jaccard(x, y) == 0
    end

    @testset "Compute weighted Jaccard similarity between Real vectors" begin
        x = [0.8, 0.1, 0.3, 0.4, 0.1]
        y = [1.0, 0.6, 0.0, 0.4, 0.5]

        @test jaccard(x, y) ==
              jaccard(y, x) ==
              (0.8+0.1+0.0+0.4+0.1) / (1.0+0.6+0.3+0.4+0.5)

        # Test Jaccard similarity between vectors with different dtypes
        x = mod.(rand(Int32, 20), 10)
        y = mod.(rand(Int64, 20), 10)
        @test jaccard(Float64.(x), Float64.(y)) ≈ jaccard(x, y)
        @test isapprox(jaccard(Float64.(x), Float64.(y)), jaccard(Float32.(x), y), atol=1e-8)
        @test isapprox(jaccard(Float64.(x), Float64.(y)), jaccard(x, Float32.(y)), atol=1e-8)
        @test jaccard(Float64.(x), Float64.(y)) ≈ jaccard(Float32.(x), Float64.(y))

        # Define the Jaccard similarity between pairs of Real vectors
        # to be zero.
        x = zeros(10)
        y = zeros(10)
        @test jaccard(x, y) == 0

        # Throw an error when any of the elements are negative, or when the
        # two vectors have different lengths.
        @test_throws(DimensionMismatch, jaccard(rand(5), rand(6)))
        @test_throws(ErrorException, jaccard(-ones(3), ones(3)))
    end

    @testset "Compute weighted Jaccard similarity between Sets" begin
        A = Set(["a", "b", "c"])
        B = Set(["b", "c", "d"])
        W = Dict("a" => 0.2, "b" => 2.4, "c" => 0.6, "d" => 1.8)

        @test jaccard(A, B, W) ≈
              jaccard(B, A, W) ≈
              (2.4 + 0.6) / (0.2 + 2.4 + 0.6 + 1.8)

        # We should throw an error when any of the weights are negative
        W["a"] = -1.0
        @test_throws(ErrorException, jaccard(A, B, W))
    end
end

@testset "Inner product similarity tests" begin
    Random.seed!(RANDOM_SEED)

    @testset "Compute ℓ^2 inner products between pairs of Vectors" begin
        x = [1, 2, 3, 4]
        y = [5, 6, 0, 0]

        @test inner_prod(x, x) ≈ dot(x, x) ≈ norm(x).^2 ≈
              1^2 + 2^2 + 3^2 + 4^2
        @test inner_prod(y, y) ≈ dot(y, y) ≈ norm(y).^2 ≈
              5^2 + 6^2
        @test inner_prod(x, y) ≈ dot(x, y) ≈ 1*5 + 2*6

        x = randn(128)
        y = randn(128)

        @test inner_prod(x, y) ≈ dot(x, y)
    end

    @testset "Compute L^2 inner products between pairs of functions" begin
        interval = LSHFunctions.@interval(-π ≤ x ≤ π)
        f(x) = 1
        g(x) = x

        @test inner_prod(f, f, interval) ≈ L2_norm(f, interval)^2 ≈ 2π
        @test inner_prod(g, g, interval) ≈ L2_norm(g, interval)^2 ≈ 2π^3 / 3
        @test isapprox(inner_prod(f, g, interval), 0.0; atol = 1e-15)

        f(x) = sin(x)
        g(x) = cos(x)

        @test inner_prod(f, f, interval) ≈ L2_norm(f, interval)^2 ≈
              quadgk(x -> sin(x)^2, -π, π)[1]
        @test inner_prod(g, g, interval) ≈ L2_norm(g, interval)^2 ≈
              quadgk(x -> cos(x)^2, -π, π)[1]

        # <f,g> = 0 via the identity sin(x)cos(x) = 1/2 sin(2x)
        @test isapprox(inner_prod(f, g, interval), 0.0; atol=1e-15)
    end
end


using MatrixNetworks

"""

Biconnected Component Decomposition
----------------------------------

Any connected graph decomposes into a tree of biconnected components called the block-cut tree of the graph.
The blocks are attached to each other at shared vertices called cut vertices or articulation points.

This implementation is based on the algorithm provided by Tarjan in "Depth-First Search and Linear Graph Algorithms".

TODO: 1) See how to output the biconnected components
      2) Compare its performance with C

"""
type Biconnected_components_output
    bcc_edge_list::Vector{Int64}  #list of bcc edges
    components::Vector{Int64} #Indices of bcc corresponding to the edges in List
    number::Int64 # Total number of Biconnected components
    A::MatrixNetwork #MatrixNetwork
end

#I = [1,2,2,3,3,4,4,5,5,1]
#J = [2,1,3,2,4,3,5,4,1,5]
#V = [1,1,1,1,1,1,1,1,1,1]

#I = [1,2,2,5,5,1,5,3,3,6,6,5,4,5]
#J = [2,1,5,2,1,5,3,5,6,3,5,6,5,4]
#V = [1,1,1,1,1,1,1,1,1,1,1,1,1,1]

#I = [1,2,2,3,3,1,4,5,5,6,4,6]
#J = [2,1,3,2,1,3,5,4,6,5,6,4]
#V = [1,1,1,1,1,1,1,1,1,1,1,1]

I = [1,2,2,3,3,4,1,4,3,5,5,6,6,7,7,8,6,8,5,9,9,10,5,10,1,3,2,5]
J = [2,1,3,2,4,3,4,1,5,3,6,5,7,6,8,7,8,6,9,5,10,9,10,5,3,1,5,2]
V = [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]

A = sparse(I,J,V)

function BiCC(A::MatrixNetwork)
    n=A.n
    rp=A.rp
    ci=A.ci
    bcc_edges=zeros(Int64,2*length(ci))
    bcc_edges_count=0
    cn=1
    low=zeros(Int64,n)
    dt=-1*ones(Int64,n)
    pred=zeros(Int64,n)
    components=-1 * ones(Int64,length(ci))
    t=0
    cs=zeros(Int64,2*length(ci))
    css=0
    rs=zeros(Int64,4*n)
    rss=0 #recursion stack (v,ri)
    root_children= 0
    #start dfs at 1
    for sv=1:n
        v=sv
        if dt[v]>0
            continue
        end
	root_children = 0
        rss=rss+1
        rs[3*rss-2]=v
	rs[3*rss-1]=v
        rs[3*rss]=rp[v] #add v,rp[v] to stack
        low[v]=t
        dt[v]=t;
        t=t+1;
        while rss>0
		grandparent = rs[3*rss-2]
		parent = rs[3*rss-1]
		children_index = rs[3*rss]
		child_index = children_index
		if (child_index != rp[parent+1])
			child = ci[child_index]
			children_index = children_index+1
			rs[3*rss] = children_index
			if (grandparent == child)
				continue		# skip tree edges
			end
			if (dt[child] >= 0)
				if (dt[child] <= dt[parent])
					if (dt[child] < low[parent]) #Update Low
						low[parent] = dt[child]
					end
					css+=1
					cs[2*css-1] = parent
					cs[2*css] = child
				end
			else
				low[child] = dt[child] = t
				t+=1
				rss+=1
        			rs[3*rss-2]=parent
				rs[3*rss-1]=child
        			rs[3*rss]=rp[child]
				css+=1
				cs[2*css-1] = parent
				cs[2*css] = child
			end
		else
			# do StopIteration
			rss-=1
			print("here")
			if (rss > 1)
				if (low[parent] >= dt[grandparent])  # Grandparent is an Articulation point
					while true
						u = cs[2*css-1]
						v = cs[2*css]
						css-=1
						bcc_edges_count+=1
						bcc_edges[2*bcc_edges_count-1]=u
						bcc_edges[2*bcc_edges_count]=v
						components[bcc_edges_count]=cn   #Assign #Bcc to the corresponding edges
						if (u == grandparent && v == parent)
							break
						end
					end
					cn+=1
				end
				if (low[parent] < low[grandparent])
					low[grandparent] = low[parent]
				end
			elseif (rss == 1)
				root_children +=1
				while true
					u = cs[2*css-1]
					v = cs[2*css]
					css-=1
					bcc_edges_count+=1
					bcc_edges[2*bcc_edges_count-1]=u
					bcc_edges[2*bcc_edges_count]=v
					components[bcc_edges_count]=cn
					if (u == grandparent && v == parent)
						break
					end
				end
				cn+=1
			end
		end
	end
  end
print(bcc_edges)
print("\n")
print(components)

end

function dfs_test()
		print(A);
		B = MatrixNetwork(A);
		print(B)
		print(BiCC(B))
end

dfs_test();


module day9

readInput() = parse.(Int, eachline(joinpath(@__DIR__, "input.txt")))

function isValid(target, list)
    for i in 1:length(list) - 1, j in i + 1:length(list)
        list[i] + list[j] == target && return true
    end
    false
end

function part1(input, width=25)
    i = width + 1
    while isValid(input[i], view(input, (i - width):(i - 1)))
        i += 1
    end
    input[i]
end

function findRange(target, list)
    i, j = 1, 2
    sum = list[i] + list[j]
    while sum != target
        if sum > target && j - 1 > 1
            sum -= list[i]
            i += 1
        elseif sum < target
            j += 1
            sum += list[j]
        end
    end
    return i, j
end

const resultP1 = part1(readInput()) # To leave part1 out of the benchmark

function part2(input, target=resultP1)
    i, j = findRange(target, input)
    +(extrema(view(input, i:j))...)
end

end


export plotcost_pernbsim, plotcost_persquare, plotcost_suboptimal

using Plots

include("markovDecision.jl")
include("simulate.jl")

function simulatedcost(board, dice, circular, start_box, nb_iter)
    allsim = Vector{Int64}()
    for i=1:nb_iter
    	push!(allsim, simulate_k(board, dice, circular, start_box))
    end
    return mean(allsim)
end

"""
    plotcost_pernbsim(board)

Plot both the simulated and expected cost for multiple number of simulations
of a game starting at square 1.
"""
function plotcost_pernbsim(board)
    fig1 = plot()
    # define board
    circular = true
    # expected cost
    (expec, dice) = markovdecision(board, circular)
    expected_cost_from_1 = expec[1]

    # cost plot
    start_box = 1
    powers = [i for i=1:7]
    nb_iter = [10^i for i in powers]
    y = [simulatedcost(board, dice, circular, start_box, x) for x in nb_iter]

    plot!(powers, y, marker="o",
        label=["Simulated cost"],
        xlabel=("Log 10 number of simulations"),
        ylabel=("Cost"))
    hline!([expected_cost_from_1]; label="Expected cost")

    # error plot
    fig2 = plot()
    error = [abs(expected_cost_from_1 - simulated_cost) for simulated_cost in y]/expected_cost_from_1
    plot!(powers, error, marker="o",
        label=["Error"],
        xlabel=("Log 10 number of simulations"),
        ylabel=("Relative error"))

    return fig1, fig2
end

"""
    plotcost_persquare(board)

Plot both the simulated and expected cost for starting the game at every square
(one square is one x-axis point).
This allows to check whether each expected cost corresponds to the simulated one,
when the game is repeated multiple times.
"""
function plotcost_persquare(board)
    fig = plot()
    # define board
    circular = true
    # expected cost
    (expec, dice) = markovdecision(board, circular)

    nb_iter = 100
    squares = [i for i=1:15]
    y = [simulatedcost(board, dice, circular, square, nb_iter) for square in squares]

    scatter!(squares, y,
        color=("blue"),
        label=["Simulated cost"]
        )

    scatter!(squares, expec,
        color=("red"),
        label=["Expected cost"],
        xlabel=("Square number"),
        ylabel=("Cost")
        )

    figname = "../../img/board_magic/cost_per_square_" * string(nb_iter) * "_iter_circ.pdf"
    return fig, figname
end

"""
    plotcost_suboptimal(board)

Plot the simulated cost for different suboptimal dice statgies
of a game starting at square 1.
"""
function plotcost_suboptimal(board)
    fig = plot()
    # define board
    circular = true
    # expected cost
    (expec, dice) = markovdecision(board, circular)
    expected_cost_from_1 = expec[1]

    # cost plot
    start_box = 1
    powers = [i for i=1:3]
    nb_iter = [10^i for i in powers]
    y_sim = [simulatedcost(board, dice, circular, start_box, x) for x in nb_iter]
    y_subopt1 = [simulatedcost(board, ones(Int64, 15), circular, start_box, x) for x in nb_iter]
    y_subopt2 = [simulatedcost(board, 2*ones(Int64, 15), circular, start_box, x) for x in nb_iter]
    y_subopt3 = [simulatedcost(board, 3*ones(Int64, 15), circular, start_box, x) for x in nb_iter]
    dice_subopt4 = ones(Int64, 15)
    dice_subopt4[1:9] = [t == 0 ? 3 : (t == 1 ? 1 : 2) for t in board[2:10]]
    dice_subopt4[11:13] = [t == 0 ? 3 : (t == 1 ? 1 : 2) for t in board[12:14]]
    y_subopt4 = [simulatedcost(board, dice_subopt4, circular, start_box, x) for x in nb_iter]

    plot!(powers, y_sim, marker="o",
        label=["Optimal cost"],
        xlabel=("Log 10 number of simulations"),
        ylabel=("Cost"))
    plot!(powers, y_subopt1, marker="o",
        label=["Cost for only security dice"])
    plot!(powers, y_subopt2, marker="o",
        label=["Cost for only normal dice"])
    plot!(powers, y_subopt3, marker="o",
        label=["Cost for only risky dice"])
    plot!(powers, y_subopt4, marker="o",
        label=["Cost for custom strategy"])

    return fig
end

board_unif_low = [0,1,0,0,2,0,1,0,2,0,0,1,0,2,0]
board_right_high = [0,0,0,0,0,2,0,0,1,1,2,2,0,0,0]
board_left_high = [0,1,2,1,2,2,1,0,0,0,0,0,0,0,0]
board_magic = [0,3,0,3,0,0,0,0,3,3,0,0,0,0,0]
# #
# plot_cost, plot_errors = plotcost_pernbsim(board_magic)
# savefig(plot_cost, "../../img/board_magic/cost_iterations_log_circ.pdf")
# savefig(plot_errors, "../../img/board_magic/error_iterations_log_circ.pdf")

# #
# p, figname = plotcost_persquare(board_magic)
# savefig(p, figname)

# #
# plot_subopt = plotcost_suboptimal(board_magic)
# savefig(plot_subopt, "../../img/board_magic/cost_subopt_log_circ.pdf")


@testset "field" begin

N = [5,6,7]
ncmp = 4

# Test the size of create_field_array().
F = create_field_array(N, ncmp=ncmp)
@test size(F) == (N..., ncmp)

# Test if field_arr2vec() returns a view.
F .= rand(eltype(F), size(F))
f = field_arr2vec(F, order_cmpfirst=false)
f_cmpfirst = field_arr2vec(F, order_cmpfirst=true)

# Test if order_cmpfirst is working as intended.
@test reshape(transpose(reshape(f_cmpfirst, ncmp, :)), :) == f

end  # @testset "field"


using TensorToolbox
using LinearAlgebra
include("params.jl")
include("newton_update.jl")

function D_KL(A, B)
    return sum(A .* log.( A ./ B ) ) - sum(A) + sum(B)
end

function TLD_newton(M ; cnt_max=20, tol = 1.0e-5, verbose = true)
    T = copy(M)
    tensor_shape = size(T)
    sumt = sum(T)
    T .= T ./ sumt

    theta = get_uniform_theta( tensor_shape )
    eta = get_uniform_eta( tensor_shape )
    eta_beta = rapid_eta_from_tensor(T)

    dkls = []
    cnt = 0
    R = undef
    prev_eta = undef
    while cnt < cnt_max
        prev_eta = eta

        theta = newton_update_theta(eta, eta_beta, theta)
        R = rapid_tensor_from_theta(theta)
        normalize!(R,1)
        eta = rapid_eta_from_tensor(R)

        dif = norm(eta - eta_beta)
        cnt += 1
        dkl = D_KL(T, R)
        append!(dkls, dkl)
        if verbose
            println("step $cnt decent error $dkl ||eat - eta_beta||_F $dif")
        end
        if dif < tol
            break
        end
    end

    R = sumt * R
    return R
end

function TLD(M ; cnt_max=100, lr=0.01, tol = 1.0e-5, verbose=true)
    T = copy(M)
    tensor_shape = size(T)
    d = ndims(T)
    sumt = sum(T)
    T .= T ./ sumt

    theta = get_uniform_theta( tensor_shape )
    eta = get_uniform_eta( tensor_shape )
    eta_beta = rapid_eta_from_tensor(T)

    dkls = []
    cnt = 0
    R = undef
    previous_eta = undef
    while cnt < cnt_max

        for k = 1:d
            for j = 1:tensor_shape[k]
                idxs = ones(Int, d)
                idxs[k] = j
                theta[ idxs... ] -= lr * (eta[idxs...] - eta_beta[idxs...])
            end
        end

        R = rapid_tensor_from_theta(theta)
        normalize!(R,1)
        eta = rapid_eta_from_tensor(R)

        cnt += 1
        dkl = D_KL(T,R)
        append!(dkls, dkl)
        dif = norm(eta - eta_beta)

        if verbose
            println("step $cnt decent error $dkl ||eat - eta_beta||_F $dif")
        end
        if dif < tol
            break
        end
    end

    R = sumt * R
    return R
end


using Plots
using Random
Random.seed!(0)
A = randn(40,30)
heatmap(A)


using RandomProcesses

d = CRT(100, 0.01)
x = rand(d)

d = CRT(100, 1.0)
x = rand(d)


isarmstrong(n::Int) = n == sum(digits(n).^length(digits(n)))


#returns squareroot of sum of squares, i.e. positive number
function  z(x::Matrix{Int64}, q::Vector{Int64}, p::Vector{Int64})
   # S is the number of students, N is the number of courses
   # x is the SxN allocation matrix
   # q is the Nx1 course capacity vector
   # p is the Nx1 price vector
   N = length(q)
   zer = zeros(N)        #Create empty Nx1 vector of clearing errors
   numstud = transpose(sum(x, dims = 1)) #Create Nx1 vector with the number of students enrolled in each course.
   for i in 1:N        #For every course
       if p[i] > 0     #As long as the price in this course is positive.
           zer[i] = numstud[i] - q[i]
       else
           zer[i] = max(numstud[i] - q[i], 0)
       end
   end
   α = sqrt(sum(zer.^2))
   return α
end

export z


# Play each machine once.
# Then always play machine j that maximizes UCB value.
# Like sqrt(log(sum(counts).^2)) / sqrt(counts[j])

type UCB <: BanditAlgorithm
  counts::Vector{Int64}
  values::Vector{Float64}
end

function UCB(n_arms::Int64)
  UCB(zeros(Int64, n_arms), zeros(n_arms))
end

function initialize(algo::UCB, n_arms::Int64)
  algo.counts = zeros(Int64, n_arms)
  algo.values = zeros(n_arms)
end

function initialize(algo::UCB, arms::Vector)
  initialize(algo, length(arms))
end

function select_arm(algo::UCB)
  if any(algo.counts .== 0)
    return find(algo.counts .== 0)[1]
  else
    ucb_values = algo.values + sqrt((2 * log(sum(algo.counts))) ./ algo.counts)
    return ind_max(ucb_values)
  end
end

function update(algo::UCB, chosen_arm::Int64, reward::Real)
  algo.counts[chosen_arm] += 1
  n = algo.counts[chosen_arm]

  value = algo.values[chosen_arm]
  algo.values[chosen_arm] = ((n - 1) / n) * value + (1 / n) * reward
end


function bt04()
#
# Brugnano and Trigiante
#
    p = poly([1;1;1; -1*[1;1;1;1]; (.5+im)*[1;1;1]; (.5-im)*[1;1;1];
                      .5*(1+im)*[1;1]; .5*(1-im)*[1;1]])

    z = [1 3; -1 4; .5+im 3; .5-im 3; .5*(1+im) 2; .5*(1-im) 2]

    p, PolyZeros(z)
end


@testset "regression" begin
    Random.seed!(14)
    beta = [10; 20; 30]
    n = 1000
    X = rand(n, 3)
    y = X*beta + randn(n)

    model = regression(y, X)

    ss = statespace(model)

    @test ss.smoother.alpha[end, :] ≈ [10; 20; 30] rtol = 1e-2
    @test ss.model.H[1] ≈ 1.0 rtol = 1e-2
end


# x25519.jl gives some modular arithmetics modulo 2^255-19 and the point
# addition and scalar multiplication on curve25519. The computation is
# implemented with the intention of implementing it on top of an FPGA.

# Carry saved adder.
function csa(a,b,c,carry_in=0)
    s = (a ⊻ b) ⊻ c
    c = (((a & b) | (a & c) | (b & c)) << 1) | carry_in
    return (s, c)
end

# Modular addition of two numbers in 0:2^255-20 modulo p=2^255-19.
function addmod_25519(a, b)
#    return (a + b) % (BigInt(2)^255-19)
    zs, zc = csa(a, b, ~(BigInt(2)^255-19) & (BigInt(2)^255-1), 1)
    sel = ((zs+zc) >> 255) != 0 ? 1 : 0
    return (sel == 1) ? (zs+zc) & (BigInt(2)^255-1) : a+b
end

# Modular subtraction of two numbers in 0:2^255-20 modulo p=2^255-19.
function submod_25519(a, b)
#    t = (a - b) % (BigInt(2)^255 - 19)
#    return (t < 0) ? t + (BigInt(2)^255 - 19) : t
     zs, zc = csa(a, ~b & (BigInt(2)^255-1), 0)
     ws, wc = csa(a, ~b & (BigInt(2)^255-1), (BigInt(2)^255-19))
     sel = ((zs+zc+1) >> 255) != 0 ? 1 : 0
     return (sel == 1) ? (zs+zc+1) & (BigInt(2)^255-1) : (ws+wc+1) & (BigInt(2)^255-1)
end

# Modular multiplication of two numbers in 0:2^255-20 modulo p=2^255-19.
# with radix-8 interleave.
function multmod_25519(x, y)
#    return (x*y) % (BigInt(2)^255 - 19)
    p = BigInt(2)^255 - 19
    lut1 = [(0,0), (y,0), (y<<1, 0), (y<<1, y),
            (y<<2, 0), (y<<2, y), (y<<2, y<<1),
            csa(y<<2,y<<1,y)]
    lut2 = [i*8*19 for i=0:23]
    s = c = n = BigInt(0)
    for i in 85:-1:1
        ms, mc = lut1[(x >> (3*i-3)) + 1]
        x = x % BigInt(2)^(3*i-3)
        s1 = 8*(s % BigInt(2)^255)
        c1 = 8*(c % BigInt(2)^255)
        s2 = (s1 ⊻ ms) ⊻ c1
        c2 = ((s1 & ms) | (s1 & c1) | (ms & c1)) << 1
        s3 = (s2 ⊻ mc) ⊻ c2
        c3 = ((s2 & mc) | (s2 & c2) | (mc & c2)) << 1
        s = (s3 ⊻ n) ⊻ c3
        c = ((s3 & n) | (s3 & c3) | (n & c3)) << 1
        n = lut2[(s >> 255) + (c >> 255) + 1]
    end
    # TODO better reduction
    # s c = (s[254:0]+19*(s>>255), c[254:0]+19*(c>>255))
    s, c = csa((s % BigInt(2)^255), (c % BigInt(2)^255), n>>3)
    n = lut2[(s >> 255) + (c >> 255) + 1]
    #println("1st reduce $(s>>253) $(c>>253)")
    s, c = csa((s % BigInt(2)^255), (c % BigInt(2)^255), n>>3)
    z = s + c
    #println("2nd reduce $(s>>253) $(c>>253) $(z>>253)")
    if (z > BigInt(2)^255 - 19)
        z -= BigInt(2)^255 - 19
    end
    return z
end

# Point addition of two points on twisted Edwards curve 25519 where
# points are represented with (x, y, t, z) cordinates. If affine variable
# is true, then the result is affinized i.e. z=1.
function point_add(x1,y1,t1,z1,x2,y2,t2,z2; affine=false)
    r1 = submod_25519(y1, x1)
    r2 = submod_25519(y2, x2)

    r3 = addmod_25519(y1, x1)
    r4 = addmod_25519(y2, x2)

    r5 = multmod_25519(r1, r2)
    r6 = multmod_25519(r3, r4)

    r7 = multmod_25519(t1, t2)
    r8 = multmod_25519(z1, z2)

    k = 16295367250680780974490674513165176452449235426866156013048779062215315747161

    r7 = multmod_25519(k, r7)
    r8 = addmod_25519(r8, r8)

    r1 = submod_25519(r6, r5)
    r2 = submod_25519(r8, r7)

    r3 = addmod_25519(r8, r7)
    r4 = addmod_25519(r6, r5)

    x3 = multmod_25519(r1, r2)
    y3 = multmod_25519(r3, r4)

    t3 = multmod_25519(r1, r4)
    z3 = multmod_25519(r2, r3)

    if (affine == true)
        zinv = invmod_25519_M(z3)
        x3 = multmod_25519(x3, zinv)
        y3 = multmod_25519(y3, zinv)
        t3 = multmod_25519(x3, y3)
        z3 = BigInt(1)
    end

    return (x3, y3, t3, z3)
end

# Load precomputed radix-16 lookup table of the scalar multiplications of
# the base point on curve25519.
# The nth line of each files is the x,y,t-coordinates of (i*16^j)B expressed in
# 64 hexadecimal digits where i = n %16, j = n ÷ 16 and B is the base point.
function load_precomp_points()
    fx=open("../x25519/x_precomp_32k.dat", "r")
    fy=open("../x25519/y_precomp_32k.dat", "r")
    ft=open("../x25519/t_precomp_32k.dat", "r")
    xlines=readlines(fx)
    ylines=readlines(fy)
    tlines=readlines(ft)
    global x_precomp_data=map(x -> parse(BigInt,x,base=16), xlines)
    global y_precomp_data=map(x -> parse(BigInt,x,base=16), ylines)
    global t_precomp_data=map(x -> parse(BigInt,x,base=16), tlines)
    close(fx)
    close(fy)
    close(ft)
end

# Scalar multiplication of the base point on curve25519 with the 16-radix
# precomputed lookup table of scalar multiplications.
# Call load_precomp_points() before calling this function.
function scalarmultB(k)
    kl = [(k >> (4*i)) % 16 for i in 0:63]

    # k0 B from lut
    px = x_precomp_data[1+kl[1]]
    py = y_precomp_data[1+kl[1]]
    pt = t_precomp_data[1+kl[1]]
    pz = BigInt(1)
    for i in 2:64
        # k_n 16^n B from lut
        qx = x_precomp_data[1+(i-1)*16+kl[i]]
        qy = y_precomp_data[1+(i-1)*16+kl[i]]
        qt = t_precomp_data[1+(i-1)*16+kl[i]]
	qz = BigInt(1)
        # p = p + q
        #println("$(i): px $(px) py $(py) qx $(qx) qy $(qy)")
	px, py, pt, pz = point_add(px, py, pt, pz, qx, qy, qt, qz,
                                   affine=(i == 64))
    end
    return px, py, pz
end

function modpow2_n(x, n)
    for _ in 1:n
      x = multmod_25519(x,x)
    end
    return x
end

# Modular inverse module 2^255-19 by the Fermat's little theorem
function invmod_25519(a)
      a2   = multmod_25519(a,a)
      a4   = multmod_25519(a2,a2)
      a8   = multmod_25519(a4,a4)
      a9   = multmod_25519(a8,a)
      a11  = multmod_25519(a9,a2)
      a22  = multmod_25519(a11,a11)
      a_2_5  = multmod_25519(a22,a9)
      b_2_10 = modpow2_n(a_2_5,5)
      a_2_10 = multmod_25519(b_2_10,a_2_5)
      b_2_20 = modpow2_n(a_2_10,10)
      a_2_20 = multmod_25519(b_2_20,a_2_10)
      b_2_40 = modpow2_n(a_2_20,20)
      a_2_40 = multmod_25519(b_2_40,a_2_20)
      b_2_50 = modpow2_n(a_2_40,10)
      a_2_50 = multmod_25519(b_2_50,a_2_10)
      b_2_100 = modpow2_n(a_2_50,50)
      a_2_100 = multmod_25519(b_2_100,a_2_50)
      b_2_200 = modpow2_n(a_2_100,100)
      a_2_200 = multmod_25519(b_2_200,a_2_100)
      b_2_250 = modpow2_n(a_2_200,50)
      a_2_250 = multmod_25519(b_2_250,a_2_50)
      b_2_255 = modpow2_n(a_2_250,5)
      inv = multmod_25519(b_2_255,a11)
      return inv
end

function even(x)
    return ((x & 1) == 0) ? true : false
end

function signbit(x)
    return (x < 0) ? 1 : 0
end

function bitsize(M)
    if (M < 0)
        M = -M
    end
    return length(string(M,base=2))
end

# Montgomery modular inverse.
# Input X in 1:M-1 and M
# Output Lrs in 1:M-1 where Lrs = Xinv 2^n mod M
#  if real_inverse is false, then n = bitsize(M) else n = 0
#  i.e. when real_inverse is true, return the real inverse instead of
#  the almost inverse.
function inv_montgomery(X, M; real_inverse=false)
    # Phase1
    k = -bitsize(M)
    Luv = BigInt(0)
    Ruv = BigInt(X) << 1
    Lrs = BigInt(0)
    Rrs = BigInt(1)
    Luv = (Luv>>1)+Ruv
    Ruv = M
    Lrs = Lrs + Rrs
    Rrs = 0
    while true
        # println("Luv $(Luv) Ruv $(Ruv) Lrs $(Lrs) Rrs $(Rrs) k $(k)")
        SLuv, SRuv = signbit(Luv), signbit(Ruv)
        if (even(Luv>>1))
            if Luv == 0 # SLuv == signbit(-Luv)
                break
            end
            Luv = Luv>>1
            Rrs = Rrs<<1
            k = k+1
        else
            tmpuv = Luv>>1
            tmprs = Lrs
            Lrs = Lrs + Rrs
            if (SLuv ⊻ SRuv) == 1
                Luv = (Luv>>1)+Ruv
            else
                Luv = (Luv>>1)-Ruv
            end
            k = k+1
            ctrl = ((~SLuv & ~SRuv) | (~SLuv & SRuv)) & 1
            if signbit(Luv) == ctrl
                Ruv = tmpuv
                Rrs = tmprs<<1
            else
                Rrs = Rrs<<1
            end
        end
    end
    # Here we have Lrs == M
    Lrs = Lrs-Rrs
    Rrs = M
    if signbit(Lrs) == 1
        Lrs = Lrs + Rrs
    end

    # Phase 2
    if (real_inverse)
        k = k + bitsize(M)
    end

    while (k != 0)
        # println("Phase2 Lrs $(Lrs) Rrs $(Rrs) k $(k)")
        k = k - 1
        if even(Lrs)
            Lrs = Lrs>>1
        else
            Lrs = (Lrs+Rrs)>>1
        end
    end
    return Lrs
end

# Modular inverse modulo 2^255-19 with Montgomery modular inverse.
function invmod_25519_M(x)
    P = BigInt(2)^255-19
    iM = inv_montgomery(x, P, real_inverse=true)
    # iM = inv_montgomery(x, P)
    # i2n = inverse of 2^255 mod P = inverse of 19 mod P
    # i2n = 21330121701610878104342023554231983025602365596302209165163239159352418617876
    # return multmod_25519(iM, i2n)
    return iM
end


"""
    LennardJones{T<:AbstractFloat} <: EmpiricalPotential{NamedTuple{(:ϵ, :σ)},NamedTuple{(:rcutoff,)}}

The Lennard-Jones potential is a simple two-body intermolecular potential with two typical parameters, primarily describing the interaction of neutral atoms. Formally, the interaction between two atoms at a distance, ``r``, is given by

```math
\\begin{equation}
V_{LJ}(r; \\epsilon, \\sigma, rcutoff, species) =
    \\begin{cases}
        0 & r > rcutoff \\\\
        4\\epsilon \\lbrack \\frac{\\sigma}{r})^{12} - (\\frac{\\sigma}{r})^6 \\rbrack & r < rcutoff.
    \\end{cases}
\\end{equation}
```

Users must supply two parameters, ``\\epsilon`` (units of energy), ``\\sigma`` (units of distance), and radial cutoff (units of distance).
"""
struct LennardJones{T<:AbstractFloat} <: EmpiricalPotential{NamedTuple{(:ϵ, :σ)},NamedTuple{(:rcutoff,)}}
    ϵ::T
    σ::T
    rcutoff::T
    species::Tuple
    function LennardJones(ϵ, σ, rcutoff, species)
        ϵ, σ, rcutoff = promote(austrip(ϵ), austrip(σ), austrip(rcutoff))
        new{typeof(rcutoff)}(ϵ, σ, rcutoff, Tuple(species))
    end
    function LennardJones{T}(; ϵ::T, σ::T, rcutoff::T, species::Tuple) where {T}
        new{T}(ϵ, σ, rcutoff, species)
    end
end

get_rcutoff(lj::LennardJones) = lj.rcutoff
get_species(lj::LennardJones) = lj.species

potential_energy(R::AbstractFloat, lj::LennardJones) = 4lj.ϵ * ((lj.σ / R)^12 - (lj.σ / R)^6)
force(R::AbstractFloat, r::SVector{3}, lj::LennardJones) = (24lj.ϵ * (2(lj.σ / R)^12 - (lj.σ / R)^6) / R^2)r


using LinearAlgebra
using RecipesBase

# polarization ellipse
@userplot PolEllipse

"""
    polellipse(S)
    polellipse!(S)

Plot the polarizatio ellipse for the given Stokes parameters. The polarization ellipse draws the path of the eletric field over one wavelength. The input vector should be a Stokes vector `[I, Q, U, V]`. This is a user recipe and can be composed with other plot recipes.
"""
polellipse

@recipe function f(pe::PolEllipse, N=1000)
    aspect_ratio --> 1
    xguide --> "x"
    yguide --> "y"
    alpha -->  range(0.2, 1, length=N)

    # extract elliptical params (a, b, θ) from Stokes vector
    stokes = first(pe.args)
    L = hypot(stokes[begin + 1], stokes[begin + 2])
    Ip = hypot(L, stokes[begin + 3])
    θ = 0.5 * atan(stokes[begin + 2], stokes[begin + 1])
    a = sqrt(0.5 * (Ip + L))
    b = sqrt(0.5 * (Ip - L))
    # draw ellipse
    t = range(0, 2π, length=N)
    x = @. a * cos(t) * cos(θ) - b * sin(t) * sin(θ)
    y = @. a * cos(t) * sin(θ) + b * sin(t) * cos(θ)
    return x, y
end


import StatsFuns.poispdf

seed!(12345)

function testPoisson(μ, low, high, N)
  function pmf(x::Int64)
    return poispdf(μ, x)
  end
  function sampler()
    return samplePoisson(μ)
  end
  return testGOFDiscrete(pmf, sampler, low, high, N)
end

@test testPoisson(3.0, 0, 12, 2^21) > 0.01
@test testPoisson(15.0, 5, 30, 2^21) > 0.01
@test testPoisson(50.0, 25, 80, 2^21) > 0.01


using SafeTestsets

@safetestset "ProspectTheory" begin
    # test values based on http://psych.fullerton.edu/mbirnbaum/calculators/cpt_calculator.htm
    using Test, UtilityModels
    import UtilityModels: compute_weights
    α = .8; β = .9; γg = .6; γl =.7; λ = 2.25
    model = ProspectTheory(;α, β, γg, γl, λ)
    p = [.3,.2,.3,.2]
    v = [10.0,3.0,-2.0,-1.0]
    gamble = Gamble(;p, v)
    eu = mean(model, gamble)
    @test eu ≈ .5672 atol = 1e-4
    ω = compute_weights(model, gamble)
    @test ω ≈ [0.1293,0.3281,0.0992,0.3165] atol = 1e-4

    α = 1.2; β = 1.5; γg = .8; γl = .9; λ = 1.5
    model = ProspectTheory(;α, β, γg, γl, λ)
    p = [.1,.2,.5,.2]
    v = [1.0,3.0,-5.0,-2.0]
    gamble = Gamble(;p, v)
    eu = mean(model, gamble)
    @test eu ≈ -8.1017 atol = 1e-4
    ω = compute_weights(model, gamble)
    @test ω ≈ [0.1811,0.4962,0.0848,0.2415] atol = 1e-4
end

@safetestset "TAX" begin
    # tested against http://psych.fullerton.edu/mbirnbaum/calculators/taxcalculator.htm
    # using gambles from Birnbaum 2008
    using Test, UtilityModels

    model = TAX()
    p = [.25,.25,.50]
    v = [100.0,0.0,-50.0]
    gamble = Gamble(;p, v)
    eu = mean(model, gamble)
    @test eu ≈ -15.5125 atol = 1e-4

    model = TAX()
    p = [.80,.10,.10]
    v = [110.0,44.0,40.0]
    gamble = Gamble(;p, v)
    eu = mean(model, gamble)
    @test eu ≈ 65.02513599372996 atol = 1e-8

    model = TAX()
    p = [.05,.05,.90]
    v = [96.0,12.0,3.0]
    gamble = Gamble(;p, v)
    eu = mean(model, gamble)
    @test eu ≈ 8.803653482548164 atol = 1e-8

    using Test, UtilityModels
    δ = 1.0; β = .9; γ = .70
    model = TAX(;δ, β, γ)
    p = [.05,.05,.90]
    v = [96.0,12.0,3.0]
    gamble = Gamble(;p, v)
    eu = mean(model, gamble)
    # calculator uses certainty equivalent
    @test (eu)^(1/β) ≈ 33.567163255247856 atol = 1e-8
end

@safetestset "ExpectedUtility" begin
    using Test, UtilityModels
    α = 1.0
    model = ExpectedUtility(α)
    p = [.3,.2,.3,.2]
    v = [10.0,3.0,-2.0,-1.0]
    gamble = Gamble(;p, v)
    eu = mean(model, gamble)
    @test eu ≈ p'*v atol = 1e-4

    α = .8
    model = ExpectedUtility(α)
    p = [.3,.2,.3,.2]
    v = [10.0,3.0,-2.0,-1.0]
    gamble = Gamble(;p, v)
    eu = mean(model, gamble)
    @test eu ≈ (sign.(v).*abs.(v).^α)'*p atol = 1e-4
end


using Plots
using LinearAlgebra

σ11 = σ22 = 1.0
σ12 = σ21 = -0.7

Σ = [
    σ11 σ12
    σ21 σ22
]

L = cholesky(Σ).L
cL = size(L, 2)

boxmuller(r1, r2) = √(-2 * log(r1)) * sin(2π * r2)
univariate(n...) = boxmuller.(rand(n...), rand(n...))

function multivariate(n)
    x = univariate(cL, n)
    return L * x
end

xy = multivariate(100)
scatter(xy[1, :], xy[2, :])


function solveKnapModel(data::KnapData, optimizer)
    I = collect(1:length(data.items))
    W = data.capacity
    w = [ item.weight for item in data.items ]
    p = [ item.profit for item in data.items ]

    model = Model(optimizer)

    @variable(model, x[j in I], Bin)
    @objective(model, Max, sum(p[j] * x[j] for j in I))
    @constraint(model, sum(w[j] * x[j] for j in I) <= W)

    optimize!(model)
    if termination_status(model) == MOI.OPTIMAL
        result = [ j for j in I if value(x[j]) > EPS ]
        return round(Int64, objective_value(model)), result
    end
end


# This file is part of the IntervalArithmetic.jl package; MIT licensed

sinh(a::Interval) = @round(sinh(a.lo), sinh(a.hi))

cosh(a::Interval) = @round(cosh(mig(a)), cosh(mag(a)))

tanh(a::Interval) = sinh(a) / cosh(a)

asinh(a::Interval{T}) where T = log(a + sqrt(sqr(a) + one(a)))

function acosh(a::Interval{T}) where T
    domain = unsafe_interval(one(T), T(Inf))
    return acosh_inner(a ∩ domain)
end
acosh_inner(a::Interval) = log(a + sqrt(a - one(a)) * sqrt(a + one(a)))

function atanh(a::Interval{T}) where T
    domain = unit_ball(T)
    return atanh_inner(a ∩ domain)
end
function atanh_inner(a::Interval{T}) where T
    abs(a) == one(a) && return emptyinterval(a)
    return unsafe_atomic(T, 0.5) * (log1p(a) - log1p(-a))
end

acoth(a::Interval) = atanh(inv(a))

coth(a::Interval) = inv(tanh(a))

csch(a::Interval) = inv(sinh(a))

sech(a::Interval) = inv(cosh(a))

for f in (:sinh, :cosh, :tanh, :asinh, :acosh, :atanh, :coth, :csch, :sech,
          :atanh_inner, :acosh_inner, :acoth)
    @eval ($f)(x::EmptyInterval) = x
end


function xavier(in_dim, out_dim)
    return randn(in_dim, out_dim) ./ sqrt(in_dim)
end

function he(in_dim, out_dim)
    return randn(in_dim, out_dim) ./ sqrt(2 / in_dim)
end


using Distributions
using LinearAlgebra
using Plots

#This file is simply for visualizing the behavior of various kernel functions

function GaussianKernel(x, y, θ::Float64=1.0)
    x = vec(x)
    y = vec(y)
    M = 1/sqrt(2*pi)*exp.(-θ*.5*(x.-y').^2)
    return M
end

function generateSample(n, θ)
    x = collect(0:1/n:1-1/n)
    K = GaussianKernel(x, x, θ) + UniformScaling(1e-8) #add diagonal terms to ensure PD-ness
    C = cholesky(K)
    z = C.L*(randn(n))
    return x, z
end

x1, z1 = generateSample(10, 50.0)
x2, z2 = generateSample(50, 50.0)
x3, z3 = generateSample(150, 50.0)
x4, z4 = generateSample(30, 50.0)

display(plot(x1, z1, seriestype = :scatter))
display(plot!(x2, z2, seriestype = :scatter))
display(plot!(x3, z3, seriestype = :scatter))
display(plot!(x4, z4, seriestype = :scatter))


using SuiteSparseGraphBLAS
function triangle_centrality1(A)
    T = mul(A, A', (+, *), mask=A)
    y = reduce(+, T, dims=2)
    k = reduce(+, y)
    return (3 * mul(A, y) - 2 * mul(T, y) .+ y) ./ k
end

function triangle_centrality2(A)
    T = mul(A, A', (+, pair), mask=A, Descriptor(structural_mask=true))
    y = reduce(+, T, dims=2)
    k = reduce(+, y)
    return map(*, eadd(eadd(3.0 * mul(A, y), 2.0 * mul(T, y, (+, second)), -), y), 1/k)
end


# Anthony Przybylski, Xavier Gandibleux, Matthias Ehrgott.
# Two phase algorithms for the bi-objective assignment problem.
# European Journal of Operational Research, Vol. 185, Iss. 2, pp. 509–533, 2008.
# http://www.sciencedirect.com/science/article/pii/S0377221707000781

# Anthony Przybylski, Xavier Gandibleux, Matthias Ehrgott.
# A two phase method for multi-objective integer programming
# and its application to the assignment problem with three objectives.
# Discrete Optimization, Vol. 7, Iss. 3, pp. 149–165, 2010.
# http://www.sciencedirect.com/science/article/pii/S1572528610000125

using NSGAIII

const C1 = [2 5 4 7 ; 3 3 5 7 ; 3 8 4 2 ; 6 5 2 5]
const C2 = [3 3 6 2 ; 5 3 7 3 ; 5 2 7 4 ; 4 6 3 5]
const C3 = [4 2 5 3 ; 5 3 4 3 ; 4 3 5 2 ; 6 4 7 3]

z(x, C) = sum(inds->C[inds...], enumerate(x))
z(x::Vector{Int}) = z(x, C1), z(x, C2), z(x, C3)

res = nsga(200, 50, ()->randperm(4), z)

for x in unique(res)
    println(x)
end


include("../src/tenedyn_module.jl")
include("../../__lib__/math/ode/src/ode_module.jl")

module modeldef
using ..tenedyn

function create_model_pi_controller_pt1(
    setpoint_amplitude,
    setpoint_time_delay,
    ctrl_kp,
    ctrl_ki,
    ctrl_integral_init_value,
    plant_T,
    plant_integral_init_value)

    # model definition
    setpoint = tenedyn.Step(setpoint_amplitude, setpoint_time_delay)

    ctrl_minus      = tenedyn.Minus()
    ctrl_prop_gain  = tenedyn.Gain(ctrl_kp)
    ctrl_int_gain   = tenedyn.Gain(ctrl_ki)
    ctrl_plus       = tenedyn.Plus()
    ctrl_integrator = tenedyn.Integrator(ctrl_integral_init_value)

    plant_pt1_minus      = tenedyn.Minus()
    plant_pt1_mult1T     = tenedyn.Gain(1/plant_T)
    plant_pt1_integrator = tenedyn.Integrator(plant_integral_init_value)

    sensor = tenedyn.Identity()

    model = tenedyn.Model()

    # add elements to the model
    push!(model, setpoint,              "ref")                  # 1

    push!(model, ctrl_minus,            "ctrl_minus")           # 2
    push!(model, ctrl_prop_gain,        "ctrl_prop_gain")       # 3
    push!(model, ctrl_int_gain,         "ctrl_int_gain")        # 4
    push!(model, ctrl_plus,             "ctrl_plus")            # 5
    push!(model, ctrl_integrator,       "ctrl_integrator")      # 6

    push!(model, plant_pt1_minus,       "plant_pt1_minus")      # 7
    push!(model, plant_pt1_mult1T,      "plant_pt1_mult1T")     # 8
    push!(model, plant_pt1_integrator,  "plant_pt1_integrator") # 9

    push!(model, sensor,                "sensor")               # 10

    # topology
    tenedyn.connect!(model, setpoint.y,             ctrl_minus.x1)

    tenedyn.connect!(model, ctrl_minus.y,           ctrl_prop_gain.x)
    tenedyn.connect!(model, ctrl_prop_gain.y,       ctrl_plus.x1)

    tenedyn.connect!(model, ctrl_minus.y,           ctrl_int_gain.x)
    tenedyn.connect!(model, ctrl_int_gain.y,        ctrl_integrator.x)
    tenedyn.connect!(model, ctrl_integrator.y,      ctrl_plus.x2)

    tenedyn.connect!(model, ctrl_plus.y,            plant_pt1_minus.x1)
    tenedyn.connect!(model, plant_pt1_integrator.y, plant_pt1_minus.x2)

    tenedyn.connect!(model, plant_pt1_minus.y,      plant_pt1_mult1T.x)
    tenedyn.connect!(model, plant_pt1_mult1T.y,     plant_pt1_integrator.x)

    tenedyn.connect!(model, plant_pt1_integrator.y, sensor.x)
    tenedyn.connect!(model, sensor.y,               ctrl_minus.x2)

    return model
end

function create_model_pi_controller_pt2(
    setpoint_time,
    setpoint_value,
    ctrl_kp,
    ctrl_ki,
    ctrl_integral_init_value,
    plant_T,
    plat_D,
    plant_x_integral_init_value,
    plant_v_integral_init_value)

    # model definition
    # setpoint = tenedyn.Step(setpoint_amplitude, setpoint_time_delay)
    setpoint = tenedyn.Curve(setpoint_time, setpoint_value)

    ctrl_minus      = tenedyn.Minus()
    ctrl_prop_gain  = tenedyn.Gain(ctrl_kp)
    ctrl_int_gain   = tenedyn.Gain(ctrl_ki)
    ctrl_plus       = tenedyn.Plus()
    ctrl_integrator = tenedyn.Integrator(ctrl_integral_init_value)

    plant_pt2_minus      = tenedyn.Minus()
    plant_pt2_plus       = tenedyn.Plus()

    plant_pt2_mult1T2    = tenedyn.Gain(1/plant_T^2)
    plant_pt2_mult2D1T   = tenedyn.Gain(-2 * plat_D / plant_T)

    plant_pt2_v_integrator = tenedyn.Integrator(plant_v_integral_init_value)
    plant_pt2_x_integrator = tenedyn.Integrator(plant_x_integral_init_value)

    sensor = tenedyn.Identity()

    model = tenedyn.Model()

    # add elements to the model
    push!(model, setpoint,                "ref")                      # 1

    push!(model, ctrl_minus,              "ctrl_minus")               # 2
    push!(model, ctrl_prop_gain,          "ctrl_prop_gain")           # 3
    push!(model, ctrl_int_gain,           "ctrl_int_gain")            # 4
    push!(model, ctrl_plus,               "ctrl_plus")                # 5
    push!(model, ctrl_integrator,         "ctrl_integrator")          # 6

    push!(model, plant_pt2_minus,         "plant_pt2_minus")          # 7
    push!(model, plant_pt2_plus,          "plant_pt2_plus")           # 8
    push!(model, plant_pt2_mult1T2,       "plant_pt2_mult1T2")        # 9
    push!(model, plant_pt2_mult2D1T,      "plant_pt2_mult2D1T")       # 10
    push!(model, plant_pt2_v_integrator,  "plant_pt2_v_integrator")   # 11
    push!(model, plant_pt2_x_integrator,  "plant_pt2_x_integrator")   # 12

    push!(model, sensor,                  "sensor")                   # 13

    # topology
    tenedyn.connect!(model, setpoint.y,                 ctrl_minus.x1)

    tenedyn.connect!(model, ctrl_minus.y,               ctrl_prop_gain.x)
    tenedyn.connect!(model, ctrl_prop_gain.y,           ctrl_plus.x1)
    tenedyn.connect!(model, ctrl_minus.y,               ctrl_int_gain.x)
    tenedyn.connect!(model, ctrl_int_gain.y,            ctrl_integrator.x)
    tenedyn.connect!(model, ctrl_integrator.y,          ctrl_plus.x2)

    tenedyn.connect!(model, ctrl_plus.y,                plant_pt2_minus.x1)

    tenedyn.connect!(model, plant_pt2_x_integrator.y,   plant_pt2_minus.x2)
    tenedyn.connect!(model, plant_pt2_minus.y,          plant_pt2_mult1T2.x)
    tenedyn.connect!(model, plant_pt2_mult1T2.y,        plant_pt2_plus.x1)
    tenedyn.connect!(model, plant_pt2_plus.y,           plant_pt2_v_integrator.x)
    tenedyn.connect!(model, plant_pt2_v_integrator.y,   plant_pt2_mult2D1T.x)
    tenedyn.connect!(model, plant_pt2_mult2D1T.y,       plant_pt2_plus.x2)
    tenedyn.connect!(model, plant_pt2_v_integrator.y,   plant_pt2_x_integrator.x)

    tenedyn.connect!(model, plant_pt2_x_integrator.y,   sensor.x)

    tenedyn.connect!(model, sensor.y,                   ctrl_minus.x2)

    return model
end

end

module modelsim
using ..tenedyn
using ..ode
using ..modeldef

function simulate(
    ode_fcn,
    init_cond;
    time_interval = ode.TimeInterval(1.0),
    solver = ode.Tsitouras45(),
    abs_tol = 1e-5,
    rel_tol = 1e-5,
    max_iter = 1000,
    step_size_controller = ode.ClassicalStepSizeControl(),
    max_step_size = 0.1)

    ode_problem = ode.OdeProblem(
        ode_fcn,
        time_interval,
        init_cond)

    basic_options = ode.BasicOdeOptions(
        ode_problem;
        abs_tol = abs_tol,
        rel_tol = rel_tol,
        max_iter = max_iter)

    stepper_options = ode.StepperOptions(;
        max_step_size = max_step_size)

    @time (ode_res, step_stat) = ode.solve(
        ode_problem,
        solver;
        basic_options        = basic_options,
        step_size_controller = step_size_controller,
        stepper_options      = stepper_options) #

    return (ode_res, step_stat)
end

end

module odeplot
using ..ode
using ..modelsim

using PyPlot
PyPlot.pygui(true)

function plot_ode_res(ode_res, indices = (1,))

    t = ode.time(ode_res)
    q = ode.variables(ode_res)

    L = length(indices)

    figh = PyPlot.figure()

    a1 = PyPlot.subplot(L, 1, 1)
    PyPlot.grid()
    PyPlot.plot( t, q[indices[1], :] )

    for ii = 2:L
        PyPlot.subplot(L, 1, ii, sharex = a1)
        PyPlot.grid()
        PyPlot.plot( t, q[indices[ii], :] )
    end

    return figh
end

end

module simpipeline
using ..ode
using ..tenedyn

using ..modeldef
using ..modelsim
using ..odeplot

function create_parametrized_model_pi_controller_pt1()
    # model parameters
    setpoint_amplitude  = 2.0
    setpoint_time_delay = 0.5

    ctrl_kp = 10.0
    ctrl_ki = 18.0
    ctrl_integral_init_value = 0.0

    plant_T = 0.2
    plant_integral_init_value = 0.5

    ode_fcn = tenedyn.create_ode_function_pipeline(
        modeldef.create_model_pi_controller_pt1,
        setpoint_amplitude,
        setpoint_time_delay,
        ctrl_kp,
        ctrl_ki,
        ctrl_integral_init_value,
        plant_T,
        plant_integral_init_value)

    return ode_fcn
end

function run_simulation_pt1(ode_fcn)

    init_cond = [0.0, 0.0]

    (ode_res, step_stat) = modelsim.simulate(
            ode_fcn,
            init_cond;
            time_interval = ode.TimeInterval(5.0),
            abs_tol = 1e-7,
            rel_tol = 1e-7,
            max_iter = 1000)

    @show step_stat

    odeplot.plot_ode_res(ode_res, (1,2))

    return ode_res
end

function create_parametrized_model_pi_controller_pt2()
    # model parameters
    setpoint_amplitude  = 2.0
    setpoint_time_delay = 0.5

    setpoint_time  = [0.0, 0.5, 0.501, 1.0, 3.0, 3.01, 4.0]
    setpoint_value = [0.0, 0.0, 2.0,   2.0, 5.0, 0.0,  0.0]

    ctrl_kp = 3.0
    ctrl_ki = 15.0
    ctrl_integral_init_value = 0.0

    plant_T = 0.05
    plant_D = 0.35

    plant_integral_x_init_value = 0.0
    plant_integral_y_init_value = 0.0

    ode_fcn = tenedyn.create_ode_function_pipeline(
        modeldef.create_model_pi_controller_pt2,
        setpoint_time,
        setpoint_value,
        ctrl_kp,
        ctrl_ki,
        ctrl_integral_init_value,
        plant_T,
        plant_D,
        plant_integral_x_init_value,
        plant_integral_y_init_value)

    return ode_fcn
end

function run_simulation_pt2(ode_fcn)

    init_cond = [0.0, 0.0, 1.0]

    (ode_res, step_stat) = modelsim.simulate(
            ode_fcn,
            init_cond;
            time_interval = ode.TimeInterval(15.0),
            abs_tol = 1e-6,
            rel_tol = 1e-6,
            max_iter = 10000)

    @show step_stat

    odeplot.plot_ode_res(ode_res, (1, 2, 3))

    return ode_res
end

# ode_fcn = create_parametrized_model_pi_controller_pt1()
# ode_res = run_simulation_pt1(ode_fcn)

ode_fcn = create_parametrized_model_pi_controller_pt2()
ode_res = run_simulation_pt2(ode_fcn)

end


function rhs!(dxyz::Array{T,1},x::T,y::T,z::T,ρ::T,β::T,s_inv::T) where {T<:AbstractFloat}
    @inbounds dxyz[1] = y-x
    @inbounds dxyz[2] = x*(ρ-z*s_inv) - y
    @inbounds dxyz[3] = x*(y*s_inv) - β*z
    return dxyz
end


using NLsolve, Distributions

@doc raw"""
	CapitalAdjustModel(; kwargs...)

Construct capital investment model with one type of capital and one
productivity shock and convex adjustment costs. The recursive formulation is:

```math
\begin{equation*}
V(K,z) = \max_a K^\alpha e^z - i K - \frac{\gamma}{2} i^2 K + \beta \mathbb{E} V(K', z') \\
i \equiv \frac{a}{K} - (1-\delta) \\
\text{where } K' = a \\
z' = \rho z + \sigma \varepsilon, \quad \varepsilon \sim \mathcal{N}(0,1)
\end{equation*}
```

The firm solves the following problem to initialize at t=0:

```math
\begin{equation*}
V_0(z_0) = \max_{K_0} V(K_0,z_0) - (1 + C0) K_0 \\
\text{where } z_0 = \sqrt{\frac{\sigma^2}{1-\rho^2}} ε_0 \\
ε_0 \sim \mathcal{N}(0,1)
\end{equation*}
```

Example:

```julia
using StructuralDDP
prob = CapitalAdjustModel(nK=150, nz=15, ρ=0.5, σ=0.3, γ=2.)
```

"""
function CapitalAdjustModel(;
	α = 0.67,
	β = 0.9,
	ρ = 0.6,
	σ = 0.3,
	δ = 0.15,
	γ = 2.0,
	C0 = 0.6,
	F = 0.,

	nK = 100,
	nz = 20,

	# options for testing
	intdim = :Separable_ExogStates,
	initialize_exact = true)

    # calculate Amin, Amax
    stdz = sqrt(σ^2/(1-ρ^2))
    minz = -3*stdz
    maxz =  3*stdz
    vz = collect(LinRange(minz, maxz, nz))

    function steadystate(z)
        # check https://www3.nd.edu/~esims1/investment_notes.pdf

        function f2!(F,x)
            (V_K, K_log) = x

            F[1] = - 1 - γ + V_K
            F[2] = V_K - β*( exp(z)*α*exp(K_log)^(α-1) + (1-δ)*V_K)
        end

        return nlsolve(f2!, [1.1; 1.])
    end

    K_ss = exp(steadystate(stdz^2/2).zero[2])
    minK_log = steadystate(-2*stdz).zero[2]
    maxK_log = steadystate(3*stdz).zero[2]
    vK   = exp.(collect(LinRange(minK_log, maxK_log, nK)))

    tStateVectors = (vK, vz) # tuple of basis vectors

	function reward(vStates, vChoices)
	    K, z = vStates
	    Kprime = vChoices[1]
		i = Kprime/K - (1-δ)
		action = Kprime != K
	    return K^α * exp(z) - i*K - F*K*action - γ/2 * i^2 * K
	end

	# including output, i.e. for partial rewardfunc
	function reward(partial_reward, vStates, vChoices)
	    K = vStates[1]
		Kprime = vChoices[1]
	    i = Kprime/K - (1-δ)
		action = Kprime != K
	    return partial_reward - i*K - F*K*action - γ/2 * i^2 * K
	end

	reward_partial(vStates) = vStates[1]^α * exp(vStates[2])

	if intdim == :All

	    transfunc = function mytransfunc(vStates, K, Shock)
	        z = vStates[2]
	        zprime  = ρ*z + σ * Shock;
	        return K, zprime
	    end
		tChoiceVectors = (vK,)

	elseif intdim == :Separable

	    transfunc = function mytransfunc1(vStates, vChoices, Shock)
	        z = vStates[2]
	        zprime  = ρ*z + σ * Shock;
	        return zprime
	    end
		tChoiceVectors = (1,)

	elseif intdim == :Separable_States
	    transfunc = function mytransfunc2(vStates, Shock)
	        z = vStates[2]
	        zprime  = ρ*z + σ * Shock;
	        return zprime
	    end
		tChoiceVectors = (1,)

	elseif intdim == :Separable_ExogStates
	    transfunc = function mytransfunc3(vExogStates, shock)
	        z = vExogStates[1]
	        zprime  = ρ*z + σ * shock;
	        return zprime
	    end
		tChoiceVectors = (1,)
	else

		error("$intdim wrong intdim")
	end

	if initialize_exact
		tChoiceVectorsZero = (1,)

		initializationproblem(value, choice) = value - (1 + (1-β)/β + C0) * choice

		function initialize(shock)
		    z0 = shock * sqrt(σ^2 / (1-ρ^2))
			return z0
		end
		shockdist_initial = Normal()
	else
		tChoiceVectorsZero = nothing
		initializationproblem = nothing
        initialize = nothing
		shockdist_initial = nothing
	end

	DDP(tStateVectors,
        tChoiceVectors,
        reward,
        transfunc,
		Normal(), # give distribution of shocks: standard normal
		β;
		intdim = intdim,
        rewardfunc_partial = reward_partial,
        initializationproblem = initializationproblem,
        initializefunc = initialize,
		shockdist_initial = shockdist_initial,
		tChoiceVectorsZero = tChoiceVectorsZero,
        )
end


commands = readlines("day14/input.txt")

function mask_bits(mask, value)
    value_bits = bitstring(value)
    out_bitstring = []
    for i in 0:35
        if mask[end-i] == 'X'
            pushfirst!(out_bitstring, value_bits[end-i])
        else
            pushfirst!(out_bitstring, mask[end-i])
        end
    end
    return parse(Int, join(out_bitstring); base=2)
end

current_mask = ""
memory = Dict{Int, Int}()
for instruction in commands
    if contains(instruction, "[")
        address, value = match(r"mem\[(\d+)\] = (\d+)", instruction).captures
        global memory[parse(Int, address)] = mask_bits(current_mask, parse(Int, value))
    else
        global current_mask = match(r"mask = (\w+)", instruction).captures[1]
    end
end

println(sum(v for v in values(memory)))

# That was fun! Ending with a sub 1k place too!


using DelimitedFiles

export fuel_required, fuel_required_including_fuel

fuel_required(mass) = clamp(mass ÷ 3 - 2, 0, typemax(Int))

function fuel_required_including_fuel(mass)
    fuel = fuel_required(mass)
    fuel == 0 ? 0 : fuel + fuel_required_including_fuel(fuel)
end

d01_inputpath = joinpath(@__DIR__, "input.txt")
d01_inputread(path) = vec(readdlm(path, '\n', Int))
d01_answer1(input) = sum(fuel_required, input)
d01_answer2(input) = sum(fuel_required_including_fuel, input)


"""
    stream_velocity(r, horseshoes, Γs, V, Ω)

Evaluate the total induced velocity at a point `r` given Horseshoes, vortex strengths `Γ`s, rotation rates `Ω`, and freestream flow vector `freestream` in the global reference frame.
"""
stream_velocity(r, horseshoes, Γs, V, Ω) = sum(x -> velocity(r, x[1], x[2], V / norm(V)), zip(horseshoes, Γs)) + V + Ω × r

"""
    streamlines(point, freestream :: Freestream, horseshoes, Γs, length, num_steps)

Compute the streamlines from a given starting point, a Freestream, Horseshoes and their associated strengths Γs with a specified length of the streamline and number of evaluation points.
"""
function streamlines(point, V, Ω, horseshoes, Γs, length, num_steps :: Integer)
    streamlines = fill(point, num_steps)
    cuck(x) = stream_velocity(x, horseshoes, Γs, V, Ω)
    for i ∈ 2:num_steps
        update = cuck(streamlines[i-1])
        streamlines[i] = streamlines[i-1] + (update / norm(update) * length / num_steps)
    end
    streamlines
end


# tests for ApproxManifoldProducts.jl

using ApproxManifoldProducts
using Test

include("basics.jl")

include("ex_1D.jl")

include("ex_2D_rot.jl")

include("testManiProductBigSmall.jl")

include("testBasicManiProduct.jl")

#


#module Scaling

export NiceScale, scaled

floorx(x) = x<0 ? ceil(x) : floor(x)
ceilx(x) = x<0 ? floor(x) : ceil(x)

mutable struct NiceScale
  minPoint::Float32
  maxPoint::Float32
  maxTicks::Float32
  tickSpacing::Float32
  range::Float32
  niceMin::Float32
  niceMax::Float32
  xfact::Float32
  xoffset::Float32

function NiceScale(min::Real,max::Real; padding=0.05,steps=10.)
     mn = min-padding*(max-min)
     mx = max+padding*(max-min)
     if mx==mn  mn=mn-1; mx=mx+1; end
     a=new(mn,mx,steps,0.,0.,0.,0.)
     a.range = niceNum(a.maxPoint-a.minPoint)
     a.tickSpacing = niceNum( a.range / (a.maxTicks-1),true)
     a.niceMin = ceil(a.minPoint / a.tickSpacing ) * a.tickSpacing
     a.niceMax = floor(a.maxPoint / a.tickSpacing ) * a.tickSpacing
     a.xoffset = (a.minPoint+a.maxPoint)/2
     a.xfact   = (a.maxPoint-a.minPoint)
     return a
end

end # NiceScale

Base.iterate(n::NiceScale,v=1) = v>n.niceMax ? nothing : (v,v+n.tickSpacing)
# Base.start(n::NiceScale) = n.niceMin
# Base.next(n::NiceScale,v) = (v,v+n.tickSpacing)
# Base.done(n::NiceScale,v) = v>n.niceMax
Base.length(n::NiceScale) = Int64((n.niceMax-n.niceMin)/n.tickSpacing)+1

function niceNum(range::Real,round::Bool=false)
   exponent = floor(log10(range))
   fraction = range / 10^exponent

   if round
     if fraction <= 1.5 niceFraction = 1
     elseif fraction <= 3 niceFraction = 2
     elseif fraction <= 7 niceFraction = 5
     else niceFraction = 5
     end
   else
     if fraction <= 1 niceFraction = 1
     elseif fraction <= 2 niceFraction = 2
     elseif fraction <= 5 niceFraction = 5
     else niceFraction = 10
     end
   end

   return niceFraction * 10.0^exponent

end

scaled(x::Real,n::NiceScale) = (x-n.xoffset)/n.xfact

#end # Scaling


"""
SamplerState for the stochastic gradient Nosé-Hoover thermostat algorithm following

Ding, N., Fang, Y., Babbush, R., Chen, C., Skeel, R. D., & Neven, H. (2014). Bayesian Sampling Using Stochastic Gradient Thermostats. In Advances in Neural Information Processing Systems (pp. 3203–3211).

and

Leimkuhler, B., & Shang, X. (2016). Adaptive Thermostats for Noisy Gradient Systems. SIAM Journal on Scientific Computing, 38(2), A712–A736.

Fields:

    - x: the current state
    - p: current momentum
    - xi: thermostat variable
    - t: iterations counter
    - stepsize: s.stepsize(s.t) gives the stepsize to be used in iteration s.t
    - mu: Constant in the thermostat equation
    - D: friction parameters
Constructors:

    - construct using `SGNHTState(x,p,stepsize::Function;mu=1.,D=1.0)` or `SGNHTState(x,p,stepsize::Float64;mu=1.,D=1.0)` for a fixed stepsize if p is not specified then          p = randn(length(x))

"""
type SGNHTState <: SamplerState
    x::Array{Float64,1}
    p::Array{Float64,1}
    xi::Array{Float64,1}

    t::Int
    stepsize::Function
    mu::Float64
    D::Float64
    function SGNHTState(x,p,stepsize::Function;mu=1.,D=1.0)
        @assert size(x) == size(p)
        xi = D
        new(x,p,xi,0,stepsize,mu,D)
    end
    function SGNHTState(x,p,stepsize::Float64;mu=1.,D=1.0)
        @assert size(x) == size(p)
        xi = D
        s(niters) = stepsize
        new(x,p,xi,0,s,mu,D)
    end
    function SGNHTState(x,stepsize::Function;mu=1.,D=1.0)
        p = randn(length(x))
        xi = [D]
        new(x,p,xi,0,stepsize,mu,D)
    end
    function SGNHTState(x,stepsize::Float64;mu=1.,D=1.0)
        p = randn(length(x))
        xi = [D]
        s(niters::Int) = stepsize
        new(x,p,xi,0,s,mu,D)
    end

end

"""
sample! performs one SGNHT update and returns the updated SGNHTState.
"""
function sample!(s::SGNHTState,grad::Function)
    # increment iterations counter
    s.t += 1

    # improve readability
    nparams = length(s.x)
    stepsize = s.stepsize(s.t)
    x = s.x
    p = s.p
    xi = s.xi
    D = s.D
    mu = s.mu

    # second order symmetric splitting scheme from Leimkuhler et al.
    add_inplace!(.5*stepsize,grad(x),p)
    add_inplace!(.5*stepsize,p,x)
    xi[:] += .5*stepsize/mu * (sum(p.*p) - nparams)
    if abs(stepsize*xi)[1] > 1e-6
    p[:] = exp(-stepsize*xi).*p + sqrt(.5*(1-exp(-2.0*stepsize.*xi))./xi * D) .* randn(nparams)
    else
    add_inplace!(sqrt(stepsize*D), randn(nparams),p)
    end
    xi[:] += .5*stepsize/mu * (sum(p.*p) - nparams)
    add_inplace!(.5*stepsize,p,x)
    add_inplace!(.5*stepsize,grad(x),p)
    s
end


using Random
using LinearAlgebra

export spqr_ssi

#==
SPQR_SSI block power method or subspace iteration applied to inv(R)
 to estimate rank, smallest singular values, and left/right singular vectors.

 [U, S, V, stats] = spqr_ssi(R, opts...)

 Uses the block power method or subspace iteration applied to the inverse of
 the triangular matrix R (implicitly) to estimate the numerical rank, smallest
 singular values, and left and right singular vectors R.  The algorithm will
 be efficient in the case the nullity of R is relatively low and inefficient
 otherwise.

 Input:

   R -- an n by n nonsingular triangular matrix
   opts (optional) -- see 'help spqr_rank_opts' for details.

 Output:

   U  -- n by k matrix containing estimates of the left singular vectors of R
       corresponding to the singular values in S. When stats.flag is 0 (and
       when the optional parameter opts.nsvals_large has its default value 1),
       r = n - k + 1 is the estimated rank of R. Also NT=U(:,2:k) is an
       orthonormal basis for the numerical null space of R'.
   S -- A k by k diagonal matrix whose diagonal entries are the estimated
       smallest k singular values of R, with k as described above.  For i=1:k,
       S(i,i) is an an estimate of singular value (r + i - 1) of R.
       Note that, unless stats.flag = 3 (see below), S(1,1)) > tol and for
       i =2:k, S(i,i) <= tol.
   V  -- n by k matrix containing estimates of the right singular vectors of R
       corresponding to the singular values in S.   When stats.flag is 0,
       N=V(:,2:k) is an orthonormal basis for the numerical null space of R.
   stats -- statistics, type 'help spqr_rank_stats' for details.

 Note that U' * R = S * V' (in exact arithmetic) and that R * V is
 approximately equal to U * S.

 output (for one or two output arguments): [s,stats] = spqr_ssi (...)
    s -- diag(S), with S as above.

 Example:
    R = sparse(matrixdepot("kahan", 100))
    U,S,V = spqr_ssi(R)
    norm_of_residual = opnorm( U' * R - diagm(S) * V' ) # should be near zero
     or
    U,S,V,stats = spqr_ssi(R, nothing, stats)
    N = V(:,2:end)   # orthonormal basis for numerical null space of R
    NT = U(:,2:end)  # orthonormal basis for numerical null space of R'
    norm_R_times_N = opnorm(R*N)
    norm_R_transpose_times_NT = opnorm(R'*NT)
     or
    opts = struct('tol',1.e-5,'nsvals_large',3)
    s,stats = spqr_ssi(R, opts, stats)
    stats  # information about several singular values

 See also spqr_basic, spqr_null, spqr_pinv, spqr_cod.

 Copyright 2012, Leslie Foster and Timothy A Davis.

 Outline of algorithm:
    let b = initial block size
    U = n by b random matrix with orthonormal columns
    repeat
       V1 = R \ U
       V,D1,X1 = svd(V1) = the compact svd of V1
       U1 = R' \ V
       U,D2,X2 = svd(U1) = the compact svd of U1
       sdot = diag(D2)
       s = 1 ./ sdot = estimates of singular values of R
       if s(end) <= tol
          increase the number of columns of U and repeat loop
       else
          repeat loop until stopping criteria (see code) is met
       end
    end
    k = smallest i with  sdot(i) > tol
    V = V*X2 (see code)
    V = first k columns of V in reverse order
    U = first k columns of U in reverse order
    s = 1 ./ (first k entries in sdot in reverse order)
    S = diag(s)

    Note for i = 1, 2, ..., k, S(i,i) is an upper bound on singular
    value (r + i - 1) of R in exact arithmetic but not necessarily
    in finite precision arithmetic.
==#

"""
start block power method to estimate the smallest singular
     values and the corresponding right singular vectors (in the
     r by nblock matrix V) and left singular vectors (in the r by
     nblock matrix U) of the n by n matrix R
"""
function spqr_ssi(R::AbstractMatrix{T}; nargout=4, opts...) where T<:Number
        ##      get_details::Int = 1,   # 0: basic statistics (default)
        # 1: detailed statistics:  basic stats, plus input options, time taken by
        #    various phases, statistics from spqr and spqr_rank subfunctions called,
        #    and other details.  Normally of interest only to the developers.
        # 2: basic statistics and a few additional statistics.  Used internally
        #    by some routines to pass needed information.
        ##      repeatable::Bool = true, # by default, results are repeatable
        ##      tol::Real = -1.0,        # a negative number means the default
        # tolerance should be computed
        ##      tol_norm_type::Int = 2, # 1: use opnorm(A, 1) to compute the default tol
                                        # 2: use normest(A, 0.01)
        ##      nsvals_large::Int = 1,  # default number of large singular values to estimate
        ##      min_block::Int = 3,     #
        ##      max_block::Int = 10,    #
        ##      min_iters::Int = 3,     #
        ##      max_iters::Int = 100,   #
        ##      nblock_increment = 5,   #
        ##      convergence_factor::Real = 0.1  #

    start_tic = time_ns()
    opts, get_details, repeatable, min_iters, max_iters, convergence_factor,
        nsvals_large, min_block, max_block, nblock_increment =
    get_opts(opts, :get_details, :repeatable, :ssi_min_iters, :ssi_max_iters,
                 :ssi_convergence_factor, :nsvals_large,
                 :ssi_min_block, :ssi_max_block, :ssi_nblock_increment)

    opts, tol, normest_R = get_tol_norm(opts, R)

    println("ssi(max_block=$max_block)")

    m, n = size(R)
    if m != n
        error("R must be square")
    end

    private_stream = repeatable ? MersenneTwister(1) : Random.GLOBAL_RNG

    #-------------------------------------------------------------------------------
    # adjust the options to reasonable ranges
    #-------------------------------------------------------------------------------

    # cannot compute more than n large singular values, where R is n-by-n
    nsvals_large = min(nsvals_large, n)

    # make the block size large enough so that there is a possiblity of
    # calculating all nsvals_large singular values
    max_block = max(max_block, nsvals_large)

    # max_block cannot be larger than n
    max_block = min(max_block, n)

    # min_block cannot be larger than n
    min_block = min(min_block, n)

    # start with nblock = min_block
    nblock = min_block
    opts = merge_opts(opts, nsvals_large=nsvals_large, ssi_max_block=max_block, ssi_min_block=min_block)
    stats = Statistics(real(T))
    stats.normest_A = normest_R
    stats.opts_used = opts.data
    stats.time_initialize = time_ns() - start_tic

    #-------------------------------------------------------------------------------
    # initializations
    #-------------------------------------------------------------------------------

    # stats.flag and stats.rank initialized in spqr_rank_get_inputs
    # set the order of the remaining stats fields
    stats.tol = tol
    stats.tol_alt = -1.0   # removed later if remains -1

    if get_details == 2
        stats.ssi_max_block_used = -1
        stats.ssi_min_block_used = -1
    end
    if (get_details == 1)
        stats.norm_R_times_N = -1
        stats.norm_R_transpose_times_NT = -1
        stats.iters = -1
        stats.nsvals_large_found = 0
        stats.final_blocksize = -1
        stats.ssi_max_block_used = -1
        stats.ssi_min_block_used = -1
        stats.time = 0
        stats.time_iters = 0
        stats.time_est_error_bounds = 0
        stats.time_svd = 0
    end

    stats.rank = 0
    if get_details == 1 || get_details == 2
        stats.ssi_max_block_used = max_block
        stats.ssi_min_block_used = nblock
    end

    get_details == 1 && ( start_iters_tic = time_ns() )

    U = randn(private_stream, n, nblock)
    U = qr(U).Q * Matrix{T}(I, n, nblock) # random orthogonal matrix with nblock cols
    # est_error_bound_calculated = 0      # set to 1 later if bound calculated
    flag_overflow = 0                     # set to 1 later if overflow occurs

    #-------------------------------------------------------------------------------
    # iterations
    #-------------------------------------------------------------------------------
    D2 = T[]
    X2 = T[]
    est_error_bound = T(0)

    if R isa SubArray{<:Number,2,<:AbstractSparseMatrix}
        R = sparse(R)
    end

    iters = 0
    while iters < max_iters
        iters += 1

        U0 = U
        V1 = R \ U
        if !all(isfinite.(V1))
            flag_overflow = 1
            break   # *************>>>>> early exit from for loop
        end

        get_details == 1 && ( time_svd = time_ns() )
        V, D1, X1 = svd(V1)
        get_details == 1 && ( stats.time_svd += time_ns() - time_svd )

        U1 = R' \ V
        # Note: with the inverse power method overflow is a potential concern
        if !all(isfinite.(U1))
            # *************>>>>> early exit from for loop
            # We know of no matrix that triggers this condition, so the next
            # two lines are untested.
            flag_overflow = 1      # untested
            break                   # untested
        end

        if get_details == 1
            time_svd = time_ns()
        end

        U, D2, X2 = svd(U1)

        if (get_details == 1)
            stats.time_svd += time_ns() - time_svd
        end

        k = findfirst(D2 .< tol^-1)
        if k == nothing && nblock == n
            # success since the numerical rank of R is zero
            break   # *************>>>>> early exit from for loop
        end

        errbound(k::Int) = norm(U0 * (X1 * (X2[:,k] ./ D1)) - U[:,k] ./ D2[k]) / sqrt(2)

        if k != nothing
            # k equals the calculated nullity + 1, k corresponds to
            # the first singular value of R that is greater than tol
            # reduce nsvals_large if necessary due to max_block limit:
            nsvals_large_old = nsvals_large
            nsvals_large = min(nsvals_large, max_block - k + 1)

            if nblock >= k + nsvals_large - 1
                # estimate error bound for singular value n - k + 1 of R:
                k2 = k + nsvals_large - 1
                est_error_bound = errbound(k)
                # When nsvals_large is 1, k and k2 are the same.  When
                # nsvals_large > 1, k2 corresponds to the largest singular
                # value of R that is returned.
                est_error_bound2 = k == k2 ? est_error_bound : errbound(k2)
            end

            # Note that
            #     [ 0     R ] [  U   ]  =    [ U0 * X1 * ( D1 \ X2 ) ]
            #     [ R'    0 ] [ V*X2 ]       [     V * ( X2 / D2 )   ]
            # It follows that, by Demmel's Applied Numerical Linear Algebra,
            # Theorem 5.5, some eigenvalue of
            #     B  =  [ 0    R ]
            #           [ R'   0 ]
            # will be within a distance of est_error_bound of 1 / d2(k), where s(1)
            # = 1 / d2(k) is our estimate of singular value n - k + 1 of R.
            # Typically the eigenvalue of B is singular value number n - k + 1 of
            # R, although this is not guaranteed.  est_error_bound is our estimate
            # of a bound on the error in using s(1) to approximate singular value
            # number n - k + 1 of R.

            if nblock >= k + nsvals_large - 1 &&
               est_error_bound <= convergence_factor * abs(1 / D2[k] - tol) &&
               est_error_bound2 <= convergence_factor * abs( 1/D2[k2] ) &&
               iters >= min_iters

                # Motivation for the tests:
                # The first test in the if statement is an attempt to insure
                # that nsvals_large singular values of R larger than tol are
                # calculated.
                #
                # Goal of the power method is to increase nblock until we find
                # sigma = (singular value n - k + 1 of R) is > tol.  If
                # this is true it is guaranteed that n - k + 1 is the
                # correct numerical rank of R.  If we let s(1) = 1 / d2(k)
                # then s(1) > tol.  However s(1) is only an estimate of sigma.
                # However, in most cases
                #     | s(1) - sigma | <= est_error_bound                   (1)
                # and to be conservative assume only that
                #  |s(1) - sigma|<= (1/convergence_factor)*est_error_bound  (2)
                # where convergence_factor<=1. By the second test in the if
                # statement
                #  est_error_bound <= convergence_factor * | s(1) - tol |   (3)
                # Equations (2) and (3) imply that
                #      | s(1) - sigma | <= | s(1) - tol |
                # This result and s(1) > tol imply that sigma > tol, as
                # desired.  Thus the second test in the if statement attempts
                # to carry out enough iterations to insure that the calculated
                # numerical rank is correct.
                #
                # The third test in the if statement checks on the accuracy of
                # the estimate for singular values n - k2 + 1.  Let sigma2 be
                # singular value n - k2 + 1 of R.  Usually it is true
                # that
                #     | s( k2 ) - sigma2 | <= est_error_bound2.             (4)
                # Assuming (4) and the third test in the if statement it
                # follows that the estimated relative
                # error in s(k2),  as measured by est_error_bound2 / s( k2) ,
                # is less that or equal to convergence_factor.  Therefore
                # the third test in the if statement attempts to insure that
                # the largest singular value returned by ssi has a relative
                # error <= convergence_factor.
                #
                # SUCCESS!!!, found singular value or R larger than tol
                break  # *************>>>>> early exit from for loop
            end
            nsvals_large = nsvals_large_old  # restore original value
        end

        if nblock == max_block && iters >= min_iters && k == nothing
            # reached max_block block size without encountering any
            # singular values of R larger than tolerance
            break    # *************>>>>> early exit from for loop
        end

        if 1 <= iters && iters < max_iters &&
            ( k == nothing  || ( k != nothing && nblock < k + nsvals_large - 1) )

            # increase block size
            nblock_prev = nblock
            if k == nothing
                nblock = min(nblock + nblock_increment, max_block)
            else
                nblock = min(k + nsvals_large - 1, max_block )
            end
            if nblock > nblock_prev
                Y = randn(private_stream, n, nblock-nblock_prev)
                Y = Y - U * ( U' * Y )
                Y = qr(Y).Q * Matrix{T}(I, n, nblock-nblock_prev)
                U = [U Y]
            end
        end
    end

    if get_details == 1
        stats.final_blocksize = nblock    # final block size
        stats.time_iters = time_ns() - start_iters_tic
        stats.iters = iters               # number of iterations taken in ssi
    end

    #-------------------------------------------------------------------------------
    # check for early return
    #-------------------------------------------------------------------------------

    if flag_overflow == 1
        warning("spqr_rank:overflow", "overflow in inverse power method")
        stats, U, S, V = spqr_failure(4, stats, get_details, start_tic)
        return nargout == 1 ? S : nargout == 2 ? (S, stats) : (U, S, V, stats)
    end

    #-------------------------------------------------------------------------------
    # determine estimated singular values of R
    #-------------------------------------------------------------------------------

    nkeep = k != nothing ? min(nblock, k + nsvals_large - 1) : nblock
    est_error_bounds = Vector{T}(undef, nkeep)

    if k != nothing
        # Note: in this case nullity = k - 1 and rank = n - k + 1
        est_error_bounds[nsvals_large] = est_error_bound
        numerical_rank = n - k + 1
    else
        k = nblock
        if nblock == n
            numerical_rank = 0
            # success since numerical rank is 0
        else
            # in this case rank not successfully determined
            # Note: In th/is case k is a lower bound on the nullity and
            #       n - k is an upper bound on the rank
            numerical_rank = n - k  #upper bound on numerical rank
            nsvals_large = 0 # calculated no singular values > tol
        end
    end

    stats.rank = numerical_rank

    if get_details == 1
        stats.nsvals_large_found = nsvals_large
    end

    #-------------------------------------------------------------------------------
    # adjust V so that R'*U = V*S (in exact arithmetic)
    #-------------------------------------------------------------------------------

    V = V * X2
    # reverse order of S, U and V and keep only nkeep singular vectors
    S = T(1) ./ D2[nkeep:-1:1]
    V = V[:,nkeep:-1:1]
    U = U[:,nkeep:-1:1]

    if get_details == 1
        t = time_ns()
    end

    # this recalculates est_error_bounds(nsvals_large)
    U0 = [ R * V - U * Diagonal(S); R' * U - V * Diagonal(S) ]
    est_error_bounds .= vec(sqrt.(sum(abs2, U0, dims=1) / 2))
    # Note that
    #    [ 0      R ]  [ U ]   -    [ U ] * S = [ R * V - U * S ]
    #    [ R'     0 ]  [ V ]   -    [ V ]       [ R'* U - V * S ].
    # It follows, by Demmel's Applied Numerical Linear Algebra,
    # Theorem 5.5, for i = 2, . . .,  k, some eigenvalue of
    #     B  =  [ 0    R ]
    #           [ R'   0 ]
    # will be within a distance of the norm of the ith column of
    # [ R * V - U * S R'*U - V*S] / sqrt(2) from S(i,i). Typically this
    # eigenvalue will be singular value number n - k + i of R,
    # although it is not guaranteed that the singular value number
    # is correct.  est_error_bounds(i) is the norm of the ith column
    # of [ R * V - U * S; R' * U - V * S ] / sqrt(2).

    if get_details == 1
        # Note that stats.time_est_error_bounds includes the time for the error
        #   bound calculations done outside of the subspace iterations loop
        stats.time_est_error_bounds = time_ns() - t
    end

    nr1 = numerical_rank - nsvals_large
    stats.sval_numbers_for_bounds = nr1 + 1 : nr1 + nkeep

    #-------------------------------------------------------------------------------
    # compute norm R*N and R'*NT
    #-------------------------------------------------------------------------------

    # compute norm R*N where N = V(:,nsvals_large+1:end) is the approximate
    #         null space of R and
    #         norm R'*NT where NT = U(:,nsvals_large+1:end) is the approximate
    #         null space of R'

    if get_details == 1
        t = time_ns()
    end

    norm_R_times_N = opnorm(R * V)
    # svals_R_transpose_times_NT = svd(R'*U)'
    norm_R_transpose_times_NT = opnorm(R' * U)

    if get_details == 1
        stats.norm_R_times_N = norm_R_times_N
        stats.norm_R_transpose_times_NT = norm_R_transpose_times_NT
        stats.time_svd += time_ns() - t
    end

    # Note: norm_R_times_N is an upper bound on sing. val. rank1+1 of R
    # and norm_R_transpose_times_NT is also an upper bound on sing. val.
    #      rank1+1 of R
    max_norm_RN_RTNT = max(norm_R_times_N, norm_R_transpose_times_NT)
    # choose max here to insure that both null spaces are good

    #-------------------s------------------------------------------------------------
    # determine flag indicating the accuracy of the rank calculation
    #-------------------------------------------------------------------------------

    if numerical_rank == 0
        # numerical rank is 0 in this case
        stats.flag = 0
    elseif (numerical_rank == n  || max_norm_RN_RTNT <= tol) &&
           (nsvals_large > 0 && S[nsvals_large] - est_error_bounds[nsvals_large] > tol )
        # in this case, assuming est_error_bounds(nsvals_large) is a true
        # error bound, then the numerical rank is correct.  Also
        # N = V(:,nsvals_large+1:end) and NT = U(:,nsvals_large+1:end)
        # are bases for the numerical null space or R and R', respectively
        stats.flag = 0
    elseif ( nsvals_large > 0 && numerical_rank == n ) ||
            ( nsvals_large > 0 &&
             S[nsvals_large] - est_error_bounds[nsvals_large] > max_norm_RN_RTNT )
        # in this case, assuming est_error_bounds(nsvals_large) is a true
        # error bound, then the numerical rank is correct with a modified
        # tolerance.  This is a rare case.
        stats.flag = 1
        tol_alt = ( S[nsvals_large] - est_error_bounds[nsvals_large] )
        tol_alt -= eps(tol_alt)         # so that tol_alt satisfies the >
                                        # portion of the inequality below
        # tol_alt = max_norm_RN_RTNT
        # Note: satisfactory values of tol_alt are in the range
        #    S[nsvals_large] - est_error_bounds[nsvals_large] > tol_alt
        #    >= max_norm_RN_RTNT
        stats.tol_alt = tol_alt
    elseif  nsvals_large > 0 && S[nsvals_large] > tol &&
            max_norm_RN_RTNT <= tol &&
            S[nsvals_large] - est_error_bounds[nsvals_large] <= max_norm_RN_RTNT
        # in this case, assuming est_error_bounds(nsvals_large) is a true
        # error bound, the error bound is too large to determine the
        # numerical rank.  This case is very rare.
        stats.flag = 2
    else
        # in this case all the calculated singular values are
        # smaller than tol or either N is not a basis for the numerical
        # null space of R with tolerance tol or NT is not such a basis for R'.
        # stats.rank is an upper bound on the numerical rank.
        stats.flag = 3
    end

    #-------------------------------------------------------------------------------
    # return results
    #-------------------------------------------------------------------------------
    stats.est_svals_of_R = S
    stats.est_error_bounds = est_error_bounds

    if get_details == 1
        stats.time = time_ns() - start_tic
    end

    nargout == 1 ? S : nargout == 2 ? (S, stats) : (U, S, V, stats)
end


function read_instructions(lines)
    instructions = Tuple{AbstractString, Int64}[]
    for line in lines
        op, value = split(line)
        push!(instructions, (op, parse(Int64, value)))
    end
    instructions
end

function find_possibly_corrupted_lines(instructions)
    indices = Int64[]
    for (ix, (op, _)) in enumerate(instructions)
        if op == "jmp" || op == "nop"
            push!(indices, ix)
        end
    end
    indices
end

function execute_correct_program(instructions)
    indices = find_possibly_corrupted_lines(instructions)
    for ix in indices
        modified_instructions = copy(instructions)
        op, k = modified_instructions[ix]
        if op == "jmp"
            modified_instructions[ix] = ("nop", k)
        elseif op == "nop"
            modified_instructions[ix] = ("jmp", k)
        end
        accumulator = run_program(modified_instructions)
        if accumulator !== nothing
            return accumulator
        end
    end
    return nothing
end

function run_program(instructions)
    n = length(instructions)
    visited = falses(n)
    ix = 1
    accumulator = 0
    while !visited[ix]
        (op, k) = instructions[ix]
        visited[ix] = true
        if op == "nop"
            ix += 1
        elseif op == "acc"
            accumulator += k
            ix += 1
        elseif op == "jmp"
            ix += k
        end

        if ix > n
            return accumulator
        end
    end
    nothing
end

function main()
    lines = readlines("../resources/input_08.txt")
    instructions = read_instructions(lines)
    result = execute_correct_program(instructions)
    println(result)
end

main()


# Distance metrics for kernel functions

δ(Φ, z) = diff(Φ) ./ diff(z)

# this is norm(a-b)^2 but more efficient
function sq_mag(a,b) # ||a - b||^2
    ll = 0.0
    @inbounds for k in 1:length(a)
        ll += (a[k]-b[k])^2
    end
    return ll
end

"""
euclidean_distance: computes the Euclidean distance (l²-norm) between two vectors
"""
@inline function euclidean_distance(a,b,z) # d(x,x') = || x - x' ||
    return sqrt(sq_mag(a,b))
end

@inline function euclidean_distance(a,b) # d(x,x') = || x - x' ||
    return sqrt(sq_mag(a,b))
end

"""
derivative_distance: computes the H¹-norm w.r.t z of two vectors
"""
function derivative_distance(a,b,z) # d(x,x') = || diff(x)./diff(z) - diff(x')./diff(z) ||
    return sqrt(sq_mag( δ(a, z), δ(b, z) ))
end

"""
antiderivative_distance: computes the H⁻¹-norm w.r.t z of two vectors
"""
function antiderivative_distance(a,b,z) # || diff(x).*diff(z) - diff(x').*diff(z) ||
    return sqrt(sq_mag(diff(a).*diff(z) , diff(b).*diff(z)))
end

# """
# l2norm_strat_penalty: computes the Euclidean distance (l²-norm) between two vectors and adds
# the a proxy for the difference in the initial stratification from the corresponding temperature
# profiles, as approximated by the stratification at the bottom.
# """
# function l2norm_strat_penalty(a,b,z) # d(x,x') = || x - x' ||
#     α_proxy(x) = x[2] - x[1]
#     println(abs(α_proxy(a)-α_proxy(b)))
#     if abs(α_proxy(a)-α_proxy(b))>0.05
#         return l2_norm(a,b) + 0.0001
#     end
#     return l2_norm(a,b)
# end