using Test

include("../src/lib.jl")

# Unit tests for functions related to delaunay triangulation
function test_delaunay()
    # create_nodes
    @testset "create_nodes" begin
        n = 100
        x_bounds = [-2, 3]
        y_bounds = [-4, 5]
        nodes = create_nodes(n, x_bounds, y_bounds)
        @test length(nodes) == (n)
        nodes_x = [a_node[1] for a_node in nodes]
        nodes_y = [a_node[2] for a_node in nodes]
        @test minimum(nodes_x) >= x_bounds[1]
        @test maximum(nodes_x) <= x_bounds[2]
        @test minimum(nodes_y) >= y_bounds[1]
        @test maximum(nodes_y) <= y_bounds[2]
    end

    # in_circumcircle
    @testset "in_circumcircle" begin
        nodes = [[0., 0.], [1., 0.], [0., 1.],   # tri nodes
                 [0.1, 0.1],               # node 4: in
                 [0.5, 2.]]                # node 5: out
        # tests are done with the triangle is counterclockwise and clockwise order
        @test in_circumcircle([1, 2, 3], 4, nodes) == true
        @test in_circumcircle([1, 3, 2], 4, nodes) == true
        @test in_circumcircle([1, 2, 3], 5, nodes) == false
        @test in_circumcircle([1, 3, 2], 5, nodes) == false
    end

    # is_clockwise(A, B, C)
    @testset "is_clockwise" begin
        A = [0., 0.]
        B = [1., 0.]
        C = [0., 1.]
        @test is_clockwise(A, B, C) == false
        @test is_clockwise(A, C, B) == true
    end

    # create_supertriangle
    @testset "create_supertriangle" begin
        n = 100
        x_bounds = [-2, 3]
        y_bounds = [-4, 5]
        nodes = create_nodes(n, x_bounds, y_bounds)
        triangle = create_supertriangle(nodes)
        @test length(triangle) == 3
        @test all([length(triangle_pt)==2 for triangle_pt in triangle])
        append!(nodes, triangle)
        triangle_ids = [n+1, n+2, n+3]
        @test all([inside_triangle(triangle_ids, node_id, nodes) for node_id=1:n])
    end

    # require_flip
    @testset "require_flip" begin
        AB = Edge([1, 2], [])
        BC = Edge([2, 3], [])
        CD = Edge([3, 4], [])
        BD = Edge([2, 4], [])
        AD = Edge([1, 4], [])
        edges = [AB, BC, CD, BD, AD]
        triangles = [Triangle([1, 4, 5]), Triangle([2, 3, 4])]
        # first try: both triangles have 2pi/3 opposite angles => flip required
        width = 1.
        height = width/tan(pi/3)
        nodes = [[0., -height], [width, 0.],  [0., height], [-width, 0.]]
        @test require_flip(1, 2, triangles, edges, nodes) == true
        # second try: both triangles have pi/3 opposite angles => flip not required
        height = width/tan(pi/6)
        nodes = [[0., -height], [width, 0.],  [0., height], [-width, 0.]]
        @test require_flip(1, 2, triangles, edges, nodes) == false
    end

    # compute_angle
    @testset "compute_angle" begin
        nodes = [[0., 0.], [1., 0.], [0., tan(pi/3)]]
        edge1 = Edge([1, 2], [])
        edge2 = Edge([1, 3], [])
        edge3 = Edge([2, 3], [])
        edge3_flip = Edge([3, 2], [])
        @test isapprox(compute_angle(edge1, edge2, nodes), pi/2)
        @test isapprox(compute_angle(edge1, edge3, nodes), 2*pi/3)
        @test isapprox(compute_angle(edge1, edge3_flip, nodes), pi/3)
        @test isapprox(compute_angle(edge2, edge3, nodes), pi/6)
    end

    # get_tri_node_ids
    @testset "get_tri_node_ids" begin
        edges = [Edge([1, 2], []), Edge([1, 3], []), Edge([2, 3], [])]
        triangles = [Triangle([1, 2, 3])]
        @test sort(get_tri_node_ids(1, triangles, edges)) == [1, 2, 3]
    end

    # inside_triangle
    @testset "inside_triangle" begin
        nodes = [[0., 0.], [1., 0.], [0., 1.],  # tri nodes
                    [0.1, 0.1],                 # node 4: in
                    [0., 0.5],                  # node 5: on an edge
                    [0., 1e-6],                 # node 6: slightly in
                    [0., -1e-6],                # node 7: slightly out
                ]
        # tests are done with the triangle is counterclockwise and clockwise order
        @test inside_triangle([1, 2, 3], 4, nodes) == true
        @test inside_triangle([1, 3, 2], 4, nodes) == true
        @test inside_triangle([1, 2, 3], 5, nodes) == false
        @test inside_triangle([1, 3, 2], 6, nodes) == true
        @test inside_triangle([1, 3, 2], 7, nodes) == false
    end

    # get_triangulation_nodes
    @testset "triangulation_nodes" begin
        edges = [
            Edge([1, 2], []),
            Edge([5, 1], []),
            Edge([3, 2], []),
            Edge([6, 3], []),
            Edge([7, 8], [])
        ]
        @test sort(get_triangulation_nodes(edges)) == [1, 2, 3, 5, 6, 7, 8]
    end
end

# Unit tests for functions related to the CLI application
function test_application()

    @testset "parse_args" begin
        args_string = "-f -g 5 -ij myFile --output-file=myOutput --use-dll 10"
        args = parse_args(args_string)
        @test "f" in keys(args)
        @test args["f"] === nothing
        @test "g" in keys(args)
        @test args["g"] == "5"
        @test "i" in keys(args)
        @test args["i"] === nothing
        @test "j" in keys(args)
        @test args["j"] === nothing
        @test "output-file" in keys(args)
        @test args["output-file"] == "myOutput"
        @test "use-dll" in keys(args)
        @test args["use-dll"] === nothing
        @test args["non_options"] == ["myFile", "10"]
    end

    @testset "print_help" begin
        original_stdout = stdout
        (read_end, ) = redirect_stdout()
        print_help()
        output = readline(read_end)
        @test output != ""
        redirect_stdout(original_stdout)
    end

    @testset "get_arg_value" begin
        args = Dict(
            "a" => nothing,
            "a_long" => nothing,
            "b" => nothing,
            "c_long" => nothing,
            "d" => "5",
            "e_long" => "10",
            "f" => "1",
            "f_long" => "2"
        )
        @test get_arg_value(["z", "z_long"], args) === false
        @test get_arg_value([], args) === false
        @test get_arg_value(["z", "a"], args) === nothing
        @test get_arg_value(["a", "a_long"], args) === nothing
        @test get_arg_value(["b", "b_long"], args) === nothing
        @test get_arg_value(["c", "c_long"], args) === nothing
        @test get_arg_value(["d", "d_long"], args) == "5"
        @test get_arg_value(["e", "e_long"], args) == "10"
        @test_throws ErrorException get_arg_value(["f", "f_long"], args)
    end

    @testset "get_main_parameters" begin
        # default values
        params = get_main_parameters("")
        @test params["create_nodes"] === false
        @test params["create_nodes_number"] == 100
        @test params["load_node_file"] === false
        @test params["load_result_files"] === false
        @test params["output"] == "./triangulation.json"
        @test params["plot_results"] === false
        @test params["save_steps"] === false
        @test params["help"] === true

        # set 1: short options
        args_string = "-c -o myOutput -h -p -s"
        params = get_main_parameters(args_string)
        @test params["create_nodes"] === true
        @test params["create_nodes_number"] == 100
        @test params["load_node_file"] === false
        @test params["load_result_files"] === false
        @test params["output"] == "myOutput"
        @test params["plot_results"] == "final"
        @test params["save_steps"] === true
        @test params["help"] === true

        # set 2: long options
        args_string = "--create-nodes --output=myOutput --help --plot --save-steps"
        params = get_main_parameters(args_string)
        @test params["create_nodes"] === true
        @test params["create_nodes_number"] == 100
        @test params["load_node_file"] === false
        @test params["load_result_files"] === false
        @test params["output"] == "myOutput"
        @test params["plot_results"] == "final"
        @test params["save_steps"] === true
        @test params["help"] === true

        # -f and --from-file: options depending on the existence of the file
        args_string = "-f"
        @test_throws ErrorException get_main_parameters(args_string)

        args_string = "-f ./wrong_file__"
        @test_throws ErrorException get_main_parameters(args_string)

        (tmp_filename, tmp_io) = Filesystem.mktemp(".", cleanup=false)
        args_string = "-f $(tmp_filename)"
        params = get_main_parameters(args_string)
        @test params["load_node_file"] == tmp_filename
        close(tmp_io)
        Filesystem.rm(tmp_filename)

        # -l and --load-result: options depending on the existence of files matching the pattern
        args_string = "-l"
        @test_throws ErrorException get_main_parameters(args_string)

        args_string = "-l ./wrong_file__"
        @test_throws ErrorException get_main_parameters(args_string)

        tmp_filename_root = "./result_"*basename(Filesystem.tempname(cleanup=false))
        filenames = ["$(tmp_filename_root)_$(i)" for i in 1:3]
        Filesystem.touch.(filenames)
        file_pattern = "$(tmp_filename_root)_*"
        args_string = "-l $(file_pattern)"
        params = get_main_parameters(args_string)
        @test params["load_result_files"] == file_pattern
        Filesystem.rm.(filenames)

    end;

end

function test_all()
    test_delaunay()
    test_application()
end
;

# execution of the file from a terminal: run test_all function
if abspath(PROGRAM_FILE) == @__FILE__
    println("Running unit tests ...")
    test_all()
end
;


module num

using DSP

export trapz, conv_same, convergent, mixing

function trapz(x,y)
    dx = x[2] - x[1]
    intgrl = 0.0
    @fastmath @inbounds for i in 1:length(y)-1
        intgrl += (y[i+1] + y[i])
    end
    return intgrl * 0.5 * dx
end

function conv_same(A,B)
    C = conv(A,B)
    lower_bound = floor(Int,length(C)/4)
    upper_bound = floor(Int,3*length(C)/4-1)
    lengthc = upper_bound-lower_bound + 1
    if lengthc != length(A)
        upper_bound = floor(Int,3*length(C)/4)
    end
    return C[lower_bound:upper_bound]
end

function convergent(value_old,value_new,ω,nω,tol)
    error = zeros(Float64,nω)
    if length(value_new) != nω
        for i = 1:nω
            error[i] = abs( sum( imag.(value_new[i,:]) .- imag.(value_old[i,:]) ) )
        end
    else
        for i = 1:nω
            error[i] = abs( imag.(value_new[i]) .- imag.(value_old[i]) )
        end
    end
    err = trapz(ω,error)
    convg = err <= tol
    return convg,err
end

function mixing(value_old,value_new,mixings)
    return (1 - mixings) .* value_old .+ mixings .* value_new
end

end


### A Pluto.jl notebook ###
# v0.18.0

using Markdown
using InteractiveUtils

# ╔═╡ d6cfceae-779d-474d-a609-e1fe6fb1d276
begin
	import Pkg
	Pkg.activate(".")
end

# ╔═╡ ea6b61fa-51d1-499a-884a-b7fba187d005
begin
	using FinSight
	using Plots
	using PlutoUI
	using DataFrames
	using Measurements
end

# ╔═╡ c2f43cb6-e226-4be6-b54c-96417ae953fa
# Illistrative "pro forma" DCF valuation of a rental property

# ╔═╡ d7e8f31d-86e9-4832-878b-be2d925767dc
begin
	# rate
	r = 0.07

	# Vacancy 5.0% of gross income
	v = 0.05

	# Capital Expenditures 10% of gross income
	ci = 0.10 ± 0  # Could be zero

	#Growth Rate 2%
	g = 0.02 ± 0.001

	# Expenses 35% of gross income
	expense_ratio = 0.35 ± 0.05

	# Exit Capitalisation Rate 5%
	y = 0.05

	# Time horizon now (year 0) plus 11 years
	year = [0,0,0, 0,0,0, 0,0,0, 0, 0,0]

	# Potential Gross Income first year $100
	pgi_fy = 100.0 ± 20

	nothing
end

# ╔═╡ 6d70da11-1f37-46c1-bae2-c9635674f4e2
# Potential Gross Income nominal cash flow
begin
	pgi = zeros(typeof(pgi_fy), 12)
	pgi[2] = pgi_fy   # first year gross income is the second value
	growth_factor = 1.0 + g

	for i = 3:12
			prior_year_income = pgi[i-1]
	 		pgi[i] =   prior_year_income * growth_factor
	end

	pgi
end

# ╔═╡ 06a77d3e-e321-446a-9e18-8a53e3ad2214
pgi  # Potential gross income per square metre of office space

# ╔═╡ 46f48d40-e8d6-4b4a-8176-06ae91298209
begin
	df = DataFrame();
	df.years = 1:length(year)
	df.pgi = pgi
	df.vacancy_allowance = df.pgi .* v
	df.effective_gross_income = df.pgi - df.vacancy_allowance
	df.operating_expenses = expense_ratio * df.pgi
	df.net_operating_income = df.effective_gross_income - df.operating_expenses
	df.capital_expenditures = ci * df.pgi
	df.net_annual_cash_flows = df.net_operating_income - df.capital_expenditures
	df.reversion_cash_flows = [0, 0,0,0,0,0, 0,0,0,0, df.net_annual_cash_flows[12] / y ,0]

	df.net_cash_flows = df.net_annual_cash_flows + df.reversion_cash_flows

	df
end

# ╔═╡ 7e23378a-5293-4e9a-b308-89324cd1cad2
# Present value @ 7.0% Discount rate
net_present_value = round(npv(r, df.net_cash_flows[2:11]))

# ╔═╡ 772f35f4-5d5c-4359-a14c-d88e704bee49
plot(df.years, df.net_cash_flows, xlab="years", ylab="Net cash flows")

# ╔═╡ b2286e8c-f911-457c-ab86-66799f380f70

# ╔═╡ Cell order:
# ╠═d6cfceae-779d-474d-a609-e1fe6fb1d276
# ╠═ea6b61fa-51d1-499a-884a-b7fba187d005
# ╠═c2f43cb6-e226-4be6-b54c-96417ae953fa
# ╠═d7e8f31d-86e9-4832-878b-be2d925767dc
# ╠═6d70da11-1f37-46c1-bae2-c9635674f4e2
# ╠═06a77d3e-e321-446a-9e18-8a53e3ad2214
# ╠═46f48d40-e8d6-4b4a-8176-06ae91298209
# ╠═7e23378a-5293-4e9a-b308-89324cd1cad2
# ╠═772f35f4-5d5c-4359-a14c-d88e704bee49
# ╠═b2286e8c-f911-457c-ab86-66799f380f70


using InvariantMeasures

@testset "Chebyshev assembler" begin

N = 16
B = InvariantMeasures.Chebyshev(N)

D = mod1_dynamic(x->2*x)
L(ϕ, x) = (ϕ(x/2)+ϕ(x/2+0.5))/2

M = assemble(B, D)
using LinearAlgebra

all_true = true
for i in 1:N
   w = mid.(L.(B[i], B.p))
   z = InvariantMeasures.chebtransform(w)
   all_true = all_true && norm(z-M[:, i], Inf)< 10^-13
end

@test all_true

end


# Parzen density estimation and classification
module ParzenClassifier
	import Distances
	using StatsFuns: softmax!
	export ParzenModel, Parzen_train, Parzen_exec

	Φ_hat(u::T) where T<:Real = abs(u)<=0.5 ? 1.0 : 0.0
	Φ_linear(u::T) where T<:Real = abs(u)<=0.5 ? 1.0-u : 0.0
	Φ_gaussian(u::T) where T<:Real = 1/sqrt(2*pi)*exp(-u^2/2)
	Φ_exponential(u::T) where T<:Real = u>0.0 ? exp(-u) : 0.0
	Φ_cosine(u::T) where T<:Real = abs(u)<=0.5 ? cos(pi/2*u) : 0.0

	# ParzenModel struct {T - type of neighbour, S - type of labels, U - type of distance}
	struct ParzenModel{S<:Union{Void,Vector{<:Real}}, U<:AbstractArray{<:AbstractFloat}, V<:Distances.Metric}
		h::Float64					# h: dimension of the encompasing hypercube
		Φ::Function 					# kernel/parzen window function
		targets::S					# targets: Vector{T<:Real} - classification, Void - density estimation
		data::U						# training data: Matrix{FloatXX}
		metric::V					# distance: Distances.Metric
		priors::Dict{Int,Float64}			# not used unless specified explicitly (not yet supported)
	end

	# Aliases
	const ParzenModelClassification{S<:Vector{Int}} = ParzenModel{S}
	const ParzenModelDensity{S<:Void} = ParzenModel{S}

	# Printers
	Base.show(io::IO, m::ParzenModelClassification) = print(io, "Parzen model, classification, h=$(m.h)")
	Base.show(io::IO, m::ParzenModelDensity) = print(io, "Parzen model, density estimation, h=$(m.h)")

	# Get kernel function
	_parse_window_(window::Symbol)::Function = begin
		# Check for the window type
		if window == :hat return Φ_hat
		elseif window == :linear return Φ_linear
		elseif window == :gaussian return Φ_gaussian
		elseif window == :exponential return Φ_exponential
		elseif window == :cosine return Φ_cosine
		else
			warn("Unrecognized kernel type, defaulting to :hat")
			return Φ_hat
		end
	end

	# Train method (classification)
	Parzen_train(x::Matrix{Float64}, y::Vector{<:Real}, h::Float64; window::Symbol=:hat, metric::Distances.Metric=Distances.Cityblock()) =
		return ParzenModel(h, _parse_window_(window), y, x, metric, Dict(yi=>sum(yi.==y)/length(y) for yi in unique(y)))

	# Train method (density estimation)
	Parzen_train(x::Matrix{Float64}, y::Void, h::Float64; window::Symbol=:hat, metric::Distances.Metric=Distances.Cityblock()) =
		return ParzenModel(h, _parse_window_(window), y, x, metric, Dict{Int, Float64}())

	# Execution methods (classification)
	Parzen_exec(m::ParzenModelClassification, x::Matrix{Float64}, classes::Vector{Int})=begin
		l = size(m.data,1)
		N = size(m.data,2)
		C = length(classes)
		out = zeros(Float64, size(x,2),C)
		@inbounds for c in 1:C
		#TODO: Improve performance of this bit
			out[:,c] = 1/(m.h^l) * 1/N * sum(m.Φ.(Distances.pairwise(m.metric, x, m.data[:,m.targets.==classes[c]])/m.h),2)
		end

		# Apply softmax
		for j in 1:size(out,1)
			softmax!(view(out,j,:))
		end
		return out'
	end

	Parzen_exec(m::ParzenModelDensity, x::Matrix{Float64}, classes::Vector{Int})=begin # classes is not used, just for method uniformity
		l = size(m.data,1)
		N = size(m.data,2)

		#TODO: Improve performance of this bit
		return ( 1/(m.h^l) * 1/N * sum(m.Φ.(Distances.pairwise(m.metric, x, m.data)/m.h),2) )'
	end
end

##########################
# FunctionCell Interface #
##########################
"""
	parzen(h=1.0 [;kwargs])

Constructs an untrained cell that when piped data inside, returns a Parzen window classifier/density estimator trained
function cell based on the input data and labels if the case.

# Arguments
  * `h`::Float64=1.0 is the shape parameter of the parzen window

# Keyword arguments
  * `window`::Symbol the Parzen window type; available: `:hat`, `:linear`, `:gaussian`, `:exponential` and `:cosine` (default `:hat`)
  * `metric`::Distances.Metric metric from `Distances.jl` which specifies how distances are calculated between samples (default `Distances.Citiblock()`)

For more information:
	[1] S. Theodoridis, K. Koutroumbas, "Pattern Recognition 4'th Ed." 2009, ISBN 978-1-59749-272-0
	[2] R. Douda, P. Hart, D. Stork, "Pattern Classification 2'nd Ed." 2001, ISBN 978-0-471-05669-0
"""
parzen(h::Float64=1.0; kwargs...) = FunctionCell(parzen, (h,), ModelProperties(), kwtitle("Parzen classifier/density estimator (h=$h)", kwargs); kwargs...)

############################
# DataCell/Array Interface #
############################
"""
	parzen(x, h=1.0 [;kwargs])

Trains a Parzen classifier / density estimator using the data `x` and the window parameter `h`.
"""
# Training (density estimation)
parzen(x::T where T<:CellDataU, h::Float64=1; kwargs...) = parzen(getx!(x), h; kwargs...)
parzen(x::T where T<:AbstractVector, h::Float64=1.0; kwargs...) = parzen(mat(x, LearnBase.ObsDim.Constant{2}()), h; kwargs...)
parzen(x::T where T<:AbstractMatrix, h::Float64=1.0; kwargs...) = begin

	@assert nobs(x) > 1 "[parzen] Expected at least 2 samples, got $(nobs(x))."

	# Train model
	parzendata = ParzenClassifier.Parzen_train(getobs(x), nothing, h; kwargs...)

	FunctionCell(parzen, Model(parzendata, ModelProperties(nvars(x), 1)), kwtitle("Parzen density estimator (h=$h)",kwargs))
end

# Training (classification)
parzen(x::T where T<:CellDataL, h::Float64=1; kwargs...) = parzen((getx!(x), gety(x)), h; kwargs...)
parzen(x::Tuple{T,S} where T<:AbstractVector where S<:AbstractVector, h::Float64=1.0; kwargs...) =
	parzen((mat(x[1], LearnBase.ObsDim.Constant{2}()), x[2]), h; kwargs...)
parzen(x::Tuple{T,S} where T<:AbstractMatrix where S<:AbstractVector, h::Float64=1.0; kwargs...) = begin

	@assert nobs(x[1]) == nobs(x[2]) "[parzen] Expected $(nobs(x[1])) labels/values, got $(nobs(x[2]))."

	# Transform labels first
	enc = labelencn(x[2])
	yenc = label2ind.(x[2],enc)

	# Train model
	parzendata = ParzenClassifier.Parzen_train(getobs(x[1]), yenc, h; kwargs...)

	# Build model properties
	modelprops = ModelProperties(nvars(x[1]), length(enc.label), enc)

	FunctionCell(parzen, Model(parzendata, modelprops), kwtitle("Parzen classifier (h=$h)",kwargs))

end

# Execution
parzen(x::T where T<:CellData, model::Model{<:ParzenClassifier.ParzenModel}) =
	datacell(parzen(getx!(x), model), gety(x))
parzen(x::T where T<:AbstractVector, model::Model{<:ParzenClassifier.ParzenModel}) =
	parzen(mat(x, LearnBase.ObsDim.Constant{2}()), model)
parzen(x::T where T<:AbstractMatrix, model::Model{<:ParzenClassifier.ParzenModel}) =
	ParzenClassifier.Parzen_exec(model.data, getobs(x), collect(1:model.properties.odim))


# multinomial idea.

# x is the input.
# function evalsquaredRKHS(   x::Vector{T},
#                             θ::SquaredFunctionsKernelType,
#                             X::Vector{Vector{T}},
#                             c::Vector{T},
#                             𝓐)::T where T <: Real
#
#     # get kernel components.
#     N = length(X)
#     @assert length(c) == N
#
#     f_x_components = collect( c[i]*evalkernel(x, X[i], θ.base_params) for i = 1:N)
#
#     # sum.
#     out = @distributed (+) for u in 𝓐
#         Combinatorics.multinomial(u...)*prod(f_x_components.^u)
#     end
#
#     return out
# end

function evalsquaredRKHS(   x::Vector{T},
                            θ::SquaredFunctionsKernelType,
                            X::Vector{Vector{T}},
                            c::Vector{T},
                            𝓐)::T where T <: Real

    # get kernel components.
    N = length(X)
    @assert length(c) == N

    f_x_components = collect( c[i]*evalkernel(x, X[i], θ.base_params) for i = 1:N)

    # sum.
    out = sum( Combinatorics.multinomial(u...)*prod(f_x_components.^u) for u in 𝓐 )

    return out
end

# assume y is strictly positive.
# to query: collect( evalsquaredRKHS(x, θp, η.X, η.c, 𝓐) for x in xq )
function fitsquarefunc( X::Vector{Vector{T}},
                        y::Vector{T},
                        θ::KT,
                        σ²::T) where {T,KT}
    #
    N = length(X)

    # fit suare root of y.
    y_sqrt = sqrt.(y)
    η = RKHSProblemType( zeros(T,N),
                         X,
                         θ,
                         σ²)
    fitRKHS!(η, y_sqrt)

    # prepare multinomial indices.
    θp = SquaredFunctionsKernelType(θ)
    𝓐 = SymTensorTools.getsubscriptarray(2,N)

    return η, 𝓐, θp
end


# Copyright 2021 The FirstOrderLp Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# Generates a Linear Programming model for solving an L1 SVM binary
# classification problem from the LIBSVM repository.

# Sample usage:
# $ julia --project=. generate_l1_svm_lp.jl --input_filename=../data/duke.tr
#     --output_filename=/tmp/duke.mps.gz --regularizer_weight=1.5

# The LP formulation for L1 SVM is taken from equation (5) in:
# Ji Zhu, Saharon Rosset, Trevor Hastie, and Rob Tibshirani. 2003. 1-norm
# support vector machines. In Proceedings of the 16th International Conference
# on Neural Information Processing Systems (NIPS'03). MIT Press, Cambridge, MA,
# USA, 49–56.
# https://papers.nips.cc/paper/2003/file/49d4b2faeb4b7b9e745775793141e2b2-Paper.pdf

import ArgParse
import JuMP
import SparseArrays
import LinearAlgebra

const SparseMatrixCSC = SparseArrays.SparseMatrixCSC
const sparse = SparseArrays.sparse
const norm = LinearAlgebra.norm
const Diagonal = LinearAlgebra.Diagonal
const nzrange = SparseArrays.nzrange

mutable struct SvmTrainingData
  feature_matrix::SparseMatrixCSC{Float64,Int64}
  labels::Vector{Float64}
end

"""
Populates 'model' with the linear program model for solving the L1 SVM
problem with training set (X, y). The model is given by
  min  sum w[i] + regularizer_weight * sum z[i]
  s.t. w >= 1 - diag(y) * [X 1] * beta
       w >= 0
       z >= beta
       z >= -beta
"""
function populate_libsvm_model(
  model::JuMP.Model,
  data::SvmTrainingData,
  regularizer_weight::Float64,
)
  n, d = size(data.feature_matrix)
  println("Generating a model with $n datapoints and $(d - 1) features.")
  JuMP.@variable(model, beta[i = 1:d])
  JuMP.@variable(model, w[i = 1:n], lower_bound = 0.0)
  JuMP.@variable(model, z[i = 1:d])
  JuMP.@objective(model, Min, sum(w) + regularizer_weight * sum(z))
  JuMP.@constraint(model, z .>= beta)
  JuMP.@constraint(model, z .>= -beta)
  JuMP.@constraint(
    model,
    w .>= (1.0 .- data.labels .* (data.feature_matrix * beta))
  )
  return model
end

"""
Defines parses and args.

# Returns
A dictionary with the values of the command-line arguments.
"""
function parse_command_line()
  arg_parse = ArgParse.ArgParseSettings()

  ArgParse.@add_arg_table! arg_parse begin
    "--input_filename"
    help = "The .train file containing the problem data."
    arg_type = String
    required = true

    "--output_filename"
    help = "Filename for the output .mps (or .mps.gz) model."
    arg_type = String
    required = true

    "--regularizer_weight"
    help = "Weight of the L1 regularizer."
    arg_type = Float64
    required = true
  end

  return ArgParse.parse_args(arg_parse)
end

"""
Loads a LIBSVM file and returns the loaded SvmTrainingData struct.
"""
function load_libsvm_file(file_name::String)
  open(file_name, "r") do io
    labels = Vector{Float64}()
    row_indices = Vector{Int64}()
    col_indices = Vector{Int64}()
    matrix_values = Vector{Float64}()

    row_index = 0
    found_label_one = false
    for line in eachline(io)
      row_index += 1
      split_line = split(line)

      label = parse(Float64, split_line[1])
      # This ensures that labels are 1 or -1. Different datasets use {-1, 1}, {0, 1}, and {1, 2}.
      if label == 1.0
        found_label_one = true
      else
        label = -1.0
      end
      push!(labels, label)

      for i in 2:length(split_line)
        push!(row_indices, row_index)
        matrix_coef = split(split_line[i], ":")
        push!(col_indices, parse(Int64, matrix_coef[1]))
        push!(matrix_values, parse(Float64, matrix_coef[2]))
      end
    end
    @assert found_label_one
    feature_matrix = sparse(row_indices, col_indices, matrix_values)
    return SvmTrainingData(feature_matrix, labels)
  end
end

function normalize_columns(feature_matrix::SparseMatrixCSC{Float64,Int64})
  norm_of_columns = vec(sqrt.(sum(t -> t^2, feature_matrix, dims = 1)))
  norm_of_columns[iszero.(norm_of_columns)] .= 1.0
  return feature_matrix * Diagonal(1.0 ./ norm_of_columns)
end

function remove_empty_columns(feature_matrix::SparseMatrixCSC{Float64,Int64})
  keep_cols = Vector{Int64}()
  for j in 1:size(feature_matrix, 2)
    if length(nzrange(feature_matrix, j)) > 0
      push!(keep_cols, j)
    end
  end
  return feature_matrix[:, keep_cols]
end

function add_intercept(feature_matrix::SparseMatrixCSC{Float64,Int64})
  return [sparse(ones(size(feature_matrix, 1))) feature_matrix]
end

function preprocess_training_data(result::SvmTrainingData)
  result.feature_matrix = remove_empty_columns(result.feature_matrix)
  result.feature_matrix = add_intercept(result.feature_matrix)
  result.feature_matrix = normalize_columns(result.feature_matrix)
  return result
end

function main()
  parsed_args = parse_command_line()

  filename = parsed_args["output_filename"]
  model = JuMP.Model()
  regularizer_weight = parsed_args["regularizer_weight"]
  input_filename = parsed_args["input_filename"]
  data = load_libsvm_file(input_filename)
  data = preprocess_training_data(data)
  populate_libsvm_model(model, data, regularizer_weight)
  JuMP.write_to_file(model, parsed_args["output_filename"])
end

main()


@testset "Valuation" begin
    @testset "Correctness (zero valuation)" begin
        @polyvar x y
        f = [x^2 - 2, x + y - 1]

        tracker, starts = coretracker_startsolutions(
            f;
            homotopy = (g, f) -> StraightLineHomotopy(g, f; gamma = 1.0 + im),
            seed = 12345,
            min_step_size = eps()^2,
            log_homotopy = true,
        )

        S = collect(starts)
        state = tracker.state
        val = HC.Valuation(S[1])
        init!(tracker, S[1], 0.0, 28.0)
        for _ in tracker
            if !state.last_step_failed
                HC.update!(val, state.x, state.ẋ, state.s)
            end
        end
        @test val.ν ≈ [0.0, 0.0] atol = 1e-8
        @test norm(val.ν̇) < 1e-2
        @test norm(val.ν̈) < 1e-2

        init!(val)
        init!(tracker, S[1], 0.0, 28.0)
        for _ in tracker
            if !state.last_step_failed
                HC.update!(val, state.x, state.ẋ, state.s, tracker.predictor)
            end
        end
        @test val.ν ≈ [0.0, 0.0] atol = 1e-8
        @test norm(val.ν̇) < 1e-7
        @test norm(val.ν̈) < 1e-6

        init!(val)
        init!(tracker, S[2], 0.0, 28.0)
        for _ in tracker
            if !state.last_step_failed
                HC.update!(val, state.x, state.ẋ, state.s, tracker.predictor)
            end
        end
        @test val.ν ≈ [0.0, 0.0] atol = 1e-8
        @test norm(val.ν̇) < 1e-7
        @test norm(val.ν̈) < 1e-6

        ## Multi-projective tracking

        # affine valuation
        @polyvar x y v w

        tracker, starts = coretracker_startsolutions(
            [x * y - 6 * v * w, x^2 - 5 * v^2],
            variable_groups = [(x, v), (y, w)],
            log_homotopy = true,
        )
        S = collect(starts)
        state = tracker.state
        val = HC.Valuation(S[1]; affine = true)
        init!(val)
        init!(tracker, S[1], 0.0, 30.0)
        for _ in tracker
            if !state.last_step_failed
                HC.update!(val, state.x, state.ẋ, state.s, tracker.predictor)
            end
        end
        @test is_success(status(tracker))
        @test val.ν ≈ [0.0, 0.0] atol = 1e-6

        val = HC.Valuation(S[1]; affine = false)
        init!(val)
        init!(tracker, S[1], 0.0, 30.0)
        for _ in tracker
            if !state.last_step_failed
                HC.update!(val, state.x, state.ẋ, state.s, tracker.predictor)
            end
        end
        @test is_success(status(tracker))
        @test val.ν ≈ [0.0, 0.0, 0.0, 0.0] atol = 1e-6
    end

    @testset "Correctness (non-zero valuation)" begin
        f = equations(griewank_osborne())
        tracker, starts = coretracker_startsolutions(
            f;
            homotopy = (g, f) -> StraightLineHomotopy(g, f; gamma = 1.0 + im),
            seed = 12345,
            min_step_size = eps()^2,
            log_homotopy = true,
        )

        S = collect(starts)
        state = tracker.state
        val = HC.Valuation(S[1])
        init!(tracker, S[1], 0.0, 20.0)
        for _ in tracker
            if !state.last_step_failed
                HC.update!(val, state.x, state.ẋ, state.s)
            end
        end
        @test val.ν ≈ [-0.5, -1] atol = 1e-4
        @test norm(val.ν̇) < 1e-4
        @test norm(val.ν̈) < 1e-4

        # Use analytic estimates for ν̇ and ν̈
        init!(val)
        @test all(isnan, val.ν)
        init!(tracker, S[1], 0.0, 20.0)
        for _ in tracker
            if !state.last_step_failed
                HC.update!(val, state.x, state.ẋ, state.s, tracker.predictor)
            end
        end
        @test val.ν ≈ [-0.5, -1] atol = 1e-4
        @test norm(val.ν̇) < 1e-4
        @test norm(val.ν̈) < 1e-4
        @test HC.judge(val; tol = 1e-4, tol_at_infinity = 1e-4) == HC.VAL_AT_INFINITY

        init!(val)
        init!(tracker, S[2], 0.0, 25.0)
        for _ in tracker
            if !state.last_step_failed
                HC.update!(val, state.x, state.ẋ, state.s, tracker.predictor)
            end
        end
        @test val.ν ≈ [1 // 3, 2 // 3] atol = 1e-3
        @test norm(val.ν̇) < 1e-3
        @test norm(val.ν̈) < 1e-3
        @test HC.judge(val; tol = 1e-3) == HC.VAL_FINITE

        init!(val)
        init!(tracker, S[2], 0.0, 25.0)
        for _ in tracker
            if !state.last_step_failed
                HC.update!(val, state.x, state.ẋ, state.s)
            end
        end
        @test val.ν ≈ [1 // 3, 2 // 3] atol = 1e-3
        @test norm(val.ν̇) < 1e-3
        @test norm(val.ν̈) < 1e-3
        @test HC.judge(val; tol = 1e-3) == HC.VAL_FINITE
        @test HC.judge(val; tol = 1e-10) == HC.VAL_INDECISIVE

        init!(val)
        init!(tracker, S[3], 0.0, 25.0)
        for _ in tracker
            if !state.last_step_failed
                HC.update!(val, state.x, state.ẋ, state.s)
            end
        end
        @test val.ν ≈ [2, -1] atol = 1e-5
        @test norm(val.ν̇) < 1e-5
        @test norm(val.ν̈) < 1e-5
        @test HC.judge(val; tol_at_infinity = 1e-4) == HC.VAL_AT_INFINITY
    end

    @testset "Correctness projective (non-zero valuation)" begin
        f = homogenize(equations(griewank_osborne()))
        tracker, starts = coretracker_startsolutions(
            f;
            homotopy = (g, f) -> StraightLineHomotopy(g, f; gamma = 1.0 + im),
            min_step_size = eps()^2,
            log_homotopy = true,
        )

        S = collect(starts)
        state = tracker.state
        val = HC.Valuation(state.x; affine = true)
        init!(tracker, S[1], 0.0, 25.0)
        for _ in tracker
            if !state.last_step_failed
                HC.update!(val, state.x, state.ẋ, state.s)
            end
        end
        @test val.ν ≈ [-0.5, -1] atol = 1e-4
        @test norm(val.ν̇) < 1e-4
        @test norm(val.ν̈) < 1e-4
        @test HC.judge(val; tol_at_infinity = 1e-4) == HC.VAL_AT_INFINITY

        # Use analytic estimates for ν̇ and ν̈
        init!(val)
        @test all(isnan, val.ν)
        init!(tracker, S[1], 0.0, 20.0)
        for _ in tracker
            if !state.last_step_failed
                HC.update!(val, state.x, state.ẋ, state.s, tracker.predictor)
            end
        end
        @test val.ν ≈ [-0.5, -1] atol = 1e-4
        @test norm(val.ν̇) < 1e-4
        @test norm(val.ν̈) < 1e-4
        @test HC.judge(val; tol_at_infinity = 1e-4) == HC.VAL_AT_INFINITY
        @test HC.judge(val; tol_at_infinity = 1e-10) == HC.VAL_INDECISIVE

        # PROJECTIVE VALUATIONS

        val = HC.Valuation(state.x; affine = false)
        init!(tracker, S[1], 0.0, 25.0)
        for _ in tracker
            if !state.last_step_failed
                HC.update!(val, state.x, state.ẋ, state.s)
            end
        end
        @test val.ν ≈ [0.5, 0, 1] atol = 1e-3
        @test norm(val.ν̇) < 1e-3
        @test norm(val.ν̈) < 1e-3
        @test HC.judge(val; tol = 1e-2) == HC.VAL_FINITE

        # Use analytic estimates for ν̇ and ν̈
        init!(val)
        @test all(isnan, val.ν)
        init!(tracker, S[1], 0.0, 25.0)
        for _ in tracker
            if !state.last_step_failed
                HC.update!(val, state.x, state.ẋ, state.s, tracker.predictor)
            end
        end
        @test val.ν ≈ [0.5, 0, 1] atol = 1e-3
        @test norm(val.ν̇) < 1e-3
        @test norm(val.ν̈) < 1e-3
        @test HC.judge(val; tol = 1e-2) == HC.VAL_FINITE
    end
end


module NodesAndModes

using DelimitedFiles # to read quadrature node data
import LinearAlgebra: diagm, eigen, Symmetric
import SpecialFunctions: gamma
include("meshgrid.jl")

# each type of element shape - used for dispatch only
abstract type AbstractElemShape end
struct Line <: AbstractElemShape end
struct Quad <: AbstractElemShape end
struct Tri <: AbstractElemShape end
struct Hex <: AbstractElemShape end
struct Wedge <: AbstractElemShape end
struct Pyr <: AbstractElemShape end
struct Tet <: AbstractElemShape end

export AbstractElemShape
export Line
export Tri, Quad
export Hex, Wedge, Pyr, Tet

# shared functions
export vandermonde, grad_vandermonde, basis
export nodes, equi_nodes, quad_nodes
include("common_functions.jl") # VDM, grad_VDM, and docs

# interp nodes for Tet(), Tri()
include("warpblend_interp_nodes.jl")

# export some convenient 1D routines by default
include("nodes_and_modes_1D.jl")
export gauss_lobatto_quad, gauss_quad
export jacobiP, grad_jacobiP

##################
# 1D elements
##################
include("line_element.jl")

# ##################
# # 2D elements
# ##################
include("tri_element.jl")
include("quad_element.jl")

##################
# 3D elements
##################
include("hex_element.jl")
include("wedge_element.jl")
include("pyr_element.jl")
include("tet_element.jl")

end # module


using Test
using ClassicAlgorithmsCollections

@testset "BIT Manipulation" begin
    # Tests for BIT Manipulation
    arr = [8, 1, 2, 12, 45, 100, 43]
    @test ClassicAlgorithmsCollections.maximum_xor_subarray(arr) == 110
    @test ClassicAlgorithmsCollections.sum_of_bit_difference(arr) == 104
    @test ClassicAlgorithmsCollections.magic_number(5) == 130
    @test ClassicAlgorithmsCollections.swapping_even_odd_bits(23) == 43
    arr_three_one = [12, 1, 12, 3, 12, 1, 1, 2, 3, 3]
    @test ClassicAlgorithmsCollections.get_single_integer(arr_three_one) == 2
    @test ClassicAlgorithmsCollections.int2binary(131) == ([1, 0, 0, 0, 0, 0, 1, 1], 1)
    @test ClassicAlgorithmsCollections.count_total_bits(17) == 35
end

@testset "Dynamic Programming" begin
    # Tests for BIT Manipulation
    arr = [8, 1, 2, 12, 45, 100, 43]
    @test ClassicAlgorithmsCollections.lcs_length("AGGTAB", "GXTXAYB") == 4
    arr = [10, 22, 9, 33, 21, 50, 41, 60]
    ClassicAlgorithmsCollections.lis_length(arr) == 5
    str1 = "sunday"
    str2 = "saturday"
    ClassicAlgorithmsCollections.minumum_operations(str1, str2) == 3
    ClassicAlgorithmsCollections.number_of_steps(8, 3) == 81
    set = [3, 34, 4, 12, 5, 2]
    sum_true = 9
    sum_false = 19
    ClassicAlgorithmsCollections.subset_sum_test(set, sum_true) == true
    ClassicAlgorithmsCollections.subset_sum_test(set, sum_false) == false
    val = [60, 100, 120, 230, 220]
    wt = [10, 20, 25, 30, 25]
    W_1 = 50
    W_2 = 10
    ClassicAlgorithmsCollections.knapsack_solver(val, wt, W_1) == 340
    ClassicAlgorithmsCollections.knapsack_solver(val, wt, W_2) == 60
end

@testset "Graph Theory" begin
    # Tests for Graph Theory
    graph = Dict(1 => [2, 3], 2 => [3], 3 => [1, 4], 4 => [4])
    @test ClassicAlgorithmsCollections.breadth_first_search(graph, 3) == [3, 1, 4, 2]
    @test ClassicAlgorithmsCollections.depth_first_search(graph, 3) == [3, 1, 2, 4]

    graph_with_weights_1 =
        Dict(1 => [(3, -2)], 2 => [(1, 4), (3, 3)], 3 => [(4, 2)], 4 => [(2, -1)])
    @test ClassicAlgorithmsCollections.shortest_path_tree(graph_with_weights_1, 2, 4) ==
          ([0 -1 -2 0; 4 0 2 4; 5 1 0 2; 3 -1 1 0], [2, 1, 3, 4])

    graph_with_weights_2 =
        Dict(1 => [(3, -2)], 2 => [(1, 4), (3, 3)], 3 => [(4, 2)], 4 => [(2, -1), (3, -1)])

    @test ClassicAlgorithmsCollections.shortest_path_tree(graph_with_weights_2) ==
          [0 -1 -2 0; 4 0 2 4; 5 1 0 2; 3 -1 -1 0]

    graph_cycle_true = Dict(1 => [2], 2 => [3], 3 => [1, 4])
    @test ClassicAlgorithmsCollections.graph_cycle_check(graph_cycle_true) == true

    graph_cycle_false = Dict(1 => [2], 2 => [5], 3 => [1, 4])
    @test ClassicAlgorithmsCollections.graph_cycle_check(graph_cycle_false) == false

    graph_with_spanning_tree =
        Dict(1 => [(2, 10), (3, 6), (4, 5)], 2 => [(4, 15)], 3 => [(4, 4)])
    @test ClassicAlgorithmsCollections.minimum_spanning_tree(graph_with_spanning_tree) ==
          [3 4 4; 1 4 5; 1 2 10]

    graph_bridge = Dict(1 => [2, 3, 4], 2 => [1, 3], 3 => [1, 2], 4 => [1, 5], 5 => [4])
    @test ClassicAlgorithmsCollections.graph_bridge_check(graph_bridge) == [4 5; 1 4]

    word_list = ["GEEKS", "FOR", "QUIZ", "GO"]
    graph_boogle = Dict(
        1 => ["G", "I", "Z"],
        2 => ["U", "E", "K"],
        3 => ["Q", "S", "E"],
        4 => ["D", "O", "P"],
        5 => ["F", "O", "R"],
    )

    @test ClassicAlgorithmsCollections.boogle_word_check(graph_boogle, word_list) ==
          ["GEEKS", "QUIZ", "FOR"]
end

@testset "Number Theory" begin
    # Tests for Number Theory
    @test ClassicAlgorithmsCollections.modular_exponentiation(2, 3, 15) == 8
    @test ClassicAlgorithmsCollections.modular_inverse(3, 11) == 4
    @test ClassicAlgorithmsCollections.modular_inverse(3, 1) == 1
    @test ClassicAlgorithmsCollections.euler_totient(20) ==
          [1, 1, 2, 2, 4, 2, 6, 4, 6, 4, 10, 4, 12, 6, 8, 8, 16, 6, 18, 8]
    @test ClassicAlgorithmsCollections.sieve_of_eratosthenes(50) ==
          [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47]
    num = [3, 4, 5]
    rem = [2, 3, 1]
    @test ClassicAlgorithmsCollections.chinese_remainder_theorem(num, rem) == 11
end

@testset "Sorting & Searching" begin
    # Test for sorting and searching
    array_sorting = [64, 34, 25, 12, 22, 11, 90]
    ref_sorting = [11, 12, 22, 25, 34, 64, 90]
    @test ClassicAlgorithmsCollections.bubble_sorting(array_sorting) == ref_sorting
    @test ClassicAlgorithmsCollections.insertion_sorting(array_sorting) == ref_sorting
    @test ClassicAlgorithmsCollections.merge_sorting(array_sorting) == ref_sorting
    @test ClassicAlgorithmsCollections.heap_sorting(array_sorting) == ref_sorting
    @test ClassicAlgorithmsCollections.quick_sorting(array_sorting) == ref_sorting

    array_searching = [10, 11, 12, 14, 16, 18, 19, 20, 21, 22, 23, 24, 33, 35, 42, 47]
    target_searching = 12
    ref_searching = 3
    @test ClassicAlgorithmsCollections.binary_search(array_searching, target_searching) ==
          ref_searching
    @test ClassicAlgorithmsCollections.interpolation_search(
        array_searching,
        target_searching,
    ) == ref_searching

    array_searching_pair = [10, 22, 28, 29, 30, 40]
    target_searching_pair = 56
    ref_searching_pair = (28, 29)
    @test ClassicAlgorithmsCollections.closest_pair_search(
        array_searching_pair,
        target_searching_pair,
    ) == ref_searching_pair

    array_unsorted_searching = [2, 5, 4, 7, 2, 8, 9, 3, 10, 2]
    target_unsorted_searching = 3
    ref_unsorted_searching = 8
    @test ClassicAlgorithmsCollections.binary_pivot_search(
        array_unsorted_searching,
        target_unsorted_searching,
    ) == ref_unsorted_searching
end

@testset "String & Array" begin
    old_string = "a!!!b.c79.d,e'f,ghi3###"
    reversed_string = "i!!!h.g79.f,e'd,cba3###"
    @test ClassicAlgorithmsCollections.reverse_pure_string(old_string) == reversed_string

    array_before_zigzag = [4, 3, 7, 8, 6, 2, 1, 10, 13, 3]
    array_after_zigzag = [3, 7, 4, 8, 2, 6, 1, 13, 3, 10]
    @test ClassicAlgorithmsCollections.zigzag_ordering(array_before_zigzag) ==
          array_after_zigzag

    arr = [5, 1, 3, 4, 7]
    sum = 12
    @test ClassicAlgorithmsCollections.count_triplet_elements(arr, sum) == 2

    array_pythagorean = [5, 1, 3, 4, 17, 8, 15, 2, 2, 13, 12]
    @test ClassicAlgorithmsCollections.count_pythagorean_elements(array_pythagorean) == 3

    arr_1 = [10, 15, 25]
    arr_2 = [5, 20, 30]
    sorted_combinations = [
        [10, 20],
        [10, 20, 25, 30],
        [10, 30],
        [15, 20],
        [15, 20, 25, 30],
        [15, 30],
        [25, 30],
    ]
    @test ClassicAlgorithmsCollections.combinations_of_2arrays(arr_1, arr_2) ==
          sorted_combinations
    subarray_in_array = [10, 12, 11, 9, 13, 14, 17, 18, 15]
    @test ClassicAlgorithmsCollections.find_maxlength_subarray(subarray_in_array) == 5

    @test ClassicAlgorithmsCollections.smallest_subset4sum(subarray_in_array, 52) == 4
    @test ClassicAlgorithmsCollections.smallest_subset4sum(subarray_in_array, 2) == 1
    @test ClassicAlgorithmsCollections.smallest_subset4sum(subarray_in_array, 5555) ==
          nothing

    sort_array = [1, 2, 5, 10, 20, 40]
    @test ClassicAlgorithmsCollections.find_smallest_nonelement(sort_array) == 4

    gradients_in_array = [100, 180, 260, 310, 40, 535, 695]
    @test ClassicAlgorithmsCollections.sum_of_postive_gradients(gradients_in_array) == 865
end


#Text provided under a Creative Commons Attribution license, CC-BY.  All code is made available under the FSF-approved BSD-3 license.  (c) Lorena A. Barba, Gilbert F. Forsyth 2017. Thanks to NSF for support via CAREER award #1149784.
#[@LorenaABarba](https://twitter.com/LorenaABarba)
# Translation to Julia by [@miguelraz_](https://twitter.com/miguelraz_)

## 12 steps to Navier–Stokes
# ======
# ***

# Hello! Welcome to the **12 steps to Navier–Stokes**. This is a practical module that is used in the beginning of an interactive
# Computational Fluid Dynamics (CFD) course taught by [Prof. Lorena Barba](http://lorenabarba.com) since Spring 2009 at Boston University.
# The course assumes only basic programming knowledge (in any language) and of course some
# foundation in partial differential equations and fluid mechanics. The practical module was inspired by the
# ideas of Dr. Rio Yokota, who was a post-doc in Barba's lab, and has been refined by Prof. Barba and
# her students over several semesters teaching the course. The course is taught entirely using Python (but this version is in Julia)
# and students who don't know Julia just learn as we work through the module.

# This [Pluto notebook](https://github.com/fonsp/Pluto.jl) will lead you through the first step of programming
# your own Navier–Stokes solver in Julia from the ground up.  We're going to dive right in.
# Don't worry if you don't understand everything that's happening at first, we'll cover it in
# detail as we move forward and you can support your learning with the videos of
# [Prof. Barba's lectures on YouTube](http://www.youtube.com/playlist?list=PL30F4C5ABCE62CB61).

# For best results, after you follow this notebook, prepare your own code for Step 1, either as a Julia script or in a clean Pluto notebook.

# To execute this Notebook, we assume you have invoked the notebook server using: `using Pluto; Pluto.run()`.

## Step 1: 1-D Linear Convection
# -----
# ***

# The 1-D Linear Convection equation is the simplest, most basic model that can be used to learn something about CFD.
# It is surprising that this little equation can teach us so much! Here it is:

# $$\frac{\partial u}{\partial t} + c \frac{\partial u}{\partial x} = 0$$

# With given initial conditions (understood as a *wave*), the equation represents the propagation of that
# initial *wave* with speed $c$, without change of shape. Let the initial condition be $u(x,0)=u_0(x)$.
# Then the exact solution of the equation is $u(x,t)=u_0(x-ct)$.

# We discretize this equation in both space and time, using the Forward Difference scheme for the time derivative
# and the Backward Difference scheme for the space derivative. Consider discretizing the spatial coordinate
# $x$ into points that we index from $i=1$ to $N$, and stepping in discrete time intervals of size $\Delta t$.

# From the definition of a derivative (and simply removing the limit), we know that:

# $$\frac{\partial u}{\partial x}\approx \frac{u(x+\Delta x)-u(x)}{\Delta x}$$

# Our discrete equation, then, is:

# $$\frac{u_i^{n+1}-u_i^n}{\Delta t} + c \frac{u_i^n - u_{i-1}^n}{\Delta x} = 0 $$

# Where $n$ and $n+1$ are two consecutive steps in time, while $i-1$ and $i$ are two neighboring points of the discretized $x$ coordinate. If there are given initial conditions, then the only unknown in this discretization is $u_i^{n+1}$.  We can solve for our unknown to get an equation that allows us to advance in time, as follows:

# $$u_i^{n+1} = u_i^n - c \frac{\Delta t}{\Delta x}(u_i^n-u_{i-1}^n)$$

# Now let's try implementing this in Julia.

# We'll start by importing a few libraries to help us out.

# * `Plots` is a 2D plotting library that we will use to plot our results

# Remember: comments in python are denoted by the pound sign
using Plots                        #here we load numpy

# Now let's define a few variables; we want to define an evenly spaced grid of points within
# a spatial domain that is 2 units of length wide, i.e., $x_i\in(0,2)$.
# We'll define a variable `nx`, which will be the number of grid points we want and `dx` will be the distance between any pair of adjacent grid points.

nx = 41  # try changing this number from 41 to 81 and Run All ... what happens?
dx = 2 / (nx-1)
nt = 25    #nt is the number of timesteps we want to calculate
dt = .025  #dt is the amount of time each timestep covers (delta t)
c = 1      #assume wavespeed of c = 1

# We also need to set up our initial conditions. The initial velocity $u_0$ is given as
# $u = 2$ in the interval $0.5 \leq x \leq 1$  and $u = 1$ everywhere else in $(0,2)$ (i.e., a hat function).

# Here, we use the function `ones()` defining an array which is `nx` elements long with every value equal to 1.

u = ones(nx)      #makes a vector of size nx filled with only 1.0s
u[Int.((.5/dx):(1/dx))] .= 2 # #setting u = 2 between 0.5 and 1 as per our I.C.s

println(u)

# Now let's take a look at those initial conditions using a Plots.jl plot.
# We've imported the `Plots` plotting library `pyplot` and the plotting function is called `plot`,
# so we'll call `pyplot.plot`. To learn about the myriad possibilities of Plots.jl,
# explore the [Documentation](https://docs.juliaplots.org/) of example plots.

# Here, we use the syntax for a simple 2D plot: `plot(x,y)`, where the `x` values are evenly distributed grid points:

using Plots
Plots.pyplot()
pyplot.plot(range(start = 0, stop = 2, length = nx), u);

# Why doesn't the hat function have perfectly straight sides? Think for a bit.

# Now it's time to implement the discretization of the convection equation using a finite-difference scheme.

# For every element of our array `u`, we need to perform the operation $u_i^{n+1} = u_i^n - c \frac{\Delta t}{\Delta x}(u_i^n-u_{i-1}^n)$

# We'll store the result in a new (temporary) array `un`, which will be the solution $u$ for the next time-step.
# We will repeat this operation for as many time-steps as we specify and then we can see how far the wave has convected.

# We first initialize our placeholder array `un` to hold the values we calculate for the $n+1$ timestep, using once again the Julia function `ones()`.

# Then, we may think we have two iterative operations: one in space and one in time (we'll learn differently later),
# so we'll start by nesting one loop inside the other. Note the use of the nifty `enumerate` function.
# When we write: `for i in nx` we will iterate through the `u` array.

un = ones(nx) #initialize a temporary array

for n in nt  #loop for values of n from 0 to nt, so it will run nt times
    un = copy(u) ##copy the existing values of u into un
    for (elem, i) in enumerate(nx)
    # for i in 1:length(nx) # Try commenting the previous line and uncommenting this one to see what happens!
        u[i] = un[i] - c * dt / dx * (un[i] - un[i-1])
    end
end

# **Note**—We will learn later that the code as written above is quite inefficient, and there are better ways to write this, Julia-style. But let's carry on.

# Now let's try plotting our `u` array after advancing in time.
plot(range(start = 0, end = 2, length = nx), u)

# OK! So our hat function has definitely moved to the right, but it's no longer a hat.  **What's going on?**

## Learn More
# -----
# ***

# For a more thorough explanation of the finite-difference method, including topics like the truncation error,
# order of convergence and other details, watch **Video Lessons 2 and 3** by Prof. Barba on YouTube.

# TODO
# from IPython.display import YouTubeVideo
# YouTubeVideo('iz22_37mMkk')

# TODO
# YouTubeVideo('xq9YTcv-fQg')
#
# For a careful walk-through of the discretization of the linear convection equation with finite differences
# (and also the following steps, up to Step 4), watch **Video Lesson 4** by Prof. Barba on YouTube.

# TODO
# YouTubeVideo('y2WaK7_iMRI')

## Last but not least

# **Remember** to rewrite Step 1 as a fresh Julia script or in *your own* Pluto notebook and
# then experiment by changing the discretization parameters.
# Once you have done this, you will be ready for [Step 2](./02_Step_2.jl).


module PoissonLagrangeMultiplierTests

using Test
using Gridap
using Gridap.Geometry

function is_left(coords)
  n = length(coords)
  x = (1/n)*sum(coords)
  ( x[1] <= 0.5 )
end

domain = (0,1,0,1)
partition = (3,3)
model = CartesianDiscreteModel(domain,partition)

labels = get_face_labeling(model)
bgface_to_mask = get_face_mask(labels,"boundary",1)
Γface_to_bgface = findall(bgface_to_mask)
model_Γ = BoundaryDiscreteModel(Polytope{1},model,Γface_to_bgface)

Γ = Triangulation(model_Γ)
Γface_coords = get_cell_coordinates(Γ)
Γface_mask = lazy_map(is_left,Γface_coords)
Γlface_Γface = findall(Γface_mask)
Γrface_Γface = findall(!,Γface_mask)
Γl = BoundaryTriangulation(model,view(Γface_to_bgface,Γlface_Γface))
Γr = BoundaryTriangulation(model,view(Γface_to_bgface,Γrface_Γface))

Λ = SkeletonTriangulation(model_Γ)

order = 2
reffe_u = ReferenceFE(lagrangian,Float64,order)
reffe_λ = ReferenceFE(lagrangian,Float64,order-1)
V = TestFESpace(model,reffe_u,conformity=:H1)
S = TestFESpace(model_Γ,reffe_λ,conformity=:L2)
U = TrialFESpace(V)
L = TrialFESpace(S)
Y = MultiFieldFESpace([V,S])
X = MultiFieldFESpace([U,L])

degree = 2*order
Ω = Triangulation(model)
dΩ = Measure(Ω,degree)
dΓl = Measure(Γl,degree)
dΓr = Measure(Γr,degree)
dΛ = Measure(Λ,degree)
n = get_normal_vector(Γ)
nΛ = get_normal_vector(Λ)

uₑ(x) = x[1]^2 + x[2]^2
f(x) = -Δ(uₑ)(x)

# Weak form. Additional non needed terms are added for testing purposes
a((u,λ),(v,η)) = ∫( ∇(u)⋅∇(v) )dΩ +
               ∫( (u+λ)*(η+v) - u*v - η*λ + 0.0*(n⋅∇(u)-λ)*(n⋅∇(v)-η) )dΓl +
               ∫( (u+λ)*(η+v) - u*v - η*λ + 0.0*(n⋅∇(u)-λ)*(n⋅∇(v)-η) )dΓr +
               ∫( 0.0*jump(n⋅∇(u))*mean(η) )dΛ +
               ∫( 0.0*jump(nΛ⋅∇(u))*mean(η) )dΛ
l((v,η)) = ∫( f*v )dΩ + ∫( uₑ*η )dΓl + ∫( uₑ*η )dΓr

op = AffineFEOperator(a,l,X,Y)
uh,λh = solve(op)

e = uh-uₑ
l2(v) = √(∑(∫(v*v)dΩ))

tol = 1.0e-10
@test l2(e) < tol

end


using ITensors,
      ITensorLocalMPO

import ITensors: op

function op(::OpName"Givens", ::SiteType"Electron", s1::Index, s2::Index; θ, θup = θ, θdn = θ)
  ampo = AutoMPO()
  ampo += (θup, "Cdagup", 1, "Cup", 2)
  ampo -= (θup, "Cdagup", 2, "Cup", 1)
  ampo += (θdn, "Cdagdn", 1, "Cdn", 2)
  ampo -= (θdn, "Cdagdn", 2, "Cdn", 1)
  return exp(prod(MPO(ampo, [s1, s2])))
end

function main(N; θ, pattern)
  s = siteinds("Electron", N; conserve_qns = true)

  U = 1.0
  a = AutoMPO()
  for n in 1:N-1
    a .+= -1.0, "Cdagup", n, "Cup", n+1
    a .+= -1.0, "Cdagup", n+1, "Cup", n
    a .+= -1.0, "Cdagdn", n, "Cdn", n+1
    a .+= -1.0, "Cdagdn", n+1, "Cdn", n
  end
  for n in 1:N
    a .+= U, "Nupdn", n
  end
  H = MPO(a, s)

  if pattern == "staircase"
    gates = [("Givens", (n, n+1), (θ = θ,)) for n in 1:N-1]
  elseif pattern == "brick"
    gates = [("Givens", (n, n+1), (θ = θ,)) for n in 1:2:N-1]
    append!(gates, [("Givens", (n, n+1), (θ = θ,)) for n in 2:2:N-1])
  else
    error("Gate pattern $pattern not supported")
  end
  U = ops(s, gates)

  # Without basis change
  UH1 = H
  mpo_evolution_succeeded = false
  UH1 = try
    UH1 = apply(U, UH1; apply_dag = true, cutoff = 1e-8)
    UH1 = apply(U, UH1; apply_dag = true, cutoff = 1e-8)
    mpo_evolution_succeeded = true
    UH1
  catch
    println("apply(U, H::MPO, apply_dag = true, [...]) failed, likely because of overflow")
  end

  # With basis change
  basis_transformation = scale_bases(H; normalize_op = "Id")

  UX, HX = combine_and_transform(U, H, basis_transformation...)
  UHX = HX
  UHX = apply(UX, UHX; cutoff = 1e-8)
  UHX = apply(UX, UHX; cutoff = 1e-8)

  # Fix normalization
  normsUHX = norm.(UHX)
  UHX ./= normsUHX
  lognormsUHX = log.(normsUHX)
  lognormUHX = sum(lognormsUHX)
  UHX .*= exp(lognormUHX / N)

  UH2 = inv_transform_and_uncombine(UHX, basis_transformation...)

  @show maxlinkdim(H), maxlinkdim(HX), maxlinkdim(UHX), maxlinkdim(UH2)
  mpo_evolution_succeeded && @show maxlinkdim(UH1)
  @show norm(H), norm(HX), norm(UHX), norm(UH2)
  mpo_evolution_succeeded && @show norm(UH1)
  @show maximum(norm, H), maximum(norm, HX), maximum(norm, UHX), maximum(norm, UH2)
  mpo_evolution_succeeded && @show maximum(norm, UH1)
  mpo_evolution_succeeded && @show inner(UH1, UH2) / (norm(UH1) * norm(UH2))

  return nothing
end


# Romeo LTI kernel
#
# Implementation of kernel function for various systems.

import SpecialFunctions

"""
    kernel([T=typeof(1.0), ] sys, N, Δt)

Given a system `sys` compute its kernel vector of length `N`, uniformly
discretized with sample time `Δt`.

When simulating the response `y` of any system to a given signal `u`, the kernel is
defined here as a sequence of samples `k` such that the response is equal to the
discrete convolution `k ⋆ u`.
"""
kernel(sys::SisoLtiSystem, N::Integer, Δt::Real) = kernel(typeof(1.0), sys, N, Δt)

kernel(T::Type{<:Number}, sys::ZeroSys, N::Integer, Δt::Real) = zeros(T, N)
kernel(T::Type{<:Number}, sys::UnitSys, N::Integer, Δt::Real) = convid(T, N)

function kernel(T::Type{<:Number}, sys::Diff{<:Number}, N::Integer, Δt::Real)
    @assert N ≥ 1 "Kernel should be at least one sample long"
    if sys.α == 0
        res = zeros(T, N)
        res[1] = one(T)
        return res
    elseif sys.α == 1
        res = zeros(T, N)
        res[1] = one(T)
        res[2] = -one(T)
        return res / Δt
    elseif real(sys.α) > 0
        # TODO: fix this branch
        ddt = kernel(T, Diff(one(typeof(sys.α))), N, Δt)
        temp = kernel(T, Diff(sys.α - 1), N, Δt)
        return convolve(ddt, temp)
    elseif real(sys.α) < 0
        ikt = (0:N) .^ (-sys.α)
        return (ikt[2:end] - ikt[1:end-1]) .* (Δt^(-sys.α) / SpecialFunctions.gamma(-sys.α + 1))
    else
        @error "Not implemented!"
    end
end

kernel(T::Type{<:Number}, sys::ScaledSystem, N::Integer, Δt::Real) = sys.k * kernel(T, sys.inner, N, Δt)
kernel(T::Type{<:Number}, sys::ParallelSystem, N::Integer, Δt::Real) = kernel(T, sys.first, N, Δt) .+ kernel(T, sys.second, N, Δt)
kernel(T::Type{<:Number}, sys::SeriesSystem, N::Integer, Δt::Real) = convolve(kernel(T, sys.first, N, Δt), kernel(T, sys.second, N, Δt))
kernel(T::Type{<:Number}, sys::RationalSystem, N::Integer, Δt::Real) = deconvolve(kernel(T, sys.num, N, Δt), kernel(T, sys.den, N, Δt))

function kernel(T::Type{<:Number}, sys::PowerSystem{<:Number}, N::Integer, Δt::Real)
    if imag(sys.α ≠ 0)
        @error "Cannot compute kernel of a complex power of an arbitrary system."
    end
    α = real(sys.α)
    if α > 0
        if floor(α) == α
            k1 = kernel(T, sys.inner, N, Δt)
            k = copy(k)
            for i = 2:floor(Int, sys.α)
                k = convolve(k, k1)
            end
            return k
        else
            h = floor(Int, α)
            r = α - h
            if r == 0.5
                k1 = convroot(kernel(T, sys.inner, N, Δt))
                k = copy(k)
                for i = 2 * floor(Int, h) + 1
                    k = convolve(k, k1)
                end
                return k
            else
                @error "Unable to compute kernel of this power system."
            end
        end
    else
        convinv(kernel(T, PowerSystem(-sys.α, sys.inner), N, Δt))
    end
end

export kernel


using LifeTable
using Test
using DataFrames
using CSV
using GLM

# Builds a lifetable for Swedish males 2014 (based on data from Statistics Sweden).
mse14 = CSV.read("data/mse14.csv")
ltmse14 = periodlifetable(mse14, 1)

@test isapprox(ltmse14[:e][1], 80.3453; atol=1e-3)

# Performs a regression on Weibull parameters for survival for Swedish
# females dying of leukemia in age intervals from 30--34 to 80--84 years
# 1951--2012 (based on data from WHO Mortality Database).

leuk_msfs = Dict[]
fse5112tot3084 = CSV.read("data/fse5112tot3084.csv")
fse5112pop3084 = CSV.read("data/fse5112pop3084.csv")
fse5112leuk3084 = CSV.read("data/fse5112leuk3084.csv")

for i in 2:63
	dths = DataFrame(age = fse5112pop3084[1], p = fse5112pop3084[i], d = fse5112tot3084[i])
	lt = periodlifetable(dths, 2, false)
	leuk_prop = fse5112leuk3084[i] ./ fse5112tot3084[i]
	leuk_life = causelife(lt, leuk_prop)
	leuk_lt = periodlifetable(leuk_life, 2, false, "rate")
	leuk_msf = mortsurvfit(leuk_lt, fse5112tot3084[i], "weibull", "surv")
	push!(leuk_msfs, leuk_msf)
end

fit_leuk_msfs = mortsurvparamsfit(leuk_msfs)

@test isapprox(coef(fit_leuk_msfs["fit"])[2], -4.5; atol=1e-1)


#---------------------------------------------------------------------#
#This function computes the Initial & Exact Solutions.
#Written by F.X. Giraldo on April 19, 2019
#           Department of Applied Mathematics
#           Naval Postgraduate School
#           Monterey; CA 93943-5216
#---------------------------------------------------------------------#
function exact_solution(coord,Npoin,time,case,DFloat)

    #Set some constants
    xmin=0
    xmax=+2
    xc=0.5*(xmax+xmin)
    xl=xmax-xmin
    rc=0.125
    sigma=1/16; #best for right convergence rates
    # sigma=1/8
    # sigma=1/4; #fatter Gaussian to compare against 2D code

    #Initialize
    qe=zeros(DFloat,Npoin)

    #timec=time - floor(time)
    timec=time
    #Generate Grid Points
    for I=1:Npoin
        x=coord[I]
        if (case == 1) #Sin
            qe[I]=sin(pi*x) + 0.01;
        end
    end #I
    return (qe)
end


#=
   SEND+MORE=MONEY in Julia + ConstraintSolver.jl

   Model created by Hakan Kjellerstrand, hakank@gmail.com
   See also my Julia page: http://www.hakank.org/julia/

=#

using ConstraintSolver, JuMP
const CS = ConstraintSolver
include("constraints_utils.jl")

function send_more_money()
    model = Model(optimizer_with_attributes(CS.Optimizer, "all_solutions"=>true,"logging"=>[]))
    @variable(model, 0 <= x[1:8] <= 9, Int)
    s,e,n,d,m,o,r,y = x

    @constraint(model, x in CS.AllDifferent())
    @constraint(model, s != 0)
    @constraint(model, m != 0)
    @constraint(model,
                              1000*s + 100*e + 10*n + d
                           +  1000*m + 100*o + 10*r + e
                 == 10000*m + 1000*o + 100*n + 10*e + y
        )

    # Solve the problem
    optimize!(model)

    status = JuMP.termination_status(model)
    println("status:$status")
    if status == MOI.OPTIMAL
        x = convert.(Integer,JuMP.value.(x))
        println(x)
        num_sols = MOI.get(model, MOI.ResultCount())
        println("num_sols:$num_sols\n")
    end
end

send_more_money()


using CompScienceMeshes
using Test

#include(Pkg.dir("CompScienceMeshes","examples","matlab_patches.jl"))
for T in [Float32, Float64]
    sphere = readmesh(joinpath(dirname(@__FILE__),"assets","sphere2.in"),T=T)
    index_in_sphere = mapper(sphere)
    for (i,_c) in enumerate(cells(sphere))
        @eval @test $index_in_sphere[$_c] == $i
    end

    isdownbelow(c) = center(chart(sphere,c))[3] < 0
    southern_hemisphere = submesh(isdownbelow, sphere)

    # build the restriction operator
    R = spzeros(Int, numcells(southern_hemisphere), numcells(sphere))
    for (i,_c) in enumerate(cells(southern_hemisphere))
        j = index_in_sphere[_c]
        R[i,j] = 1
    end

    @test nnz(R) == numcells(southern_hemisphere)

    rect = meshrectangle(T(1.0), T(1.0), T(1.0), 3)
    @test numcells(rect) == 2
    tri = Mesh(rect.vertices, rect.faces[1:1])
    @test numcells(tri) == 1

    R = restriction(tri, rect)
    @test R == [1 0]
end


export  PlaneStrainViscoelasticityProny, PlaneStressViscoelasticityProny, ViscoelasticityProny

@doc raw"""
    mutable struct ViscoelasticityProny
        ρ::Float64 # density
        E::Float64 # Young's Modulus
        ν::Float64 # Poisson's ratio
        τ::Float64 # relaxation time
        c::Float64 # coefficient
        planestress::Bool
        g0::Float64
        g1::Float64
        J::Float64
        σ0::Array{Float64} # stress at last time step
        σ0_::Array{Float64} # σ0 to be updated in `commitHistory`
        ε0::Array{Float64} # strain at last time step
        ε0_::Array{Float64} # ε0 to be updated in `commitHistory`
    end

Prony series viscoelasticity model.

$$E(t) = E_0 - cE_0(1-\exp(-t/\tau))$$

In principle, we can have different Prony series for both shear modulus $G$ and bulk modulus $K$,

$$\sigma_{ij} = 3K \left( \frac{1}{3} \epsilon_{kk}\delta_{ij}\right) + 2G \left( \epsilon_{ij} - \frac{1}{3}\epsilon_{kk} \delta_{ij} \right)$$

We only implemented $E(t)$ case for simplicity.
"""
mutable struct ViscoelasticityProny
    ρ::Float64 # density
    E::Float64 # Young's Modulus
    ν::Float64 # Poisson's ratio
    τ::Float64 # relaxation time
    c::Float64 # coefficient
    planestress::Bool
    g0::Float64
    g1::Float64
    J::Array{Float64,1}
    J_::Array{Float64,1}
    H::Array{Float64, 2}
    σ0::Array{Float64} # stress at last time step
    σ0_::Array{Float64} # σ0 to be updated in `commitHistory`
    ε0::Array{Float64} # strain at last time step
    ε0_::Array{Float64} # ε0 to be updated in `commitHistory`
end

function ViscoelasticityProny(prop::Dict{String, Any})
    ρ = Float64(prop["rho"])
    E = Float64(prop["E"])
    ν = Float64(prop["nu"])
    τ = Float64(prop["tau"])
    c = Float64(prop["c"])
    planestress = Bool(prop["planestress"])
    H = zeros(3,3)
    if planestress
        H[1,1] = E/(1. -ν*ν)
        H[1,2] = H[1,1]*ν
        H[2,1] = H[1,2]
        H[2,2] = H[1,1]
        H[3,3] = E/(2.0*(1.0+ν))
    else
        H[1,1] = E*(1. -ν)/((1+ν)*(1. -2. *ν));
        H[1,2] = H[1,1]*ν/(1-ν);
        H[2,1] = H[1,2];
        H[2,2] = H[1,1];
        H[3,3] = H[1,1]*0.5*(1. -2. *ν)/(1. -ν);
    end

    planestress = Bool(prop["planestress"])
    σ0 = zeros(3); σ0_ = zeros(3)
    ε0 = zeros(3); ε0_ = zeros(3)
    J = zeros(3);  J_ = zeros(3)
    G0 = E/(2(1+ν))
    g0 = G0*(1-c)
    g1 = G0*c
    ViscoelasticityProny(ρ, E, ν, τ, c, planestress, g0, g1, J, J_, H, σ0, σ0_, ε0, ε0_)
end

function getStress(mat::ViscoelasticityProny,  strain::Array{Float64},  Dstrain::Array{Float64}, Δt::Float64)
    τ = mat.τ
    g0, g1, J = mat.g0, mat.g1, mat.J
    σ = mat.H * strain - 2*g0*Dstrain + 2*g1*(- Dstrain) + exp(-Δt/τ)*J
    J = exp(-Δt/τ) * J + 2*g1*(strain - Dstrain)
    dΔσdΔε = mat.H
    mat.σ0_ = σ
    mat.ε0_ = strain
    mat.J_ = J
    return σ, dΔσdΔε
end

function getTangent(mat::ViscoelasticityProny)
    error("Not implemented")
end

function commitHistory(mat::ViscoelasticityProny)
    mat.σ0 = mat.σ0_
    mat.ε0 = mat.ε0_
    mat.J = mat.J_
end

"""
    PlaneStrainViscoelasticityProny(prop::Dict{String, Any})

Returns the plane strain viscoelasticity Prony shear series material model.
"""
function PlaneStrainViscoelasticityProny(prop::Dict{String, Any})
    prop["planestress"] = false
    ViscoelasticityProny(prop)
end

"""
    PlaneStrainViscoelasticityProny(prop::Dict{String, Any})

Returns the plane stress viscoelasticity Prony shear series material model.
"""
function PlaneStressViscoelasticityProny(prop::Dict{String, Any})
    prop["planestress"] = true
    ViscoelasticityProny(prop)
end


function simulate_logit(J,T, beta, v)
# --------------------------------------------------%
# Simulate logit with outside option
# --------------------------------------------------%
# J = number of products
# T = number of markets
# beta = coefficient on delta
# v = standard deviation of market-product demand shock

zt = 0.9 .* rand(T,J) .+ 0.05;
xit = randn(T,J).*v;
pt = 2 .*(zt .+ rand(T,J)*0.1).+xit;

denominator = 1;
s = zeros(T,J)
for j = 1:1:J
    denominator = denominator .+ exp.(beta*pt[:,j] .+ xit[:,j]);
end
for j = 1:1:J
    s[:,j] = exp.(beta*pt[:,j] .+ xit[:,j])./denominator;
end
s, pt, zt, xit
end