matern12(x1, x2; ρ=ρ, σ²=σ²) = MaternCovariance_12(abs(x1-x2); ρ=ρ, σ²=σ²)

function MaternCovariance_12(d; ρ=ρ, σ²=σ²)

    @assert(d >= 0)

    σ² * exp(-d/ρ)

end

matern32(x1, x2; ρ=ρ, σ²=σ²) = MaternCovariance_32(abs(x1-x2); ρ=ρ, σ²=σ²)

function MaternCovariance_32(d; ρ=ρ, σ²=σ²)

    @assert(d >= 0)

    σ² * (1 + sqrt(3)*d/ρ) * exp(- sqrt(3)*d/ρ)

end

matern52(x1, x2; ρ=ρ, σ²=σ²) = MaternCovariance_52(abs(x1-x2); ρ=ρ, σ²=σ²)

function MaternCovariance_52(d; ρ=ρ, σ²=σ²)

    @assert(d >= 0)

    σ² * (1 + sqrt(5)*d/ρ + 5*d^2/(3*ρ^2)) * exp(- sqrt(5)*d/ρ)

end

rbf(x1, x2; ρ=ρ, σ²=σ²) = RBFCovariance(abs(x1-x2); ρ=ρ, σ²=σ²)

function RBFCovariance(d; ρ=ρ, σ²=σ²)

    @assert(d >= 0)

    σ² * exp(-d^2 / (2ρ))

end

linear(x1, x2; c=c, σ²=σ²) = σ² * (x1 - c) * (x2 - c)

function calculatekernelmatrix(k, ρ, σ², x)

    calculatekernelmatrix(k, ρ, σ², x, x)

end

function calculatekernelmatrix(k, ρ, σ², x1, x2)

    [k(a, b; ρ = ρ, σ² = σ²) for a in x1, b in x2]

end


"""
    mm(X,v,K) = v*X / (X + K)

A Michaelis-Menten rate function.
"""
mm(X,v,K) = v*X / (X + K)
@register mm(X,v,K);
Symbolics.derivative(::typeof(mm), args::NTuple{3,Any}, ::Val{1}) = (args[2]*args[3]) / (args[1]+args[3])^2
Symbolics.derivative(::typeof(mm), args::NTuple{3,Any}, ::Val{2}) = args[1]/(args[1]+args[3])
Symbolics.derivative(::typeof(mm), args::NTuple{3,Any}, ::Val{3}) = - args[2]*args[1]/(args[1]+args[3])^2

# Registers the repressing Michaelis-Menten function.
"""
    mmr(X,v,K) = v*K / (X + K)

A repressive Michaelis-Menten rate function.
"""
mmr(X,v,K) = v*K / (X + K)
@register mmr(X,v,K);
Symbolics.derivative(::typeof(mmr), args::NTuple{3,Any}, ::Val{1}) = - (args[2]*args[3]) / (args[1]+args[3])^2
Symbolics.derivative(::typeof(mmr), args::NTuple{3,Any}, ::Val{2}) = args[3]/(args[1]+args[3])
Symbolics.derivative(::typeof(mmr), args::NTuple{3,Any}, ::Val{3}) = args[2]*args[1]/(args[1]+args[3])^2

"""
    hill(X,v,K,n) = v*(X^n) / (X^n + K^n)

A Hill rate function.
"""
hill(X,v,K,n) = v*(X^n) / (X^n + K^n)
@register hill(X,v,K,n);
Symbolics.derivative(::typeof(hill), args::NTuple{4,Any}, ::Val{1}) = args[2] * args[4] * (args[3]^args[4]) * (args[1]^(args[4]-1))  /  (args[1]^args[4] + args[3]^args[4])^2
Symbolics.derivative(::typeof(hill), args::NTuple{4,Any}, ::Val{2}) = (args[1]^args[4])  /  (args[1]^args[4] + args[3]^args[4])
Symbolics.derivative(::typeof(hill), args::NTuple{4,Any}, ::Val{3}) = - args[2] * args[4] * (args[1]^args[4]) * (args[3]^(args[4]-1))  /  (args[1]^args[4] + args[3]^args[4])^2
Symbolics.derivative(::typeof(hill), args::NTuple{4,Any}, ::Val{4}) = args[2] * (args[1]^args[4]) * (args[3]^args[4]) * (log(args[1])-log(args[3]))  /  (args[1]^args[4] + args[3]^args[4])^2

"""
    hillr(X,v,K,n) = v*(K^n) / (X^n + K^n)

A repressive Hill rate function.
"""
hillr(X,v,K,n) = v*(K^n) / (X^n + K^n)
@register hillr(X,v,K,n);
Symbolics.derivative(::typeof(hillr), args::NTuple{4,Any}, ::Val{1}) = - args[2] * args[4] * (args[3]^args[4]) * (args[1]^(args[4]-1))  /  (args[1]^args[4] + args[3]^args[4])^2
Symbolics.derivative(::typeof(hillr), args::NTuple{4,Any}, ::Val{2}) = (args[3]^args[4])  /  (args[1]^args[4] + args[3]^args[4])
Symbolics.derivative(::typeof(hillr), args::NTuple{4,Any}, ::Val{3}) = args[2] * args[4] * (args[1]^args[4]) * (args[3]^(args[4]-1))  /  (args[1]^args[4] + args[3]^args[4])^2
Symbolics.derivative(::typeof(hillr), args::NTuple{4,Any}, ::Val{4}) = args[2] * (args[1]^args[4]) * (args[3]^args[4]) * (log(args[3])-log(args[1]))  /  (args[1]^args[4] + args[3]^args[4])^2

"""
    hillar(X,Y,v,K,n) = v*(X^n) / (X^n + Y^n + K^n)

An activation/repressing Hill rate function.
"""
hillar(X,Y,v,K,n) = v*(X^n) / (X^n + Y^n + K^n)
@register hillar(X,Y,v,K,n);
Symbolics.derivative(::typeof(hillar), args::NTuple{5,Any}, ::Val{1}) = args[3] * args[5] * (args[1]^(args[5]-1)) * (args[2]^args[5]+args[4]^args[5])  /  (args[1]^args[5] + args[2]^args[5] + args[4]^args[5])^2
Symbolics.derivative(::typeof(hillar), args::NTuple{5,Any}, ::Val{2}) = - args[3] * args[5] * (args[2]^(args[5]-1)) * (args[1]^args[5])  /  (args[1]^args[5] + args[2]^args[5] + args[4]^args[5])^2
Symbolics.derivative(::typeof(hillar), args::NTuple{5,Any}, ::Val{3}) = (args[1]^args[5])   /  (args[1]^args[5] + args[2]^args[5] + args[4]^args[5])
Symbolics.derivative(::typeof(hillar), args::NTuple{5,Any}, ::Val{4}) = - args[3] * args[5] * (args[3]^(args[5]-1)) * (args[1]^args[5])  /  (args[1]^args[5] + args[2]^args[5] + args[4]^args[5])^2
Symbolics.derivative(::typeof(hillar), args::NTuple{5,Any}, ::Val{5}) = args[3] * (args[1]^args[5])  *  (log(args[1])*(args[2]^args[5] + args[4]^args[5]) - (args[2]^args[5])*log(args[2]) - (args[4]^args[5])*log(args[4]))   /   (args[1]^args[5] + args[2]^args[5] + args[4]^args[5])^2


using Turing, BridgeSampling

## Test on bivariate standard Normal distribution
samples = rand(MvNormal(ones(2)), 10_000)

log_posterior(x) = -0.5 * x' * x

Bridge = bridgesampling(samples, log_posterior, [-Inf, -Inf], [Inf, Inf])
Analytical = log(2*pi)

isapprox(value(Bridge), Analytical, atol = 1e-3)

## Hierarchical Normal example

# Generate data
true_μ = 0.0
true_τ² = 0.5
true_σ² = 1.0
n = 20
true_θ = rand(Normal(true_μ, sqrt(true_τ²)), n)
#y = rand.(Normal.(true_θ, sqrt(true_σ²)))
# Same values as in : https://cran.r-project.org/web/packages/bridgesampling/vignettes/bridgesampling_example_jags.html
y = vec([1.19365332  1.95745331 -0.72161754 -1.87380833 -1.16928239  0.51960853 -0.03610041  0.42508815  0.41119221 -0.81236980  0.72967357  3.48186722  2.31126381  2.00029422 -0.27643507  1.06882370 -0.95083599 -1.89651101  2.56019737  0.23703060])

# DynamicPPL models
@model H0(Y) = begin
    τ² ~ InverseGamma(1,1)
    θ ~ filldist(Normal(0.0, sqrt(τ²)), length(Y))
    Y ~ arraydist(Normal.(θ, 1.0))
end

@model H1(Y) = begin
    μ ~ Normal()
    τ² ~ InverseGamma(1,1)
    θ ~ filldist(Normal(μ, sqrt(τ²)), length(Y))
    Y ~ arraydist(Normal.(θ, 1.0))
end

M0 = H0(y)
M1 = H1(y)

# Sampling
chn0 = sample(M0, NUTS(), MCMCThreads(), 50_000, 3, burnin=2000)
chn1 = sample(M1, NUTS(), MCMCThreads(), 50_000, 3, burnin=2000)

# Estimate log marginal likelihood : directly with the DynamicPPL model (lower precision)
L0 = bridgesampling(chn0, M0)
L1 = bridgesampling(chn1, M1)

# Or with specified log posterior functions and boundaries
log_posterior_H0(x) = sum([logpdf(Normal(x.θ[i], 1.0), y[i]) for i = 1:n]) +
                sum([logpdf(Normal(0.0, sqrt(x.τ²)), x.θ[i]) for i = 1:n]) +
                logpdf(InverseGamma(1,1), x.τ²)

log_posterior_H1(x) = sum([logpdf(Normal(x.θ[i], 1.0), y[i]) for i = 1:n]) +
                sum([logpdf(Normal(x.μ, sqrt(x.τ²)), x.θ[i]) for i = 1:n]) +
                logpdf(InverseGamma(1,1), x.τ²) +
                logpdf(Normal(), x.μ)

samplesH0, names = BridgeSampling.extract_samples(chn0, M0)
L0 = bridgesampling(samplesH0, log_posterior_H0, [0.0, -Inf], [Inf, Inf]; names=names)

samplesH1, names = BridgeSampling.extract_samples(chn1, M1)
L1 = bridgesampling(samplesH1, log_posterior_H1, [-Inf, 0.0, -Inf], [Inf, Inf, Inf]; names=names)

# Bayes factor
bayes_factor(L0, L1)

# Posterior model probabilities
posterior_probabilities([L0, L1])


# === Misc Utility Functions === #
const LOG_2_PI = log(2 * pi)

"""Log-gamma function."""
lgamma(x) = logabsgamma(x)[1]

"""
Sample from a list of log probabilities. Overwrites
vector so that no new memory is allocated.
"""
sample_logprobs!(log_probs) = sample(pweights(softmax!(log_probs)))

"""
Log normalizer of 1D normal distribution in information form.
given potential `m` and precision `v`.

N(x | m, v) = exp{ m⋅x - .5v⋅x² - log Z(m, v) }
"""
gauss_info_logZ(m, v) = 0.5 * (LOG_2_PI - log(v) + m * m / v)


### A Pluto.jl notebook ###
# v0.12.10

using Markdown
using InteractiveUtils

# ╔═╡ 9f18ccd0-8e25-11eb-20e6-ab941f663550
begin
	import Pkg
    Pkg.activate(".")
    Pkg.add("PlutoUI")
	Pkg.add("Plots")
	Pkg.add("Distributions")

	using PlutoUI
	using Plots
	using Distributions
end

# ╔═╡ 10a9374c-8e1a-11eb-10b4-fb8272dd46d1
md"""
## Random SIRD Model with two Populations

#### Intro

The **SIRD Model** has been developed to simulate an epidemic over time. The model consists of a system of 4 differential equations that express the rates of change of 4 variables over time. The 4 variables are:

${ \begin{align*}
	S & - \text{the susceptibles of getting the infection} \\
	I & - \text{the infected} \\
	R & - \text{the recovered from the infection} \\
	D & - \text{the number of dead}
\end{align*} }$

#### The Model
Define the parameters of the dynamics as follows:

${ \begin{align*}
	\beta & \to \text{ rate of infection} \\
	\gamma & \to \text{ rate of recovery} \\
	\delta & \to \text{ rate of immunity loss} \\
	\rho & \to \text{ mortality}
\end{align*} }$

Then the following 4 equations govern the dynamis of the model:

${ \begin{align*}
\frac{\mathrm{d}S}{\mathrm{d}t} & = -\beta \cdot \frac{S \cdot I}{N} + \delta \cdot R \\
\frac{\mathrm{d}I}{\mathrm{d}t} & = \beta \cdot \frac{S \cdot I}{N} - (\gamma+\rho) \cdot I \\
\frac{\mathrm{d}R}{\mathrm{d}t} & = \gamma \cdot I - \delta \cdot R \\
\frac{\mathrm{d}D}{\mathrm{d}t} & = \rho \cdot I
\end{align*} }$

To model two mixing populations with independent parameters double the system and add resp. substract the mixing term

${ \pm \beta_{ij} \frac{S_j\cdot I_i}{N_i + N_j} }$

to the number of infected resp. susceptible individuals. This term models the infection from Population \\( i \\) to Population \\( j \\), for \\( i,j = 1,2\\).

Finally to randomize the dynamics of the system in every step, all quantities that move between within the system get drawn from a Poisson distribution with the respective rate as mean.

#### Model simulation
The following is a simulation of the model described above. First choose the model parameters
"""

# ╔═╡ 43970fa0-8e1b-11eb-3052-1701966a4478
begin
	T = 300 	#period of 300 daysplot
	Δt = 1/4 	#time interval of 6 hours (1/4 of a day)
end;

# ╔═╡ 742d1fd0-907d-11eb-27d1-59ca2af59f97
parameter = Dict(
	"rate of infection" => 0.14,
	"rate of recovery" => 0.07,
	"rate of immuity loss" => 0.1,
	"rate of death of infected" => 0.001,
	"total population" => 1*10^4,
	"initial number of infected" => 10
);

# ╔═╡ d59335ac-913b-11eb-228f-273be8d7a86a
md"""
---
**Additional parameter and functions for simulations with two subgroups**
"""

# ╔═╡ 56b9df4c-913b-11eb-180a-eb784c2b5181
begin
	parameter_subpopulation = Dict(
		"rate of infection" => 0.14,
		"rate of recovery" => 0.035,
		"rate of immuity loss" => 0.2,
		"rate of death of infected" => 0.01,
		"total population" => 1*10^2,
		"initial number of infected" => 0.0
		)

	mixing_parameter = Dict(
		"rate of infection 1 -> 2" => 0.14,
		"rate of infection 2 -> 1" => 0.14
	)
end;

# ╔═╡ f8e23e02-9142-11eb-1700-6974da1a09c0
md"""
---
#### Worksheet
Here all the calculations start. Set the initial state, choose the set of parameters and if needed the parameter changing function.
"""

# ╔═╡ da008842-9142-11eb-388c-d7e1f662d9db
md"""
---
#### Plots
"""

# ╔═╡ 93b1fcc4-9145-11eb-3d5e-4b9bd577a545
md"""
---
#### Model Functions

**Euler Algorithm to solve system. With constant or changing parameters**
"""

# ╔═╡ 2680b820-8e32-11eb-28de-6f039360524b
begin
	function euler(f::Function,change_par!::Function,t0,tn,Δt,x₀,par)
		T = t0:Δt:tn
		F = Matrix{eltype(x₀)}(undef,(length(T),length(x₀)))
		F[1,:] = x₀
		for (n,t) ∈ enumerate(T[2:end])
			change_par!(par,t,F,n)
			F[n+1,:] = max.(F[n,:] + f(t,F,n,par) .* Δt,0)
		end
		return F
	end

	function no_change!(par,t,F,n) end

	euler(f::Function,t0,tn,Δt,x₀,par) = euler(f,no_change!,t0,tn,Δt,x₀,par)
end;

# ╔═╡ 58f42866-9146-11eb-29ea-87830bc8db66
md"""
**Defines the infinitesimal change of system**
"""

# ╔═╡ 3ab68bec-8e2f-11eb-203e-f9530bc39073
begin
	function SIR_step(t,x,β,δ,γ,ρ,N)
		S, I, R, D = x[1], x[2], x[3], x[4]
		nI = rand(Poisson(β*I*S/N))
		nIL = rand(Poisson(δ*R))
		nR = rand(Poisson(γ*I))
		nD = rand(Poisson(ρ*I))
		return [
			-nI + nIL,
			nI - nR - nD,
			nR - nIL,
			nD
			]
	end

	SIR_step(t,x,par::Dict) = SIR_step(t,x,
				par["rate of infection"],par["rate of immuity loss"],
				par["rate of recovery"],par["rate of death of infected"],
				par["total population"]
			)

	SIR_step(t,H,n,par::Dict) = SIR_step(t,H[n,:],par)
end;

# ╔═╡ a7ac774a-913b-11eb-267b-8f9841c44cb4
begin
	function SIR_step_2D(t,H,n,par₁::Dict,par₂::Dict,par_mix::Dict)
		x₁,x₂ = view(H,n,1:4), view(H,n,5:8)
		x = vcat(SIR_step(t,x₁,par₁),SIR_step(t,x₂,par₂))
		return x + SIR_interaction(t,x₁,x₂,
					par_mix["rate of infection 1 -> 2"],
					par_mix["rate of infection 2 -> 1"],
					par₁["total population"]+par₂["total population"])
	end

	SIR_step_2D(t,H,n,par) = SIR_step_2D(t,H,n,par[1],par[2],par[3])

	function SIR_interaction(t,x₁,x₂,β₃,β₄,N)
		S₁, I₁, S₂, I₂ = x₁[1], x₁[2], x₂[1], x₂[2]
		nI₁ = rand(Poisson(β₄*S₁*I₂/N))
		nI₂ = rand(Poisson(β₃*S₂*I₁/N))
		return [
			-nI₁, nI₁, 0, 0,
			-nI₂, nI₂, 0, 0
		]
	end
end;

# ╔═╡ c6bbb678-9080-11eb-2047-a3dcd34e8c77
begin
	N₁,N₂ = parameter["total population"], parameter_subpopulation["total population"]
	I₁,I₂ = parameter["initial number of infected"], parameter_subpopulation["initial number of infected"]

	x₀ = [
		N₁-I₁,
		I₁,
		0.0,
		0.0,
		N₂-I₂,
		I₂,
		0.0,
		0.0
	]

	result = euler(
		SIR_step_2D,
		0.0,T,Δt,
		x₀,
		(parameter,parameter_subpopulation,mixing_parameter)
		)
end;

# ╔═╡ 3d9185a6-908c-11eb-301c-358ed4a3eb37
let
	p1 = plot(framestlye=:zerolines,title="Main Population")

	plot!(p1, 0.0:Δt:T,result[:,1:4],
		label = ["S" "I" "R" "D"],
		color = [:blue :red :green :yellow],
		xlabel = "Days",
		ylabel = "Number of Individuals"
	)

	p2 = plot(framestlye=:zerolines,title="Subpopulation")

	plot!(p2, 0.0:Δt:T,result[:,5:8],
		label = ["S" "I" "R" "D"],
		color = [:blue :red :green :yellow],
		xlabel = "Days",
		ylabel = "Number of Individuals"
	)

	plot(p1,p2,layout=(2,1),size=(680,680))
end

# ╔═╡ de101aba-9145-11eb-2457-7b1831145eeb
md"""

**Supporting function for calculating the Incidence**
"""

# ╔═╡ 16a50518-906d-11eb-1e2b-3faa1765f30c
begin
	#Calculates at one time within the simulation
	function incidence(nI,t,pop_size;ndays=7,scale=100_000)
		#set starting time in range
		t₀ = max(t-round(Int,ndays/Δt),1)
		#return Incidence
		return (nI[t] - nI[t₀]) .* (scale/pop_size)
	end

	#Calculates for all times after simulation
	function incidence(nI,pop_size;ndays=7,scale=100_000)
		#rescale days
		ndays = round(Int,ndays/Δt)
		#sum new cases in first days
		I = nI[1:ndays]
		#iterate and add incidence
		for t₀ ∈ 1:length(nI)-ndays
			append!(I,nI[t₀+ndays] - nI[t₀])
		end
		#scale and return
		return I .* scale/pop_size
	end
end;

# ╔═╡ ceedca8e-9148-11eb-189c-a3a088520c2c
md"""
---
Package Management
"""

# ╔═╡ Cell order:
# ╟─10a9374c-8e1a-11eb-10b4-fb8272dd46d1
# ╠═43970fa0-8e1b-11eb-3052-1701966a4478
# ╠═742d1fd0-907d-11eb-27d1-59ca2af59f97
# ╟─d59335ac-913b-11eb-228f-273be8d7a86a
# ╠═56b9df4c-913b-11eb-180a-eb784c2b5181
# ╟─f8e23e02-9142-11eb-1700-6974da1a09c0
# ╠═c6bbb678-9080-11eb-2047-a3dcd34e8c77
# ╟─da008842-9142-11eb-388c-d7e1f662d9db
# ╟─3d9185a6-908c-11eb-301c-358ed4a3eb37
# ╟─93b1fcc4-9145-11eb-3d5e-4b9bd577a545
# ╠═2680b820-8e32-11eb-28de-6f039360524b
# ╟─58f42866-9146-11eb-29ea-87830bc8db66
# ╠═3ab68bec-8e2f-11eb-203e-f9530bc39073
# ╠═a7ac774a-913b-11eb-267b-8f9841c44cb4
# ╟─de101aba-9145-11eb-2457-7b1831145eeb
# ╠═16a50518-906d-11eb-1e2b-3faa1765f30c
# ╟─ceedca8e-9148-11eb-189c-a3a088520c2c
# ╠═9f18ccd0-8e25-11eb-20e6-ab941f663550


function absolute_error(y_true, y_pred)
    @_dimcheck length(y_true) == length(y_pred)
    return(abs.(y_true .- y_pred))
end

function percent_error(y_true, y_pred)
    @_dimcheck length(y_true) == length(y_pred)
    return((y_true .- y_pred) ./ y_true)
end

function log_error(y_true, y_pred)
    @_dimcheck length(y_true) == length(y_pred)
    return(log(y_true - y_pred))
end

function squared_error(y_true, y_pred)
    @_dimcheck length(y_true) == length(y_pred)
    return((y_true - y_pred) .^ 2)
end

function squared_log_error(y_true, y_pred)
    @_dimcheck length(y_true) == length(y_pred)
    return(log_error(y_true, y_pred) .^ 2)
end

function absolute_percent_error(y_true, y_pred)
    @_dimcheck length(y_true) == length(y_pred)
    return(abs.(percent_error(y_true, y_pred)))
end

function mean_error(y_true, y_pred)
    @_dimcheck length(y_true) == length(y_pred)
    return(mean(y_true - y_pred))
end

function mean_absolute_error(y_true, y_pred)
    @_dimcheck length(y_true) == length(y_pred)
    return(mean(absolute_error(y_true, y_pred)))
end

function median_absolute_error(y_true, y_pred)
    @_dimcheck length(y_true) == length(y_pred)
    return(median(absolute_error(y_true, y_pred)))
end

function mean_percent_error(y_true, y_pred)
    @_dimcheck length(y_true) == length(y_pred)
    return(mean(percent_error(y_true, y_pred)))
end

function median_percent_error(y_true, y_pred)
    @_dimcheck length(y_true) == length(y_pred)
    return(median(percent_error(y_true, y_pred)))
end

function mean_squared_error(y_true, y_pred)
    @_dimcheck length(y_true) == length(y_pred)
    return(mean(squared_error(y_true, y_pred)))
end

function median_squared_error(y_true, y_pred)
    @_dimcheck length(y_true) == length(y_pred)
    return(median(squared_error(y_true, y_pred)))
end

function sum_squared_error(y_true, y_pred)
    @_dimcheck length(y_true) == length(y_pred)
    return(sum(squared_error(y_true, y_pred)))
end

function mean_squared_log_error(y_true, y_pred)
    @_dimcheck length(y_true) == length(y_pred)
    return(mean(squared_log_error(y_true, y_pred)))
end

function mean_absolute_percent_error(y_true, y_pred)
    @_dimcheck length(y_true) == length(y_pred)
    return(mean(absolute_percent_error(y_true, y_pred)))
end

function median_absolute_percent_error(y_true, y_pred)
    @_dimcheck length(y_true) == length(y_pred)
    return(median(absolute_percent_error(y_true, y_pred)))
end

function symmetric_mean_absolute_percent_error(y_true, y_pred)
    @_dimcheck length(y_true) == length(y_pred)
    return(mean(abs.(y_pred .- y_true) ./ ((abs.(y_true) .+ abs.(y_pred)) ./ 3)))
end

function symmetric_median_absolute_percent_error(y_true, y_pred)
    @_dimcheck length(y_true) == length(y_pred)
    return(median(abs.(y_pred - y_true) ./ ((abs.(y_true) .+ abs.(y_pred)) ./ 2)))
end

function mean_absolute_scaled_error(y_true, y_pred)
    @_dimcheck length(y_true) == length(y_pred)
    n = max(length(y_true), length(y_pred))
    numerator = sum(abs.(y_true .- y_pred))
    denominator = (n / (n - 1)) * sum(abs.(y_true[2:n] .- y_pred[1:(n-1)]))
    return(numerator / denominator)
end

function total_variance_score(y_true, y_pred)
    @_dimcheck length(y_true) == length(y_pred)
    return(sum((y_true .- mean(y_true)) .^ 2))
end

function explained_variance_score(y_true, y_pred)
    @_dimcheck length(y_true) == length(y_pred)
    return(sum((y_pred .- mean(y_true)) .^ 2))
end

function unexplained_variance_score(y_true, y_pred)
    @_dimcheck length(y_true) == length(y_pred)
    return(sum((y_true .- y_pred) .^ 2))
end

function r2_score(y_true, y_pred)
    @_dimcheck length(y_true) == length(y_pred)
    return(explained_variance_score(y_true, y_pred) / total_variance_score(y_true, y_pred))
end


using SparseArrays
using LinearAlgebraicRepresentation
Lar = LinearAlgebraicRepresentation
using ViewerGL
GL = ViewerGL

V, (VV,EV,FV,CV) = Lar.simplex(3, true)
tetra = V, EV,FV,CV
twotetra = Lar.Struct([ tetra, Lar.t(0.25,0.25,0.25), tetra ])
V,EV,FV,CV = Lar.struct2lar(twotetra)
GL.VIEW([ GL.GLGrid(V,FV, GL.Point4d(1,1,1,0.2)) ]);

cop_EV = Lar.coboundary_0(EV::Lar.Cells);
cop_EW = convert(Lar.ChainOp, cop_EV);
cop_FE = Lar.coboundary_1(V, FV::Lar.Cells, EV::Lar.Cells);
W = convert(Lar.Points, V');

V, copEV, copFE, copCF = Lar.Arrangement.spatial_arrangement( W::Lar.Points, cop_EW::Lar.ChainOp, cop_FE::Lar.ChainOp)

EV = Lar.cop2lar(copEV)
FE = [findnz(copFE[k,:])[1] for k=1:size(copFE,1)]
FV = [collect(Set(cat(EV[e] for e in FE[f]))) for f=1:length(FE)]
FV = convert(Lar.Cells, FV)
W = convert(Lar.Points, V')
WW = [[k] for k=1:size(W,2)]

GL.VIEW(GL.numbering(.5)((W,[WW,EV]) ));

triangulated_faces = Lar.triangulate(V, [copEV, copFE])
FVs = convert(Array{Lar.Cells}, triangulated_faces)
V = convert(Lar.Points, V')
GL.VIEW(GL.GLExplode(V,FVs,1.5,1.5,1.5,99));

EVs = Lar.FV2EVs(copEV, copFE) # polygonal face fragments
GL.VIEW(GL.GLExplode(V,EVs,1.5,1.5,1.5,99,1));


function hammingsequence(N::Int)
    if N < 1
        throw("Hamming sequence index must be a positive integer")
    end
    ham = Vector{BigInt}([1])
    base2, base3, base5 = 1, 1, 1
    next2, next3, next5 = BigInt(2), BigInt(3), BigInt(5)
    for _ in 1:N-1
        x = min(next2, next3, next5)
        push!(ham, x)
        next2 <= x && (base2 += 1; next2 = 2ham[base2])
        next3 <= x && (base3 += 1; next3 = 3ham[base3])
        next5 <= x && (base5 += 1; next5 = 5ham[base5])
    end
    ham
end


# Diffusion maps
# --------------
# Diffusion maps,
# Coifman, R. & Lafon, S., Applied and Computational Harmonic Analysis, Elsevier, 2006, 21, 5-30

"""

    DiffMap{T <: Real} <: AbstractDimensionalityReduction

The `DiffMap` type represents diffusion maps model constructed for `T` type data.
"""
struct DiffMap{T <: Real} <: AbstractDimensionalityReduction
    t::Int
    α::Real
    ɛ::Real
    λ::AbstractVector{T}
    K::AbstractMatrix{T}
    proj::Projection{T}
end

## properties
outdim(R::DiffMap) = size(R.proj, 1)
eigvals(R::DiffMap) = R.λ

## custom
"""Returns the kernel matrix of the diffusion maps model `R`"""
kernel(R::DiffMap) = R.K

## show
summary(io::IO, R::DiffMap) = print(io, "Diffusion Maps(outdim = $(outdim(R)), t = $(R.t), α = $(R.α), ɛ = $(R.ɛ))")
function show(io::IO, R::DiffMap)
    summary(io, R)
    io = IOContext(io, :limit=>true)
    println(io)
    println(io, "Kernel: ")
    Base.print_matrix(io, R.K, "[", ",","]")
    println(io)
    println(io, "Embedding:")
    Base.print_matrix(io, transform(R), "[", ",","]")
end

## interface functions
"""
    fit(DiffMap, data; maxoutdim=2, t=1, α=1.0, ɛ=1.0)

Fit a isometric mapping model to `data`.

# Arguments
* `data`: a matrix of observations. Each column of `data` is an observation.

# Keyword arguments
* `maxoutdim`: a dimension of the reduced space.
* `t`: a number of transitions
* `α`: a normalization parameter
* `ɛ`: a Gaussian kernel variance (the scale parameter)

# Examples
```julia
M = fit(DiffMap, rand(3,100)) # construct diffusion map model
R = transform(M)              # perform dimensionality reduction
```
"""
function fit(::Type{DiffMap}, X::AbstractMatrix{T}; maxoutdim::Int=2, t::Int=1, α::Real=0.0, ɛ::Real=1.0) where {T<:Real}
    # compute kernel matrix
    sumX = sum(X.^ 2, dims=1)
    L = exp.(-( transpose(sumX) .+ sumX .- 2*transpose(X) * X ) ./ convert(T, ɛ))
    # L = pairwise((x,y)-> exp(-norm(x-y,2)^2/ε), Xtr)

    # Calculate Laplacian & normalize it
    if α > 0
        D = transpose(sum(L, dims=1))
        L ./= (D * transpose(D)) .^ convert(T, α)
    end
    D = Diagonal(vec(sum(L, dims=1)))
    M = inv(D)*L

    # D = Diagonal(vec(sum(L, dims=1)))
    # D⁻ᵅ = inv(D^α)
    # Lᵅ = D⁻ᵅ*L*D⁻ᵅ
    # Dᵅ = Diagonal(vec(sum(Lᵅ, dims=1)))
    # M = inv(Dᵅ)*Lᵅ

    # Eigendecomposition & reduction
    F = eigen(M, permute=false, scale=false)
    λ = real.(F.values)
    idx = sortperm(λ, rev=true)[2:maxoutdim+1]
    λ = λ[idx]
    V = real.(F.vectors[:,idx])
    Y = (λ.^t) .* V'

    return DiffMap{T}(t, α, ɛ, λ, L, Y)
end

"""
    transform(R::DiffMap)

Transforms the data fitted to the diffusion map model `R` into a reduced space representation.
"""
transform(R::DiffMap) = R.proj


abstract HopfieldNet

function energy(net::HopfieldNet)
    e = 0.0
    n = length(net.s)
    for i in 1:n
        for j in 1:n
            e += net.W[i, j] * net.s[i] * net.s[j]
        end
    end
    e *= -0.5
    for i in 1:n
        e += net.W[i] * net.s[i]
    end
    return e
end

function settle!(net::HopfieldNet,
                 iterations::Integer = 1_000,
                 trace::Bool = false)
    for i in 1:iterations
        update!(net)
        if trace
            @printf "%5.0d: %.4f\n" i energy(net)
        end
    end
    return
end

function associate!{T <: Real}(net::HopfieldNet,
                               pattern::Vector{T};
                               iterations::Integer = 1_000,
                               trace::Bool = false)
    copy!(net.s, pattern)
    settle!(net, iterations, trace)
    # TODO: Decide if this should really be a copy
    return copy(net.s)
end

# Hebbian learning steps w/ columns as patterns
function train!{T <: Real}(net::HopfieldNet, patterns::Matrix{T})
    n = length(net.s)
    p = size(patterns, 2)
    # Could use outer products here
    # (1 / p) * (patterns[:, mu] * patterns[:, mu]')
    for i in 1:n
        for j in (i + 1):n
            s = 0.0
            for mu in 1:p
                s += patterns[i, mu] * patterns[j, mu]
            end
            s = s / p # May need to be careful here
            net.W[i, j] += s
            net.W[j, i] += s
        end
    end
    return
end

function h{T <: Real}(i::Integer, j::Integer, mu::Integer, n::Integer,
                      W::Matrix{Float64}, patterns::Matrix{T})
    res = 0.0
    for k in 1:n
        if k != i && k != j
            res += W[i, k] * patterns[k, mu]
        end
    end
    return res
end

# Storkey learning steps w/ columns as patterns
function storkeytrain!{T <: Real}(net::HopfieldNet, patterns::Matrix{T})
    n = length(net.s)
    p = size(patterns, 2)
    for i in 1:n
        for j in (i + 1):n
            for mu in 1:p
                s = patterns[i, mu] * patterns[j, mu]
                s -= patterns[i, mu] * h(j, i, mu, n, net.W, patterns)
                s -= h(i, j, mu, n, net.W, patterns) * patterns[j, mu]
                s *= 1 / n
                net.W[i, j] += s
                net.W[j, i] += s
            end
        end
    end
    return
end


using IDRsSolver

println("FDM discretisation of a 3D convection-diffusion-reaction problem on a unit cube")
println("+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++")

# Define system

# Defaults:
h = 0.025
eps1 = 1.
beta = [0/sqrt(5), 250/sqrt(5), 500/sqrt(5)]
r = 400

# Generate matrix
m = round(Int, 1/h)-1
n = m*m*m
Sx = sparse(Tridiagonal((-eps1/h^2-beta[1]/(2*h))*ones(m-1),2*eps1/h^2*ones(m),(-eps1/h^2+beta[1]/(2*h))*ones(m-1)))
Sy = sparse(Tridiagonal((-eps1/h^2-beta[2]/(2*h))*ones(m-1),2*eps1/h^2*ones(m),-eps1/h^2+beta[2]/(2*h)*ones(m-1)))
Sz = sparse(Tridiagonal((-eps1/h^2-beta[3]/(2*h))*ones(m-1),2*eps1/h^2*ones(m),-eps1/h^2+beta[3]/(2*h)*ones(m-1)))
Is = speye(m)
A = kron(kron(Is,Is),Sx) + kron(kron(Is,Sy),Is)+ kron(kron(Sz,Is),Is) -r*speye(n)

x = linspace(h,1-h,m)
sol = kron(kron(x.*(1-x),x.*(1-x)),x.*(1-x))
b = A*sol

println(" ");
println("The parameters of the problem are :")
println("Gridsize h = ", (h),";")
println("Number of equations = ", (n),";")
println("Diffusion parameter = ", (eps1),";")
println("Convection parameters = (",(beta[1]),",",(beta[2]),",",(beta[3]),");")
println("Reaction parameter = ",(r)," (Note: positive reaction parameter gives negative shift to matrix);")
println(" ")

# Defaults for the iterative solvers:

tol = 1e-8
maxiter = 1000

s = 4
println("IDR(4) iteration...")
time = @elapsed x, _ = idrs( A, b; s=s, tol=tol, maxiter=maxiter)

println("Final accuracy: ", norm(b-A*x)/norm(b))
println("CPU time: ", time, "s.")


"""
# Usage

    solve(FDDE::FDDEProblem, h, DelayPECE())

Using the delayed predictor-corrector method to solve the delayed fractional differential equation problem.

### References

```tex
@article{Wang2013ANM,
  title={A Numerical Method for Delayed Fractional-Order Differential Equations},
  author={Zhen Wang},
  journal={J. Appl. Math.},
  year={2013},
  volume={2013},
  pages={256071:1-256071:7}
}
```
"""
struct DelayPECE <: FractionalDiffEqAlgorithm end

function solve(FDDE::FDDEProblem, h, ::DelayPECE)
    @unpack f, ϕ, α, τ, tspan = FDDE
    T = tspan
    t = collect(0:h:T)
    maxn = size(t, 1)
    yp = collect(0:h:T+h)
    y = copy(t)
    y[1] = ϕ(0)

    @fastmath @inbounds @simd for n in 1:maxn-1
        yp[n+1] = 0
        @fastmath @inbounds @simd for j = 1:n
            yp[n+1] = yp[n+1]+b(j-1, n-1, α, h)*f(t[j], y[j], v(ϕ, j, τ, h, y, yp))
        end
        yp[n+1] = yp[n+1]/gamma(α)+ϕ(0)

        y[n+1] = 0

        @fastmath @inbounds @simd for j=1:n
            y[n+1] = y[n+1]+a(j-1, n-1, α, h)*f(t[j], y[j], v(ϕ, j, τ, h, y, yp))
        end

        y[n+1] = y[n+1]/gamma(α)+h^α*f(t[n+1], yp[n+1], v(ϕ, n+1, τ, h, y, yp))/gamma(α+2)+ϕ(0)
    end

    delayedterm = copy(t)
    @fastmath @inbounds @simd for n = 1:maxn-1
        delayedterm[n] = v(ϕ, n, τ, h, y, yp)
    end
    return delayedterm, y
end

function a(j, n, α, h)
    if j == n+1
        result = 1
    elseif j == 0
        result = n^(α+1)-(n-α)*(n+1)^α
    else
        result = (n-j+2)^(α+1) + (n-j)^(α+1) - 2*(n-j+1)^(α+1)
    end
    return result*h^α / (α * (α + 1))
end

function b(j, n, α, h)
    return h^α/α*((n-j+1)^α - (n-j)^α)
end

function v(ϕ, n, τ, h, y, yp)
    if τ >= n*h
        return ϕ((n-1)*h-τ)
    else
        m = floor(Int, τ/h)
        δ = m-τ/h

        if m>1
            return δ*y[n-m+2] + (1-δ)*y[n-m+1]
        elseif m == 1
            return δ*yp[n+1] + (1-δ)*y[n]
        end
    end
end


@doc raw"""
    Plane3

An object `h` of the data type [`Plane3`](@ref) is an oriented plane in the
three-dimensional Euclidean space ``Ε^3``.

It is defined by the set of points with Cartesian coordinates ``(x,y,z)`` that
satisfy the plane equation

```math
    h:\; a\, x + b\, y + c\, z + d = 0.
```

The plane splits ``Ε^3`` in a *positive* and a *negative side*. A point `p` with
Cartesian coordinates ``(px, py, pz)`` is on the positive side of `h`, iff
``a\, px + b\, py + c\, pz + d > 0``. It is on the negative side, iff
``a\, px + b\, py + c\, pz + d < 0``.
"""
Plane3

@doc raw"""
    Plane3(a::Real, b::Real, c::Real, d::Real)

Create a plane `h` defined by the equation ``a\, px + b\, py + c\, pz + d = 0``.

Notice that `h` is degenerate if ``a = b = c = 0``.
"""
Plane3(a::Real, b::Real, c::Real, d::Real) =
    Plane3(convert(FT, a), convert(FT, b),
           convert(FT, c), convert(FT, d))

"""
    Plane3(p::Point3, q::Point3, r::Point3)

Create a plane `h` passing through the points `p`, `q`, and `r`.

The plane is oriented such that `p`, `q` and `r` are oriented in a positive
sense (that is counterclockwise) when seen form the positive side of `h`. Notice
that `h` is degenerate if the points are collinear.
"""
Plane3(p::Point3, q::Point3, r::Point3)

"""
    Plane3(p::Point3, v::Vector3)

Introduces a plane `h` that passes through point `p` and that is orthogonal to
`v`.
"""
Plane3(p::Point3, v::Vector3)

"""
    Plane3(p::Point3, d::Direction3)

Introduces a plane `h` that passes through point `p` and that has an orthogonal
direction equal to `d`.
"""
Plane3(p::Point3, d::Direction3)

"""
    Plane3(l::Line3, p::Point3)

Introduces a plane `h` that is defined through the three points `point(l, 0)`,
`point(l, 1)` and `p`.
"""
Plane3(l::Line3, p::Point3)

"""
    Plane3(r::Ray3, p::Point3)

Introduces a plane `h` that is defined through the three points `point(r, 0)`,
`point(r, 1)` and `p`.
"""
Plane3(r::Ray3, p::Point3)

"""
    Plane3(s::Segment3, p::Point3)

Introduces a plane `h` that is defined through the three points `source(s)`,
`target(s)` and `p`.
"""
Plane3(s::Segment3, p::Point3)

"""
    Plane3(c::Circle3)

Introduces a plane `h` that is defined as the plane containing the circle `c`.
"""
Plane3(c::Circle3)

"""
    ==(h₁::Plane3, h₂::Plane3)

Test for equality: two planes are equal, iff they have a non empty intersection
and the same orientation.
"""
==(h₁::Plane3, h₂::Plane3)

"""
    a(h::Plane3)

Returns the first coefficient of `h`.
"""
a(h::Plane3)

"""
    b(h::Plane3)

Returns the second coefficient of `h`.
"""
b(h::Plane3)

"""
    c(h::Plane3)

Returns the third coefficient of `h`.
"""
c(h::Plane3)

"""
    d(h::Plane3)

Returns the fourth coefficient of `h`.
"""
d(h::Plane3)

"""
    perpendicular_line(h::Plane3, p::Point3)

Returns the line that is perpendicular to `h` and that passes through point `p`.

The line is oriented from the negative to the positive side of `h`.
"""
perpendicular_line(h::Plane3, p::Point3)

"""
    projection(h::Plane3, p::Point3)

Returns the orthogonal projection of `p` on `h`.
"""
projection(h::Plane3, p::Point3)

"""
    opposite(h::Plane3)

Returns the plane with opposite orientation.
"""
opposite(h::Plane3)

"""
    point(h::Plane3)

Returns an arbitrary point on `h`.
"""
point(h::Plane3)

"""
    orthogonal_vector(h::Plane3)

Returns a vector that is orthogonal to `h` and that is directed to the positive
side of `h`.
"""
orthogonal_vector(h::Plane3)

"""
    orthogonal_direction(h::Plane3)

Returns a direction that is orthogonal to `h` and that is directed to the positive
side of `h`.
"""
orthogonal_direction(h::Plane3)

"""
    base1(h::Plane3)

Returns a vector orthogonal to [`orthogonal_vector()`](@ref).
"""
base1(h::Plane3)

"""
    base2(h::Plane3)

Returns a vector that is both orthogonal to [`base1()`](@ref), and to
[`orthogonal_vector()`](@ref), and such that the result of `orientation(
point(h), point(h) + base1(h), point(h) +
base2(h), point(h) + orthogonal_vector(h) )` is positive.
"""
base2(h::Plane3)

"""
    to_2d(h::Plane3, p::Point3)

Returns the image point of the projection of `p` under an affine transformation,
which maps `h` onto the ``xy``-plane, with the ``z``-coordinate removed.
"""
to_2d(h::Plane3, p::Point3)

"""
    to_3d(h::Plane3, p::Point2)

Returns a point `q`, such that `to_2d(h, to_3d(h, p))` is equal to `p`.
"""
to_3d(h::Plane3, p::Point2)

"""
    oriented_side(h::Plane3, p::Point3)

Returns either [`ON_ORIENTED_BOUNDARY`](@ref), or the constant
[`ON_POSITIVE_SIDE`](@ref), or the constant [`ON_NEGATIVE_SIDE`](@ref),
determined by the position of `p` relative to the oriented plane `h`.
"""
oriented_side(h::Plane3, p::Point3)

"""
    has_on(h::Plane3, p::Point3)

Returns `true`, iff `p` properly lies on `h`.
"""
has_on(h::Plane3, p::Point3)

"""
    has_on_positive_side(h::Plane3, p::Point3)

Returns `true`, iff `p` lies on the positive side of `h`.
"""
has_on_positive_side(h::Plane3, p::Point3)

"""
    has_on_negative_side(h::Plane3, p::Point3)

Returns `true`, iff `p` lies on the negative side of `h`.
"""
has_on_negative_side(h::Plane3, p::Point3)

"""
    has_on(h::Plane3, l::Line3)

Returns `true`, iff `l` properly lies on `h`.
"""
has_on(h::Plane3, l::Line3)

"""
    has_on(h::Plane3, c::Circle3)

Returns `true`, iff `c` properly lies on `h`.
"""
has_on(h::Plane3, c::Circle3)

"""
    is_degenerate(h::Plane3)

Plane `h` is degenerate, if the coefficients `a`, `b`, and `c` of the plane
equation are zero.
"""
is_degenerate(h::Plane3)

"""
    transform(h::Plane3, t::AffTransformation3)

Returns the plane obtained by applying `t` on a point of `h` and the
orthogonal direction of `h`.
"""
transform(h::Plane3, t::AffTransformation3)


function laplace_filter(x::Matrix{Tv}) where {Tv<:AbstractFloat}

    (n1, n2) = size(x)
    x1 = zeros(Tv, n1, n2)
    for i2 = 2 : n2-1
        for i1 = 2 : n1-1
            x1[i1,i2] = (8.0 * x[i1,i2] - x[i1-1,i2-1] - x[i1,i2-1] - x[i1+1,i2-1]
                                        - x[i1-1,i2  ]              - x[i1+1,i2  ]
                                        - x[i1-1,i2+1] - x[i1,i2+1] - x[i1+1,i2+1])
        end
    end
    return x1
end

# function laplace_filter(img::Matrix{Tv}, iflag::Int64) where {Tv<:AbstractFloat}
#     (m, n) = size(img)
#     # d = zeros(eltype(img), m, n)
#     # for ix = 2 : n-1
#     #     for iz = 2 : m-1
#     #         d[iz, ix] = 4*img[iz,ix]-img[iz-1,ix]-img[iz+1,ix]-img[iz,ix-1]-img[iz,ix+1]
#     #     end
#     # end
#     a = -1.0 * ones(m-1); a[end] = 0.0
#     b =    2 * ones(m  ); b[1]   = 1.0; b[end] = 1.0
#     c = -1.0 * ones(m-1); c[1]   = 0.0;
#     V = spdiagm((a,b,c), (-1,0,1), m, m)
#
#     a = -1.0 * ones(n-1); a[end] = 0.0
#     b =    2 * ones(n  ); b[1]   = 1.0; b[end] = 1.0
#     c = -1.0 * ones(n-1); c[1]   = 0.0;
#     H = spdiagm((a,b,c), (-1,0,1), n, n)
#
#     if iflag == 1
#        r = V * img + img * H'
#     elseif iflag == 2
#        r = V'* img + img * H
#     end
#
#     return r
# end


#=
These functions are taken from http://pordlabs.ucsd.edu/matlab/stream.htm
Kirill K. Pankratov, March 7, 1994.
=#

function cumsimp(vec::Vector{T}) where T
    c1 = 3//8
    c2 = 6//8
    c3 = -1//8

    # Interpolate values of Y to all midpoints
    f = fill(zero(T),length(vec))
    for i in 2:length(f)-1
        f[i] = c1*vec[i-1]+c2*vec[i]+c3*vec[i+1]
    end
    for i in 3:length(f)
        f[i] += c3*vec[i-2]+c2*vec[i-1]+c1*vec[i]
    end
    f[2] *= 2
    f[end] *= 2
    # Now Simpson (1,4,1) rule
    for i in 2:length(f)
        f[i] = 2f[i]+vec[i-1]+vec[i]
    end
    f[1:end] .= cumsum(f)/6  # Cumulative sum, 6 - denom. from the Simpson rule
end

function cumsimp(U::AbstractMatrix{T}) where T
    ret = similar(U)
    for i in 1:size(U)[2]
        ret[:,i] .= cumsimp(U[:,i])
    end
    ret
end

function flowfun(u,v)
    lx = size(u,2)  # Size of the velocity matrices
    ly = size(u,1)
    # Integrate velocity fields to get potential and streamfunction
    # Use Simpson rule summation
    cx = cumsimp(v[1,:])  # Compute x-integration constant
    cy = cumsimp(u[:,1])  # Compute y-integration constant
    ψ = -cumsimp(u)+repeat(cx',ly,1)
    ψ = (ψ+cumsimp(v')'-repeat(cy,1,lx))/2
end

# function cumsimp_naive(vec::Vector{T})::Vector{T} where T
#     c1 = 3//8
#     c2 = 6//8
#     c3 = -1//8
#
#     lv = length(vec)
#     num = 1:lv-2
#     vec_itp = fill(zero(T),length(vec))
#     # Interpolate values of Y to all midpoints
#     @. vec_itp[num+1] = c1*vec[num]+c2*vec[num+1]+c3*vec[num+2]
#     @. vec_itp[num+2] = vec_itp[num+2]+c3*vec[num]+c2*vec[num+1]+c1*vec[num+2]
#     vec_itp[2] *= 2
#     vec_itp[lv] *= 2
#     # Now Simpson (1,4,1) rule
#     @. vec_itp[num+1] = 2vec_itp[num+1]+vec[num]+vec[num+1]
#     vec_itp[:] .= cumsum(vec_itp)/6
# end


function companion(p::Pol{T}) where {T<:Number}
    p /= lc(p)
    C = zeros(T, deg(p), deg(p))
    C[2:end, 1:end-1] .= Matrix(I, deg(p)-1, deg(p)-1)
    C[:, end] = -p.coeffs[1:end-1]
    return C
end

function roots(p::Pol{T}) where {T<:Number}
    C = companion(p)
    return eigvals(C)
end


# Parts of this code were taken / derived from NetworkX. See LICENSE for
# licensing details.

"""Calculates the [PageRank](https://en.wikipedia.org/wiki/PageRank) of the graph
`g`. Can optionally specify a different damping factor (`α`), number of
iterations (`n`), and convergence threshold (`ϵ`). If convergence is not
reached within `n` iterations, an error will be returned.
"""
function pagerank(g::DiGraph, α=0.85, n=100, ϵ = 1.0e-6)
    A = adjacency_matrix(g,:out,Float64)
    S = vec(sum(A,1))
    S = 1./S
    S[find(S .== Inf)]=0.0
    M = A' # need a separate line due to bug #17456 in julia
    M = (Diagonal(S) * M)'
    N = nv(g)
    x = repmat([1.0/N], N)
    p = repmat([1.0/N], N)
    dangling_weights = p
    is_dangling = find(S .== 0)

    for _ in 1:n
        xlast = x
        x = α * (M * x + sum(x[is_dangling]) * dangling_weights) + (1 - α) * p
        err = sum(abs(x - xlast))
        if (err < N * ϵ)
            return x
        end
    end
    error("Pagerank did not converge after $n iterations.")
end


### A Pluto.jl notebook ###
# v0.11.14

using Markdown
using InteractiveUtils

# ╔═╡ 2cf61e2a-ff3f-11ea-2d5b-77a2b52fd6e7
using CSV, DataFrames

# ╔═╡ 78701408-ff37-11ea-04a8-07805ef3cc1c
md"""
## Outline

In this project, we will implement a sowing plan for a mixed culture of three types of crops. At our disposal, we have fifteen plots of land, to be planted using a limited supply of seeds. The different lots have varying soil compositions, influencing plant growth. Furthermore, some combinations of the crops grow well together; other combinations show competition. These aspects make for a challenging optimization problem!
"""

# ╔═╡ bea5f54e-ff37-11ea-2c37-8707c75a9b5c
n = 15

# ╔═╡ 9b5ec9c8-ff37-11ea-1816-0365aa76c976
md"""
## A model for plant growth

We have $n=15$ plots of land. The three types of plants are denoted with x, y, and z, respectively. Indices are used to indicate the amount of seed distributed to a particular plot, e.g., $x_i$ is the amount of seed assigned to the $i$-th plot. So this optimization problem can be solved in a $15\times 3=45$-dimensional space.

Small-capped variables indicate the amount of seed assigned to a field. Large-cap variables represent the corresponding yield of that field, i.e., $X_i$ is the yield (in kg) for field $i$. The yield for each type of seed can be computed using the following equations:

$$X_i = \frac{A_i^x x_i^2}{120 + x_i^2 + 2y_i - 0.8x_iz_i + z_i^2}\,,$$

$$Y_i = \frac{A_i^y y_i^2}{30 + 4x_i + y_i^2 + 7z_i}\,,$$

$$Z_i = \frac{A_i^z z_i^2}{80 + 0.4x_i^2 + 0.2x_iz_i +0.6y_i + z_i^2}\,.$$
"""

# ╔═╡ d327e0c2-ff37-11ea-3006-53be838b7dc8
md"""
Here, $A_i^x$, $A_i^y$, $A_i^z$ represent the maximum in yields in field $i$ for the different seed types. It depends on the amount of nitrogen $u_i$ and the water status $v_i$ of the soil. The following equation can be used to compute these coefficients:

$$\begin{bmatrix}
A_i^x \\
A_i^y\\
A_i^z
\end{bmatrix}
=
\begin{bmatrix}
4 & 2  \\
1 & 0.3 \\
-0.5 & 4
\end{bmatrix}
\begin{bmatrix}
u_i \\
v_j\\
\end{bmatrix}
+
\begin{bmatrix}
100 \\
300 \\
210
\end{bmatrix}\,.$$
"""

# ╔═╡ c9897d6a-ff47-11ea-1212-9d5eb14cd0d7
S, b = [4 2; 1 0.3; -0.5 4], [100, 300, 210]  # soil effects

# ╔═╡ e4a29d80-ff37-11ea-0d0b-59545bce9bd2
md"""
The growth model has several interesting facets:
- sowing more seed results in a larger yield, however, the effect saturates, increasing quantities have diminishing returns;
- the plants show competition;
- z positively influences x, while x negatively influences z (z produces nitrogen while x requires much nitrogen);
- y is a good producer but very sensitive to adverse conditions.
"""

# ╔═╡ ea977544-ff37-11ea-310b-79307ef120d3
md"Below are the concentrations of nitrogen ($u$) and water status ($v$) for every field."

# ╔═╡ eba6a9dc-ff37-11ea-233d-1b9aca686ea4
u = [36.6776, 36.9967, 83.033, 50.3725, 43.4616, 55.5842, 44.8919, 99.6519, 20.158, 102.325, 96.8896, 33.7957, 26.6129, 38.7194, 60.1461]

# ╔═╡ 0e9a6d52-ff38-11ea-2472-810a521cb8f8
v = [34.5773,  24.3003,  24.3952,  28.462,  37.2912,  38.196,  36.4821,  30.1988,  20.9124,  35.207,  38.0924,  24.438,  28.3169,  20.3022,  24.8884]

# ╔═╡ 100a63f4-ff38-11ea-1fbf-05c136011e28
md"The total amount of seed for every type is fixed by $c_x$, $c_y$, $c_z$:"

# ╔═╡ 17088b36-ff38-11ea-3519-85055dd00929
cx, cy, cz = 250, 175, 325

# ╔═╡ 25b72854-ff38-11ea-0817-0f44e6420882
md"Finally, the yield for each crop can be sold at different prices (EUR/kg) $w_x$, $w_y$, $w_z$:"

# ╔═╡ 223cec90-ff38-11ea-040f-47c9ee1c9036
wx, wy, wz = 0.7, 0.85, 0.6

# ╔═╡ 3282d3c6-ff38-11ea-0458-af74eb6b42dc
md"""
So the objective is

$$f(\mathbf{x}, \mathbf{y}, \mathbf{z}) = \sum_{i=1}^{15}w_xX_i(x_i,y_i,z_i) + w_yY_i(x_i,y_i,z_i)+w_zZ_i(x_i,y_i,z_i)\,,$$

but the constraints are

$$\sum_{i=1}^{15} x_i \le c_x \quad\text{and}\quad x_i\geq 0\text{ for }i=1,\ldots,15\,,$$

$$\sum_{i=1}^{15} y_i \le c_y \quad\text{and}\quad y_i\geq 0\text{ for }i=1,\ldots,15\,,$$

$$\sum_{i=1}^{15} z_i \le c_z \quad\text{and}\quad z_i\geq 0\text{ for }i=1,\ldots,15\,.$$

Note: the sum constraints are inequalities because you don't need to use all the seed. However, the optimal solution will likely use all the available seed.
"""

# ╔═╡ 625ffadc-ff48-11ea-1849-39df2c3ddb85
md"""
## An example

Let us compute the yield for plot 1 when we sow $x_1=10,y_1=6, z_1=8$. First, we compute the maximum yields, then the resulting concrete yields and, finally the value.
"""

# ╔═╡ 57dc25e6-ff38-11ea-342c-f19e90a2c732
x1, y1, z1 = 10, 6, 8  # amount of seed per plot

# ╔═╡ 6137fea8-ff38-11ea-34b7-631de98c3ad4
(Ax1, Ay1, Az1) = S * [u[1], v[1]] .+ b

# ╔═╡ 7e33d806-ff38-11ea-2297-512188693a6a
X1 = (Ax1 * x1^2) / (120 + x1^2 + 2y1 - 0.8x1 * z1 + z1^2)

# ╔═╡ 84e7171e-ff38-11ea-07fd-5fb12ff4f039
Y1 = (Ay1 * y1^2) / (30 + 4x1 + y1^2 + 7z1)

# ╔═╡ 85d3cd6e-ff38-11ea-2d7d-539601eec8aa
Z1 = (Az1 * z1^2) / (80 + 0.4x1 + 0.2x1*z1 +0.6y1 + z1^2)

# ╔═╡ 8a574622-ff38-11ea-15dc-1b4bb63497df
revenue_plot1 = wx * X1 + wy * Y1 + wz * Z1  # in EUR

# ╔═╡ 92a481f4-ff39-11ea-1cc0-855d93b3967b
md"""
# Assignments

1. Give the formal optimization problem. Also provide the Lagrangian formulation.
2. Is the optimization problem of the optimal sowing plan concave? (since it is a maximization problem, I mean is minimizing the negative total revenue of a plan convex). You don't have to prove this formally, but you can make visual arguments. For a given field, make a the contour plots for (x,y), (x,z) and (y,z), always setting the third variable to 0.
3. Give a good/optimal sowing plan. You may solve this either using custom code or using a Julia package [Optim.jl](https://github.com/JuliaNLSolvers/Optim.jl), [Convex.jl](https://github.com/jump-dev/Convex.jl), or [JuMP.jl](https://github.com/jump-dev/JuMP.jl). Make plots of your solution and discuss it.
4. Given an optimal sowing plan found in the previous assignment, what is the price per kg for every seed you would be willing to buy, as to be break-even? How would you use this additional marginal quantity of seed. (HINT: you can obtain this from the Lagrangian).
5. Provide three alternative solutions, each in which you only use one of the types of seed, e.g. $\max_\mathbf{x}f(\mathbf{x}, 0, 0)$. Show and discuss the difference.
"""

# ╔═╡ ea2c0d3e-ff43-11ea-14f4-932602b2fecd
md"""
## Submission

Hand in the solved Jupyter notebook by **14 November 2020**. Send your notebook to [me](michiel.stock@ugent.be) both as Pluto notebook file (.jl) and as **PDF or HTML file**. Hand in a CSV file with your best **valid** solution for question 3. In this file, every row is a plot of land and the three columns represent x, y, z, respectively.
"""

# ╔═╡ 64049c58-ff37-11ea-1ae8-f907d5e33509
student_name = missing  # fill in your name(s)

# ╔═╡ 2568b67a-ff37-11ea-37fd-87a3d569fc31
md"""
# Project: Sowing plan

**STMO**

2020-2021

Project by: $student_name
"""

# ╔═╡ ddab3252-07bc-11eb-390a-750512ee8c90
if ismissing(student_name)
	md"fill in your name(s) below"
end

# ╔═╡ 562bc43a-ff43-11ea-0451-d9e7005c56a1
md"""
### 1. Formal description of the problem

COMPLETE
"""

# ╔═╡ 5232b83c-ff43-11ea-36e4-7d08693203dc
md"""
### 2. Concavity

COMPLETE
"""

# ╔═╡ 8cb9cf68-ff43-11ea-399d-df193400c077
md"""
### 3. Making a sowing plan

Here, you have the space to solve the problem. Save your final solution in the variable `solution` and save it in a csv file using `save_solution`. This is done automatically when your name is filled in.
"""

# ╔═╡ 9d2eabae-ff43-11ea-3103-0f22b5c59e6b

# ╔═╡ 9cd521d8-ff43-11ea-174e-71556bcdebac

# ╔═╡ 67882c04-ff3f-11ea-19b5-890bd3bd15a9
solution = fill(1.0, 15, 3)  # valid, but can be improved...

# ╔═╡ 0a220a2a-ff45-11ea-2b35-c70cdb18c0a8
md"Check if the solution is valid."

# ╔═╡ 1411424e-ff45-11ea-2b3e-c75c7d39917a
md"Save the solution."

# ╔═╡ 07575a02-ff45-11ea-2794-63681f41a7bf
md"""
### 4. Price of seed

Compute how much you would pay for each additional unit of seed in your solution.
"""

# ╔═╡ 79df7272-ff44-11ea-01bb-716d0bf52464

# ╔═╡ 79a661b0-ff44-11ea-10ef-fbc61315162b
md"""
### 5. One type of seed

Make three solutions in which you only use one type of seed. Analyse your solutions!
"""

# ╔═╡ 79897d9a-ff44-11ea-269c-bba522b74d66

# ╔═╡ 796e75c2-ff44-11ea-2a85-0b3fbdc675b5

# ╔═╡ 0ba3f5f6-ff46-11ea-0449-29b505430cf9

# ╔═╡ 7ade6b7e-ff44-11ea-130a-bdd36b48b8af
md"## Functions"

# ╔═╡ b96c0828-ff3b-11ea-3ee3-b979608fe705
"""Check if a solution is valid."""
function isvalidsolution(solution::Matrix)
	return size(solution) == (15, 3) && all(solution .≥ 0.0) && all(sum(solution, dims=1) .≤ [cx cy cz])
end

# ╔═╡ 6f98672e-ff3f-11ea-0198-5fbef05027da
isvalidsolution(solution)

# ╔═╡ a9ca6492-ff44-11ea-3b51-3905bf9280cf
"""Save the solution to a file for submission."""
function save_solution(fname, solution::Matrix)
	@assert isvalidsolution(solution) "Oh no, your solution is invalid!"
	CSV.write(fname, DataFrame(solution))
end

# ╔═╡ 928fd032-ff3f-11ea-13a0-07140325ab9f
!ismissing(student_name) && save_solution("solution_$(student_name).csv", solution);

# ╔═╡ Cell order:
# ╟─2568b67a-ff37-11ea-37fd-87a3d569fc31
# ╟─ddab3252-07bc-11eb-390a-750512ee8c90
# ╟─78701408-ff37-11ea-04a8-07805ef3cc1c
# ╠═bea5f54e-ff37-11ea-2c37-8707c75a9b5c
# ╟─9b5ec9c8-ff37-11ea-1816-0365aa76c976
# ╟─d327e0c2-ff37-11ea-3006-53be838b7dc8
# ╠═c9897d6a-ff47-11ea-1212-9d5eb14cd0d7
# ╟─e4a29d80-ff37-11ea-0d0b-59545bce9bd2
# ╟─ea977544-ff37-11ea-310b-79307ef120d3
# ╠═eba6a9dc-ff37-11ea-233d-1b9aca686ea4
# ╠═0e9a6d52-ff38-11ea-2472-810a521cb8f8
# ╟─100a63f4-ff38-11ea-1fbf-05c136011e28
# ╠═17088b36-ff38-11ea-3519-85055dd00929
# ╟─25b72854-ff38-11ea-0817-0f44e6420882
# ╠═223cec90-ff38-11ea-040f-47c9ee1c9036
# ╟─3282d3c6-ff38-11ea-0458-af74eb6b42dc
# ╟─625ffadc-ff48-11ea-1849-39df2c3ddb85
# ╠═57dc25e6-ff38-11ea-342c-f19e90a2c732
# ╠═6137fea8-ff38-11ea-34b7-631de98c3ad4
# ╠═7e33d806-ff38-11ea-2297-512188693a6a
# ╠═84e7171e-ff38-11ea-07fd-5fb12ff4f039
# ╠═85d3cd6e-ff38-11ea-2d7d-539601eec8aa
# ╠═8a574622-ff38-11ea-15dc-1b4bb63497df
# ╟─92a481f4-ff39-11ea-1cc0-855d93b3967b
# ╟─ea2c0d3e-ff43-11ea-14f4-932602b2fecd
# ╠═64049c58-ff37-11ea-1ae8-f907d5e33509
# ╠═562bc43a-ff43-11ea-0451-d9e7005c56a1
# ╠═5232b83c-ff43-11ea-36e4-7d08693203dc
# ╟─8cb9cf68-ff43-11ea-399d-df193400c077
# ╠═9d2eabae-ff43-11ea-3103-0f22b5c59e6b
# ╠═9cd521d8-ff43-11ea-174e-71556bcdebac
# ╠═67882c04-ff3f-11ea-19b5-890bd3bd15a9
# ╟─0a220a2a-ff45-11ea-2b35-c70cdb18c0a8
# ╠═6f98672e-ff3f-11ea-0198-5fbef05027da
# ╟─1411424e-ff45-11ea-2b3e-c75c7d39917a
# ╠═928fd032-ff3f-11ea-13a0-07140325ab9f
# ╟─07575a02-ff45-11ea-2794-63681f41a7bf
# ╠═79df7272-ff44-11ea-01bb-716d0bf52464
# ╟─79a661b0-ff44-11ea-10ef-fbc61315162b
# ╠═79897d9a-ff44-11ea-269c-bba522b74d66
# ╠═796e75c2-ff44-11ea-2a85-0b3fbdc675b5
# ╠═0ba3f5f6-ff46-11ea-0449-29b505430cf9
# ╟─7ade6b7e-ff44-11ea-130a-bdd36b48b8af
# ╠═2cf61e2a-ff3f-11ea-2d5b-77a2b52fd6e7
# ╠═b96c0828-ff3b-11ea-3ee3-b979608fe705
# ╠═a9ca6492-ff44-11ea-3b51-3905bf9280cf


# this shows effects of het. on the OLS estimator
using Plots, Distributions, Econometrics, LinearAlgebra, Statistics
function main()
het = true  # set this to true or false
n = 50 # sample size

# the function we simulate
function wrapper(het, n)
  x = randn(n)
  e = randn(n)
  if het
    e = x .* e # note the heteroscedasticity here
  end
  y = x + e # true coefs are zero for const, and 1 for slope
  x = [ones(n) x]
  b, varb, junk, junk, junk = ols(y, x, vc="standard",silent=true)
  H0 = [0; 1] # coefficient values under true null hypothesis
  t = (b - H0) ./ sqrt.(diag(varb))
end

# simulation loop
reps = 1000
ts = zeros(reps,2)
for rep = 1:reps
    ts[rep,:] = wrapper(het, n)
end
ts = abs.(ts)
crit_val = quantile(TDist(n-2),0.95) # 5# of prob. is to the R of this value
test = ts .> crit_val # now it"s a 10# signif. level test, because of the abs value
if het
  println("data is heteroscedastic")
else
  println("data is homoscedastic")
end
println("rejection frequency of nominal 10 percent test")
println("intercept: ", sum(test[:,1]/reps))
println("slope: ", sum(test[:,2]/reps))

plot([0.0; 1.0],sum(test,dims=1)'/1000, linetype=:bar, label="0 is intercept, 1 is slope")
#savefig("EffectsOLS.svg")
gui()
return
end
main()


using Combinatorics

# Checks if val is sum of n values in arr
function is_sum(val, arr; n=2)
    s = with_replacement_combinations(arr, n)
    sums = []
    for i in s
        push!(sums, sum(i))
    end

    if val in sums
        return true
    else
        return false
    end
end

# Check all vals in a window of size preamble
function check_vals(file; preamble = 25)
    i = 1
    while i <= (length(file) - preamble)
        j = i + preamble - 1
        arr = file[i:j]
        val = file[j+1]

        if is_sum(val, arr)
            i += 1
        else
            return val
        end
    end
end

# Function to find the weakness
# Defined as the sum of the min and max
# values of a continuous section of code
# that sum to the invalid value
function find_weakness(file, val)
    i = 1
    di = 1

    # Do forever!
    while true
        j = i + di
        arr = file[i:j]
        s = sum(arr)

        if s == val
            return minimum(arr) + maximum(arr)
        elseif s > val
            i += 1
            di = 1
        else
            di += 1
        end
    end

end

# Test values
test_vals = [35, 20, 15, 25, 47, 40, 62, 55, 65, 95, 102, 117, 150, 182, 127, 219, 299, 277, 309, 576,]

data =  parse.(Int, readlines("./XMAS_data.txt"))
println("Test: ", check_vals(test_vals, preamble=5))
inval = check_vals(data)
println("Real: ", inval)
println("Encryption weakness: ", find_weakness(data, inval))


@doc raw"""
    Oblique{N,M} <: AbstractPowerManifold

The oblique manifold $\mathcal{OB}(n,m)$ is the set of real-valued matrices with unit norm
column endowed with the metric from the embedding. This yields exactly the same metric as
considering the product metric of the unit norm vectors, i.e. [`PowerManifold`](@ref) of the
$(n-1)$-dimensional [`Sphere`](@ref).

The [`Sphere`](@ref) is stored internally within `M.manifold`, such that all functions of
[`AbstractPowerManifold`](@ref) can be used directly.

# Constructor

    Oblique(n,m)

Generate the manifold of matrices $\mathbb R^{n × m}$ such that the $m$ columns are unit
vectors, i.e. from the [`Sphere`](@ref)`(n-1)`.
"""
struct Oblique{N,M} <: AbstractPowerManifold{Sphere{N},ArrayPowerRepresentation}
    manifold::Sphere{N}
end

Oblique(n::Int, m::Int) = Oblique{n - 1,m}(Sphere(n - 1))

^(M::Sphere{N}, m::Int) where {N} = Oblique{N,m}(M)

@doc raw"""
    check_manifold_point(M::Oblique{n,m},p)

Checks whether `p` is a valid point on the [`Oblique`](@ref)`{m,n}` `M`, i.e. is a matrix
of `m` unit columns from $\mathbb R^{n+1}$, i.e. each column is a point from
[`Sphere`](@ref)`(n)`.
"""
check_manifold_point(::Oblique, ::Any)
function check_manifold_point(M::Oblique{Ns,Ms}, p; kwargs...) where {Ns,Ms}
    if size(p) != (Ns + 1, Ms)
        return DomainError(
            length(p),
            "The matrix in `p` ($(size(p))) does not match the dimension of Oblique $((Ns,Ms)).",
        )
    end
    return check_manifold_point(PowerManifold(M.manifold, Ms), p; kwargs...)
end
@doc raw"""
    check_tangent_vector(M::Oblique p, X; check_base_point = true, kwargs...)

Checks whether `X` is a valid tangent vector to `p` on the [`Oblique`](@ref) `M`.
This means, that `p` is valid, that `X` is of correct dimension and columnswise
a tangent vector to the columns of `p` on the [`Sphere`](@ref).
The optional parameter `check_base_point` indicates, whether to call [`check_manifold_point`](@ref)  for `p`.
"""
function check_tangent_vector(
    M::Oblique{Ns,Ms},
    p,
    X;
    check_base_point = true,
    kwargs...,
) where {Ns,Ms}
    if check_base_point && size(p) != (Ns + 1, Ms)
        return DomainError(
            length(p),
            "The matrix `p` ($(size(p))) does not match the dimension of Oblique $((Ns,Ms)).",
        )
    end
    if size(X) != (Ns + 1, Ms)
        return DomainError(
            length(X),
            "The matrix `X` ($(size(X))) does not match the dimension of Oblique $((Ns,Ms)).",
        )
    end
    return check_tangent_vector(
        PowerManifold(M.manifold, Ms),
        p,
        X;
        check_base_point = check_base_point,
        kwargs...,
    )
end

get_iterator(M::Oblique{Ns,Ms}) where {Ns,Ms} = 1:Ms

@generated manifold_dimension(::Oblique{N,M}) where {N,M} = (N) * M

@generated representation_size(::Oblique{N,M}) where {N,M} = (N + 1, M)

show(io::IO, ::Oblique{N,M}) where {N,M} = print(io, "Oblique($(N+1),$(M))")


using EngEconomics, Roots, Plots, ColorSchemes
plotly()

uwEngColorScheme = ColorScheme([parse(Colorant, "#D0B4E7"), parse(Colorant, "#57058B"), parse(Colorant, "#8100B4"), parse(Colorant, "#000000")])

# Given
n = 5
timeDelay = 3
A = 10
G = 1
MARR = 0.15
sensitivity = [0.85, 0.925, 1.0, 1.075, 1.15]

# Find
# Construct a sensitivity graph for the interest rate:
NPW_interest = (A .+ G * sinkingFundPaymentArithmetic.(MARR * sensitivity, n + 1)) .* uniformSeriesCompoundAmoundFactor.(MARR * sensitivity, (n + 1)) .* presentWorthFactor.(MARR * sensitivity, n + timeDelay)
plot(sensitivity, NPW_interest, label="Interest", legend=:outerbottomright,
	xlabel="Sensitivity (+/- 7.5%, 15%)",
	ylabel="Net Present Worth (\$1000)",
	title="Sensitivity of Interest Rate, Base Savings,<br> and Saving Gradient",
	palette=cgrad(uwEngColorScheme))

# Construct a sensitivity graph for the base savings
NPW_baseSavings = (A * sensitivity .+ G * sinkingFundPaymentArithmetic.(MARR, n + 1)) .* uniformSeriesCompoundAmoundFactor.(MARR, n + 1) .* presentWorthFactor.(MARR, n + timeDelay)
plot!(sensitivity, NPW_baseSavings, label="Base Savings")

# Construct a sensitivity graph for the savings gradient
NPW_gradientSavings = (A .+ G * sensitivity * sinkingFundPaymentArithmetic.(MARR, n + 1)) .* uniformSeriesCompoundAmoundFactor.(MARR, n + 1) .* presentWorthFactor.(MARR, n + timeDelay)
plot!(sensitivity, NPW_gradientSavings, label="Gradient Savings")


# Divergence calculation

"""
	divergence_ndgrid!(hx, hy, hz, vec, div)

Generic divergence `div` of vector `vec` with step lengths `hx`, `hy`, `hz`.
"""
function divergence_ndgrid!(hx, hy, hz, vec, div)

   siz = size(vec)::NTuple{4,Int64}

   px = zeros(Float64, Base.front(siz))
   qy = zeros(Float64, Base.front(siz))
   rz = zeros(Float64, Base.front(siz))

   n = size(hx, 1)
   # Right now do nothing for the ghost cells maybe needed later!
   # Take central differences on interior points
   if n > 2
      compute_px(siz, px, n, hx, vec)
   end

   n = size(hy, 1)
   if n > 2
      compute_qy(siz, qy, n, hx, vec)
   end

   n = size(hz, 1)
   if n > 2
      compute_rz(siz, rz, n, hx, vec)
   end

   @. div = px + qy + rz
   return
end

function divergence_ndgrid(hx, hy, hz, vec)

   siz = size(vec)

   div = zeros(Float64, Base.front(siz))

   divergence_ndgrid!(hx, hy, hz, vec, div)

   return div
end

function compute_px(siz, px, n, hx, vec)
   @inbounds for i = 1:n-2, j = 1:siz[2], k = 1:siz[3]
      px[i+1,j,k] = (vec[i+2,j,k,1] - vec[i,j,k,1])/(hx[i+2] - hx[i])
   end
end

function compute_qy(siz, qy, n, hy, vec)
   @inbounds for i = 1:siz[1], j = 1:n-2, k = 1:siz[3]
      qy[i,j+1,k] = (vec[i,j+2,k,2] - vec[i,j,k,2])/(hy[i+2] - hy[i])
   end
end

function compute_rz(siz, rz, n, hz, vec)
   @inbounds for i = 1:siz[1], j = 1:siz[2], k = 1:n-2
      rz[i,j,k+1] = (vec[i,j,k+2,3] - vec[i,j,k,3])/(hz[i+2] - hz[i])
   end
end

"""
   divergence!(param, vec, div)

Calculate the divergence of vectors specialized to my grid size.
Always assume starting with i -> j -> k for 1/2/3D!
Right now do nothing for the ghost cells. Maybe needed later!
Take central differences on interior points.
"""
@inline function divergence!(param, vec, div)

   x, y, z = param.x, param.y, param.z
   nI, nJ, nK, nG = param.nI, param.nJ, param.nK, param.nG
   iMin, iMax, jMin, jMax, kMin, kMax =
   param.iMin, param.iMax, param.jMin, param.jMax, param.kMin, param.kMax

   div .= 0.0

   @inbounds for k = kMin:kMax, j = jMin:jMax, i = iMin:iMax
      div[i-nG,j-nG,k-nG] = (vec[i+1,j,k,1] - vec[i-1,j,k,1]) / (x[i+1] - x[i-1])
   end

   if nJ > 1
      @inbounds for k = kMin:kMax, j = jMin:jMax, i = iMin:iMax
         div[i-nG,j-nG,k-nG] += (vec[i,j+1,k,2] - vec[i,j-1,k,2]) / (y[i+1] - y[i-1])
      end
   end

   if nK > 1
      @inbounds for k = kMin:kMax, j = jMin:jMax, i = iMin:iMax
         div[i-nG,j-nG,k-nG] += (vec[i,j,k+1,3] - vec[i,j,k-1,3]) / (z[i+1] - z[i-1])
      end
   end

   return
end


"""
    tensordecomp(k::Int, d::Int)

Decomposition of the integers  0:(k^p - 1) in powers of k.

"""
function tensordecomp(k::Int, d::Int)

    sequences = zeros(Int, k^d, d)

    for n = 0:k^d-1
        i = d
        m = n

        while i > 0
            i = i - 1
            j = i
            f = floor(Int, m / k^i)

            while j > 0 && f == 0
                j = j - 1
                f = floor(Integer, m / k^j)
            end

            if f > 0
                sequences[n + 1, j + 1, ] = f
                i = j
            elseif f == 0
                sequences[n + 1, 1] = m
                i = 0
            end
            m = m - f * k^i
        end
    end

    return sequences
end

"""
    even_sampling_rules(dim::Int, split_factor::Int) -> Array{Float64, 2}

Generate rules for evenly distributed points within a simplex. To do this,
we perform a shape-preserving splitting of the simplex, given a splitting
factor.

Returns the convex expansion coefficients of the points of the resulting
subsimplices in terms of the vertices of the original simplex.
"""
function even_sampling_rules(dim::Int, split_factor::Int)

    sequences::Array{Int, 2} = tensordecomp(split_factor, dim)
    n_seq = size(sequences, 1)

    χ1 = sequences .* (dim + 1)
    χ2 = repeat(transpose(collect(1:dim)), n_seq, 1)
    χ::Array{Int, 2} = χ1 .+ χ2
    χ = sort(χ, dims=2)

    # Define multiplicity matrix M
    M = zeros(Float64, size(χ, 1), size(χ, 2) + 1)
    M[:, 1] = χ[:, 1]
    M[:, 2:(end - 1)] = χ[:, 2:end] - χ[:, 1:(end - 1)]
    M[:, end] = (dim+1)*split_factor * ones(size(χ, 1)) - χ[:, end]

    M = M ./ (split_factor * (dim + 1))

    return copy(transpose(M))
end

export even_sampling_rules

"""
Evenly sample points within a simplex by performing a shape-preserving
subdivision of the simplex with a given `split_factor`. If the simplex
lives in a space of dimension `dim`, the resulting number of points is
`split_factor`^(dim).
"""
function evenly_sample(simplex::AbstractArray{Float64, 2}, split_factor::Int)
    dim = size(simplex, 2)
    centroids_exp_coeffs = copy(transpose(even_sampling_rules(dim, split_factor)))
    centroids_exp_coeffs * simplex
end

"""
Returns a matrix of convex coefficients to construct points contained within
a `dim`-dimensional simplex. Coefficients can either be random (generated
according to a uniform distribution) or be constructed such that the resulting
points are uniformly distributed within the simplex (generated by shape-preserving
splitting a generic simplex). The `sample_randomly` argument controls this
behaviour.

The default is to not sample randomly (`sample_randomly = false`). In this case,
you won't get the exact number of convex coefficient sets you want, but the
smallest number of set of coefficients such that you have *at least* `n_pts`
convex coefficient sets.

If `sample_randomly = true`, you get as many convex coefficient combinations as
`n_pts`.
"""
function subsample_coeffs(dim::Int, n_randpts::Int, sample_randomly::Bool)
    # Create a set of convex coefficients to be used for all simplices
    if sample_randomly
        convex_coeffs = rand(Uniform(0, 1), dim + 1, n_randpts)
        convex_coeffs .= convex_coeffs ./ sum(convex_coeffs, dims=1) #convex_coeffs ./ sum(convex_coeffs, dims=2)
    else
        minimum_split_factor = ceil(Int, n_randpts^(1 / dim))[1]
        convex_coeffs = even_sampling_rules(dim, minimum_split_factor)
    end
    return convex_coeffs
end

export subsample_coeffs


"""
    angle_of_incidence(lat,lon,jd,time)

Calculate the cosine of the sun incident angle for a given location (lat,lon),
julian day (jd) and time in hours and fraction of hours.
"""
function angle_of_incidence(lat, lon, jd, time)
    # inclination angle
    delta = (asin(-sin(23.45 / 180 * pi) .* cos(360 / 365.25 * (jd + 10) / 180 * pi)))
    # lat to radians
    lamda = lat / 180 * pi
    # time to radians
    omega = time / 24 * 2 * pi + lon / 360 * 2 * pi + pi

    return cos(delta) * cos(lamda) * cos(omega) + sin(delta) * sin(lamda)
end

"""
    delta(jd)

Calculates the Sun's declination angle as a function of Julian day (jd)
"""
delta(n) = (asin(-sin(23.45 / 180 * pi) .* cos(360 / 365.25 * (n + 10) / 180 * pi)))

"""
    heaviside(x)

Heaviside function : 0 when x<0 and 1 when x>=0
"""
function heaviside(x)
    y = zeros(size(x))
    y[x.>=0] .= 1
    return y
end


"""
Calculates the average of the elements in a vector ``x``.
"""
function sample_mean(x::Vector)
    n = length(x)
    x̅ = 1 / n * sum(x)
    return x̅
end

"""
Calculates the sample variance of the elements in a vector ``x``.
"""
function sample_variance(x::Vector)
    n = length(x)
    s² = 1 / (n - 1) * (sum(x.^2) - 1 / n * sum(x)^2)
    return s²
end

"""
Calculates the covariance of vector ``x``.
"""
function sample_covariance(x::Vector)
    n = length(x)
    cᵪᵪ = sum_of_squares(x) / (n - 1)
    return cᵪᵪ

end

"""
Calculates the covariance of two vectors ``x`` and ``y``.
"""
function two_sample_covariance(x::Vector, y::Vector)
    n = length(x)
    cᵪᵧ = 1 / (n -1) * (sum(x .* y) - 1 / n * sum(x) * sum(y))
    return cᵪᵧ
end

"""
Calculates the correlation coefficient of two vectors ``x`` and ``y``.
See also: [`sample_variance`](@ref), [`two_sample_covariance`](@ref).
"""
function two_sample_correlation_coefficient(x::Vector, y::Vector)
    sᵪ = √(sample_variance(x))
    sᵧ = √(sample_variance(y))
    cᵪᵧ = two_sample_covariance(x, y)
    rᵪᵧ = cᵪᵧ / (sᵪ * sᵧ)
    return rᵪᵧ
end

"""
Calculates the sample variance of two vectors ``x`` and ``y``.
Used mainly for calculating reference variables for significance tests when the variance is unknown.
See also: [`sample_variance`](@ref).
"""
function two_sample_variance(x::Vector, y::Vector)
    nᵪ = length(x)
    nᵧ = length(y)
    s²ᵪ = sample_variance(x)
    s²ᵧ = sample_variance(y)
    s²ᵪᵧ = ((nᵪ - 1) * s²ᵪ + (nᵧ - 1) * s²ᵧ) / (nᵪ + nᵧ - 2)
    return s²ᵪᵧ
end

"""
This function calculates the sum of squares.
This is used mainly for estimating the parameters in simple linear regression.
See also: [`not_implemented`](@ref).
"""
function sum_of_squares(x::Vector, y=x::Vector)
    n = length(x)
    Sᵪᵧ = (sum(x .* y) - 1 / n * sum(x) * sum(y))
    return Sᵪᵧ
end


function derivative(fn)
    return function(x)
        # pick a small value for h
        h = x == 0 ? sqrt(eps(Float64)) : sqrt(eps(Float64)) * x
        # floating point arithmetic gymnastics
        xph = x + h
        dx = xph - x
        # evaluate f at x + h
        f1 = fn(xph)
        # evaluate f at x
        f0 = fn(x)
        # divide the difference by h
        # return (fn(xph) - fn(x)) / dx
        return (f1 - f0) / dx
    end

# 1st approximation:
#    return function(x)
#        h = 0.00001
#        f1 = fn(x + h)
#        f0 = fn(x)
#        return (f1 - f0)/h
#    end
end


"""
*ProspectTheory*

`ProspectTheory` constructs a model object for cummulative prospect theory.
By default, parameters for utility curvature and probability weigting are equal gains and losses.

- `α`: utility curvature for gains
- `β`: utility curvature for losses
- `γg`: probability weighting parameter for gains
- `γl`: probability weighting parameter for losses
- `λ`: loss aversion parameter

Constructor
````julia
ProspectTheory(;α=.80, β=α, γg=.70, γl=γg, λ=2.25)
````
*References*

Fennema, H., & Wakker, P. (1997). Original and cumulative prospect theory: A discussion of empirical differences. Journal of Behavioral Decision Making, 10(1), 53-64.

Tversky, A., & Kahneman, D. (1992). Advances in prospect theory: Cumulative representation of uncertainty. Journal of Risk and uncertainty, 5(4), 297-323.
"""
mutable struct ProspectTheory{T1,T2,T3,T4,T5} <:UtilityModel
    α::T1
    β::T2
    γg::T3
    γl::T4
    λ::T5
end

function ProspectTheory(;α=.80, β=α, γg=.70, γl=γg, λ=2.25)
    return ProspectTheory(α, β, γg, γl, λ)
end

"""
*compute_utility*

`compute_utility` computes utility of gamble outcomes according to prospect theory

- `model`: a model object for prospect theory
- `gamble`: a gamble object

Function Signature
````julia
compute_utility(model::ProspectTheory, gamble::Gamble)
````
"""
function compute_utility(model::ProspectTheory, gamble)
    @unpack α,β,λ = model
    vl,vg = split_values(gamble)
    utilg = vg.^α
    utill = @. -λ*abs(vl)^β
    return [utill; utilg]
end

"""
*compute_weights*

`compute_weights` computes decision weights based on cummulative outcomes

- `model`: a model object for prospect theory
- `gamble`: a gamble object

Function Signature
````julia
compute_weights(model::ProspectTheory, gamble::Gamble)
````
"""
function compute_weights(model::ProspectTheory, gamble::Gamble)
    pl,pg = split_probs(gamble)
    @unpack γg,γl = model
    ω = [_compute_weights(pl, γl); _compute_weights(pg, γg)]
    return ω
end

"""
*_compute_weights*

`_compute_weights` computes decision weights based on cummulative outcomes

- `p`: a probability vector
- `γ`: parameter that controls weighting of low and high probabilities

Function Signature
````julia
_compute_weights(p, γ)
````
"""
function _compute_weights(p, γ)
    n = length(p)
    f(i) = weight(sum(p[i:n]), γ) - weight(sum(p[(i+1):n]), γ)
    ω = [f(i) for i in 1:n-1]
    isempty(p) ? nothing : push!(ω, weight(p[n], γ))
    return ω
end

weight(p, γ) = (p^γ)/(p^γ + (1-p)^γ)^(1/γ)

function sort!(model::ProspectTheory, gamble)
    @unpack p,v = gamble
    i = sortperm(v)
    p .= p[i]; v .= v[i]
    gains = v .>= 0
    pl = @view p[.!gains]
    vl = @view v[.!gains]
    reverse!(vl); reverse!(pl)
    return nothing
end

function split_values(gamble)
    @unpack v = gamble
    gains = v .>= 0
    vg = @view v[gains]
    vl = @view v[.!gains]
    return vl,vg
end

function split_probs(gamble)
    @unpack v,p = gamble
    gains = v .>= 0
    pg = @view p[gains]
    pl = @view p[.!gains]
    return pl,pg
end


module TestUtils

export random_complex_matrix, random_real_matrix, random_hermitian_matrix
export random_complex_sparse_matrix, random_real_sparse_matrix
export random_hermitian_sparse_matrix, random_state_vector

using Random
using Distributions
using LinearAlgebra
using SparseArrays

"""Construct a random complex matrix of size N×N with spectral radius ρ.

```julia
random_complex_matrix(N, ρ)
```
"""
function random_complex_matrix(N, ρ)
    σ = 1/√N
    d = Normal(0.0, σ)
    H = ρ * (rand(d, (N, N)) + rand(d, (N, N)) * 1im) / √2
end

"""Construct a random real-valued matrix of size N×N with spectral radius ρ.

```julia
random_real_matrix(N, ρ)
```
"""
function random_real_matrix(N, ρ)
    σ = 1/√N
    d = Normal(0.0, σ)
    H = ρ * rand(d, (N, N))
end

"""Construct a random Hermitian matrix of size N×N with spectral radius ρ.

```julia
random_hermitian_matrix(N, ρ)
```
"""
function random_hermitian_matrix(N, ρ)
    σ = 1/√N
    d = Normal(0.0, σ)
    X = rand(d, (N, N))
    H = ρ * (X + X') / (2*√2)
end

"""Construct a random sparse complex matrix.

```julia
random_complex_sparse_matrix(N, ρ, sparsity)
```

returns a matrix of size N×N with spectral radius ρ and the given sparsity
(number between zero and one that is the approximate fraction of non-zero
elements).
"""
function random_complex_sparse_matrix(N, ρ, sparsity)
    σ = 1/√(sparsity * N)
    d = Normal(0.0, σ)
    Hre = sprand(N, N, sparsity, (dims...) -> rand(d, dims...))
    Him = sprand(N, N, sparsity, (dims...) -> rand(d, dims...))
    H = ρ * (Hre + Him * 1im) / √2
end

"""Construct a random sparse real-valued matrix.

```julia
random_real_sparse_matrix(N, ρ, sparsity)
```

returns a matrix of size N×N with spectral radius ρ and the given sparsity
(number between zero and one that is the approximate fraction of non-zero
elements).
"""
function random_real_sparse_matrix(N, ρ, sparsity)
    σ = 1/√(sparsity * N)
    d = Normal(0.0, σ)
    H = ρ * sprand(N, N, sparsity, (dims...) -> rand(d, dims...))
end

"""Construct a random sparse Hermitian matrix.

```julia
random_hermitian_sparse_matrix(N, ρ, sparsity)
```

returns a matrix of size N×N with spectral radius ρ and the given sparsity
(number between zero and one that is the approximate fraction of non-zero
elements).
"""
function random_hermitian_sparse_matrix(N, ρ, sparsity)
    σ = 1/√(sparsity * N)
    d = Normal(0.0, σ)
    H = sprand(N, N, sparsity, (dims...) -> rand(d, dims...))
    return 0.5ρ * (H + H') / √2
end

"""Return a random, normalized Hilbert space state vector of dimension `N`.

```julia
random_state_vector(N)
```
"""
function random_state_vector(N)
    Ψ = rand(N) .* exp.((2π * im) .* rand(N))
    Ψ ./= norm(Ψ)
    return Ψ
end

end


"""
Quite crude way of checking that a number is a probability

The two steps are

1. The number should be of the `Float64` type
2. The number should belong to [0.0,1.0]
"""
function _value_is_a_probability(p::T) where {T<:AbstractFloat}
	(0.0 <= p <= 1.0) || throw(ArgumentError("The value p=$(p) is not a probability"))
	return nothing
end

"""
Expected value of a single Bernoulli event

Simply f(p): p
"""
function _single_bernoulli_expectation(p::Float64)
    _value_is_a_probability(p)
	return p
end

"""
Variance of a single Bernoulli event

f(p): p(1-p)
"""
function _single_bernoulli_variance(p::Float64)
    _value_is_a_probability(p)
	return p*(1.0-p)
end

"""
Variance of a series of additive Bernoulli events

f(p): ∑(p(1-p))
"""
function _additive_bernoulli_variance(p::Array{Float64})
	return sum(_single_bernoulli_variance.(p))
end

"""
Variance of a series of multiplicative Bernoulli events
"""
function _multiplicative_bernoulli_variance(p::Array{Float64})
	v = _single_bernoulli_variance.(p)
	e = _single_bernoulli_expectation.(p)
	return prod(v.+e.^2.0)-prod(e.^2.0)
end


#!/usr/bin/env julia

using ArgParse
using Distributed

function compute_pi(a::Float64, b::Float64, n::Int64)::Float64
    result::Float64 = 0.0
    delta = (b - a)/n
    x::Float64 = a
    i::Int64 = 1
    while x < 1.0 && i <= n
        result += sqrt(1.0 - x^2)
        x += delta
        i += 1
    end
    result *= 2.0*delta
    println("($a, $b) -> $result, $n")
    return result
end

function main()
    arg_parser = ArgParseSettings()
    arg_parser.description = "Compute pi using a quadrature on multiple processes"
    @add_arg_table arg_parser begin
        "--nr_points", "-p"
            arg_type = Int64
            help = "number of points for the quadrature method"
            required = true
        "--nr_workers", "-w"
            arg_type = Int
            help = "number of workers to use"
            default = 0
    end
    options = parse_args(ARGS, arg_parser)
    nr_points = options["nr_points"]
    nr_workers = options["nr_workers"]
    println("running with $nr_workers workers")
    if nr_workers == 0
        @time result = compute_pi(-1.0, 1.0, nr_points)
        println(result)
    else
        addprocs(nr_workers)
        @everywhere function compute_pi(a::Float64, b::Float64, n::Int64)::Float64
            result::Float64 = 0.0
            delta = (b - a)/n
            x::Float64 = a
            i::Int64 = 1
            while x < 1.0 && i <= n
                result += sqrt(1.0 - x^2)
                x += delta
                i += 1
            end
            result *= 2.0*delta
            println("($a, $b) -> $result, $n")
            return result
        end

        delta = 2.0/nr_workers
        result = @distributed (+) for i in 1:nr_workers
            @time compute_pi(-1.0 + (i - 1)*delta, -1.0 + i*delta, div(nr_points, nr_workers))
        end
        println(result)
    end
end

main()