using Test

using JacobiDavidson, LinearAlgebra

@testset "Schur permutations" begin
    A = rand(ComplexF64, 10, 10)
    B = rand(ComplexF64, 10, 10)
    F = schur(A, B)

    τ = 1.0 + 0.0im
    eigenvalues = F.alpha ./ F.beta

    # Test whether the ordering is correct
    closest_to_one = first(schur_permutation(Near(τ), F, 1:1))
    @test abs.(eigenvalues[closest_to_one] - τ) ≤ minimum(abs.(eigenvalues .- τ))

    largest_magnitude = first(schur_permutation(LargestMagnitude(τ), F, 1:1))
    @test abs(eigenvalues[largest_magnitude]) ≥ maximum(abs.(eigenvalues))

    smallest_magnitude = first(schur_permutation(SmallestMagnitude(τ), F, 1:1))
    @test abs(eigenvalues[smallest_magnitude]) ≤ minimum(abs.(eigenvalues))

    largest_real_part = first(schur_permutation(LargestRealPart(τ), F, 1:1))
    @test real(eigenvalues[largest_real_part]) ≥ maximum(real.(eigenvalues))

    smallest_real_part = first(schur_permutation(SmallestRealPart(τ), F, 1:1))
    @test real(eigenvalues[smallest_real_part]) ≤ minimum(real.(eigenvalues))

    largest_imaginary_part = first(schur_permutation(LargestImaginaryPart(τ), F, 1:1))
    @test imag(eigenvalues[largest_imaginary_part]) ≥ maximum(imag.(eigenvalues))

    smallest_imaginary_part = first(schur_permutation(SmallestImaginaryPart(τ), F, 1:1))
    @test imag(eigenvalues[smallest_imaginary_part]) ≤ minimum(imag.(eigenvalues))

    # Test whether one can reorder the Schur decomp with multiple eigenvalues
    take = 1 : 3; keep = 4 : 10
    schur_sort!(Near(τ), F, take)
    sorted = F.alpha ./ F.beta
    @test maximum(abs.(sorted[take] .- τ)) ≤ minimum(abs.(sorted[keep] .- τ))

    # Test whether one can reorder the Schur decomp with a single eigenvalue
    schur_sort!(SmallestMagnitude(τ), F, 1)
    sorted = F.alpha ./ F.beta
    @test all(abs(sorted[1]) .≤ abs.(sorted[2 : end]))
end


export FactorSize, Factor, VectorFactor, MatrixFactor
export factorsize, factorranks, factorndims
export factor, factormatrix, factorrankselect, block
export factorvcat, factorhcat, factordcat, factorltcat, factorutcat
export factorranktranspose, factormodetranspose
export factorcontract, factormp, factorkp, factorhp
export factorqr!, factorqradd, factorsvd!

const FactorSize = Vector{Int}
const Factor{T,N} = Array{T,N} where {T<:Number,N}
const VectorFactor{T} = Factor{T,3}
const MatrixFactor{T} = Factor{T,4}

function factorsize(U::Factor{T,N}) where {T<:Number,N}
	if length(U) == 0
		throw(ArgumentError("the factor should not be empty"))
	end
	sz = size(U)
	if length(sz) < 3
		throw(ArgumentError("the factor should have two rank dimensions and at least one mode dimension"))
	end
	return FactorSize(collect(sz[2:end-1]))
end

function factorranks(U::Factor{T,N}) where {T<:Number,N}
	if length(U) == 0
		throw(ArgumentError("the factor should not be empty"))
	end
	sz = size(U)
	if length(sz) < 3
		throw(ArgumentError("the factor should have two rank dimensions and at least one mode dimension"))
	end
	return sz[1],sz[end]
end

function factorndims(U::Factor{T,N}) where {T<:Number,N}
	if N < 3
		throw(ArgumentError("the factor should have two rank dimensions and at least one mode dimension"))
	end
	return N-2
end

function factor(U::Array{T,N}) where {T<:Number,N}
	U = reshape(U, (1,size(U)...,1))
	U = Factor{T,N+2}(U)
	return U
end

function factor(U::Matrix{T}, m::Union{Int,NTuple{M,Int},Vector{Int},Vector{Any}}, n::Union{Int,NTuple{N,Int},Vector{Int},Vector{Any}}, π::Union{NTuple{K,Int},Vector{Int}}) where {T<:Number,K,M,N}
	d = length(π)
	if d == 0
		throw(ArgumentError("π is empty"))
	end
	if isa(m, Vector{Any}) && length(m) > 0
		throw(ArgumentError("m should be an integer, a vector or tuple of integers or an empty vector or tuple"))
	end
	isa(m, Int) && (m = [m])
	isa(m, Vector{Any}) && (m = Vector{Int}(undef, 0))
	isa(m, Tuple{}) && (m = Vector{Int}(undef, 0))
	isa(m, Vector{Int}) || (m = collect(m))
	dm = length(m)
	if isa(n, Vector{Any}) && length(n) > 0
		throw(ArgumentError("n should be an integer, a vector or tuple of integers or an empty vector or tuple"))
	end
	isa(n, Int) && (n = [n])
	isa(n, Vector{Any}) && (n = Vector{Int}(undef, 0))
	isa(n, Tuple{}) && (n = Vector{Int}(undef, 0))
	isa(n, Vector{Int}) || (n = collect(n))
	dn = length(n)
	if dm+dn ≠ d || !isperm(π)
		throw(ArgumentError("π is not a valid permutation of 1:length(m)+length(n)"))
	end
	sz = size(U); p = sz[1]÷prod(m); q = sz[2]÷prod(n)
	if sz[1] ≠ prod(m)*p
		throw(DimensionMismatch("the first dimension of U is not divisible by prod(m)"))
	end
	if sz[2] ≠ prod(n)*q
		throw(DimensionMismatch("the second dimension of U is not divisible by prod(n)"))
	end
	U = reshape(U, (m...,p,n...,q))
	U = permutedims(U, (dm+1,π[1:dm]...,(π[dm+1:d].+1)...,d+2))
	return U
end

factor(U::Matrix{T}, m::Union{Int,NTuple{M,Int},Vector{Int},Vector{Any}}, n::Union{Int,NTuple{N,Int},Vector{Int},Vector{Any}}) where {T<:Number,M,N} = factor(U, m, n, collect(1:length(m)+length(n)))
factor(U::Vector{T}, m::Union{Int,NTuple{M,Int},Vector{Int},Vector{Any}}, π::Union{NTuple{M,Int},Vector{Int}}) where {T<:Number,M} = factor(U[:,:], m, (), π)
factor(U::Vector{T}, m::Union{Int,NTuple{M,Int},Vector{Int},Vector{Any}}) where {T<:Number,M} = factor(U[:,:], m, (), collect(1:length(m)))

function factormatrix(U::Factor{T,K}, π::Indices, σ::Indices) where {T<:Number,K}
	d = factorndims(U)
	if d == 0
		throw(ArgumentError("U should have at least one mode dimension"))
	end
	if isa(π, Vector{Any}) && length(π) > 0
		throw(ArgumentError("π should be an integer, a vector or tuple of integers, a colon or an empty vector or tuple"))
	end
	π = indvec(π; min=1, max=d); dπ = length(π)
	if isa(σ, Vector{Any}) && length(σ) > 0
		throw(ArgumentError("σ should be an integer, a vector or tuple of integers, a colon or an empty vector or tuple"))
	end
	σ = indvec(σ; min=1, max=d); dσ = length(σ)
	τ = vcat(π,σ)
	if dπ+dσ ≠ d || !isperm(τ)
		throw(ArgumentError("π and σ do not constitute a valid permutation of the mode dimensions of U"))
	end
	n = factorsize(U); p,q = factorranks(U)
	U = permutedims(U, ((π.+1)...,1,(σ.+1)...,d+2))
	U = reshape(U, p*prod(n[π]), q*prod(n[σ]))
	return U
end

function factorrankselect(U::Factor{T,N}, α::Indices, β::Indices) where {T<:Number,N}
	isa(α, Int) && (α = [α])
	isa(β, Int) && (β = [β])
	p,q = factorranks(U)
	α = indvec(α; min=1, max=p)
	β = indvec(β; min=1, max=q)
	if length(α) == 0
		throw(ArgumentError("the index or range for the first rank is empty"))
	end
	if α ⊈ 1:p
		throw(ArgumentError("the index or range for the first rank is incorrect"))
	end
	if length(β) == 0
		throw(ArgumentError("the index or range for the second rank is empty"))
	end
	if β ⊈ 1:q
		throw(ArgumentError("the index or range for the second rank is incorrect"))
	end
	d = N-2
	V = U
	V = selectdim(V, d+2, β)
	V = selectdim(V, 1, α)
	V = copy(V)
	return V
end

function block(U::Factor{T,N}, α::Int, β::Int) where {T<:Number,N}
	p,q = factorranks(U)
	if α ∉ 1:p
		throw(ArgumentError("the first rank index is out of range"))
	end
	if β ∉ 1:q
		throw(ArgumentError("the second rank index is out of range"))
	end
	d = N-2
	V = U
	V = selectdim(V, d+2, β)
	V = selectdim(V, 1, α)
	V = copy(V)
	return V
end

function factorvcat(U::Factor{T,N}, V::Factor{T,N}, W::Vararg{Factor{T,N},M}) where {T<:Number,N,M}
	m = factorsize(U); _,p = factorranks(U)
	W = (V,W...)
	for V ∈ W
		if factorsize(V) ≠ m
			throw(ArgumentError("the factors are incompatible in mode size"))
		end
		_,q = factorranks(V)
		if q ≠ p
			throw(ArgumentError("the factors are incompatible in the second rank"))
		end
	end
	return cat(U, W...; dims=1)
end

function factorhcat(U::Factor{T,N}, V::Factor{T,N}, W::Vararg{Factor{T,N},M}) where {T<:Number,N,M}
	m = factorsize(U); p,_ = factorranks(U)
	W = (V,W...)
	for V ∈ W
		if factorsize(V) ≠ m
			throw(ArgumentError("the factors are incompatible in mode size"))
		end
		q,_ = factorranks(V)
		if q ≠ p
			throw(ArgumentError("the factors are incompatible in the first rank"))
		end
	end
	d = N-2
	return cat(U, W...; dims=d+2)
end

function factordcat(U::Factor{T,N}, V::Factor{T,N}, W::Vararg{Factor{T,N},M}) where {T<:Number,N,M}
	m = factorsize(U)
	W = (V,W...)
	for V ∈ W
		if factorsize(V) ≠ m
			throw(ArgumentError("the factors are incompatible in mode size"))
		end
	end
	d = N-2
	return cat(U, W...; dims=(1,d+2))
end

function factorutcat(U₁₁::Factor{T,N}, U₁₂::Factor{T,N}, U₂₂::Factor{T,N}) where {T<:Number,N}
	n₁₁ = factorsize(U₁₁); p₁₁,q₁₁ = factorranks(U₁₁)
	n₁₂ = factorsize(U₁₂); p₁₂,q₁₂ = factorranks(U₁₂)
	n₂₂ = factorsize(U₂₂); p₂₂,q₂₂ = factorranks(U₂₂)
	if n₁₂ ≠ n₁₁ || n₂₂ ≠ n₁₁
		throw(ArgumentError("the factors are incompatible in mode size"))
	end
	if p₁₁ ≠ p₁₂
		throw(ArgumentError("U₁₁ and U₁₂ are incompatible in the first rank"))
	end
	if q₁₂ ≠ q₂₂
		throw(ArgumentError("U₁₂ and U₂₂ are incompatible in the second rank"))
	end
	U₂₁ = zeros(T, (p₂₂,n₁₁...,q₁₁))
	d = N-2
	return cat(cat(U₁₁, U₂₁; dims=1), cat(U₁₂, U₂₂; dims=1); dims=d+2)
end

function factorltcat(U₁₁::Factor{T,N}, U₂₁::Factor{T,N}, U₂₂::Factor{T,N}) where {T<:Number,N}
	n₁₁ = factorsize(U₁₁); p₁₁,q₁₁ = factorranks(U₁₁)
	n₂₁ = factorsize(U₂₁); p₂₁,q₂₁ = factorranks(U₂₁)
	n₂₂ = factorsize(U₂₂); p₂₂,q₂₂ = factorranks(U₂₂)
	if n₂₁ ≠ n₁₁ || n₂₂ ≠ n₁₁
		throw(ArgumentError("the factors are incompatible in mode size"))
	end
	if p₂₁ ≠ p₂₂
		throw(ArgumentError("U₂₁ and U₂₂ are incompatible in the first rank"))
	end
	if q₁₁ ≠ q₂₁
		throw(ArgumentError("U₁₁ and U₂₁ are incompatible in the second rank"))
	end
	U₁₂ = zeros(T, (p₁₁,n₁₁...,q₂₂))
	d = N-2
	return cat(cat(U₁₁, U₂₁; dims=1), cat(U₁₂, U₂₂; dims=1); dims=d+2)
end

function factorranktranspose(U::Factor{T,N}) where {T<:Number,N}
	d = N-2
	prm = vcat(d+2,(1:d).+1,1)
	U = permutedims(U, prm)
	return U
end

function factormodetranspose(U::Factor{T,N}, π::Union{NTuple{K,Int},Vector{Int}}) where {T<:Number,N,K}
	d = N-2
	if d == 0
		throw(ArgumentError("the factor should have at least one mode dimension"))
	end
	if length(π) ≠ d || !isperm(π)
		throw(ArgumentError("π is not a valid permutation of the mode dimensions of U"))
	end
	isa(π, Vector{Int}) || (π = collect(π))
	prm = vcat(1,π.+1,d+2)
	U = permutedims(U, prm)
	return U
end

factormodetranspose(U::Factor{T,N}) where {T<:Number,N} = factormodetranspose(U, collect(factorndims(U):-1:1))

function factorcontract(U::Factor{T,N}, V::Factor{T,N}; rev::Bool=false, major::String="last") where {T<:Number,N}
	if major ∉ ("first","last")
		throw(ArgumentError("major should be either \"last\" (default) or \"first\""))
	end
	rev && ((U,V) = (V,U))
	m = factorsize(U); (p,r) = factorranks(U)
	n = factorsize(V); (s,q) = factorranks(V)
	if r ≠ s
		throw(ArgumentError("U and V have inconsistent ranks"))
	end
	d = length(m)
	if d == 0
		throw(ArgumentError("U has no mode dimensions"))
	end
	U = reshape(U,(p*prod(m),r)); V = reshape(V,(r,prod(n)*q))
	W = U*V; W = reshape(W, (p,m...,n...,q))
	prm = collect(1:2d); prm = reshape(prm, (d,2))
	(major == "first") && (prm = reverse(prm; dims=2))
	prm = prm'; prm = prm[:]
	W = permutedims(W, [1,(prm.+1)...,2*d+2])
	k = [m..., n...]; k = k[prm]; k = reshape(k, (2,d)); k = prod(k; dims=1)
	W = reshape(W, (p,k...,q))
	W = Factor{T}(W)
	return W
end

function factormp(U₁::Factor{T,N₁}, σ₁::Indices, U₂::Factor{T,N₂}, σ₂::Indices) where {T<:Number,N₁,N₂}
	n₁ = factorsize(U₁); d₁ = factorndims(U₁)
	n₂ = factorsize(U₂); d₂ = factorndims(U₂)
	if isa(σ₁, Vector{Any}) && length(σ₁) > 0
		throw(ArgumentError("if σ₁ is passed as a vector, it should be a vector of the type Vector{Int} or an empty vector of the type Vector{Any}"))
	end
	if isa(σ₂, Vector{Any}) && length(σ₂) > 0
		throw(ArgumentError("if σ₂ is passed as a vector, it should be a vector of the type Vector{Int} or an empty vector of the type Vector{Any}"))
	end
	isa(σ₁, Vector{Int}) || (σ₁ = indvec(σ₁; max=d₁))
	isa(σ₂, Vector{Int}) || (σ₂ = indvec(σ₂; max=d₂))
	if length(σ₁) ≠ length(σ₂)
		throw(ArgumentError("the specified sets of modes of σ₁ and σ₂ are inconsistent"))
	end
	if σ₁ ⊈ 1:d₁ || unique(σ₁) ≠ σ₁
		throw(ArgumentError("the set of modes of U₁ is specified incorrectly"))
	end
	if σ₂ ⊈ 1:d₂ || unique(σ₂) ≠ σ₂
		throw(ArgumentError("the set of modes of U₂ is specified incorrectly"))
	end
	if n₁[σ₁] ≠ n₂[σ₂]
		throw(ArgumentError("U₁ and U₂ are inconsistent with respect to the specified modes"))
	end
	τ₁ = setdiff(1:d₁, σ₁)
	τ₂ = setdiff(1:d₂, σ₂)
	(p₁,q₁) = factorranks(U₁); (p₂,q₂) = factorranks(U₂)
	U₁ = permutedims(U₁, (1,d₁+2,(τ₁.+1)...,(σ₁.+1)...))
	U₁ = reshape(U₁, p₁*q₁*prod(n₁[τ₁]), prod(n₁[σ₁]))
	U₂ = permutedims(U₂, [(σ₂.+1)...,(τ₂.+1)...,1,d₂+2])
	U₂ = reshape(U₂, prod(n₂[σ₂]), prod(n₂[τ₂])*p₂*q₂)
	U = U₁*U₂; n = [n₁[τ₁]..., n₂[τ₂]...]; d = length(τ₁) + length(τ₂)
	if d == 0
		n = [1]; d = 1
	end
	U = reshape(U, (p₁,q₁,n...,p₂,q₂))
	U = permutedims(U, (1,d+3,(3:d+2)...,2,d+4))
	U = reshape(U, (p₁*p₂,n...,q₁*q₂))
	return U
end

# function factorkp(U::Factor{T,N}, V::Vararg{Factor{T,N},M}) where {T<:Number,N,M}
# 	d = factorndims(U)
# 	nf = 1+length(V)
# 	n = Matrix{Int}(undef, d, nf)
# 	p = Vector{Int}(undef, nf)
# 	q = Vector{Int}(undef, nf)
# 	n[:,1] = factorsize(U); p[1],q[1] = factorranks(U)
# 	for k ∈ 2:nf
# 		n[:,k] = factorsize(V[k-1]); p[k],q[k] = factorranks(V[k-1])
# 		U = factormp(U, [], V[k-1], [])
# 	end
# 	prm = collect(1:nf*d); prm = reshape(prm, d, nf)
# 	prm = prm'; prm = prm[:]
# 	U = permutedims(U, [1,(prm.+1)...,nf*d+2])
# 	p,q = prod(p),prod(q); n = prod(n; dims=2)
# 	U = reshape(U, p, n..., q)
# 	return U
# end

# function factorkp2(U::Factor{T,N}, V::Factor{T,N}) where {T<:Number,N}
# 	d = factorndims(U)
# 	n = factorsize(U).*factorsize(V)
# 	p,q = factorranks(U).*factorranks(V)
# 	W = factormp(U, [], V, [])
# 	prm = collect(1:2*d); prm = reshape(prm, d, 2)
# 	prm = prm'; prm = prm[:]
# 	W = permutedims(W, [1,(prm.+1)...,2*d+2])
# 	W = reshape(W, p, n..., q)
# 	return W
# end

function factorkp(U::Union{Factor{T,N},Tuple{Factor{T,N},Int}}, V::Vararg{Union{Factor{T,N},Tuple{Factor{T,N},Int}},M}) where {T<:Number,N,M}
	V = (U,V...)
	nf = length(V)
	U = Vector{Factor{T,N}}(undef, nf)
	s = Vector{Int}(undef, nf)
	for k ∈ 1:nf
		W = V[k]
		if isa(W, Tuple)
			if W[2] < 0
				throw(ArgumentError("all the specified exponents should be nonnegative"))
			end
			U[k] = W[1]
			s[k] = W[2]
		else
			U[k] = W
			s[k] = 1
		end
	end
	m = findfirst(s .> 0)
	d = factorndims(U[m])
	W = U[m]
	n = factorsize(U[m])
	p,q = factorranks(U[m])
	for i ∈ 2:s[m]
		W = factormp(W, [], U[m], [])
		n = n.*factorsize(U[m])
		p,q = (p,q).*factorranks(U[m])
	end
	for k ∈ m+1:nf
		for i ∈ 1:s[k]
			W = factormp(W, [], U[k], [])
			n = n.*factorsize(U[k])
			p,q = (p,q).*factorranks(U[k])
		end
	end
	nf = sum(s)
	prm = collect(1:nf*d); prm = reshape(prm, d, nf)
	prm = prm'; prm = prm[:]
	W = permutedims(W, [1,(prm.+1)...,nf*d+2])
	W = reshape(W, p, n..., q)
	return W
end

function factorhp(U::Factor{T,N}, V::Factor{T,N}) where {T<:Number,N}
	m = factorsize(U); (p,q) = factorranks(U)
	n = factorsize(V); (r,s) = factorranks(V)
	if m ≠ n
		throw(ArgumentError("U and V should have the same mode size"))
	end
	m = prod(n); U = reshape(U, (p,m,q)); V = reshape(V, (r,m,s))
	W = Array{T,5}(undef, p, r, m, q, s)
	for i ∈ 1:m
		W[:,:,i,:,:] = reshape(kron(V[:,i,:], U[:,i,:]), (p,r,q,s))
	end
	W = reshape(W, (p*r,n...,q*s))
	return W
end

function factorqr!(U::Factor{T,N}; rev::Bool=false) where {T<:FloatRC{<:AbstractFloat},N}
	n = factorsize(U); (p,q) = factorranks(U); m = ones(Int, length(n))
	if rev
		U = reshape(U, p, prod(n)*q)
		R,U = lq!(U); R,U = Matrix(R),Matrix(U)
		ρ = size(R, 2)
		R,U = reshape(R, (p,m...,ρ)),reshape(U, (ρ,n...,q))
	else
		U = reshape(U, p*prod(n), q)
		U,R = qr!(U); U,R = Matrix(U),Matrix(R)
		ρ = size(R, 1)
		U,R = reshape(U, (p,n...,ρ)),reshape(R, (ρ,m...,q))
	end
	U = Factor{T,N}(U)
	R = Factor{T,N}(R)
	return U,R
end

factorqr!(U::Factor{T,N}, ::Val{false}; rev::Bool=false) where {T<:FloatRC{<:AbstractFloat},N} = factorqr!(U;  rev=rev)

function factorqr!(U::Factor{T,N}, ::Val{true}; rev::Bool=false) where {T<:FloatRC{<:AbstractFloat},N}
	n = factorsize(U); (p,q) = factorranks(U); m = ones(Int, length(n))
	if rev
		U = reshape(U, p, prod(n)*q)
		U = Matrix(U') # reallocation
		f = qr!(U, Val(true))
		U,R = Matrix(f.Q),Matrix(f.R*adjoint(f.P))
		R,U = Matrix(R'),Matrix(U')
		ρ = size(R, 2)
		R,U = reshape(R, (p,m...,ρ)),reshape(U, (ρ,n...,q))
	else
		U = reshape(U, p*prod(n), q)
		f = qr!(U, Val(true))
		U,R = Matrix(f.Q),Matrix(f.R*adjoint(f.P))
		ρ = size(R, 1)
		U,R = reshape(U, (p,n...,ρ)),reshape(R, (ρ,m...,q))
	end
	U = Factor{T,N}(U)
	R = Factor{T,N}(R)
	return U,R
end

function factorqradd(Q::Factor{T,N}, R::Union{Factor{T,N},Nothing}, U::Factor{T,N}; rev::Bool=false) where {T<:FloatRC{<:AbstractFloat},N}
	# assumes that Q is orthogonal w.r.t the first rank if rev==true and w.r.t the second rank if rev==false
	n = factorsize(Q); m = ones(Int, length(n))
	if factorsize(U) ≠ n
		throw(ArgumentError("Q and U are incompatible in mode size"))
	end
	if rev
		r,q = factorranks(Q); s,qq = factorranks(U)
		if isa(R, Nothing)
			R = reshape(Matrix{T}(I, r, r), (r,m...,r)); p = r
		else
			if factorsize(R) ≠ m
				throw(ArgumentError("R should have the same number of mode dimensions as Q and unitary mode sizes"))
			end
			p,rr = factorranks(R)
			if rr ≠ r
				throw(ArgumentError("R and Q are incompatible in rank"))
			end
		end
		if qq ≠ q
			throw(ArgumentError("Q and U are incompatible in the second rank"))
		end
		R = reshape(R, p, r)
		Q = reshape(Q, r, prod(n)*q)
		U = reshape(U, s, prod(n)*q)
		R,Q = lqaddrows(R, Q, U); r = size(R, 2)
		R,Q = reshape(R, (p+s,m...,r)),reshape(Q, (r,n...,q))
	else
		p,r = factorranks(Q); pp,s = factorranks(U)
		if isa(R, Nothing)
			R = reshape(Matrix{S}(I, r, r), (r,m...,r)); q = r
		else
			if factorsize(R) ≠ m
				throw(ArgumentError("R should have the same number of mode dimensions as Q and unitary mode sizes"))
			end
			rr,q = factorranks(R)
			if rr ≠ r
				throw(ArgumentError("Q and R are incompatible in rank"))
			end
		end
		if pp ≠ p
			throw(ArgumentError("Q and U are incompatible in the first rank"))
		end
		R = reshape(R, r, q)
		Q = reshape(Q, p*prod(n), r)
		U = reshape(U, p*prod(n), s)
		Q,R = qraddcols(Q, R, U); r = size(R, 1)
		Q,R = reshape(Q, (p,n...,r)),reshape(R, (r,m...,q+s))
	end
	Q,R = Factor{T,N}(Q),Factor{T,N}(R)
	return Q,R
end

"""
	factorsvd!(W, m, n; aTol=0, rTol=0, rank=0, major="last", rev=false)

produces U and V such that W ≈ U ⋈ V if rev == false and W ≈ V ⋈ U if rev == true
U is orthogonal — with respect to the second or first rank index
                  if rev == false or rev == true respectively
m and n are the mode-size vectors of U and V respectively (at most one of the two arguments may be replaced by ":"
major determines whether the "first" or "last" factor in the SKP carries the major bits
rank=0 means no rank thresholding
"""
function factorsvd!(W::Factor{T,N},
                    m::Union{FactorSize,Colon},
                    n::Union{FactorSize,Colon};
                    aTol::S=convert(S, 0),
                    rTol::S=convert(S, 0),
                    rank::Int=0,
                    major::String="last",
					rev::Bool=false)	where {S<:AbstractFloat,T<:FloatRC{S},N}
	d = factorndims(W)
	k = factorsize(W)
	if isa(m, Colon) && isa(n, Colon)
		throw(ArgumentError("to avoid ambiguity, at lease one of the two mode-size parameters should be specified as a vector"))
	end
	if isa(m, FactorSize)
		if any(m .≤ 0)
			throw(ArgumentError("the elements of the first mode-size parameter, when it is specified, should be positive"))
		end
		if length(m) ≠ d
			throw(DimensionMismatch("the number of entries in the first mode-size vector is inconsistent with the specified factor"))
		end
	else
		m = k.÷n
		if k ≠ m.*n
			throw(DimensionMismatch("not every mode dimension of W is divisible by the corresponding element of the specified mode-size vector"))
		end
	end
	if isa(n, FactorSize)
		if any(n .≤ 0)
			throw(ArgumentError("the elements of the second mode-size vector, when it is specified, should be positive"))
		end
		if length(n) ≠ d
			throw(DimensionMismatch("the number of entries in the second mode-size vector is inconsistent with the specified factor"))
		end
	else
		n = k.÷m
		if k ≠ m.*n
			throw(DimensionMismatch("not every mode dimension of W is divisible by the corresponding element of the specified mode-size vector"))
		end
	end
	if k ≠ m.*n
		throw(DimensionMismatch("the specified mode-size vectors are inconsistent with the specified factor"))
	end
	if major ∉ ("first","last")
		throw(ArgumentError("major should be either \"last\" (default) or \"first\""))
	end
	#
	if aTol < 0 || !isfinite(aTol)
		throw(ArgumentError("aTol, when specified, should be nonnegative and finite"))
	end
	#
	aTol² = aTol.^2
	if !isfinite(aTol²)
		throw(ErrorException("overflow encountered while squaring aTol, which was passed finite"))
	end
	if aTol > 0 && aTol² == 0
		throw(ErrorException("underflow encountered while squaring aTol, which was passed positive"))
	end
	#
	if rTol < 0 || !isfinite(rTol)
		throw(ArgumentError("rTol, when specified, should be nonnegative and finite"))
	end
	#
	if rank < 0
		throw(ArgumentError("the optional argument rank should be nonnegative"))
	end
	ρ = rank
	#
	(p,q) = factorranks(W); prm = collect(1:d)
	k = rev ? [n; m] : [m; n]
	prm = (major == "last") ? [2*prm.-1; 2*prm] : [2*prm; 2*prm.-1]
	W = reshape(W, (p,k[invperm(prm)]...,q))
	W = permutedims(W, vcat(1, prm.+1, 2d+2))
	W = reshape(W, p*prod(k[1:d]), prod(k[d+1:2d])*q)
	U,Σ,V = svd!(W; full=false); V = V';
	σ = copy(Σ); μ = norm(σ)
	if !isfinite(μ)
		throw(ErrorException("overflow encountered while computing the norm of the decomposition"))
	end
	rTol = min(rTol, 1.0);
	τ = μ*rTol
	if μ > 0 && rTol > 0 && τ == 0
		throw(ErrorException("underflow encountered while computing the absolute accuracy threshold from the relative one"))
	end
	τ² = τ^2
	if μ > 0 && rTol > 0 && τ² == 0
		throw(ErrorException("underflow encountered while computing the squared absolute accuracy threshold from the squared relative one"))
	end
	τ² = [aTol²,τ²]; ind = (τ² .> 0)
	τ² = any(ind) ? minimum(τ²[ind]) : 0.0
	if μ == 0
		ε = 0.0; δ = 0.0; ρ = 1
		U = zeros(T, (p,k[1:d]...,1))
		V = zeros(T, (1,k[d+1:2d]...,q))
	else
		ε²,ρ = threshold(σ.^2, τ², ρ); ε = sqrt(ε²); δ = ε/μ
		ρ = max(ρ,1)
		U = U[:,1:ρ]; Σ = Σ[1:ρ]; V = V[1:ρ,:]
		rev ? U = U*Diagonal(Σ) : V = Diagonal(Σ)*V
		U = reshape(U, (p,k[1:d]...,ρ))
		V = reshape(V, (ρ,k[d+1:2d]...,q))
		rev && ((U,V) = (V,U))
	end
	U = Factor{T,N}(U)
	V = Factor{T,N}(V)
	return U,V,ε,δ,μ,ρ,σ
end


# compare-hyperparameters-with-kernels
#
# This script compares the quality of samples generated from different
# step size hyperparameter settings of a Stochastic Gradient Langevin Dynamics
# sampler for a Gaussian mixture model posterior target distribution.

include("experiment_utils.jl")

# set the discrepancy
@parsestringcli discrepancytype "k" "discrepancytype" "maternradial"

using Iterators: product

using SteinDistributions:
    SteinGMMPosterior,
    randgmm,
    numdimensions,
    numdatapoints,
    gradlogdensity
using SteinDiscrepancy:
    SteinGaussianKernel,
    SteinMaternRadialKernel,
    SteinGaussianPowerKernel,
    SteinInverseMultiquadricKernel,
    ksd
using SteinSamplers: runsgld

## Experiment settings
# Target size of sample to draw from sampler
n = 1000
# Run one complete experiment per random number generator seed
seeds = 1:1
# Sampler settings for SGLD
distname = "sgld-gmm"
# set the kernel
kernel = nothing
if discrepancytype == "maternradial"
    kernel = SteinMaternRadialKernel()
elseif discrepancytype == "gaussian"
    kernel = SteinGaussianKernel()
elseif discrepancytype == "gaussianpower"
    kernel = SteinGaussianPowerKernel(2.0)
elseif discrepancytype == "inversemultiquadric"
    kernel = SteinInverseMultiquadricKernel(0.5)
end

## Target distribution settings for SteinGMMPosterior
# Generate random dataset from GMM
truex = [0.0, 1.0] # Parameter vector used to generate dataset
L = 100 # Number of datapoints to generate
data_generation_seed = 1
y = @setseed data_generation_seed randgmm(truex, L)
# save the y values for later
save_json(
    Dict{Any, Any}("y" => y);
    dir="compare-hyperparameters-with-kernels-y",
    numsamples=L,
    x=string(truex)
)
# Create posterior distribution based on dataset
target = SteinGMMPosterior(y)
# define gradlogp
function gradlogp(x::Array{Float64,1})
    gradlogdensity(target, x)
end
# Fix mini-batch size
batchsize = 5
# Number of sweeps to make through the data
numsweeps = round(Int, ceil(n / floor(numdatapoints(target)/batchsize)))
# Hyperparameter to vary: step size epsilon
epsilons = [5e-2, 1e-2, 5e-3, 1e-3, 5e-4, 1e-4, 5e-5, 1e-5]
# Define SGLD sampler with step size epsilon and starting sample value x0
sampler(x0,epsilon) = runsgld(target, x0;
                              epsilonfunc=t -> epsilon,
                              numsweeps=numsweeps,
                              batchsize=batchsize)

## Draw and evaluate samples for different hyperparameter and seed combinations
d = numdimensions(target)
for (epsilon, seed) in product(epsilons, seeds)
    # Generate starting sample value x0
    x0 = @setseed seed randn(d)

    println("Generating sample for seed=$(seed), epsilon=$(epsilon)")
    # Run sampler
    X = []; numgrad = NaN
    @trycatchcontinue(
        begin
            (X, numgrad) = @setseed seed sampler(x0,epsilon)
            if any(isnan(X))
                println("[$(distname):seed=$(seed),epsilon=$(epsilon)] NaNs found. Skipping.")
                continue
            end
        end,
        println("[$(distname):seed=$(seed),epsilon=$(epsilon)]:")
    )

    # Compute Stein discrepancy for first n points in sample
    res = nothing
    @trycatchcontinue(
        begin
            res = ksd(points=X[1:n,:], gradlogdensity=gradlogp, kernel=kernel)
        end,
        println("[n=$(n)|sampler=$(distname)]:")
    )
    if res == nothing
        continue
    end
    # compute the coercive function
    kerneldiscrepancy = sqrt(res.discrepancy2)

    # Package results
    instance_data = Dict{Any, Any}(
        "distname" => distname,
        "discrepancytype" => discrepancytype,
        "n" => n,
        "d" => d,
        "seed" => seed,
        "epsilon" => epsilon,
        "X" => X[1:n,:],
        "q" => res.weights,
        "steindiscrepancy" => kerneldiscrepancy,
        "numgrad" => numgrad * (n / size(X, 1)),
        "solvetime" => res.solvetime,
    )

    # Save results
    save_json(
        instance_data;
        dir="compare-hyperparameters-with-kernels",
        distname=distname,
        discrepancytype=discrepancytype,
        n=n,
        epsilon=epsilon,
        d=d,
        seed=seed
    )
end


"""
    guesswork_lower_bound(
        p::AbstractVector{T},
        ρBs::AbstractVector{<:AbstractMatrix};
        solver,
        c = T[1:length(p)..., 10_000],
        verbose::Bool = false,
    )

See [`guesswork`](@ref) for the meaning of the arguments. Computes a lower bound
to the optimal expected number of guesses by solving a relaxed version of the
primal SDP. For `J` states, only needs `J^2` PSD variables subject to two linear
constraints.
"""
function guesswork_lower_bound(
    p::AbstractVector{T},
    ρBs::AbstractVector{<:AbstractMatrix};
    solver,
    c = T[1:length(p)..., 10_000],
    verbose::Bool = false,
) where {T<:Number}
    length(p) == length(ρBs) ||
    throw(ArgumentError("Length of prior and vector of side information must match J"))
    J = length(p)
    dB = size(ρBs[1], 1)
    all(ρB -> size(ρB) == (dB, dB), ρBs) ||
    throw(ArgumentError("All side-information states must be square matrices of the same size"))

    ℰs = [ComplexVariable(dB, dB) for j = 1:J, k = 1:J]

    objective = real(sum(c[j] * tr(ℰs[j, k] * p[k] * ρBs[k]) for j = 1:J for k = 1:J))

    constraints = Convex.Constraint[ℰ ⪰ 0 for ℰ in ℰs] |> vec

    for j = 1:J
        push!(constraints, sum(ℰs[j, :]) == I(dB))
        push!(constraints, sum(ℰs[:, j]) == I(dB))
    end

    problem = compatible_problem(Convex.minimize, objective, constraints, T)

    solve!(problem, solver; verbose = verbose)

    ℰs = evaluate.(ℰs)

    data = (J = J, K = J, c = c, p = p, ρBs = ρBs, dB = dB)

    return (optval = problem.optval, status = problem.status, ℰs = ℰs, data...)
end


using JuMP, MosekTools, COSMO
using SketchyCGAL
using LinearAlgebra, SparseArrays
using BSON

include("utils.jl")

G = graph_from_file(joinpath(@__DIR__, "data/gset/G1"))
n = size(G, 1)

function solve_with_jump(C; optimizer=Mosek.Optimizer)
    sdp = Model(optimizer)
    @variable(sdp, X[1:n, 1:n] in PSDCone())
    @constraint(sdp, diag(X) .== 1)
    @objective(sdp, Min, sum(C.*X))
    optimize!(sdp)
    Xopt = value.(X)

    # Optionally, we can apply a correction to enforce feasibility
    # Xopt[diagind(X)] .= ones(n)
    return Xopt
end

# Builds the LMI of the dual problem
function build_affine_exp(C, y)
    n = size(C, 1)
    aff_exp = spzeros(GenericAffExpr{Float64, VariableRef}, n, n)

    # This is a bit of a hack
    for (j, k, v) ∈ zip(findnz(C)...)
        aff_exp[j,k] = 1
    end

    for i in 1:n
        add_to_expression!(aff_exp[i,i], -y[i])
    end
    for (j, k, v) ∈ zip(findnz(C)...)
        add_to_expression!(aff_exp[j,k], C[j,k])
    end

    # hack part 2
    for (j, k, v) ∈ zip(findnz(C)...)
        aff_exp[j,k] -= 1
    end

    return aff_exp
end

function solve_dual_with_jump(C; optimizer=Mosek.Optimizer)
    sdp = Model(optimizer)
    @variable(sdp, y[1:n])
    @objective(sdp, Max, sum(y))
    A = Symmetric(build_affine_exp(C, y))
    psd_constraint = @constraint(sdp, A in PSDCone())
    optimize!(sdp)
    Xopt = value.(dual(psd_constraint))
    return Xopt
end

## ---------- Problem setup ----------
# Parameters
n = size(G, 1)
d = n

# Data
# objective = min -1/4( ∑_ij w_ij*(1-yi*yj) ) = -1/4⟨diag(∑ᵢ w_ij) - W), Y⟩
# note that this reformulation uses the fact that Tr(Y) = 1
C = -0.25*(Diagonal(G*ones(n)) - G)
b = ones(n)

# Scaling variables -- so trace is bounded by 1
scale_C = 1 / norm(C)
scale_X = 1 / n

# Linear map
# AX = diag(X)
function A!(y, X)
    @views y .= X[diagind(X)]
    return nothing
end

# Adjoint
# A*z = Diagonal(z)
function A_adj!(S::SparseMatrixCSC, z)
    @views S[diagind(S)] .+= z
    return nothing
 end

# primative 1: u -> C*u
function C_u!(v, u)
    mul!(v, C_const, u)
end

#primative 2: (u, z) -> (A*z)u
function Aadj_zu!(v, u, z)
    v .= u .* z
end

#primative 3: u -> A(uu^T)
function A_uu!(z, u)
    z .= u.*u
end

## Solve with JuMP + COSMO
Xopt = solve_with_jump(C)

# We can also try the dual version of the problem + chordal decomposition,
#   but in this case, this approach seems much slower
# Xopt = solve_dual_with_jump(C;
#     optimizer=with_optimizer(
#         COSMO.Optimizer,
#         decompose = true,
#         merge_strategy = COSMO.CliqueGraphMerge
# ))

# Uncomment the line below to save the optimal soln
# BSON.bson("G67_Xopt", Dict("Xopt" => Xopt))

pstar_jump = sum(C .* Xopt)
@show pstar_jump

## Solve with CGAL
@time XT, yT = SketchyCGAL.cgal_full(
    C, b, A!, A_adj!; n=n, d=d, scale_X=scale_X, scale_C=scale_C,
    max_iters=250,
    print_iter=25
)

pstar_cgal = sum(C .* XT * 1/scale_X)
@show pstar_cgal

## Solve with SketchyCGAL
R = 20
ηt(t) = 2.0/(t + 1.0)
δt(t) = 1.0
tt = @timed scgal_full(
    C, b, A!, A_adj!, A_uu!; n=n, d=d, scale_X=scale_X, scale_C=scale_C,
    max_iters=1_000,
    print_iter=100,
    R=R,
    # logging=true,
    # logging_primal=true,
    ηt=ηt,
    # δt=δt
)

soln = tt.value
trial_time = tt.time
trial_alloc = tt.bytes/1e6 #(MB)

Xhat = SketchyCGAL.construct_Xhat(soln)
pstar_scgal = sum(C .* Xhat) * 1/scale_X
@show pstar_scgal

## Compute true objective (MAXCUT after rounding scheme)
cache = zeros(n, R)
@time true_obj_scgal = SketchyCGAL.compute_objective(C, soln.UT, soln.ΛT, cache=cache)# * 1/scale_X
@show true_obj_scgal

## Compute primal infeasibility
@time p_infeas_scgal = SketchyCGAL.compute_primal_infeas_mc(soln.UT, soln.ΛT, cache=cache)
@show p_infeas_scgal


"""
    cor_bounds(dA::UnivariateDistribution, dB::UnivariateDistribution, C::Type{<:Correlation}=Pearson; n_samples::Real=100_000)

Compute the stochastic lower and upper correlation bounds between two marginal
distributions.

This method relies on sampling from each distribution and then estimating the
specified correlation between the sorted samples. Because the samples are random,
there will be some variation in the answer for each call to `cor_bounds`. Increasing
the number of samples will increase the accuracy of the estimate, but will also
take longer to sort. Therefore ≈100,000 samples (the default) are recommended so
that it runs fast while still returning a good estimate.

The possible correlation types are:
  * [`Pearson`](@ref)
  * [`Spearman`](@ref)
  * [`Kendall`](@ref)

# Examples
```julia-repl
julia> using Distributions

julia> A = Normal(78, 10); B = LogNormal(3, 1);

julia> cor_bounds(A, B)
(lower = -0.7646512417819491, upper = 0.7649206069306482)

julia> cor_bounds(A, B, n_samples=Int(1e9))
(lower = -0.7629776825238167, upper = 0.7629762333824238)

julia> cor_bounds(A, B, n_samples=Int(1e4))
(lower = -0.7507010142250724, upper = 0.7551879647701095)

julia> cor_bounds(A, B, Spearman)
(lower = -1.0, upper = 1.0)
```
"""
function cor_bounds(dA::UD, dB::UD, C::Type{<:Correlation}=Pearson; n_samples::Real=100_000)
    a = rand(dA, Int(n_samples))
    b = rand(dB, Int(n_samples))

    sort!(a)
    sort!(b)
    upper = cor(a, b, C)

    reverse!(b)
    lower = cor(a, b, C)

    (lower = lower, upper = upper)
end

"""
    cor_bounds(margins::Vector{<:UD}, C::Type{<:Correlation}=Pearson; n_samples::Real=100_000)

Compute the stochastic pairwise lower and upper correlation bounds between a set
of marginal distributions.

The possible correlation types are:
  * [`Pearson`](@ref)
  * [`Spearman`](@ref)
  * [`Kendall`](@ref)
"""
function cor_bounds(margins::Vector{<:UD}, C::Type{<:Correlation}=Pearson; n_samples::Real=100_000)
    d = length(margins)
    lower, upper = zeros(Float64, d, d), zeros(Float64, d, d)
    n_samples = Int(n_samples)

    @threads for i in collect(subsets(1:d, Val{2}()))
        l, u = cor_bounds(margins[i[1]], margins[i[2]], C, n_samples=n_samples)
        lower[i...] = l
        upper[i...] = u
    end

    lower .= Symmetric(lower)
    cor_constrain!(lower)

    upper .= Symmetric(upper)
    cor_constrain!(upper)

    (lower = lower, upper = upper)
end


# this file is executed for building a custom system image of this project

using TimetableSolver

C9 = Division(9, 3)
D10 = Division(10, 4)

ajay = Teacher("Ajay Magotra", "AJA", ["MATH",], [9, 10])
kotwal = Teacher("Kotwal Singh", "KOT", ["MATH",], [9,])
deepika = Teacher("Deepika Ibikunle", "DPK", ["ENG",], [9, 10])
ganguly = Teacher("Ganguly A", "GNG", ["SPORTS",], [9,])
sanjay = Teacher("Sanjay Pandey", "SNJ", ["SCIENCE",], [10,])

subjectcounts_C9 = OrderedDict(
    "MATH"=>2, "ENG"=>2, "SPORTS"=>1
)
subjectcounts_D10 = OrderedDict(
    "MATH"=>2, "ENG"=>1, "SCIENCE"=>2
)

tt_C9 = Timetable(
    [3, 2],
    subjectcounts_C9,
    [ajay, kotwal, deepika, ganguly],
    C9
)
tt_D10 = Timetable(
    [3, 2],
    subjectcounts_D10,
    [ajay, deepika, sanjay],
    D10
)

schedule = Schedule(tt_C9, tt_D10)

vardata = get_data(schedule)
m, modelvars = get_model(vardata)

vardata = get_data(schedule)
model, modelvars = get_model(vardata)
rawsolution, status = get_solution(model, modelvars, vardata)
solution = convertsolution(rawsolution, vardata)
applysolution!(schedule, solution)
solution, status = solve!(schedule)


### A Pluto.jl notebook ###
# v0.18.1

using Markdown
using InteractiveUtils

# This Pluto notebook uses @bind for interactivity. When running this notebook outside of Pluto, the following 'mock version' of @bind gives bound variables a default value (instead of an error).
macro bind(def, element)
    quote
        local iv = try Base.loaded_modules[Base.PkgId(Base.UUID("6e696c72-6542-2067-7265-42206c756150"), "AbstractPlutoDingetjes")].Bonds.initial_value catch; b -> missing; end
        local el = $(esc(element))
        global $(esc(def)) = Core.applicable(Base.get, el) ? Base.get(el) : iv(el)
        el
    end
end

# ╔═╡ da1d65a0-ec42-11ea-0141-334c9eeeb035
begin
	import ImageMagick
	using PlutoUI
	using Images
end

# ╔═╡ 5ef51c3a-70a7-11eb-2023-31113399a57f
html"""

Section 1.2

 Intro to Abstractions

Lecture Video

"""

# ╔═╡ 60ae819a-70a7-11eb-31d4-750c7f5dc6ca
PlutoUI.TableOfContents(aside=true)

# ╔═╡ 792c6a62-ec41-11ea-01f3-73e7eee23cc7
md"""
#### Intializing packages

_When running this notebook for the first time, this could take up to 15 minutes. Hang in there!_
"""

# ╔═╡ ef1bfa16-70ea-11eb-189c-a54db292cd6f
md"
## Introduction

The goal of this section is to introduce you to the notion of abstraction. You can think of abstraction as an opposite to specialization. We will illustrate this by looking at the following example.

### What is _one_?

Before we get lost talking about the foundations of number theory, I will present you with a few examples that represent one to me.
"

# ╔═╡ 6fcac482-70ee-11eb-0b80-ff41c708053b
md"Each of the items in this list is a specific, or **_specialized_** representation of _one_:
1. as an integer
1. as a float
1. as a string
1. as a rational number
1. as a cute picture
1. as an 2x2 identity matrix
1. as a singular dog

Of course, these are just a few examples of _one_. People have been representing _one_ for ages in different langauges, scripts, artistic expression, etc.

The difference between these ones to me is clear. In fact, I just articulated it to you. Now, let's turn to how a computer sees _one_ differently based on what I type.
"

# ╔═╡ 9ebc079a-70f0-11eb-07d9-f9e80f3f4584
md"So to a computer, all of these are different types."

# ╔═╡ 15f7f90a-70f0-11eb-0d41-63677e4023f4
md"### What is a collection of _one_s?

Now, I want to make a collection of ones for some reason. Below is a way for you to experiment building this collection with different _one_s. As you do this experiment, I want you to look at what stays in the same in the Julia output, and what doesn't."

# ╔═╡ f6886d90-70ed-11eb-07c4-471ee267e7c1
md"""
Before we even look at the output, I am amazed that this code even ran. Are you telling me that the computer doesn't care which _one_ I am using in my array?

Yes! That's exactly what abstraction is. By stepping back, we can now think and operate at a level that doesn't care about which _specific_ one I am using. This is what we mean by **abstraction is the opposite of specialization**.

The information that Julia gives back is quite informative. Here is an example of the first line of the output for a few different types:

```
array = 3x4 Array{Int64, 2}
array = 3x4 Array{Float64, 2}
array = 3x4 Array{Rational{Int64}, 2}
```

Notice that for all of these, we have the same `3x4 Array{***, 2}`.
"""

# ╔═╡ 3c1a3cf8-70f8-11eb-3c18-375207f321eb
md"""
## First Taste of Abstraction

Now, I want to do something to a collection of ones, that doesn't care about which one I'm using. So I'm going to write a function that takes in my collection, and add a corgi whereever I desire.
"""

# ╔═╡ 19f4ddb0-ec44-11ea-20b9-5d97fb2b1cf4
function insert(new, A, i, j)
	B = copy(A)
	B[i,j] = new
	return B
end

# ╔═╡ 424f5f10-ec44-11ea-076d-f3cba4435e0c
begin
	A = fill(0, 3, 4)
	md"""
	$(@bind i Slider(1: size(A,1), show_value=true))
	$(@bind j Slider(1: size(A,2), show_value=true))
	"""
end

# ╔═╡ 71ac08ea-7145-11eb-237d-5506adfb9533
begin
	one_number_array = fill(1,3,4)
	insert(5, one_number_array, i, j)
end

# ╔═╡ ee43d808-70fa-11eb-0cc6-337279f41494
md"This is still amazing. I wrote one function that just cares about how to insert an object into an array, without knowing anything about what's inside, and it worked for two completely different arrays, _collections of ones of **any kind**_."

# ╔═╡ 263a8a0a-70ee-11eb-236d-c941ba63dff3
md"
## Conclusion
The key idea here is that a computer language should allow you to do operations that make sense. Often times, an operation can make sense for many different objects. So we can abstract away the specifics of the object in our implementation. It should let you step back from there.
"

# ╔═╡ 52461588-ea1a-4e7d-aec2-3de388d31656
md"""
## Appendix
"""

# ╔═╡ 1a2a9000-ec43-11ea-3f39-8312ea286a92
begin
	oneimage = load(download("https://gallery.yopriceville.com/var/albums/Free-Clipart-Pictures/Decorative-Numbers/Cute_Number_One_PNG_Clipart_Image.png?m=1437447301"))
	corgi = load(download("https://i.barkpost.com/wp-content/uploads/2015/01/corgi2.jpg?q=70&fit=crop&crop=entropy&w=808&h=500"))
	nothing
end

# ╔═╡ 0504ac94-70ee-11eb-1c4e-977d9e7d35c9
one = [
	1,
	1.0,
	"one",
	1//1,
	oneimage,
	[1 0; 0 1],
	corgi,
]

# ╔═╡ 0b1668ba-ec42-11ea-3e50-ed97c5b17ced
computer_ones = typeof.(one)

# ╔═╡ b2239b96-70ef-11eb-0b85-21ecab25dc9f
begin
	one_keys = ["1", "1.0", "one", "1//1", "Cute One", "2x2 Identity", "One Corgi"]
	selections = one_keys .=> one_keys
	lookup_element = Dict(one_keys .=> one)
	md"$(@bind element_key Select(selections))"
end

# ╔═╡ 4251f668-70aa-11eb-3d89-35f8d53b7d9b
# your chosen one
element = lookup_element[element_key]

# ╔═╡ f1568d10-ec41-11ea-3dd2-a9cb273ce5b8
# its type
typeof(element)

# ╔═╡ ab02d850-ec41-11ea-10b2-a1b600b12658
# a 3x4 array of this one.
array = fill(element,3,4)

# ╔═╡ 5363a400-ec44-11ea-284e-d13a8872551c
begin
	one_image_array = fill(oneimage,3,4)
	insert(corgi, one_image_array, i, j)
end

# ╔═╡ 00000000-0000-0000-0000-000000000001
PLUTO_PROJECT_TOML_CONTENTS = """
[deps]
ImageMagick = "6218d12a-5da1-5696-b52f-db25d2ecc6d1"
Images = "916415d5-f1e6-5110-898d-aaa5f9f070e0"
PlutoUI = "7f904dfe-b85e-4ff6-b463-dae2292396a8"

[compat]
ImageMagick = "~1.2.1"
Images = "~0.24.1"
PlutoUI = "~0.7.9"
"""

# ╔═╡ 00000000-0000-0000-0000-000000000002
PLUTO_MANIFEST_TOML_CONTENTS = """
# This file is machine-generated - editing it directly is not advised

[[AbstractFFTs]]
deps = ["LinearAlgebra"]
git-tree-sha1 = "485ee0867925449198280d4af84bdb46a2a404d0"
uuid = "621f4979-c628-5d54-868e-fcf4e3e8185c"
version = "1.0.1"

[[Adapt]]
deps = ["LinearAlgebra"]
git-tree-sha1 = "84918055d15b3114ede17ac6a7182f68870c16f7"
uuid = "79e6a3ab-5dfb-504d-930d-738a2a938a0e"
version = "3.3.1"

[[ArgTools]]
uuid = "0dad84c5-d112-42e6-8d28-ef12dabb789f"

[[ArrayInterface]]
deps = ["IfElse", "LinearAlgebra", "Requires", "SparseArrays", "Static"]
git-tree-sha1 = "d84c956c4c0548b4caf0e4e96cf5b6494b5b1529"
uuid = "4fba245c-0d91-5ea0-9b3e-6abc04ee57a9"
version = "3.1.32"

[[Artifacts]]
uuid = "56f22d72-fd6d-98f1-02f0-08ddc0907c33"

[[AxisAlgorithms]]
deps = ["LinearAlgebra", "Random", "SparseArrays", "WoodburyMatrices"]
git-tree-sha1 = "a4d07a1c313392a77042855df46c5f534076fab9"
uuid = "13072b0f-2c55-5437-9ae7-d433b7a33950"
version = "1.0.0"

[[AxisArrays]]
deps = ["Dates", "IntervalSets", "IterTools", "RangeArrays"]
git-tree-sha1 = "d127d5e4d86c7680b20c35d40b503c74b9a39b5e"
uuid = "39de3d68-74b9-583c-8d2d-e117c070f3a9"
version = "0.4.4"

[[Base64]]
uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f"

[[CEnum]]
git-tree-sha1 = "215a9aa4a1f23fbd05b92769fdd62559488d70e9"
uuid = "fa961155-64e5-5f13-b03f-caf6b980ea82"
version = "0.4.1"

[[CatIndices]]
deps = ["CustomUnitRanges", "OffsetArrays"]
git-tree-sha1 = "a0f80a09780eed9b1d106a1bf62041c2efc995bc"
uuid = "aafaddc9-749c-510e-ac4f-586e18779b91"
version = "0.2.2"

[[ChainRulesCore]]
deps = ["Compat", "LinearAlgebra", "SparseArrays"]
git-tree-sha1 = "4ce9393e871aca86cc457d9f66976c3da6902ea7"
uuid = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4"
version = "1.4.0"

[[ColorTypes]]
deps = ["FixedPointNumbers", "Random"]
git-tree-sha1 = "024fe24d83e4a5bf5fc80501a314ce0d1aa35597"
uuid = "3da002f7-5984-5a60-b8a6-cbb66c0b333f"
version = "0.11.0"

[[ColorVectorSpace]]
deps = ["ColorTypes", "FixedPointNumbers", "LinearAlgebra", "SpecialFunctions", "Statistics", "TensorCore"]
git-tree-sha1 = "a66a8e024807c4b3d186eb1cab2aff3505271f8e"
uuid = "c3611d14-8923-5661-9e6a-0046d554d3a4"
version = "0.9.6"

[[Colors]]
deps = ["ColorTypes", "FixedPointNumbers", "Reexport"]
git-tree-sha1 = "417b0ed7b8b838aa6ca0a87aadf1bb9eb111ce40"
uuid = "5ae59095-9a9b-59fe-a467-6f913c188581"
version = "0.12.8"

[[Compat]]
deps = ["Base64", "Dates", "DelimitedFiles", "Distributed", "InteractiveUtils", "LibGit2", "Libdl", "LinearAlgebra", "Markdown", "Mmap", "Pkg", "Printf", "REPL", "Random", "SHA", "Serialization", "SharedArrays", "Sockets", "SparseArrays", "Statistics", "Test", "UUIDs", "Unicode"]
git-tree-sha1 = "4866e381721b30fac8dda4c8cb1d9db45c8d2994"
uuid = "34da2185-b29b-5c13-b0c7-acf172513d20"
version = "3.37.0"

[[CompilerSupportLibraries_jll]]
deps = ["Artifacts", "Libdl"]
uuid = "e66e0078-7015-5450-92f7-15fbd957f2ae"

[[ComputationalResources]]
git-tree-sha1 = "52cb3ec90e8a8bea0e62e275ba577ad0f74821f7"
uuid = "ed09eef8-17a6-5b46-8889-db040fac31e3"
version = "0.3.2"

[[CoordinateTransformations]]
deps = ["LinearAlgebra", "StaticArrays"]
git-tree-sha1 = "6d1c23e740a586955645500bbec662476204a52c"
uuid = "150eb455-5306-5404-9cee-2592286d6298"
version = "0.6.1"

[[CustomUnitRanges]]
git-tree-sha1 = "1a3f97f907e6dd8983b744d2642651bb162a3f7a"
uuid = "dc8bdbbb-1ca9-579f-8c36-e416f6a65cce"
version = "1.0.2"

[[DataAPI]]
git-tree-sha1 = "bec2532f8adb82005476c141ec23e921fc20971b"
uuid = "9a962f9c-6df0-11e9-0e5d-c546b8b5ee8a"
version = "1.8.0"

[[DataStructures]]
deps = ["Compat", "InteractiveUtils", "OrderedCollections"]
git-tree-sha1 = "7d9d316f04214f7efdbb6398d545446e246eff02"
uuid = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8"
version = "0.18.10"

[[Dates]]
deps = ["Printf"]
uuid = "ade2ca70-3891-5945-98fb-dc099432e06a"

[[DelimitedFiles]]
deps = ["Mmap"]
uuid = "8bb1440f-4735-579b-a4ab-409b98df4dab"

[[Distances]]
deps = ["LinearAlgebra", "Statistics", "StatsAPI"]
git-tree-sha1 = "9f46deb4d4ee4494ffb5a40a27a2aced67bdd838"
uuid = "b4f34e82-e78d-54a5-968a-f98e89d6e8f7"
version = "0.10.4"

[[Distributed]]
deps = ["Random", "Serialization", "Sockets"]
uuid = "8ba89e20-285c-5b6f-9357-94700520ee1b"

[[DocStringExtensions]]
deps = ["LibGit2"]
git-tree-sha1 = "a32185f5428d3986f47c2ab78b1f216d5e6cc96f"
uuid = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae"
version = "0.8.5"

[[Downloads]]
deps = ["ArgTools", "LibCURL", "NetworkOptions"]
uuid = "f43a241f-c20a-4ad4-852c-f6b1247861c6"

[[EllipsisNotation]]
deps = ["ArrayInterface"]
git-tree-sha1 = "8041575f021cba5a099a456b4163c9a08b566a02"
uuid = "da5c29d0-fa7d-589e-88eb-ea29b0a81949"
version = "1.1.0"

[[FFTViews]]
deps = ["CustomUnitRanges", "FFTW"]
git-tree-sha1 = "70a0cfd9b1c86b0209e38fbfe6d8231fd606eeaf"
uuid = "4f61f5a4-77b1-5117-aa51-3ab5ef4ef0cd"
version = "0.3.1"

[[FFTW]]
deps = ["AbstractFFTs", "FFTW_jll", "LinearAlgebra", "MKL_jll", "Preferences", "Reexport"]
git-tree-sha1 = "463cb335fa22c4ebacfd1faba5fde14edb80d96c"
uuid = "7a1cc6ca-52ef-59f5-83cd-3a7055c09341"
version = "1.4.5"

[[FFTW_jll]]
deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"]
git-tree-sha1 = "3676abafff7e4ff07bbd2c42b3d8201f31653dcc"
uuid = "f5851436-0d7a-5f13-b9de-f02708fd171a"
version = "3.3.9+8"

[[FileIO]]
deps = ["Pkg", "Requires", "UUIDs"]
git-tree-sha1 = "3c041d2ac0a52a12a27af2782b34900d9c3ee68c"
uuid = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549"
version = "1.11.1"

[[FixedPointNumbers]]
deps = ["Statistics"]
git-tree-sha1 = "335bfdceacc84c5cdf16aadc768aa5ddfc5383cc"
uuid = "53c48c17-4a7d-5ca2-90c5-79b7896eea93"
version = "0.8.4"

[[Graphics]]
deps = ["Colors", "LinearAlgebra", "NaNMath"]
git-tree-sha1 = "2c1cf4df419938ece72de17f368a021ee162762e"
uuid = "a2bd30eb-e257-5431-a919-1863eab51364"
version = "1.1.0"

[[IdentityRanges]]
deps = ["OffsetArrays"]
git-tree-sha1 = "be8fcd695c4da16a1d6d0cd213cb88090a150e3b"
uuid = "bbac6d45-d8f3-5730-bfe4-7a449cd117ca"
version = "0.3.1"

[[IfElse]]
git-tree-sha1 = "28e837ff3e7a6c3cdb252ce49fb412c8eb3caeef"
uuid = "615f187c-cbe4-4ef1-ba3b-2fcf58d6d173"
version = "0.1.0"

[[ImageAxes]]
deps = ["AxisArrays", "ImageCore", "Reexport", "SimpleTraits"]
git-tree-sha1 = "794ad1d922c432082bc1aaa9fa8ffbd1fe74e621"
uuid = "2803e5a7-5153-5ecf-9a86-9b4c37f5f5ac"
version = "0.6.9"

[[ImageContrastAdjustment]]
deps = ["ColorVectorSpace", "ImageCore", "ImageTransformations", "Parameters"]
git-tree-sha1 = "2e6084db6cccab11fe0bc3e4130bd3d117092ed9"
uuid = "f332f351-ec65-5f6a-b3d1-319c6670881a"
version = "0.3.7"

[[ImageCore]]
deps = ["AbstractFFTs", "Colors", "FixedPointNumbers", "Graphics", "MappedArrays", "MosaicViews", "OffsetArrays", "PaddedViews", "Reexport"]
git-tree-sha1 = "db645f20b59f060d8cfae696bc9538d13fd86416"
uuid = "a09fc81d-aa75-5fe9-8630-4744c3626534"
version = "0.8.22"

[[ImageDistances]]
deps = ["ColorVectorSpace", "Distances", "ImageCore", "ImageMorphology", "LinearAlgebra", "Statistics"]
git-tree-sha1 = "6378c34a3c3a216235210d19b9f495ecfff2f85f"
uuid = "51556ac3-7006-55f5-8cb3-34580c88182d"
version = "0.2.13"

[[ImageFiltering]]
deps = ["CatIndices", "ColorVectorSpace", "ComputationalResources", "DataStructures", "FFTViews", "FFTW", "ImageCore", "LinearAlgebra", "OffsetArrays", "Requires", "SparseArrays", "StaticArrays", "Statistics", "TiledIteration"]
git-tree-sha1 = "bf96839133212d3eff4a1c3a80c57abc7cfbf0ce"
uuid = "6a3955dd-da59-5b1f-98d4-e7296123deb5"
version = "0.6.21"

[[ImageIO]]
deps = ["FileIO", "Netpbm", "OpenEXR", "PNGFiles", "TiffImages", "UUIDs"]
git-tree-sha1 = "13c826abd23931d909e4c5538643d9691f62a617"
uuid = "82e4d734-157c-48bb-816b-45c225c6df19"
version = "0.5.8"

[[ImageMagick]]
deps = ["FileIO", "ImageCore", "ImageMagick_jll", "InteractiveUtils", "Libdl", "Pkg", "Random"]
git-tree-sha1 = "5bc1cb62e0c5f1005868358db0692c994c3a13c6"
uuid = "6218d12a-5da1-5696-b52f-db25d2ecc6d1"
version = "1.2.1"

[[ImageMagick_jll]]
deps = ["JpegTurbo_jll", "Libdl", "Libtiff_jll", "Pkg", "Zlib_jll", "libpng_jll"]
git-tree-sha1 = "1c0a2295cca535fabaf2029062912591e9b61987"
uuid = "c73af94c-d91f-53ed-93a7-00f77d67a9d7"
version = "6.9.10-12+3"

[[ImageMetadata]]
deps = ["AxisArrays", "ColorVectorSpace", "ImageAxes", "ImageCore", "IndirectArrays"]
git-tree-sha1 = "ae76038347dc4edcdb06b541595268fca65b6a42"
uuid = "bc367c6b-8a6b-528e-b4bd-a4b897500b49"
version = "0.9.5"

[[ImageMorphology]]
deps = ["ColorVectorSpace", "ImageCore", "LinearAlgebra", "TiledIteration"]
git-tree-sha1 = "68e7cbcd7dfaa3c2f74b0a8ab3066f5de8f2b71d"
uuid = "787d08f9-d448-5407-9aad-5290dd7ab264"
version = "0.2.11"

[[ImageQualityIndexes]]
deps = ["ColorVectorSpace", "ImageCore", "ImageDistances", "ImageFiltering", "OffsetArrays", "Statistics"]
git-tree-sha1 = "1198f85fa2481a3bb94bf937495ba1916f12b533"
uuid = "2996bd0c-7a13-11e9-2da2-2f5ce47296a9"
version = "0.2.2"

[[ImageShow]]
deps = ["Base64", "FileIO", "ImageCore", "OffsetArrays", "Requires", "StackViews"]
git-tree-sha1 = "832abfd709fa436a562db47fd8e81377f72b01f9"
uuid = "4e3cecfd-b093-5904-9786-8bbb286a6a31"
version = "0.3.1"

[[ImageTransformations]]
deps = ["AxisAlgorithms", "ColorVectorSpace", "CoordinateTransformations", "IdentityRanges", "ImageCore", "Interpolations", "OffsetArrays", "Rotations", "StaticArrays"]
git-tree-sha1 = "e4cc551e4295a5c96545bb3083058c24b78d4cf0"
uuid = "02fcd773-0e25-5acc-982a-7f6622650795"
version = "0.8.13"

[[Images]]
deps = ["AxisArrays", "Base64", "ColorVectorSpace", "FileIO", "Graphics", "ImageAxes", "ImageContrastAdjustment", "ImageCore", "ImageDistances", "ImageFiltering", "ImageIO", "ImageMagick", "ImageMetadata", "ImageMorphology", "ImageQualityIndexes", "ImageShow", "ImageTransformations", "IndirectArrays", "OffsetArrays", "Random", "Reexport", "SparseArrays", "StaticArrays", "Statistics", "StatsBase", "TiledIteration"]
git-tree-sha1 = "8b714d5e11c91a0d945717430ec20f9251af4bd2"
uuid = "916415d5-f1e6-5110-898d-aaa5f9f070e0"
version = "0.24.1"

[[Imath_jll]]
deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"]
git-tree-sha1 = "87f7662e03a649cffa2e05bf19c303e168732d3e"
uuid = "905a6f67-0a94-5f89-b386-d35d92009cd1"
version = "3.1.2+0"

[[IndirectArrays]]
git-tree-sha1 = "c2a145a145dc03a7620af1444e0264ef907bd44f"
uuid = "9b13fd28-a010-5f03-acff-a1bbcff69959"
version = "0.5.1"

[[Inflate]]
git-tree-sha1 = "f5fc07d4e706b84f72d54eedcc1c13d92fb0871c"
uuid = "d25df0c9-e2be-5dd7-82c8-3ad0b3e990b9"
version = "0.1.2"

[[IntelOpenMP_jll]]
deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"]
git-tree-sha1 = "d979e54b71da82f3a65b62553da4fc3d18c9004c"
uuid = "1d5cc7b8-4909-519e-a0f8-d0f5ad9712d0"
version = "2018.0.3+2"

[[InteractiveUtils]]
deps = ["Markdown"]
uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240"

[[Interpolations]]
deps = ["AxisAlgorithms", "ChainRulesCore", "LinearAlgebra", "OffsetArrays", "Random", "Ratios", "Requires", "SharedArrays", "SparseArrays", "StaticArrays", "WoodburyMatrices"]
git-tree-sha1 = "61aa005707ea2cebf47c8d780da8dc9bc4e0c512"
uuid = "a98d9a8b-a2ab-59e6-89dd-64a1c18fca59"
version = "0.13.4"

[[IntervalSets]]
deps = ["Dates", "EllipsisNotation", "Statistics"]
git-tree-sha1 = "3cc368af3f110a767ac786560045dceddfc16758"
uuid = "8197267c-284f-5f27-9208-e0e47529a953"
version = "0.5.3"

[[IrrationalConstants]]
git-tree-sha1 = "f76424439413893a832026ca355fe273e93bce94"
uuid = "92d709cd-6900-40b7-9082-c6be49f344b6"
version = "0.1.0"

[[IterTools]]
git-tree-sha1 = "05110a2ab1fc5f932622ffea2a003221f4782c18"
uuid = "c8e1da08-722c-5040-9ed9-7db0dc04731e"
version = "1.3.0"

[[JLLWrappers]]
deps = ["Preferences"]
git-tree-sha1 = "642a199af8b68253517b80bd3bfd17eb4e84df6e"
uuid = "692b3bcd-3c85-4b1f-b108-f13ce0eb3210"
version = "1.3.0"

[[JSON]]
deps = ["Dates", "Mmap", "Parsers", "Unicode"]
git-tree-sha1 = "8076680b162ada2a031f707ac7b4953e30667a37"
uuid = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
version = "0.21.2"

[[JpegTurbo_jll]]
deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"]
git-tree-sha1 = "d735490ac75c5cb9f1b00d8b5509c11984dc6943"
uuid = "aacddb02-875f-59d6-b918-886e6ef4fbf8"
version = "2.1.0+0"

[[LazyArtifacts]]
deps = ["Artifacts", "Pkg"]
uuid = "4af54fe1-eca0-43a8-85a7-787d91b784e3"

[[LibCURL]]
deps = ["LibCURL_jll", "MozillaCACerts_jll"]
uuid = "b27032c2-a3e7-50c8-80cd-2d36dbcbfd21"

[[LibCURL_jll]]
deps = ["Artifacts", "LibSSH2_jll", "Libdl", "MbedTLS_jll", "Zlib_jll", "nghttp2_jll"]
uuid = "deac9b47-8bc7-5906-a0fe-35ac56dc84c0"

[[LibGit2]]
deps = ["Base64", "NetworkOptions", "Printf", "SHA"]
uuid = "76f85450-5226-5b5a-8eaa-529ad045b433"

[[LibSSH2_jll]]
deps = ["Artifacts", "Libdl", "MbedTLS_jll"]
uuid = "29816b5a-b9ab-546f-933c-edad1886dfa8"

[[Libdl]]
uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb"

[[Libtiff_jll]]
deps = ["Artifacts", "JLLWrappers", "JpegTurbo_jll", "Libdl", "Pkg", "Zlib_jll", "Zstd_jll"]
git-tree-sha1 = "340e257aada13f95f98ee352d316c3bed37c8ab9"
uuid = "89763e89-9b03-5906-acba-b20f662cd828"
version = "4.3.0+0"

[[LinearAlgebra]]
deps = ["Libdl", "libblastrampoline_jll"]
uuid = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"

[[LogExpFunctions]]
deps = ["ChainRulesCore", "DocStringExtensions", "IrrationalConstants", "LinearAlgebra"]
git-tree-sha1 = "34dc30f868e368f8a17b728a1238f3fcda43931a"
uuid = "2ab3a3ac-af41-5b50-aa03-7779005ae688"
version = "0.3.3"

[[Logging]]
uuid = "56ddb016-857b-54e1-b83d-db4d58db5568"

[[MKL_jll]]
deps = ["Artifacts", "IntelOpenMP_jll", "JLLWrappers", "LazyArtifacts", "Libdl", "Pkg"]
git-tree-sha1 = "5455aef09b40e5020e1520f551fa3135040d4ed0"
uuid = "856f044c-d86e-5d09-b602-aeab76dc8ba7"
version = "2021.1.1+2"

[[MacroTools]]
deps = ["Markdown", "Random"]
git-tree-sha1 = "5a5bc6bf062f0f95e62d0fe0a2d99699fed82dd9"
uuid = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09"
version = "0.5.8"

[[MappedArrays]]
git-tree-sha1 = "e8b359ef06ec72e8c030463fe02efe5527ee5142"
uuid = "dbb5928d-eab1-5f90-85c2-b9b0edb7c900"
version = "0.4.1"

[[Markdown]]
deps = ["Base64"]
uuid = "d6f4376e-aef5-505a-96c1-9c027394607a"

[[MbedTLS_jll]]
deps = ["Artifacts", "Libdl"]
uuid = "c8ffd9c3-330d-5841-b78e-0817d7145fa1"

[[Missings]]
deps = ["DataAPI"]
git-tree-sha1 = "bf210ce90b6c9eed32d25dbcae1ebc565df2687f"
uuid = "e1d29d7a-bbdc-5cf2-9ac0-f12de2c33e28"
version = "1.0.2"

[[Mmap]]
uuid = "a63ad114-7e13-5084-954f-fe012c677804"

[[MosaicViews]]
deps = ["MappedArrays", "OffsetArrays", "PaddedViews", "StackViews"]
git-tree-sha1 = "b34e3bc3ca7c94914418637cb10cc4d1d80d877d"
uuid = "e94cdb99-869f-56ef-bcf0-1ae2bcbe0389"
version = "0.3.3"

[[MozillaCACerts_jll]]
uuid = "14a3606d-f60d-562e-9121-12d972cd8159"

[[NaNMath]]
git-tree-sha1 = "bfe47e760d60b82b66b61d2d44128b62e3a369fb"
uuid = "77ba4419-2d1f-58cd-9bb1-8ffee604a2e3"
version = "0.3.5"

[[Netpbm]]
deps = ["ColorVectorSpace", "FileIO", "ImageCore"]
git-tree-sha1 = "09589171688f0039f13ebe0fdcc7288f50228b52"
uuid = "f09324ee-3d7c-5217-9330-fc30815ba969"
version = "1.0.1"

[[NetworkOptions]]
uuid = "ca575930-c2e3-43a9-ace4-1e988b2c1908"

[[OffsetArrays]]
deps = ["Adapt"]
git-tree-sha1 = "c870a0d713b51e4b49be6432eff0e26a4325afee"
uuid = "6fe1bfb0-de20-5000-8ca7-80f57d26f881"
version = "1.10.6"

[[OpenBLAS_jll]]
deps = ["Artifacts", "CompilerSupportLibraries_jll", "Libdl"]
uuid = "4536629a-c528-5b80-bd46-f80d51c5b363"

[[OpenEXR]]
deps = ["Colors", "FileIO", "OpenEXR_jll"]
git-tree-sha1 = "327f53360fdb54df7ecd01e96ef1983536d1e633"
uuid = "52e1d378-f018-4a11-a4be-720524705ac7"
version = "0.3.2"

[[OpenEXR_jll]]
deps = ["Artifacts", "Imath_jll", "JLLWrappers", "Libdl", "Pkg", "Zlib_jll"]
git-tree-sha1 = "923319661e9a22712f24596ce81c54fc0366f304"
uuid = "18a262bb-aa17-5467-a713-aee519bc75cb"
version = "3.1.1+0"

[[OpenSpecFun_jll]]
deps = ["Artifacts", "CompilerSupportLibraries_jll", "JLLWrappers", "Libdl", "Pkg"]
git-tree-sha1 = "13652491f6856acfd2db29360e1bbcd4565d04f1"
uuid = "efe28fd5-8261-553b-a9e1-b2916fc3738e"
version = "0.5.5+0"

[[OrderedCollections]]
git-tree-sha1 = "85f8e6578bf1f9ee0d11e7bb1b1456435479d47c"
uuid = "bac558e1-5e72-5ebc-8fee-abe8a469f55d"
version = "1.4.1"

[[PNGFiles]]
deps = ["Base64", "CEnum", "ImageCore", "IndirectArrays", "OffsetArrays", "libpng_jll"]
git-tree-sha1 = "e14c485f6beee0c7a8dcf6128bf70b85f1fe201e"
uuid = "f57f5aa1-a3ce-4bc8-8ab9-96f992907883"
version = "0.3.9"

[[PaddedViews]]
deps = ["OffsetArrays"]
git-tree-sha1 = "646eed6f6a5d8df6708f15ea7e02a7a2c4fe4800"
uuid = "5432bcbf-9aad-5242-b902-cca2824c8663"
version = "0.5.10"

[[Parameters]]
deps = ["OrderedCollections", "UnPack"]
git-tree-sha1 = "2276ac65f1e236e0a6ea70baff3f62ad4c625345"
uuid = "d96e819e-fc66-5662-9728-84c9c7592b0a"
version = "0.12.2"

[[Parsers]]
deps = ["Dates"]
git-tree-sha1 = "438d35d2d95ae2c5e8780b330592b6de8494e779"
uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0"
version = "2.0.3"

[[Pkg]]
deps = ["Artifacts", "Dates", "Downloads", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "REPL", "Random", "SHA", "Serialization", "TOML", "Tar", "UUIDs", "p7zip_jll"]
uuid = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"

[[PkgVersion]]
deps = ["Pkg"]
git-tree-sha1 = "a7a7e1a88853564e551e4eba8650f8c38df79b37"
uuid = "eebad327-c553-4316-9ea0-9fa01ccd7688"
version = "0.1.1"

[[PlutoUI]]
deps = ["Base64", "Dates", "InteractiveUtils", "JSON", "Logging", "Markdown", "Random", "Reexport", "Suppressor"]
git-tree-sha1 = "44e225d5837e2a2345e69a1d1e01ac2443ff9fcb"
uuid = "7f904dfe-b85e-4ff6-b463-dae2292396a8"
version = "0.7.9"

[[Preferences]]
deps = ["TOML"]
git-tree-sha1 = "00cfd92944ca9c760982747e9a1d0d5d86ab1e5a"
uuid = "21216c6a-2e73-6563-6e65-726566657250"
version = "1.2.2"

[[Printf]]
deps = ["Unicode"]
uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7"

[[ProgressMeter]]
deps = ["Distributed", "Printf"]
git-tree-sha1 = "afadeba63d90ff223a6a48d2009434ecee2ec9e8"
uuid = "92933f4c-e287-5a05-a399-4b506db050ca"
version = "1.7.1"

[[REPL]]
deps = ["InteractiveUtils", "Markdown", "Sockets", "Unicode"]
uuid = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb"

[[Random]]
deps = ["SHA", "Serialization"]
uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"

[[RangeArrays]]
git-tree-sha1 = "b9039e93773ddcfc828f12aadf7115b4b4d225f5"
uuid = "b3c3ace0-ae52-54e7-9d0b-2c1406fd6b9d"
version = "0.3.2"

[[Ratios]]
deps = ["Requires"]
git-tree-sha1 = "7dff99fbc740e2f8228c6878e2aad6d7c2678098"
uuid = "c84ed2f1-dad5-54f0-aa8e-dbefe2724439"
version = "0.4.1"

[[Reexport]]
git-tree-sha1 = "45e428421666073eab6f2da5c9d310d99bb12f9b"
uuid = "189a3867-3050-52da-a836-e630ba90ab69"
version = "1.2.2"

[[Requires]]
deps = ["UUIDs"]
git-tree-sha1 = "4036a3bd08ac7e968e27c203d45f5fff15020621"
uuid = "ae029012-a4dd-5104-9daa-d747884805df"
version = "1.1.3"

[[Rotations]]
deps = ["LinearAlgebra", "StaticArrays", "Statistics"]
git-tree-sha1 = "2ed8d8a16d703f900168822d83699b8c3c1a5cd8"
uuid = "6038ab10-8711-5258-84ad-4b1120ba62dc"
version = "1.0.2"

[[SHA]]
uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce"

[[Serialization]]
uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b"

[[SharedArrays]]
deps = ["Distributed", "Mmap", "Random", "Serialization"]
uuid = "1a1011a3-84de-559e-8e89-a11a2f7dc383"

[[SimpleTraits]]
deps = ["InteractiveUtils", "MacroTools"]
git-tree-sha1 = "5d7e3f4e11935503d3ecaf7186eac40602e7d231"
uuid = "699a6c99-e7fa-54fc-8d76-47d257e15c1d"
version = "0.9.4"

[[Sockets]]
uuid = "6462fe0b-24de-5631-8697-dd941f90decc"

[[SortingAlgorithms]]
deps = ["DataStructures"]
git-tree-sha1 = "b3363d7460f7d098ca0912c69b082f75625d7508"
uuid = "a2af1166-a08f-5f64-846c-94a0d3cef48c"
version = "1.0.1"

[[SparseArrays]]
deps = ["LinearAlgebra", "Random"]
uuid = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"

[[SpecialFunctions]]
deps = ["ChainRulesCore", "LogExpFunctions", "OpenSpecFun_jll"]
git-tree-sha1 = "a322a9493e49c5f3a10b50df3aedaf1cdb3244b7"
uuid = "276daf66-3868-5448-9aa4-cd146d93841b"
version = "1.6.1"

[[StackViews]]
deps = ["OffsetArrays"]
git-tree-sha1 = "46e589465204cd0c08b4bd97385e4fa79a0c770c"
uuid = "cae243ae-269e-4f55-b966-ac2d0dc13c15"
version = "0.1.1"

[[Static]]
deps = ["IfElse"]
git-tree-sha1 = "a8f30abc7c64a39d389680b74e749cf33f872a70"
uuid = "aedffcd0-7271-4cad-89d0-dc628f76c6d3"
version = "0.3.3"

[[StaticArrays]]
deps = ["LinearAlgebra", "Random", "Statistics"]
git-tree-sha1 = "3240808c6d463ac46f1c1cd7638375cd22abbccb"
uuid = "90137ffa-7385-5640-81b9-e52037218182"
version = "1.2.12"

[[Statistics]]
deps = ["LinearAlgebra", "SparseArrays"]
uuid = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"

[[StatsAPI]]
git-tree-sha1 = "1958272568dc176a1d881acb797beb909c785510"
uuid = "82ae8749-77ed-4fe6-ae5f-f523153014b0"
version = "1.0.0"

[[StatsBase]]
deps = ["DataAPI", "DataStructures", "LinearAlgebra", "Missings", "Printf", "Random", "SortingAlgorithms", "SparseArrays", "Statistics", "StatsAPI"]
git-tree-sha1 = "8cbbc098554648c84f79a463c9ff0fd277144b6c"
uuid = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91"
version = "0.33.10"

[[Suppressor]]
git-tree-sha1 = "a819d77f31f83e5792a76081eee1ea6342ab8787"
uuid = "fd094767-a336-5f1f-9728-57cf17d0bbfb"
version = "0.2.0"

[[TOML]]
deps = ["Dates"]
uuid = "fa267f1f-6049-4f14-aa54-33bafae1ed76"

[[Tar]]
deps = ["ArgTools", "SHA"]
uuid = "a4e569a6-e804-4fa4-b0f3-eef7a1d5b13e"

[[TensorCore]]
deps = ["LinearAlgebra"]
git-tree-sha1 = "1feb45f88d133a655e001435632f019a9a1bcdb6"
uuid = "62fd8b95-f654-4bbd-a8a5-9c27f68ccd50"
version = "0.1.1"

[[Test]]
deps = ["InteractiveUtils", "Logging", "Random", "Serialization"]
uuid = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

[[TiffImages]]
deps = ["ColorTypes", "DocStringExtensions", "FileIO", "FixedPointNumbers", "IndirectArrays", "Inflate", "OffsetArrays", "OrderedCollections", "PkgVersion", "ProgressMeter"]
git-tree-sha1 = "632a8d4dbbad6627a4d2d21b1c6ebcaeebb1e1ed"
uuid = "731e570b-9d59-4bfa-96dc-6df516fadf69"
version = "0.4.2"

[[TiledIteration]]
deps = ["OffsetArrays"]
git-tree-sha1 = "52c5f816857bfb3291c7d25420b1f4aca0a74d18"
uuid = "06e1c1a7-607b-532d-9fad-de7d9aa2abac"
version = "0.3.0"

[[UUIDs]]
deps = ["Random", "SHA"]
uuid = "cf7118a7-6976-5b1a-9a39-7adc72f591a4"

[[UnPack]]
git-tree-sha1 = "387c1f73762231e86e0c9c5443ce3b4a0a9a0c2b"
uuid = "3a884ed6-31ef-47d7-9d2a-63182c4928ed"
version = "1.0.2"

[[Unicode]]
uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5"

[[WoodburyMatrices]]
deps = ["LinearAlgebra", "SparseArrays"]
git-tree-sha1 = "59e2ad8fd1591ea019a5259bd012d7aee15f995c"
uuid = "efce3f68-66dc-5838-9240-27a6d6f5f9b6"
version = "0.5.3"

[[Zlib_jll]]
deps = ["Libdl"]
uuid = "83775a58-1f1d-513f-b197-d71354ab007a"

[[Zstd_jll]]
deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"]
git-tree-sha1 = "cc4bf3fdde8b7e3e9fa0351bdeedba1cf3b7f6e6"
uuid = "3161d3a3-bdf6-5164-811a-617609db77b4"
version = "1.5.0+0"

[[libblastrampoline_jll]]
deps = ["Artifacts", "Libdl", "OpenBLAS_jll"]
uuid = "8e850b90-86db-534c-a0d3-1478176c7d93"

[[libpng_jll]]
deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg", "Zlib_jll"]
git-tree-sha1 = "94d180a6d2b5e55e447e2d27a29ed04fe79eb30c"
uuid = "b53b4c65-9356-5827-b1ea-8c7a1a84506f"
version = "1.6.38+0"

[[nghttp2_jll]]
deps = ["Artifacts", "Libdl"]
uuid = "8e850ede-7688-5339-a07c-302acd2aaf8d"

[[p7zip_jll]]
deps = ["Artifacts", "Libdl"]
uuid = "3f19e933-33d8-53b3-aaab-bd5110c3b7a0"
"""

# ╔═╡ Cell order:
# ╟─5ef51c3a-70a7-11eb-2023-31113399a57f
# ╟─60ae819a-70a7-11eb-31d4-750c7f5dc6ca
# ╟─792c6a62-ec41-11ea-01f3-73e7eee23cc7
# ╠═da1d65a0-ec42-11ea-0141-334c9eeeb035
# ╟─ef1bfa16-70ea-11eb-189c-a54db292cd6f
# ╠═0504ac94-70ee-11eb-1c4e-977d9e7d35c9
# ╟─6fcac482-70ee-11eb-0b80-ff41c708053b
# ╠═0b1668ba-ec42-11ea-3e50-ed97c5b17ced
# ╟─9ebc079a-70f0-11eb-07d9-f9e80f3f4584
# ╟─15f7f90a-70f0-11eb-0d41-63677e4023f4
# ╠═b2239b96-70ef-11eb-0b85-21ecab25dc9f
# ╠═4251f668-70aa-11eb-3d89-35f8d53b7d9b
# ╠═f1568d10-ec41-11ea-3dd2-a9cb273ce5b8
# ╠═ab02d850-ec41-11ea-10b2-a1b600b12658
# ╟─f6886d90-70ed-11eb-07c4-471ee267e7c1
# ╟─3c1a3cf8-70f8-11eb-3c18-375207f321eb
# ╠═19f4ddb0-ec44-11ea-20b9-5d97fb2b1cf4
# ╠═424f5f10-ec44-11ea-076d-f3cba4435e0c
# ╠═5363a400-ec44-11ea-284e-d13a8872551c
# ╠═71ac08ea-7145-11eb-237d-5506adfb9533
# ╟─ee43d808-70fa-11eb-0cc6-337279f41494
# ╟─263a8a0a-70ee-11eb-236d-c941ba63dff3
# ╟─52461588-ea1a-4e7d-aec2-3de388d31656
# ╠═1a2a9000-ec43-11ea-3f39-8312ea286a92
# ╟─00000000-0000-0000-0000-000000000001
# ╟─00000000-0000-0000-0000-000000000002


# parse the networks
using CSV
data = CSV.read("srep00196-s2.csv",comment="#")
##
allcompounds = [data[:,1]; data[:,2]]
##
@show length(unique(data[:,1]))
@show length(unique(data[:,2]))
@show length(unique(allcompounds))
## Build a sparse matrix out of this data
using CategoricalArrays
##
using SparseArrays
x=CategoricalArray(allcompounds,ordered=false)
allingreds = levels(x)
ids = Int.(CategoricalArrays.order(x.pool)[x.refs])
src = ids[1:size(data,1)]
dst = ids[size(data,1)+1:end]
A = sparse(src,dst,data[:,3],length(allingreds),length(allingreds))
## A is only upper-triangular
using LinearAlgebra
@assert(iszero(A-triu(A,1)))
A = max.(A,A')
##
function writeSMAT(filename::AbstractString, A::SparseMatrixCSC{T,Int}; values::Bool=true) where T
    open(filename, "w") do outfile
        write(outfile, join((size(A,1), size(A,2), nnz(A)), " "), "\n")

        rows = rowvals(A)
        vals = nonzeros(A)
        m, n = size(A)
        for j = 1:n
           for nzi in nzrange(A, j)
              row = rows[nzi]
              val = vals[nzi]
              if values
                write(outfile, join((row-1, j-1, val), " "), "\n")
              else
                write(outfile, join((row-1, j-1, 1), " "), "\n")
              end
           end
        end
    end
end
writeSMAT("ingredient-net.smat", Int.(sparse(A)))

##
using DelimitedFiles
writedlm("ingredient-net.labels", allingreds)

##
# build the id map
ingred2id = Dict(allingreds .=> 1:length(allingreds))
##
recipes = readlines("srep00196-s3.csv")
ingred = Vector{Vector{String}}(undef,0)
cusines = Vector{String}(undef,0)
for line in recipes
  if startswith(line, "#")
    continue
  else
    parts = split(line, ",")
    push!(cusines, parts[1])
    push!(ingred, collect(parts[2:end]))
  end
end

## map to indices
# we are missing startch and condiment
recipes = [[ingred2id[x] for x in recipe if (x != "starch" && x != "condiment")]
              for recipe in ingred]
R = sparse(collect(Iterators.flatten([r*ones(Int,length(recipes[r])) for r in 1:length(recipes)])),
           collect(Iterators.flatten(recipes)),1)
##
writeSMAT("recipes.smat", R)
##
writedlm("recipes.labels", cusines)


using Interpolations
using BenchmarkTools

function interpolate!(a_hi, a_lo, x_hi, x_lo)
    interp_a = interpolate((x_lo, x_lo, x_lo), a_lo, (Gridded(Linear()), Gridded(Linear()), Gridded(Linear())))
    a_hi[:, :, :] .= interp_a(x_hi, x_hi, x_hi)
end

n_hi = 256
n_lo = 128

dx_hi = 1//n_hi
dx_lo = 1//n_lo

x_hi = 1//2*dx_hi:dx_hi:1
x_lo = -1//2*dx_lo:dx_lo:1+1//2*dx_lo

a0_lo = rand(n_lo+2, n_lo+2, n_lo+2)
a0_hi = zeros(n_hi, n_hi, n_hi)

a1_lo = rand(n_lo+2, n_lo+2, n_lo+2)
a1_hi = zeros(n_hi, n_hi, n_hi)

a2_lo = rand(n_lo+2, n_lo+2, n_lo+2)
a2_hi = zeros(n_hi, n_hi, n_hi)

a3_lo = rand(n_lo+2, n_lo+2, n_lo+2)
a3_hi = zeros(n_hi, n_hi, n_hi)

@btime begin
    @sync begin
        Threads.@spawn interpolate!(a0_hi, a0_lo, x_hi, x_lo)
        Threads.@spawn interpolate!(a1_hi, a1_lo, x_hi, x_lo)
        Threads.@spawn interpolate!(a2_hi, a2_lo, x_hi, x_lo)
        Threads.@spawn interpolate!(a3_hi, a3_lo, x_hi, x_lo)
    end
end


using JSON
using Random
include("../../runExpe.jl")

"""
Represents an instance of the knapsack problem
"""
mutable struct KnapsackInstance

    weights::Vector{Int}

    values::Vector{Int}

    function KnapsackInstance()
        return new()
    end
end

"""
KnapsackInstance constructor
"""
function KnapsackInstance(jsonPath::String)

    this = KnapsackInstance()

    stringData = join(readlines(jsonPath))
    instanceData = JSON.parse(stringData)

    this.weights = instanceData["weights"]
    this.values = instanceData["values"]

    return this
end

"""
Solve a knapsack instance by adding random objects to random knapsacks
"""
function randomResolution(param::Dict{String, Any})
    return knapsackHeuristicResolution(param, isRandom = true)
end

"""
Solve a knapsack instance by sorting the objects according to their value/weight and adding them to a knapsack as full as possible
"""
function ratioResolution(param::Dict{String, Any})
    return knapsackHeuristicResolution(param, isRandom = false)
end

"""
Heuristically solve a knapsack instance.
The order in which the objects are sorted and the knapsack in which they are added are defined by the argument "isRandom".
"""
function knapsackHeuristicResolution(param::Dict{String, Any}; isRandom::Bool=false)

    instance = KnapsackInstance(param["instancePath"])

    n = length(instance.weights)
    m = param["knapsacksCount"]
    K = param["knapsacksSize"]

    order = nothing

    if isRandom
        # Get a random order for the objects
        order = shuffle(1:n)
    else
        # Otherwise order the objects according to the ratio value/weight
        ratio = instance.values ./ instance.weights
        order = reverse(sortperm(ratio))
    end

    # Current value of the objective and weight of each knapsack
    objectiveValue = 0
    knapsacksWeight = Vector{Int}(zeros(m))
    knapsacksContent = Vector{Vector{Int}}()

    for knapsackId in 1:m
        push!(knapsacksContent, Vector{Int}())
    end

    # For each object
    for objectId in order

        knapsackId = -1

        # If the knapsack is chosen randomly
        if isRandom
            knapsackId = getRandomKnapsack(K, instance.weights[objectId], knapsacksWeight)
        else
            knapsackId = getFullestKnapsack(K, instance.weights[objectId], knapsacksWeight)
        end

        # If the object fits in a knapsack
        if knapsackId != -1
            knapsacksWeight[knapsackId] += instance.weights[objectId]
            objectiveValue += instance.values[objectId]
            push!(knapsacksContent[knapsackId], objectId)
        end
    end

    results = Dict{String, Any}()
    results["objectiveValue"] = objectiveValue
    results["knapsacksWeight"] = knapsacksWeight
    results["knapsacksContent"] = knapsacksContent

    return results
end

function getRandomKnapsack(K::Int, objectWeight::Int, knapsacksWeight::Vector{Int})

    m = length(knapsacksWeight)

    # Randomly choose a knapsack to add the object
    knapsackId = ceil(Int, m * rand())
    knapsackTested = 0
    validKnapsackFound = false

    # While all the knapsack have not been tested and a knapsack in which the object fits has not been found
    while knapsackTested < m && !validKnapsackFound

        # If the object fits
        if knapsacksWeight[knapsackId] + objectWeight <= K
            validKnapsackFound = true
        else
            # Get the next knapsack id
            knapsackId = max(1, rem(knapsackId + 1, m+1))
            knapsackTested += 1
        end
    end

    if !validKnapsackFound
        return -1
    else
        return knapsackId
    end
end

function getFullestKnapsack(K::Int, objectWeight::Int, knapsacksWeight::Vector{Int})

    m = length(knapsacksWeight)
    bestKnapsackId = -1
    bestRemainingWeight = 0

    for knapsackId in 1:m

        remainingWeight = K - knapsacksWeight[knapsackId] + objectWeight

        # If the object fits in the knapsack and
        # - it is the first one; or
        # - there is less remaining weight in it than in the currently best knapsack found
        if  remainingWeight > 0 && (bestKnapsackId == -1 || remainingWeight < bestRemainingWeight)
            bestKnapsackId = knapsackId
            bestRemainingWeight = remainingWeight
        end
    end

    return bestKnapsackId
end

"""
Let assume that we need the value of n in the result tables but we forgot
to add it in already obtained results files.

Two potentially expensive solutions would be:
- to add n manually in each result; or
- to restart the experiment from scratch.

To do it faster you can:
1 - define this function which returns n for a given instance;
2 - create a json file which only contains a String with the name of this function (i.e.,  "addNToResults");
3 - use function addToSavedResults (from runExpe.jl) with the json file as an input.
"""
function addNToResults(instancePath::String)
    instance = KnapsackInstance(instancePath)
    results = Dict{String, Any}()
    results["n"] = length(instance.weights)
    return results
end


using DataStructures
import Base: convert, promote_rule, +, -, *, /, >, <, log

type RDiff <: Real
    val::Float64
    adj::Float64
    stack::Nullable{Stack}
    backprop::Function

    RDiff(val, adj, stack, backprop) = new(val, adj, stack, backprop)
    RDiff(val, adj, stack) = new(val, adj, stack)
end
typealias Converts Union{Float64, Int, Irrational, Rational}
RDiff(x::Converts) = RDiff(x, 0., Nullable{Stack}())
RDiff(x::Converts, s::Stack) = RDiff(x, 0., Nullable(s))
RDiff(x::Converts, s::Nullable{Stack}) = RDiff(x, 0., s)

convert(::Type{RDiff}, x::Converts) = RDiff(x)
promote_rule(::Type{Bool}, ::Type{RDiff}) = RDiff
promote_rule(::Type{Float64}, ::Type{RDiff}) = RDiff
promote_rule{T<:Integer}(::Type{T}, ::Type{RDiff}) = RDiff

value(x::RDiff) = x.val
adjoint(x::RDiff) = x.adj
>(x::RDiff, y::RDiff) = x.val > y.val
<(x::RDiff, y::RDiff) = x.val < y.val

function grad(f)
    const varstack = Stack(RDiff)

    function ∇f(x)
        y = map(x -> RDiff(x, 0., varstack), x)
        f(y)
        top(varstack).adj = 1.
        for v in varstack
            v.backprop()
        end
        clear!(varstack)
        map(value, y)
    end

    return ∇f

end

function clear!(s::Stack)
    while !isempty(s)
        pop!(s)
    end
    nothing
end

#= TODO
- make a function to call backprop on entire Stack
    - should set outcome.adj = 1. and call backprop on every element
- separate stack for every function?
    - would be created in a call to rdiff and closed over
    - RDiff variables need a pointer to this, so that functions know what
      stack to push to
    - can initialize without this, but all inputs need to have it set
- what happens with overwriting?, inplace?, loops?
=#

macro diff_rule(sig, code)
    fn = _fn_of_vals(sig)
    argsyms = Symbol[s.args[1] for s in sig.args[2:end]]

    ex = quote
        function $(sig.args[1])($(sig.args[2:end]...))
            vstack = get_stack($(argsyms...))
            this = RDiff($fn, 0.0, vstack)
            function backprop()
                $code
                nothing
            end
            this.backprop = backprop
            push!(vstack, this)
            this
        end
    end
    esc(ex)
end

_fn_of_vals(x) = x
function _fn_of_vals(ex::Expr)
    if ex.head == :(::)
        out = :(value($(ex.args[1])))
    else
        out = copy(ex)
        for i in eachindex(out.args)
            out.args[i] = _fn_of_vals(out.args[i])
        end
    end
    out
end

function get_stack(vars...)
    has_stack = filter(x -> !isnull(x.stack), vars)
    get(first(has_stack).stack)
end

@diff_rule log(x::RDiff) begin
    x.adj += this.adj / x.val
end

@diff_rule +(x::RDiff, y::RDiff) begin
    x.adj += this.adj
    y.adj += this.adj
end

@diff_rule -(x::RDiff, y::RDiff) begin
    x.adj += this.adj
    y.adj += -this.adj
end

@diff_rule *(x::RDiff, y::RDiff) begin
    x.adj += this.adj * y.val
    y.adj += this.adj * x.val
end

@diff_rule /(x::RDiff, y::RDiff) begin
    x.adj += this.adj / y.val
    y.adj += -this.adj * x.val / y.val^2
end

@diff_rule -(x::RDiff) begin
    x.adj += -this.adj
end


cd(@__DIR__); include("setups/rgc100.jl");
pyplot(dpi = 200)

##
ϵ = 0.3
for J = 1:2
    Uf = unitary_folding_operator(W, GP; ϵ = ϵ, J = J)
    gplot(W, X; width = 1)
    scatter_gplot!(X; marker = diag(Uf), plotOrder = :l2s, ms = 4)
    plt = plot!(xlims = [-180, 220], xlabel = "x(μm)", ylabel = "y(μm)",
          clims = (0.6, 1), frame = :box, size = (520, 500))
    savefig(plt, "../figs/RGC100_unitary_folding_diag_J$(J).png")
end


@testset "MInt" begin

@test Metaprogramming.MInt_from_VoidTuple(()) === MInt{0}
@test Metaprogramming.MInt_from_VoidTuple((nothing,)) === MInt{1}
@test Metaprogramming.MInt_from_VoidTuple((nothing,nothing,nothing)) === MInt{3}

@test Metaprogramming.VoidTuple_from_MInt(MInt{0}) === ()
@test Metaprogramming.VoidTuple_from_MInt(MInt{1}) === (nothing,)
@test Metaprogramming.VoidTuple_from_MInt(MInt{3}) === (nothing,nothing,nothing)

@test begin
    a = MInt{2}
    b = MInt{10}
    a+b === MInt{12}
end

@test begin
    a = MInt{0}
    b = MInt{0}
    a+b === MInt{0}
end

@test begin
    a = MInt{0}
    b = MInt{3}
    a+b === MInt{3}
end

@test begin
    a = MInt{10}
    b = MInt{2}
    a-b === MInt{8}
end

@test begin
    a = MInt{0}
    b = MInt{0}
    a-b === MInt{0}
end

@test begin
    a = MInt{4}
    b = MInt{0}
    a-b === MInt{4}
end

@test begin
    a = MInt{4}
    b = MInt{4}
    a-b === MInt{0}
end

end


# This file contains all linear algebra code
# All functions are defined and called from the C library for interoperability

# Vector2
const Vector2 = NamedTuple{(:x, :y), Tuple{Float64, Float64}}

function Vector2(x::Float64, y::Float64)
  return Vector2((x, y))
end

function Base.:+(a::Vector2, b::Vector2)
  return ccall((:add2, @fullLibraryPath), Vector2, (Vector2, Vector2), a, b)
end

function Base.:-(a::Vector2, b::Vector2)
  return ccall((:subtract2, @fullLibraryPath), Vector2, (Vector2, Vector2), a, b)
end

function dot(a::Vector2, b::Vector2)
  return ccall((:dot2, @fullLibraryPath), Cdouble, (Vector2, Vector2), a, b)
end

function Base.:*(v::Vector2, s::Float64)
  return ccall((:scalar2, @fullLibraryPath), Vector2, (Vector2, Cdouble), v, s)
end

function magnitude(v::Vector2)
  return ccall((:magnitude2, @fullLibraryPath), Cdouble, (Vector2,), v)
end

function normalize(v::Vector2)
  return ccall((:normalize2, @fullLibraryPath), Vector2, (Vector2,), v)
end

# Vector3
const Vector3 = NamedTuple{(:x, :y, :z), Tuple{Float64, Float64, Float64}}

function Vector3(x::Float64, y::Float64, z::Float64)
  return Vector3((x, y, z))
end

function Base.:+(a::Vector3, b::Vector3)
  return ccall((:add3, @fullLibraryPath), Vector3, (Vector3, Vector3), a, b)
end

function Base.:-(a::Vector3, b::Vector3)
  return ccall((:subtract3, @fullLibraryPath), Vector3, (Vector3, Vector3), a, b)
end

function dot(a::Vector3, b::Vector3)
  return ccall((:dot3, @fullLibraryPath), Cdouble, (Vector3, Vector3), a, b)
end

function Base.:*(a::Vector3, b::Vector3)
  return ccall((:cross, @fullLibraryPath), Vector3, (Vector3, Vector3), a, b)
end

function Base.:*(v::Vector3, s::Float64)
  return ccall((:scalar3, @fullLibraryPath), Vector3, (Vector3, Cdouble), v, s)
end

function magnitude(v::Vector3)
  return ccall((:magnitude3, @fullLibraryPath), Cdouble, (Vector3,), v)
end

function normalize(v::Vector3)
  return ccall((:normalize3, @fullLibraryPath), Vector3, (Vector3,), v)
end

# Vector4
const Vector4 = NamedTuple{(:x, :y, :z, :w), Tuple{Float64, Float64, Float64, Float64}}

function Vector4(x::Float64, y::Float64, z::Float64, w::Float64)
  return Vector4((x, y, z, w))
end

function Base.:+(a::Vector4, b::Vector4)
  return ccall((:add4, @fullLibraryPath), Vector4, (Vector4, Vector4), a, b)
end

function Base.:-(a::Vector4, b::Vector4)
  return ccall((:subtract4, @fullLibraryPath), Vector4, (Vector4, Vector4), a, b)
end

function dot(a::Vector4, b::Vector4)
  return ccall((:dot4, @fullLibraryPath), Cdouble, (Vector4, Vector4), a, b)
end

function Base.:*(v::Vector4, s::Float64)
  return ccall((:scalar4, @fullLibraryPath), Vector4, (Vector4, Float64), v, s)
end

function magnitude(v::Vector4)
  return ccall((:magnitude4, @fullLibraryPath), Cdouble, (Vector4,), v)
end

function normalize(v::Vector4)
  return ccall((:normalize4, @fullLibraryPath), Vector4, (Vector4,), v)
end

# Matrices
const Matrix4x4 = NamedTuple{(:a11, :a12, :a13, :a14, :a21, :a22, :a23, :a24, :a31, :a32, :a33, :a34, :a41, :a42, :a43, :a44),
  Tuple{Float64, Float64, Float64, Float64, Float64, Float64, Float64, Float64, Float64, Float64, Float64, Float64, Float64, Float64, Float64, Float64}}

function Base.:*(a::Matrix4x4, b::Matrix4x4)
  return ccall((:multiplyMatrix4x4, @fullLibraryPath), Matrix4x4, (Matrix4x4, Matrix4x4), a, b)
end

function identityMatrix()
  return ccall((:identityMatrix4x4, @fullLibraryPath), Matrix4x4, ())
end

function invertMatrix(m)
  return ccall((:inverseMatrix4x4, @fullLibraryPath), Matrix4x4, (Matrix4x4,), m)
end

function translationMatrix(translation::Vector3)
  return ccall((:translationMatrix, @fullLibraryPath), Matrix4x4, (Vector3,), translation)
end

function scalingMatrix(scale::Vector3)
  return ccall((:scalingMatrix, @fullLibraryPath), Matrix4x4, (Vector3,), scale)
end

function rotationXMatrix(angle::Float64)
  return ccall((:rotationXMatrix, @fullLibraryPath), Matrix4x4, (Cdouble,), angle)
end

function rotationYMatrix(angle::Float64)
  return ccall((:rotationYMatrix, @fullLibraryPath), Matrix4x4, (Cdouble,), angle)
end

function rotationZMatrix(angle::Float64)
  return ccall((:rotationZMatrix, @fullLibraryPath), Matrix4x4, (Cdouble,), angle)
end

function Base.:*(a::Matrix4x4, b::Vector4)
  return ccall((:matrix4x4timesVector4, @fullLibraryPath), Vector4, (Matrix4x4, Vector4), a, b)
end

function perspective(zNear::Float64, zFar::Float64, fovy::Float64, aspect::Float64)
  return ccall((:perspectiveProjectionMatrix, @fullLibraryPath), Matrix4x4, (Float64, Float64, Float64, Float64), zNear, zFar, fovy, aspect)
end


using MIRT
using MATLAB
using Plots

# setup matlab to use MIRT
if !@isdefined(irtdir)
	ENV["MATLAB_ROOT"] = "/Applications/freeware/matlab"

	irtdir = "/Users/fessler/src/matlab/alg"
	tmp = "addpath('$irtdir')"
	eval_string(tmp)
	mat"setup"
end

fov = 500.
ig = image_geom( ; nx=512, ny=504, fov=fov)
down = 2 # small size for debug
ig = ig.down(down)

mat"ig = image_geom('nx', $(ig.nx), 'ny', $(ig.ny), 'fov', $fov, 'dy', 'dx');"

sg = sino_geom(:par, nb=888, na=984, offset=0.25, d=541/959)
sg = sg.down(down)

mat"sg = sino_geom('par', 'nb', $(sg.nb), 'na', $(sg.na), 'dr', $(sg.d), 'strip_width', 'd');"

ell = [30. 0 120 100 0 1]
ell = [0. 0 200 100 0 1]
ell = [0. 40 200 200 0 1;
60. 0 120 100 0 1]

im_julia = ellipse_im(ig, ell)

@mput ell
mat"im_mat = ellipse_im(ig, ell)"

@mget im_mat

plot(jim(im_julia, "julia"), jim(im_mat, "matlab"), jim(im_mat - im_julia))

sino_julia = ellipse_sino(sg, ell)

mat"[sino_mat, pos, ang] = ellipse_sino(sg, ell)"
mat"sizeof(sino_mat)"
#@mget sino_mat

#plot(jim(sino_julia, "julia"), jim(sino_mat, "matlab"), jim(sino_mat - sino_julia))


using ArgParse,CairoMakie

CairoMakie.activate!(type="pdf")

function parse_argument()

end

function binominal_prob_desity(n,p,x)
    return binomial(n,x)*p^x*(1-p)^(n-x)
end

function main()
    graph_name = "binominal_prob_mass"
    xs_max = 50
    xs = 0:xs_max
    fig = Figure(resolution = (800, 600))

    axis = Axis(fig[1,1], title = "binominal probability mass function")

    graph_params = [Dict("prob"=>0.1, "color"=>:gray0,  "marker"=>:circle,  "label"=>"p=0.1"),
                    Dict("prob"=>0.5, "color"=>:gray30, "marker"=>:diamond, "label"=>"p=0.5"),
                    Dict("prob"=>0.7, "color"=>:gray50, "marker"=>:rect,    "label"=>"p=0.7")]

    for params in graph_params
        p_s = [ binominal_prob_desity(xs_max,params["prob"],x) for x in xs ]
        stem!(fig[1,1], xs, p_s, color = params["color"], marker = params["marker"], label=params["label"])
    end

    axislegend(axis)
    save("$graph_name.pdf", fig)

end

main()


"""Utility functions"""

# """
# Compute the squared L2 norm.
# """
# norm²(x) = sum(abs2, x)
"""
Spectral flattening.
"""
function spectrumflatten(x::AbstractArray{T,1}, Nnorm::Int) where T <:Real
    if Nnorm >= length(x)
        xfilt = x.-median(x)
        xfilt[xfilt.<0] .= 0
        return xfilt
    end
    M = map(median, Subsequence(x, Nnorm, Nnorm-1))
    xfilt = x.-M
    xfilt[xfilt.<0] .= 0
    xfilt
end
function spectrumflatten(x::AbstractArray{T,2}, Nnorm::Int; dims::Int=2) where T <: Real
    mapslices(v->spectrumflatten(v, Nnorm), x, dims=dims)
end

"""
Get myriad constant given α and scale.
"""
function myriadconstant(α, scale)
    (α/(2-α+eps()))*(scale^2)
end

"""
Get myriad constant given `x`.
"""
function myriadconstant(x::AbstractVector{T}) where T<:Real
    d = fit(AlphaStable, x)
    myriadconstant(d.α, d.scale)
end

# """
# Get vector myriad constants given α and Rₘ.
# """
# vmyriadconstant(α::T, Rₘ::AbstractMatrix{T}) where T = (α/(2-α+eps())), Rₘ

# """
# Get vector myriad constants given `x`.
# """
# function vmyriadconstant(x::AbstractVector{T}, m::Integer=4) where T<:Real
#     d = fit(AlphaSubGaussian, x, m)
#     vmyriadconstant(d.α, d.R)
# end

"""
Convert a real signal `x` to an acoustic pressure signal in micropascal.
"""
function pressure(x::AbstractVector{T}, sensitivity::T, gain::T; voltparams::Union{Nothing, Tuple{Int, T}}=nothing) where T<:Real
    ν = exp10(sensitivity/20)
    G = exp10(gain/20)
    if voltparams !== nothing
        nbits, vref = voltparams
        x .*= vref/(2^(nbits-1))
    end
    x./(ν*G)
end

"""
Generate a Hilbert envelope of a real signal `x`.
"""
function envelope(x::AbstractVector{T}) where T<:Real
    abs.(hilbert(x))
end

"""
Get the normalized envelope of of a real signal `x`.
"""
function normalize_envelope(x::AbstractVector{T}) where T<:Real
    env = envelope(x)
    env/sum(env)
end

"""
Get the normalized spectrum of a real signal `x`.
"""
function normalize_spectrum(s::AbstractMatrix{T}) where T<:Real
    sf = sum(s, dims=2)
    sf/sum(sf)
end

"""
Compute ordinal patterns of a real signal `x`.
"""
function ordinalpatterns(x::AbstractVector{T}, m::Integer, τ::Integer=1) where T<:Real
    n = length(x) - τ*m + 1
    ps = Vector{Int}[]
    counts = Int[]
    @inbounds for t in 1:n
        s = @view x[t:τ:t+τ*(m-1)]
        p = sortperm(s)
#        v = [p == ptmp for ptmp in ps]
        cntindex = nothing
        for (i, ptmp) in enumerate(ps)
            if p == ptmp
                cntindex = i
                break
            end
        end
        if cntindex !== nothing #any(v)
            counts[cntindex] += 1
        else
            push!(ps, p)
            push!(counts, 1)
        end
    end
    counts ./ sum(counts)
end


# # Wavefield integration
#
# The process of mode conversion between two different segments of
# [`HomogeneousWaveguide`](@ref)'s
# requires the computation of electromagnetic wavefields from ground to a great
# height in the ionosphere.
#
# [Pitteway (1965)](https://doi.org/10.1098/rsta.1965.0004) presents one well-known method of
# integrating the wavefields that breaks the solution for the differential wave
# equations in the anisotropic ionosphere into solutions for a _penetrating_ and
# _non-penetrating_ mode. Because of the amplitude of the two waves varies greatly
# during the integration, Pitteway scales and orthogonalizes the solutions to maintain
# their independence.
#
# In this example we'll reproduce the figures in Pitteway's paper.
#
# First we'll import the necessary packages.

using CSV, Interpolations
using Plots
using Plots.Measures
using OrdinaryDiffEq

using TimerOutputs

using LongwaveModePropagator
using LongwaveModePropagator: QE, ME
const LMP = LongwaveModePropagator
nothing  #hide

# ## The ionosphere

# Pitteway uses an ionosphere presented in
# [Piggot et. al., 1965](https://doi.org/10.1098/rsta.1965.0005).
#
# ```@raw html
# <img src="../../images/Piggott_ionosphere.png"/>
# ```
#
# He begins with the midday profile with a 16 kHz radio wave at an angle of incidence of
# 40° from normal. We'll also assume the magnetic field has a strength of 50,000 nT
# at a dip angle of 68° and azimuth of 111°. Piggott was concerned with a path from Rugby
# to Cambridge, UK, which, according to the
# [Westinghouse VLF effective-conductivity map](https://apps.dtic.mil/sti/citations/AD0675771),
# has a ground conductivity index of 8.
# This can be specified as `GROUND[8]`.

ea = EigenAngle(deg2rad(40))
frequency = Frequency(16e3)
bfield = BField(50e-6, deg2rad(68), deg2rad(111))
ground = GROUND[8]

# To define the electron density and collision frequency profile, we will read in a
# [digitized table](@__REPO_ROOT_URL__/examples/piggott1965_data.csv)
# of the curves from Piggott and linearly interpolate them.
# While we're at it, we'll also prepare the nighttime ionosphere.

root_dir = dirname(dirname(pathof(LongwaveModePropagator)))
examples_dir = joinpath(root_dir, "examples")
data = CSV.File(joinpath(examples_dir, "piggott1965_data.csv"))

## interpolation object
day_itp = interpolate((data.day_ne_z,), data.day_ne, Gridded(Linear()))
day_etp = extrapolate(day_itp, Line())
dayfcn(z) = (v = day_etp(z); v > 0 ? v : 0.001)

## night has some rows of `missing` data
filtnight_z = collect(skipmissing(data.night_ne_z))
filtnight = collect(skipmissing(data.night_ne))
night_itp = interpolate((filtnight_z,), filtnight, Gridded(Linear()))
night_etp = extrapolate(night_itp, Line())
nightfcn(z) = (v = night_etp(z); v > 0 ? v : 0.001)

## so does the collision frequency
filtnu_z = collect(skipmissing(data.nu_z))
filtnu = collect(skipmissing(data.nu))
nu_itp = interpolate((filtnu_z,), filtnu, Gridded(Linear()))
nu_etp = extrapolate(nu_itp, Line())
nufcn(z) = (v = nu_etp(z); v > 0 ? v : 0.001)

day = Species(QE, ME, dayfcn, nufcn)
night = Species(QE, ME, nightfcn, nufcn)

nothing  #hide

# ## Scaled, integrated wavefields
#
# We use the coordinate frame where ``z`` is directed upward into the ionosphere, ``x`` is
# along the propagation direction, and ``y`` is perpendicular to complete the
# right-handed system.
# Where the ionosphere only varies in the ``z`` direction, the ``E_z``
# and ``H_z`` fields can be eliminated so that we only need to study the remaining four.
# The differential equations for the wave in the ionosphere can be written in matrix
# form for our coordinate system as
# ```math
# \frac{\mathrm{d}\bm{e}}{\mathrm{d}z} = -ik\bm{T}\bm{e}
# ```
# where
# ```math
# \bm{e} = \begin{pmatrix}
# E_x \\ -E_y \\ H_x \\ H_y
# \end{pmatrix}
# ```
# and ``\bm{T}`` is the ``4 \times 4`` matrix presented in
# [Clemmow and Heading (1954)](https://doi.org/10.1017/S030500410002939X).
# ``\bm{T}`` consists of elements of the susceptibility matrix for the ionosphere
# (and can be calculated with [`LongwaveModePropagator.tmatrix`](@ref)).
#
# To compute the wavefields, we calculate them at some height ``z``, use
# ``\mathrm{d}\bm{e}/\mathrm{d}z`` to step to a new height, and repeat from a point high
# in the ionosphere down to the ground.
# The initial solution comes from the Booker Quartic, which is a solution for the
# wavefields in a homogeneous ionosphere.
# At a great height in the ionosphere, we are interested in the two quartic solutions
# corresponding to upward-going waves.
# Therefore, we are integrating two sets of 4 complex variables simultaneously.
# Throughout the integration, the two solutions lose their independence because of
# numerical accuracy limitations over a wide range of field magnitudes.
# To maintain accuracy, the wavefields are orthonomalized repeatedly during the downward
# integration, and the scaling values are stored so that the fields can be "recorrected"
# after the integration is complete.
#
# Here are what the real component of the ``E_{x,1}`` and ``H_{x,2}`` wavefields
# looks like with and without the "recorrection".

zs = 110e3:-50:0
zskm = zs/1000

e = LMP.integratewavefields(zs, ea, frequency, bfield, day; unscale=true)
e_unscaled = LMP.integratewavefields(zs, ea, frequency, bfield, day; unscale=false)

ex1 = getindex.(e, 1)
ex1_unscaled = getindex.(e_unscaled, 1)
hx2 = getindex.(e, 7)
hx2_unscaled = getindex.(e_unscaled, 7)

p1 = plot(real(ex1), zskm; title="\$E_{x,1}\$",
          ylims=(0, 90), xlims=(-1.2, 1.2), linewidth=1.5, ylabel="altitude (km)",
          label="corrected", legend=:topleft);
plot!(p1, real(ex1_unscaled),
      zskm; linewidth=1.5, label="scaled only");

p2 = plot(real(hx2), zskm; title="\$H_{x,2}\$",
          ylims=(0, 90), xlims=(-1.2, 1.2), linewidth=1.5, legend=false);
plot!(p2, real(hx2_unscaled),
      zskm; linewidth=1.5);

plot(p1, p2; layout=(1,2))
#md savefig("wavefields_scaling.png"); nothing #hide
#md # ![](wavefields_scaling.png)
#

# ## Differential equations solver
#
# Pitteway (1965) used a basic form of a Runge-Kutta integrator with fixed step size.
# Julia has a fantastic [DifferentialEquations suite](https://diffeq.sciml.ai/stable/)
# for integrating many different forms of differential equations.
# Although wavefields are only computed two times for every transition in a
# [`SegmentedWaveguide`](@ref), we would still like to choose an efficient
# solver that requires
# relatively few function calls while still maintaining good accuracy.
#
# Let's try a few different solvers and compare their runtime for the
# day and night ionospheres.
#
# We can pass [`IntegrationParams`](@ref) through the [`LMPParams`](@ref) struct.
# Where necessary, we set the `lazy` interpolant option  of the solvers to `false`
# because we discontinuously scale the fields in the middle of the integration and the
# `lazy` option is not aware of this.

TO = TimerOutput()

zs = 110e3:-50:0
solvers = [Tsit5(), BS5(lazy=false), OwrenZen5(),
           Vern6(lazy=false), Vern7(lazy=false), Vern8(lazy=false), Vern9(lazy=false)]

solverstrings = replace.(string.(solvers), "OrdinaryDiffEq."=>"")

day_es = []
night_es = []
for s in eachindex(solvers)
    ip = IntegrationParams(solver=solvers[s], tolerance=1e-6)
    params = LMPParams(wavefieldintegrationparams=ip)

    ## make sure method is compiled
    LMP.integratewavefields(zs, ea, frequency, bfield, day; params=params);
    LMP.integratewavefields(zs, ea, frequency, bfield, night; params=params);

    solverstring = solverstrings[s]
    let day_e, night_e
        ## repeat 200 times to average calls
        for i = 1:200
            ## day ionosphere
            @timeit TO solverstring begin
                day_e = LMP.integratewavefields(zs, ea, frequency, bfield, day; params=params)
            end
            ## night ionosphere
            @timeit TO solverstring begin
                night_e = LMP.integratewavefields(zs, ea, frequency, bfield, night; params=params)
            end
        end
        push!(day_es, day_e)
        push!(night_es, night_e)
    end
end

# A quick plot to ensure none of the methods have problems.

day_e1s = [getindex.(e, 1) for e in day_es]

plot(real(day_e1s), zs/1000;
     label=permutedims(solverstrings), legend=:topleft)
#md savefig("wavefields_day.png"); nothing #hide
#md # ![](wavefields_day.png)

#
# And at night...

night_e1s = [getindex.(e, 1) for e in night_es]

plot(real(night_e1s), zs/1000;
     label=permutedims(solverstrings), legend=:topright)
#md savefig("wavefields_night.png"); nothing #hide
#md # ![](wavefields_night.png)

#
# The times to run each...

TO

# The `Tsit5`, `BS5`, and 6th, 7th, and 8th order Vern methods all have similar performance.
# In fact, rerunning these same tests multiple times can result in different solvers
# being "fastest".
#
# We use `Tsit5()`as the default in LongwaveModePropagator, which is similar to MATLAB's
# `ode45`, but usually more efficient.
#
# Why not a strict `RK4` or a more basic method? I tried them and they produce some
# discontinuities around the reflection height. It is admittedly difficult to tell
# if this is a true failure of the methods at this height or a problem related
# to the scaling and saving callbacks that occur during the integration.
# In any case, none of the methods tested above exhibit that issue.

# ## Pitteway figure 2
#
# Let's reproduce the wavefields in figure 2 of Pitteway (1965).

zs = 110e3:-500:50e3
zskm = zs/1000

e = LMP.integratewavefields(zs, ea, frequency, bfield, day; unscale=true)

ex1 = getindex.(e, 1)
ey1 = getindex.(e, 2)
ex2 = getindex.(e, 5)
hx2 = getindex.(e, 7)

function plotfield(field; kwargs...)
    p = plot(real(field), zskm; color="black", linewidth=1.5, legend=false,
             xlims=(-0.8, 0.8), label="real",
             framestyle=:grid; kwargs...)
    plot!(p, imag(field), zskm; color="black",
          linewidth=1.5, linestyle=:dash, label="imag")
    plot!(p, abs.(field), zskm; color="black", linewidth=3, label="abs")
    plot!(p, -abs.(field), zskm; color="black", linewidth=3, label="")
    return p
end

fs = 10
ex1p = plotfield(ex1; ylims=(49, 81), yaxis=false, yformatter=_->"");
annotate!(ex1p, 0.2, 79, text("\$E_{x,1}\$", fs));
ey1p = plotfield(ey1; ylims=(49, 81));
annotate!(ey1p, 0.2, 79, text("\$E_{y,1}\$", fs));
annotate!(ey1p, -0.98, 83, text("height (km)", fs));
ex2p = plotfield(ex2; ylims=(49, 86), yaxis=false, yformatter=_->"");
annotate!(ex2p, 0.3, 84, text("\$E_{x,2}\$", fs));
hx2p = plotfield(hx2; ylims=(49, 86));
annotate!(hx2p, 0.35, 84, text("\$H_{x,2}\$", fs, :center));
plot(ex1p, ey1p, ex2p, hx2p; layout=(2,2), size=(400,600), top_margin=5mm)
#md savefig("wavefields_fig2.png"); nothing #hide
#md # ![](wavefields_fig2.png)
#

# ```@raw html
# <img src="../../images/Pitteway1965_fig2.png"/>
# ```

# The envelopes of the two are very similar.
# The precise position of the real and imaginary wave components are not important
# because they each represent an instant in time and change based on the starting
# height of the integration.
#
# ## Pitteway figure 3
#
# Figure 3 of Pitteway (1965) uses a different scenario.
# A wave of frequency 202 kHz is at normal incidence through the nighttime ionosphere
# presented in Piggott (1965).
#
# First let's set up the new scenario.

ea = EigenAngle(0)
frequency = Frequency(202e3)
bfield = BField(50e-6, deg2rad(68), deg2rad(111))
ground = GROUND[8]
nothing  #hide

#  Now integrating the wavefields.

zs = 110e3:-50:70e3
zskm = zs/1000

e = LMP.integratewavefields(zs, ea, frequency, bfield, night)

ey1 = getindex.(e, 2)
hx2 = getindex.(e, 7)

ey1p = plotfield(ey1; ylims=(75, 102), title="\$E_{y,1}\$");
hx2p = plotfield(hx2; ylims=(75, 102), title="\$H_{x,2}\$");
plot(ey1p, hx2p; layout=(1,2), size=(400,500))
#md savefig("wavefields_fig3.png"); nothing #hide
#md # ![](wavefields_fig3.png)
#

# ```@raw html
# <img src="../../images/Pitteway1965_fig3.png"/>
# ```