module MLP
    using Random
    using Statistics
    using Plots
    using ProgressMeter

    export
        SimpleMLP,
        fit!,
        adaptive_fit!,
        predict,
        save_figures

    abstract type NeuralNetwork end
    abstract type FeedForwardNN <: NeuralNetwork end
    abstract type MultiLayerPerceptron <: FeedForwardNN end

    error(ŷ, y) = ŷ - y
    σ(x) = 1 / (1 + exp(-x))
    σ′(x) = x * (1 - x)

    mutable struct SimpleMLP{T} <: MultiLayerPerceptron where T <: AbstractFloat
        train_data
        test_data

        loss
        activation
        activation_gradient
        n_epochs::Integer
        η::T

        wL₁::Matrix{T}
        wL₂::Matrix{T}
        loss_history::Vector{T}
        train_accuracy_history::Vector{T}
        test_accuracy_history::Vector{T}

        function SimpleMLP{T}(
                data_X::Matrix{T},
                data_Y::Matrix{T},
                ;
                train_test_ratio::AbstractFloat=0.8,
                loss=error,
                activation=σ,
                activation_gradient=σ′,
                n_hidden_layers::Integer=size(data_X, 2),
                n_epochs::Integer=100,
                η::T=convert(T, 1e-1),
        ) where T <: AbstractFloat
            data = shuffle(collect(zip(eachrow(data_X), eachrow(data_Y))))
            n_train_data = round(Integer, length(data) * train_test_ratio)
            new(data[1:n_train_data], data[n_train_data+1:end],
                loss, activation, activation_gradient, n_epochs, η,
                rand(n_hidden_layers, size(data_X, 2)), rand(size(data_Y, 2), n_hidden_layers), T[], T[], T[])
        end
    end

    function fit!(mlp::MultiLayerPerceptron)
        last_up_wL₂ = zeros(size(mlp.wL₂))
        new_up_wL₂ = zeros(size(mlp.wL₂))
        last_up_wL₁ = zeros(size(mlp.wL₁))
        new_up_wL₁ = zeros(size(mlp.wL₁))
        @showprogress for c ∈ 1:mlp.n_epochs
            for (x, y) ∈ mlp.train_data
                x = hcat(x)
                y = hcat(y)
                aL₁ = mlp.activation.(mlp.wL₁ * x)
                aL₂ = mlp.activation.(mlp.wL₂ * aL₁)
                e_nn = mlp.loss(aL₂, y)
                dL₂ = mlp.activation_gradient.(aL₂) .* e_nn
                dL₁ = mlp.activation_gradient.(aL₁) .* (mlp.wL₂' * dL₂)

                ΔL₂ = dL₂ * aL₁'
                ΔL₁ = dL₁ * x'
                ΔL₂ *= mlp.η
                ΔL₁ *= mlp.η
                mlp.wL₂ -= ΔL₂
                mlp.wL₁ -= ΔL₁
                push!(mlp.loss_history, mean(e_nn))
            end
            check_accuracy!(mlp)
        end
    end

    function adaptive_fit!(mlp::MultiLayerPerceptron)
        ϵ = eps()
        GwL₂ = zeros(size(mlp.wL₂))
        GwL₁ = zeros(size(mlp.wL₁))
        @showprogress for c ∈ 1:mlp.n_epochs
            for (x, y) ∈ mlp.train_data
                x = hcat(x)
                y = hcat(y)
                aL₁ = mlp.activation.(mlp.wL₁ * x)
                aL₂ = mlp.activation.(mlp.wL₂ * aL₁)
                e_nn = mlp.loss(aL₂, y)
                dL₂ = mlp.activation_gradient.(aL₂) .* e_nn
                dL₁ = mlp.activation_gradient.(aL₁) .* (mlp.wL₂' * dL₂)

                ΔL₂ = dL₂ * aL₁'
                ΔL₁ = dL₁ * x'
                GwL₂ += ΔL₂ .^ 2
                GwL₁ += ΔL₁ .^ 2
                @. ΔL₂ *= mlp.η / (√GwL₂ + ϵ)
                @. ΔL₁ *= mlp.η / (√GwL₁ + ϵ)
                mlp.wL₂ -= ΔL₂
                mlp.wL₁ -= ΔL₁
                push!(mlp.loss_history, mean(e_nn))
            end
            check_accuracy!(mlp)
        end
    end

    function predict(mlp::MultiLayerPerceptron, x::Matrix)
        mlp.activation.(mlp.wL₂ * mlp.activation.(mlp.wL₁ * x))
    end

    function save_figures(mlp::MultiLayerPerceptron, fn::AbstractString="figs/")
        savefig(plot(
            mlp.loss_history,
            label="loss",
            title="loss",
            legend=:outertopright,
            size=(1024, 512),
        ), "$(fn)loss_fig.png")
        savefig(plot(
            [mlp.train_accuracy_history mlp.test_accuracy_history],
            label=["train accuracy" "test accuracy"],
            title="accuracy",
            legend=:outertopright,
            size=(1024, 512),
        ), "$(fn)accuracy_fig.png")
    end

    function check_accuracy!(mlp::MultiLayerPerceptron)
        train_true_prediction = 0
        for (x, y) ∈ mlp.train_data
            x = hcat(x)
            y = hcat(y)
            out = predict(mlp, x)
            train_true_prediction += argmax(out) == argmax(y)
        end
        train_acc = train_true_prediction / length(mlp.train_data)
        push!(mlp.train_accuracy_history, train_acc)

        test_true_prediction = 0
        for (x, y) ∈ mlp.test_data
            x = hcat(x)
            y = hcat(y)
            out = predict(mlp, x)
            test_true_prediction += argmax(out) == argmax(y)
        end
        test_acc = test_true_prediction / length(mlp.test_data)
        push!(mlp.test_accuracy_history, test_acc)
    end
end


export BinomialGeomRWModel, CRRModel, TianModel, JRModel

"""
    BinomialGeomRWModel

A model for a Binomial Geometric Random Walk (aka Binomial tree).

Arguments:

* `startdate` : start date of process
* `enddate` : start date of process
* `nsteps` : number of steps in the tree
* `S₀` : inital value
* `Δt` : the time-difference between steps, typically `days(startdate - enddate) / (365*nsteps)`
* `iR` : discount rate, `exp(-Δt*interestrate)`
* `u`  : scale factor for up
* `d`  : scale factor for down
* `p`  : up probability
* `q`  : down probability, `1-p`

"""
struct BinomialGeomRWModel{T,V,S} <: AbstractModel
    startdate::Date
    enddate::Date
    nsteps::Int
    S₀::T
    Δt::Float64
    iR::Float64 # exp(-r*Δt)
    logu::V #Float64
    logd::V #Float64
    p::S #Float64
    q::S #Float64
end

abstract type BinomialModel end
struct TianModel <: BinomialModel end
struct CRRModel <: BinomialModel end
struct JRModel <: BinomialModel end
struct JRrnModel <: BinomialModel end

numeraire(m::BinomialGeomRWModel{T}) where {T<:CurrencyQuantity} = unit(m.S₀)
numeraire(m::BinomialGeomRWModel{T}) where {T<:Real} = one(m.S₀)

"""
    CRRModel

Cox-Ross-Rubenstein binomial model.
"""
function CRRModel(startdate::Date, enddate::Date, nsteps::Int,
               startvalue, interestrate::Float64, carryrate::Float64, volatility::T) where T
    Δt = days(enddate - startdate) / (365 * nsteps)
    σ  = volatility
    b  = interestrate - carryrate
    B  = exp(b*Δt)
    iR = exp(-interestrate*Δt)

    logu = σ*√Δt
    logd = -σ*√Δt
    u = exp(logu)
    d = exp(logd)
    p = (B-d)/(u-d)
    q = (u-B)/(u-d)
    BinomialGeomRWModel(startdate,enddate,nsteps,startvalue,Δt,
                                iR,logu,logd,p,q)
end

function JRModel(startdate::Date, enddate::Date, nsteps::Int,
               startvalue, interestrate::Float64, carryrate::Float64, volatility::T) where T

    Δt = days(enddate - startdate) / (365 * nsteps)
    σ  = volatility
    b  = interestrate - carryrate
    iR = exp(-interestrate*Δt)

    logu = (b-σ^2/2)*Δt + σ*√Δt
    logd = (b-σ^2/2)*Δt - σ*√Δt
    p = q = 0.5

    BinomialGeomRWModel(startdate,enddate,nsteps,startvalue,Δt,
                                iR,logu,logd,p,q)
end
function JRrnModel(startdate::Date, enddate::Date, nsteps::Int,
               startvalue, interestrate::Float64, carryrate::Float64, volatility::T) where T
    Δt = days(enddate - startdate) / (365 * nsteps)
    σ  = volatility
    b  = interestrate - carryrate
    B  = exp(b*Δt)
    iR = exp(-interestrate*Δt)

    logu = (b-σ^2/2)*Δt + σ*√Δt
    logd = (b-σ^2/2)*Δt - σ*√Δt
    u = exp(logu)
    d = exp(logd)
    p = (B-d)/(u-d)
    q = (u-B)/(u-d)

    BinomialGeomRWModel(startdate,enddate,nsteps,startvalue,Δt,
                                iR,logu,logd,p,q)
end

# the value at step `n`, index `i` (i.e. `i` ups, `n-i` downs)
@inline valueat(m::BinomialGeomRWModel{T}, s::SingleStock, n, i) where {T} =
    m.S₀*exp(m.logu*i + m.logd*(n-i))

function value(m::BinomialGeomRWModel{T,V}, c::WhenAt{C}) where {C,T,V}
    m.enddate == maturitydate(c) || error("Binomial end date must match maturity of option")
    N = m.nsteps
    S = typeof(valueat(m, c.c, N, N))
    X = S[valueat(m, c.c, N, i) for i = 0:N]
    for n = N-1 : -1 : 0
        for i = 0:n
            X[i+1] = m.iR*(m.q*X[i+1] + m.p*X[i+2])
        end
    end
    X[1]
end
function value(m::BinomialGeomRWModel{T}, c::AnytimeBefore{C}) where {C,T}
    m.enddate == maturitydate(c) || error("Binomial end date must match maturity of option")
    N = m.nsteps
    S = typeof(valueat(m, c.c, N, N))
    X = S[valueat(m, c.c, N, i) for i = 0:N]
    for n = N-1 : -1 : 0
        for i = 0:n
            X[i+1] = max(m.iR*(m.q*X[i+1] + m.p*X[i+2]),
                         valueat(m, c.c, n, i))
        end
    end
    X[1]
end

## Tian
"""
    TianModel

Tian binomial model.
"""
function TianModel(startdate::Date, enddate::Date, nsteps::Int,
               startvalue, interestrate::Float64, carryrate::Float64, volatility::T) where T
    Δt = days(enddate - startdate) / (365 * nsteps)
    σ = volatility
    v = exp(σ^2*Δt)
    r = interestrate - carryrate
    iR = exp(-interestrate*Δt)
    u = 1/2*exp(r*Δt)*v*(v+1+sqrt(v^2+2v-3))
    d = 1/2*exp(r*Δt)*v*(v+1-sqrt(v^2+2v-3))
    logu = log(u)
    logd = log(d)
    p = (exp(r*Δt)-d)/(u-d)
    q = (u-exp(r*Δt))/(u-d)
    BinomialGeomRWModel(startdate,enddate,nsteps,startvalue,Δt,
                                iR,logu,logd,p,q)
end

# From http://www.goddardconsulting.ca/option-pricing-binomial-alts.html

## Cox-Ross-Rubinstein
# u = exp(σ*√Δt)
# d = exp(-σ*√Δt)
# p = (exp(r*Δt)-d)/(u-d)
# q = (u-exp(r*Δt))/(u-d)

## Jarrow-Rudd
# u = exp((r-σ^2/2)*Δt + σ*√Δt)
# d = exp((r-σ^2/2)*Δt - σ*√Δt)
# p = q = 0.5
# NOTE: not risk-neutral

## Jarrow-Rudd risk-neutral
# u = exp((r-σ^2/2)*Δt + σ*√Δt)
# d = exp((r-σ^2/2)*Δt - σ*√Δt)
# p = (exp(r*Δt)-d)/(u-d)
# q = (u-exp(r*Δt))/(u-d)

## Tian
# u = 1/2*exp(r*Δt)*v*(v+1+sqrt(v^2+2v-3))
# d = 1/2*exp(r*Δt)*v*(v+1-sqrt(v^2+2v-3))
#   where v = exp(σ^2*Δt)
# p = (exp(r*Δt)-d)/(u-d)
# q = (u-exp(r*Δt))/(u-d)

## CRR with drift
# u = exp(η*Δt + σ*√Δt)
# d = exp(η*Δt - σ*√Δt)
# p = (exp(r*Δt)-d)/(u-d)
# q = (u-exp(r*Δt))/(u-d)
#   for some η
#   a common choice is η = (log(K)-log(S0))/T

## Leisen-Reimer
# p = h_inv(d1)
# p2 = h_inv(d2)
# u = exp(r*Δt) * p2/p
# d = (exp(r*Δt) - p*u) / (1-p)
#  (ensures standard form for p,q)
# h_inv is a discrete approximation to Φ


struct binned1dDensity{T}
    grid::Vector{Float64}
    cumarr::Vector{T}
    density::Function
end

function binned1dDensity(grid, weights; density::Function = x->(println("warning: no callback provided!");1))
    _weights = weights ./ sum(weights)
    cumarr = [0;cumsum(vcat(_weights...), dims=1)]
    return binned1dDensity(grid, cumarr, density)
end

function getbinned1dDensity(g, lims, Nbins)
    grid = collect(range(lims[1], lims[2], length=Nbins))
    #
    weights = [g(v) for v in (grid[2:end] .+ grid[1:end-1]) ./ 2]
    return binned1dDensity(grid, weights; density=g)
end

import Base:rand
function rand(bD::binned1dDensity; removenegative::Bool=false)
    # get
    binind = findfirst(bD.cumarr .> rand())-1
    σl, σr = bD.grid[binind], bD.grid[binind+1]
    σ = σl+rand()*(σr-σl)
    removenegative && bD.density(σ) == 0.0 && return rand(bD,removenegative)
    return σ
end

function generate(d::T where T <: AbstractFunctionWithParameters, N::Int;
    p=freepars(d), Nbins=100, removenegative::Bool=false)
    grid = collect(range(lims(d)..., length=Nbins))
    centers = (grid[2:end] .+ grid[1:end-1]) ./ 2
    #
    weights = d(centers; p)
    bD = binned1dDensity(grid, weights; density=x->d(x;p))
    return [rand(bD; removenegative) for _ in 1:N]
end
rand(d::T where T <: AbstractFunctionWithParameters, N::Int=1) = generate(d, N)


### A Pluto.jl notebook ###
# v0.17.1

using Markdown
using InteractiveUtils

# ╔═╡ 250252f0-6e61-11ec-2ffe-ebddaa9f0fb3
begin
	using Pkg
	Pkg.activate("../../")
	using Revise
end

# ╔═╡ 0eeb5bb1-bb41-43cc-8c14-eb9c1ebf6fad
using ImgProcMic, FourierTools, IndexFunArrays, ImageShow, FFTW, TestImages, Colors

# ╔═╡ 1b2ebe1a-f3a7-494c-bcca-18df5403ee65
using PlutoTest, Noise, Plots

# ╔═╡ 4b46e310-cc26-4a96-8c2e-0c89307d4e34
md"# Structured Illumination Microscopy

In this homework we are going to work through the full pipeline from capturing a dataset of a SIM to reconstructing the data.

For the demonstration we focus on the 2D case and consider the resolution enhancement only along one direction. That allows us to observe the effect clearly.
"

# ╔═╡ 7054db2c-606e-48eb-ab93-8c0260cb7a81
md"## 1 Simulate PSF"

# ╔═╡ edaacc75-fd91-4d46-a31f-21e738253708
"""
	simulate_PSF(s, r, [T=Float32])

Simulate the incoherent 2D PSF with radius `r` and total output size `s`.
This functions returns the PSF centered around the first entry (top left corner).
Furthermore, it returns the `freq_limit` of the PSF.
The `freq_limit` is the frequency at which the OTF becomes 0.

The procedure is as following:
* create a disc with values of 1 and radius `r` (use `rr2`)
* take the `ift` of it.
* abs2.
* normalize the sum to 1
* center pixcel should be located in the top left
"""
function simulate_PSF(s, r, T=Float32)
	disc = rr2(T, (s)) .< r^2

	# is equivalent of `ifft(disc)` is equivalent to `ifftshift(ift(disc))`
	# but only because
	# we removed the phase by abs2
	disc_ift = ift(disc)

	psf = abs2.(disc_ift)
	psf ./= sum(psf)

	# shift from center to first entry
	psf = psf
	# don't change freq_limit, is already

	freq_limit = r / (s[1] / 2)
	return ifftshift(psf), freq_limit
end

# ╔═╡ b6061e31-e06a-45ad-86f0-006b8309cca7
rr2((4,4)) .< 1.3^2

# ╔═╡ cd5bef41-6246-4d88-91d9-c83b7a47e110
# leave this value equal to 40!
radius = 40

# ╔═╡ 29e6311e-eb5b-4073-81dd-7372ac06498e
psf, freq_limit = simulate_PSF((512,512), radius);

# ╔═╡ 86e0df4f-4dfe-40c3-a6d3-1a394442a9f7
# MTF = magnitude of the optical transfer function (OTF)
plot(fftshift(fftfreq(512)), abs.(ffts(psf)[:, 257]))

# ╔═╡ 963c4f23-e97b-41fd-9423-1080fae412e9
freq_limit

# ╔═╡ c4c8b57b-e09a-43f8-b371-bb1f0761215d
gray_show(psf)

# ╔═╡ 599aabf7-6333-4272-ae07-fec07bdb02aa
img = Float32.(testimage("resolution_test_512"));

# ╔═╡ 920c26f4-5750-4c83-871d-9622b3dca134
gray_show(img);

# ╔═╡ a550fc7f-ff7c-4fd5-9e40-d8ed00da200f
md"### 1 Test"

# ╔═╡ 7cba5a99-21a7-45d6-a92a-7eb1e75e39d3
PlutoTest.@test simulate_PSF((5, 5), 2)[1] ≈ (Float32[0.36 0.10472136 0.01527864 0.01527864 0.10472136; 0.10472136 0.030462677 0.0044444446 0.0044444446 0.030462677; 0.01527864 0.0044444446 0.00064843567 0.00064843567 0.0044444446; 0.01527864 0.0044444446 0.00064843567 0.00064843567 0.0044444446; 0.10472136 0.030462677 0.0044444446 0.0044444446 0.030462677], 0.8)[1]

# ╔═╡ eafd2e8a-6bbd-4fe4-a5fd-d721b78604e1
PlutoTest.@test simulate_PSF((5, 5), 2)[2] == 0.8

# ╔═╡ 75f2ea94-ba30-48d3-b676-8423f14c453c
delta = zeros((5,5));

# ╔═╡ 9b856e9c-b818-4e49-a27d-0c0db623698e
delta[1] = 1;

# ╔═╡ 11009f9c-de4c-4ade-ad30-375ed47c3901
PlutoTest.@test simulate_PSF((5, 5), 3)[1] ≈ delta

# ╔═╡ 4ff25625-e450-47e4-97ae-bb24b903ed98
md"## 2 Structured Illumination

The key idea behind SIM is to illuminate the sample with an illumination pattern of the form

$I_n = 1 + \cos(k_g \cdot x + \theta_n)$

where $k_g$ is the frequency of the illumination pattern (twice the grating constant). $\theta_n$ is a illumination pattern shift which is different for each image. Such an illumination allows to increase the resolution along the first dimension.

We capture three images ($n=\{1,2,3\}$) of the sample which can be expressed as

$Y_n = (I_n \cdot S) * h$
where $h$ is the PSF, $S$ the ideal sample and $Y$ the measurement.

In our example we choose the following 3 different illuminations

$I_{1} = 1 + \cos\left(k_g \cdot x \cdot 2 \pi + 1\cdot \frac{2 \pi}{3} \right)$

$I_2 = 1 + \cos\left(k_g \cdot x  \cdot 2 \pi+ 2 \cdot \frac{2 \pi}{3}\right)$

$I_3 = 1 + \cos\left(k_g \cdot x  \cdot 2 \pi+ 3 \cdot \frac{2 \pi}{3}\right)$

"

# ╔═╡ e62fc2f1-a1e2-4b8a-be08-94444e12b533
begin
	plot(1 .+ cos.(2π/3 .+ 2π .* (0.0:0.01:3)))
	plot!(1 .+ cos.(4π/3 .+ 2π .* (0.0:0.01:3)))
	plot!(1 .+ cos.(2π .* (0.0:0.01:3)))
	plot!(3 .+  cos.(2π/3 .+ 2π .* (0.0:0.01:3)) .+ cos.(4π/3 .+ 2π .* (0.0:0.01:3)) .+ cos.(6π/3 .+ 2π .* (0.0:0.01:3)))
end

# ╔═╡ ca8618e8-8389-4d30-8112-af0e987de417
"""
	illuminate(arr, kg)

Returns a vector of `arr` illuminated three times according to the SIM principle.
Implement the equations from above. Only illumination, no convolution with PSF.

You should return a vector of the three illumination arrays.
`[I_1, I_2, I_3]`.
"""
function illuminate(arr, kg)
	# x coordinates
	x = 1:size(arr, 1)

	# todo fix illumination
	I₁ = (1 .+ cos.(x .* kg .* 2π .+ 1 * 2π / 3)) .* arr
	I₂ = (1 .+ cos.(x .* kg .* 2π .+ 2 * 2π / 3)) .* arr
	I₃ = (1 .+ cos.(x .* kg .* 2π .+ 3 * 2π / 3)) .* arr

	# todo return correct results
	[I₁, I₂, I₃]
end

# ╔═╡ 20aaf9b2-2660-413f-90a8-1f4fdc91d8c7
# toy example
illuminate(ones((4,4)), 0.5)

# ╔═╡ 4898da63-4da0-4538-b08d-cd4fe419584b
Illus = illuminate(ones((512, 512)), freq_limit * 0.8);

# ╔═╡ 8457cfeb-705f-41e8-a203-298c8ee4a7ab
# display the three illuminations
gray_show(hcat(Illus...))

# ╔═╡ 28784e50-285f-47a8-a311-4122ec8f47ce
# check whether illumination still pass through the system.
# if you see the same stripes but with lower contrast, then everything
# is perfect
gray_show(conv(Illus[1], psf))#simulate_PSF(size(Illus[1]), 31)[1]))

# ╔═╡ 6e2814c9-5644-47b6-8a6e-3308e1623d5e
md"### 2 Test"

# ╔═╡ ca0e55ca-ec4f-4cd8-a1f7-a2a63ba9782d
PlutoTest.@test illuminate(ones((4, 4)), 0.5) ≈ [[1.4999999999999993 1.4999999999999993 1.4999999999999993 1.4999999999999993; 0.5000000000000008 0.5000000000000008 0.5000000000000008 0.5000000000000008; 1.4999999999999991 1.4999999999999991 1.4999999999999991 1.4999999999999991; 0.500000000000001 0.500000000000001 0.500000000000001 0.500000000000001], [1.5000000000000004 1.5000000000000004 1.5000000000000004 1.5000000000000004; 0.49999999999999867 0.49999999999999867 0.49999999999999867 0.49999999999999867; 1.5000000000000013 1.5000000000000013 1.5000000000000013 1.5000000000000013; 0.49999999999999845 0.49999999999999845 0.49999999999999845 0.49999999999999845], [0.0 0.0 0.0 0.0; 2.0 2.0 2.0 2.0; 0.0 0.0 0.0 0.0; 2.0 2.0 2.0 2.0]]

# ╔═╡ fa957648-0b4a-48eb-96d5-bb731966693c
Ittt = illuminate(ones((10, 10)), 1/3);

# ╔═╡ 287d85ae-efba-4095-b7ce-58d90bd22492
PlutoTest.@test Ittt[1] .+ Ittt[2] .+ Ittt[3] ≈ 3 .* ones((10, 10))

# ╔═╡ fce697cc-67d1-4373-b2b0-a6a60b8a5893
PlutoTest.@test all(illuminate([1 1; 0 0], 0) .≈ [[0.49999999999999956 0.49999999999999956; 0.0 0.0], [0.5000000000000002 0.5000000000000002; 0.0 0.0],  [2.0 2.0; 0.0 0.0],])

# ╔═╡ b961b3e0-6935-4f29-9f94-be6e21f302e3
PlutoTest.@test  illuminate([1 1; 0 0], 0.2) ≈ [[0.02185239926619431 0.02185239926619431; 0.0 0.0], [1.6691306063588578 1.6691306063588578; 0.0 0.0], [1.3090169943749477 1.3090169943749477; 0.0 0.0]]

# ╔═╡ 6e904b1d-2071-4d48-ad21-ab180e333474
md"## 3 Forward Imaging Process
Now we complete the forward modelling part from the image to the three measurements.

The grating constant should be small enough such that illumination
passes through the system.
Since the optical system has a bandlimit, not arbitrary small illumination patterns pass through the system.
The larger you choose it, the more of the reconstruction is affected by noise.
By  selecting `0.8 * freq_limit` we are definitely below the frequency threshold (e.g. 1.1 would be above)
"

# ╔═╡ e3994f91-a7bc-4811-b6e6-1f67692ff554
# don't change!
kg = 0.8 * freq_limit

# ╔═╡ 71af2b63-475c-429f-a15e-9a212f724a69
# the amount the three OTFs are displaced with respect to each other in Fourier space
# we need the value later for the reconstruction
# this value should stay at 64!
fourier_space_shift = radius * 2 * 0.8

# ╔═╡ 2a78c81d-f8f9-4d99-9c24-1aefb9941294
"""
	forward(img, psf, kg)

From the input `img` create three output images.
The output images are produced by multiplying the three different illumination
to the input image and convolving the three resulting `img`s with the `psf`.
Apply `poisson` to each of the image with a photon number of 1000 (use `N_phot`).
Use the function `poisson` for that.

## Procedure
* Multiplication with illumination
* convolution (use `FourierTools.conv`) with `psf`.
* Add noise with `poisson(img_without_noise, N_phot)`
* Return vectors of the three measuremens,e g. `[meas_1, meas_2, meas_3]`

"""
function forward(img, psf, kg, N_phot=1000)
	# already the illuminated images
	illus = illuminate(img, kg)

	# convolve and add Poisson noise
	Y₁ = poisson(conv(illus[1], psf), N_phot)
	Y₂ = poisson(conv(illus[2], psf), N_phot)
	Y₃ = poisson(conv(illus[3], psf), N_phot)

	# return correct result
	return [Y₁, Y₂, Y₃]
end

# ╔═╡ d68d9f24-bcea-4bcc-b597-3c051e77d99c
Is = forward(img, psf, kg);

# ╔═╡ 23c98291-4c72-4a60-b722-910eadaf7e77
md"Three images which look all slightly different because"

# ╔═╡ 9a75e192-9605-4787-acee-706ee7c90549
gray_show(hcat(Is...))

# ╔═╡ 524b208d-7fc3-4d96-bdc1-2bed72f2a9ee
md"In comparison without any structured illumination"

# ╔═╡ 2c1a5b9e-8b1c-4619-b303-69d29181a5c4
[gray_show(conv(img, psf)) gray_show(Is[1])]

# ╔═╡ bb1e5dd4-0159-47b5-a803-5327952438ee
gray_show(img)

# ╔═╡ 09593275-85bf-42ee-860e-ebbb74866a40
md"## 3 Test"

# ╔═╡ 1103b35d-cb86-4970-beb7-c8463e374f78
# test random example
PlutoTest.@test all(.≈(forward([1 2; 3 4], [0.55 0.15; 0.15 0.15], 0.1, 100_000_000),  [[0.09645833671795113 0.13103484317669054; 0.08808351198799098 0.09683198111576993], [2.6912285614539946 3.133633214436668; 4.25256575017253 4.919802755223418], [2.9120113512125654 3.6367083598655254; 3.7589874298841632 4.283291164312841]], rtol=0.01))

# ╔═╡ ba74ec4a-b38f-4fcb-9b43-2448976153eb
PlutoTest.@test radius == 40

# ╔═╡ ab56310f-671d-4b54-b9c2-9be102c826e3
PlutoTest.@test kg == 1/8

# ╔═╡ db20d906-b22f-4f16-b355-eba9c82809d7
PlutoTest.@test fourier_space_shift == 64

# ╔═╡ b7f11d85-6859-4b5a-814e-003946fbd894
md"# 4 Unmixing
The three measurements are not the different parts of the Fourier spectrum yet.
But they contain the information to high frequencies!
Therefore, we want to unmix the three parts of the Fourier space.
"

# ╔═╡ 96b8f1e6-9159-4616-be08-c88f17e9829e
"""
	extract_components(Is)

Extract the different Fourier components from the 3 measurements (the vector `Is`).
This is achieved by inverting the mixing matrix (`inv(M)`) and multiplying (use a `*` for matrix multiplication, not a pointwise `.*`) it from the left to
`[fft(Is[1]), fft(Is[2]), fft(Is[3])]`.

See the lecture slides for more information.
"""
function extract_components(Is)
	θ₁ = 2π / 3 * 1
	θ₂ = 2π / 3 * 2
	θ₃ = 2π / 3 * 3

	# TODO create correct matrix
	M̂ = [exp(-1im * θ₁) 1 exp(1im * θ₁);
		 exp(-1im * θ₂) 1 exp(1im * θ₂);
		 exp(-1im * θ₃) 1 exp(1im * θ₃)]

	# is the same Ĩ = fft.(Is)
	Ĩ = [fft(Is[1]), fft(Is[2]), fft(Is[3])]

	# TODO return correct result
	return inv(M̂) * Ĩ
end

# ╔═╡ 53af3f77-cbc2-4809-8dda-743e096df10c
Cₙ = extract_components(Is);

# ╔═╡ 4c136f35-e489-4d6e-83e0-e153bfc7040d
md"Here you can see the three different components still. They are not shifted to the correct position, yet.
"

# ╔═╡ bf97497a-e149-4552-9bdd-9fbeb5c24987
gray_show(log1p.(abs.(hcat(fftshift.(Cₙ)...))))

# ╔═╡ b3528818-74f7-471b-8d33-ad7b9fcfc70b
gray_show(log1p.(abs.(ft(img))));

# ╔═╡ 1ff22cf9-ac3b-4849-bd4c-8b0d3a901447
circshift([1,2,3], 1)

# ╔═╡ c091d147-4874-4a19-9855-c78ded59b67a
gray_show(real.(fft(Cₙ[1])))

# ╔═╡ c922f283-3b31-43bd-9855-933be8a9e095
md"### 4 Test"

# ╔═╡ 460a8ea2-d077-48fc-804e-7367ac16eff8
# check if three output array
PlutoTest.@test size(extract_components([randn((2,2)) for i = 1:3])) == (3,)

# ╔═╡ ba0e90f8-a310-40f9-a306-fb8700ea473f
# check if structure and type is ok
PlutoTest.@test typeof(extract_components([randn((2,2)) for i = 1:3])) == Vector{Matrix{ComplexF64}}

# ╔═╡ 5d263158-03b0-4c87-ad90-a81c24493b66
# check a random example for correctness
PlutoTest.@test extract_components([[1 2; 3 4], [1 3; 4 5], [1 10; 20 30]]) ≈ Matrix{ComplexF64}[[16.50000000000001 - 0.8660254037844412im -5.500000000000003 + 0.2886751345948141im; -11.500000000000005 + 0.28867513459481564im 0.5000000000000002 + 0.28867513459481253im], [28.00000000000001 + 1.5597088666161704e-15im -8.000000000000002 - 3.563506943082403e-16im; -16.000000000000007 - 6.01679086153965e-16im 1.1102230246251565e-16 - 1.1102230246251565e-16im], [16.5 + 0.8660254037844446im -5.5 - 0.2886751345948149im; -11.5 - 0.28867513459481736im 0.5 - 0.2886751345948127im]]

# ╔═╡ 515d2174-56e3-4ebc-a475-b84f911a5a2f
md"# 5 Reconstruction
Having the three parts of the Fourier space separated, we can try to combine them in a reasonable way to one Fourier spectrum.
Simply adding them would not be optimal with respect to Signal-to-Noise-Ratio (SNR).
"

# ╔═╡ a97d7c94-e69b-44cd-ae4c-61d8d86808a5
"""
	reconstruct(psf, Cₙ, fourier_space_shift)

`psf` is the PSF of the single image (without SIM part).
`Cₙ` is an vector containing the three components of the Fourier spectrum.
`fourier_space_shift` is the shift the three pictures are shifted with respect to the center.

## Procedure
* First shift the spectrum (`otf = fft(psf)`) by `fourier_space_shift` along the first dimension (`circshift`)
* apply the same shift for the weighting factors
* Combine the Fourier spectra using _weighted averaging_ (see slides)
* also return the effective OTF
"""
function reconstruct(psf, Cₙ, fourier_space_shift)
	# Int shift
	Δ = round(Int, fourier_space_shift)

	otf = fft(psf)
	# TODO fix the weights
	w₋₁ = 0.5 * circshift(otf, +Δ)
	w₀ = otf
	w₁ =  0.5 * circshift(otf, -Δ)

	# TODO fix the mixing
	C₋₁ = circshift(Cₙ[1], +Δ)
	C₀ = Cₙ[2]
	C₁ = circshift(Cₙ[3], -Δ)

	# the small 1f-8 factor is added to prevent division by zero
	res_fourier_space = ((w₋₁ .* C₋₁ .+ w₀ .* C₀ .+ w₁ .* C₁ .+ 1f-8^2) ./
		  (w₋₁ .+ w₀ .+ w₁ .+ 1f-8))

	# todo calculate similarly to res the eff_otf but adapt it according to the slides
	eff_otf = ((w₋₁.^2 .+ w₀.^2 .+ w₁.^2 .+ 1f-8^2) ./
		      (w₋₁ .+ w₀ .+ w₁ .+ 1f-8))

	# todo adapt res_fourier_space to be a real space image
	res = real(ifft(res_fourier_space))

	# don't change return args
	return res, eff_otf
end

# ╔═╡ 9e64def4-b397-4292-8fe9-8a828e08ee0a
res, eff_otf = reconstruct(psf, Cₙ, fourier_space_shift);

# ╔═╡ 54fa3a01-5a8d-4dd3-a521-a60a2f69b541
md"Unfiltered reconstruction"

# ╔═╡ d9c33b8c-6ccf-462f-b5c2-267fd7b93ba3
gray_show(res)

# ╔═╡ 2f14fb93-91bc-44af-8a6d-cebaa1607a51
md"Unfiltered Fourier spectrum"

# ╔═╡ fd53c66d-ba90-4d96-9a11-eabc1dd666dc
gray_show(log1p.(abs.(ft(res))))

# ╔═╡ d7091577-a5f9-4edf-b96f-88b13d9c6f3c
gray_show(log1p.(abs.(ft(img))))

# ╔═╡ 19546bad-d7be-4c09-a248-f7e98c53d42d
"""
	wiener_filter(img, otf, ϵ)

Copy the wiener filter from previous homeworks and adapt it (OTF instead of the PSF is passed).

"""
function wiener_filter(img, otf, ϵ)
	S = 1
	# wiener kernel in fourier space
	W = conj.(otf) .* S ./ (abs2.(otf) .* S .+ ϵ)

	img = real(ifft(fft(img) .* W))
	return img
end

# ╔═╡ 33c156b6-5c18-4377-9920-32c7865898e9
md"### Final Inspection"

# ╔═╡ 0cdf07a5-a87e-449e-ab09-871924cee6d1
md"
If everything is correct, we should definitely see an resolution improvement in the left image (the SIM reconstruction).
The resolution increase is mainly along the first dimension.
"

# ╔═╡ 0a729667-599a-41f1-98f3-ff94608c15bb
[ gray_show(wiener_filter(res, eff_otf, 1e-3)) gray_show(wiener_filter(poisson(conv(img, psf), 300), fft(psf), 1e-3)) gray_show(img)]

# ╔═╡ 0d78c765-1912-4ded-a5e8-aa98afe73cb4
gray_show(log1p.(abs.(ft(wiener_filter(res, eff_otf, 1e-1)))));

# ╔═╡ 5566b0c6-ac96-4342-a62b-0c292469c2eb
[gray_show(wiener_filter(res, eff_otf, 1e-3)) gray_show(wiener_filter(poisson(conv(img, psf), 300), fft(psf), 1e-3))]

# ╔═╡ 3bed0c72-ab5c-4dbf-85a5-6f6d062dec97
md"The left image shows the effective OTF for the SIM system.
On the right we can see the OTF of the same microscope without structured illumination"

# ╔═╡ d32536af-b369-4b39-87f6-4e1172d971ae
[gray_show(fftshift(log1p.(abs.(eff_otf))))  gray_show(log1p.(abs.(ffts(psf))))]

# ╔═╡ 35cdd4f9-87f7-408e-a3fd-7456b7238dd5
md"## 5 Test"

# ╔═╡ 1320b506-d378-4fa1-bbf1-c2f994abfff1
PlutoTest.@test wiener_filter([1, 2, 3, 4, 5], [1, -2, 3, 4, 0], 0.001) ≈ [2.9553001744303087, 3.5816540338359086, 2.9970029970029977, 2.4123519601700862, 3.0387058195756858]

# ╔═╡ Cell order:
# ╠═250252f0-6e61-11ec-2ffe-ebddaa9f0fb3
# ╠═0eeb5bb1-bb41-43cc-8c14-eb9c1ebf6fad
# ╠═1b2ebe1a-f3a7-494c-bcca-18df5403ee65
# ╟─4b46e310-cc26-4a96-8c2e-0c89307d4e34
# ╟─7054db2c-606e-48eb-ab93-8c0260cb7a81
# ╠═edaacc75-fd91-4d46-a31f-21e738253708
# ╠═86e0df4f-4dfe-40c3-a6d3-1a394442a9f7
# ╠═963c4f23-e97b-41fd-9423-1080fae412e9
# ╠═b6061e31-e06a-45ad-86f0-006b8309cca7
# ╠═cd5bef41-6246-4d88-91d9-c83b7a47e110
# ╠═29e6311e-eb5b-4073-81dd-7372ac06498e
# ╠═c4c8b57b-e09a-43f8-b371-bb1f0761215d
# ╠═599aabf7-6333-4272-ae07-fec07bdb02aa
# ╠═920c26f4-5750-4c83-871d-9622b3dca134
# ╟─a550fc7f-ff7c-4fd5-9e40-d8ed00da200f
# ╠═7cba5a99-21a7-45d6-a92a-7eb1e75e39d3
# ╠═eafd2e8a-6bbd-4fe4-a5fd-d721b78604e1
# ╠═75f2ea94-ba30-48d3-b676-8423f14c453c
# ╠═9b856e9c-b818-4e49-a27d-0c0db623698e
# ╠═11009f9c-de4c-4ade-ad30-375ed47c3901
# ╟─4ff25625-e450-47e4-97ae-bb24b903ed98
# ╠═e62fc2f1-a1e2-4b8a-be08-94444e12b533
# ╠═ca8618e8-8389-4d30-8112-af0e987de417
# ╠═20aaf9b2-2660-413f-90a8-1f4fdc91d8c7
# ╠═4898da63-4da0-4538-b08d-cd4fe419584b
# ╠═8457cfeb-705f-41e8-a203-298c8ee4a7ab
# ╠═28784e50-285f-47a8-a311-4122ec8f47ce
# ╟─6e2814c9-5644-47b6-8a6e-3308e1623d5e
# ╠═ca0e55ca-ec4f-4cd8-a1f7-a2a63ba9782d
# ╠═fa957648-0b4a-48eb-96d5-bb731966693c
# ╠═287d85ae-efba-4095-b7ce-58d90bd22492
# ╠═fce697cc-67d1-4373-b2b0-a6a60b8a5893
# ╠═b961b3e0-6935-4f29-9f94-be6e21f302e3
# ╟─6e904b1d-2071-4d48-ad21-ab180e333474
# ╠═e3994f91-a7bc-4811-b6e6-1f67692ff554
# ╠═71af2b63-475c-429f-a15e-9a212f724a69
# ╠═2a78c81d-f8f9-4d99-9c24-1aefb9941294
# ╠═d68d9f24-bcea-4bcc-b597-3c051e77d99c
# ╟─23c98291-4c72-4a60-b722-910eadaf7e77
# ╠═9a75e192-9605-4787-acee-706ee7c90549
# ╟─524b208d-7fc3-4d96-bdc1-2bed72f2a9ee
# ╠═2c1a5b9e-8b1c-4619-b303-69d29181a5c4
# ╠═bb1e5dd4-0159-47b5-a803-5327952438ee
# ╟─09593275-85bf-42ee-860e-ebbb74866a40
# ╠═1103b35d-cb86-4970-beb7-c8463e374f78
# ╠═ba74ec4a-b38f-4fcb-9b43-2448976153eb
# ╠═ab56310f-671d-4b54-b9c2-9be102c826e3
# ╠═db20d906-b22f-4f16-b355-eba9c82809d7
# ╟─b7f11d85-6859-4b5a-814e-003946fbd894
# ╠═96b8f1e6-9159-4616-be08-c88f17e9829e
# ╠═53af3f77-cbc2-4809-8dda-743e096df10c
# ╟─4c136f35-e489-4d6e-83e0-e153bfc7040d
# ╠═bf97497a-e149-4552-9bdd-9fbeb5c24987
# ╠═b3528818-74f7-471b-8d33-ad7b9fcfc70b
# ╠═1ff22cf9-ac3b-4849-bd4c-8b0d3a901447
# ╠═c091d147-4874-4a19-9855-c78ded59b67a
# ╟─c922f283-3b31-43bd-9855-933be8a9e095
# ╠═460a8ea2-d077-48fc-804e-7367ac16eff8
# ╠═ba0e90f8-a310-40f9-a306-fb8700ea473f
# ╠═5d263158-03b0-4c87-ad90-a81c24493b66
# ╟─515d2174-56e3-4ebc-a475-b84f911a5a2f
# ╠═a97d7c94-e69b-44cd-ae4c-61d8d86808a5
# ╠═9e64def4-b397-4292-8fe9-8a828e08ee0a
# ╟─54fa3a01-5a8d-4dd3-a521-a60a2f69b541
# ╠═d9c33b8c-6ccf-462f-b5c2-267fd7b93ba3
# ╟─2f14fb93-91bc-44af-8a6d-cebaa1607a51
# ╠═fd53c66d-ba90-4d96-9a11-eabc1dd666dc
# ╠═d7091577-a5f9-4edf-b96f-88b13d9c6f3c
# ╠═19546bad-d7be-4c09-a248-f7e98c53d42d
# ╟─33c156b6-5c18-4377-9920-32c7865898e9
# ╟─0cdf07a5-a87e-449e-ab09-871924cee6d1
# ╠═0a729667-599a-41f1-98f3-ff94608c15bb
# ╠═0d78c765-1912-4ded-a5e8-aa98afe73cb4
# ╠═5566b0c6-ac96-4342-a62b-0c292469c2eb
# ╟─3bed0c72-ab5c-4dbf-85a5-6f6d062dec97
# ╠═d32536af-b369-4b39-87f6-4e1172d971ae
# ╟─35cdd4f9-87f7-408e-a3fd-7456b7238dd5
# ╠═1320b506-d378-4fa1-bbf1-c2f994abfff1


#========================================================================================#
#	Laboratory 5
#
# Exploratory data analysis, datasets and Dataframes.
#
# Author: Niall Palfreyman, 04/01/2022
#========================================================================================#
[
	Activity(
		"""
		In this laboratory we use Exploratory Data Analysis (EDA) and the Julia DataFrames
		package to explore one of the over 750 industry standard statistical RDatasets.

		Since we haven't used the RDatasets package before, we must first add it to our Julia
		system. To do this, type ']' at the Julia prompt - this starts Julia's Package Manager,
		which you can exit at any time by typing <Backspace>. Within the Package Manager, enter:

		add RDatasets

		This will download and compile around 200 packages that are relevant to the RDatasets
		package (this may take a couple of minutes). When it has finished, exit Package Manager,
		then enter:

		using RDatasets						# Takes a few seconds ...
		RDatasets.datasets()

		How many Rows are in the "affairs" dataset?
		""",
		"\"affairs\" is the first dataset in the list of datasets that datasets() prints out.",
		x -> x==601
	),
	Activity(
		"""
		OK, now we've got RDatasets up and running, we'll look specifically at the Iris dataset
		compiled by Edgar Anderson and Ronald Fisher in the early days of genetics research in
		1936. This dataset contains the length and width (in cm) of the sepals and petals of 50
		samples from each of several iris flower species. Enter this code to load the Iris dataset:

		iris = dataset("datasets","iris")

		What is the SepalLength of the first specimen in iris?
		""",
		"This is the specimen in Row 1",
		x -> x==5.1
	),
	Activity(
		"""
		The Iris dataset is an object iris of type DataFrame: a table of rows and columns. Each
		row represents a single specimen, and each column represents an attribute field of that
		specimen. To view the fields of the Iris dataset, enter names(iris). How many fields
		does iris have?
		""",
		"",
		x -> x==5
	),
	Activity(
		"""
		What is the size() of the dataset?
		""",
		"size(iris)",
		x -> x==size(Main.iris)
	),
	Activity(
		"""
		We can view the first five rows of iris using the usual array indexing with square
		brackets []:

		iris[1:5,:]

		Report back to me the last 5 rows of the Iris dataset:
		""",
		"iris[end-4:end,:]",
		x -> x==Main.iris[end-4:end,:]
	),
	Activity(
		"""
		We've seen that the first rows of iris concern the species setosa, and the last rows
		concern the species virginica. What other species does iris contain. To find out, we
		want to group the dataset by Species using the command:

		species_groups = groupby(iris,"Species")

		How many groups are contained in this grouping of the data?
		""",
		"size(species_groups)",
		x -> x==3
	),
	Activity(
		"""
		OK, so we've seen that the first of three Species groups contains "iris setosa" specimens,
		and the last Species group contains "iris virginica" specimens. To inspect all three
		Species groups, we will combine specimens from each group into a single row:

		combine(species_groups,nrow)

		What is the name of the second iris species?
		""",
		"",
		x -> occursin(lowercase(x),"versicolor")
	),
	Activity(
		"""
		Don't worry if you don't yet understand how to use the combine() function - we'll soon
		find out! For now, we'd like to understand the Iris dataset a little better. A good
		first step is to describe() the dataset - this provides an overview of each attribute
		column in the table. What is the median petal length of all 150 iris specimens?
		""",
		"describe(iris)",
		x -> x==4.35
	),
	Activity(
		"""
		We can also extend the describe() function using additional descriptors such as the
		25th percentile (:q25) or the standard deviation (:std). Which iris attribute has the
		greatest standard deviation?
		""",
		"describe(iris,:std)",
		x -> x=="PetalLength"
	),
	Activity(
		"""
		We can also calculate these values ourselves by loading and applying the functions
		in Julia's Statistics package:

		using Statistics
		std(iris[:,"PetalLength"])

		What is the median sepal length?
		""",
		"median(iris[:,\"SepalLength\"])",
		x -> x==5.8
	),
	Activity(
		"""
		Let's investigate the covariance attributes in the Iris database. Enter this code:

		attributes = names(iris)[1:end-1]				# Leave out the non-numeric Species
		for x in attributes, y in attributes
			if x > y
				println( "\$x \t \$y \t \$(cov(iris[:,x],iris[:,y]))")
			end
		end

		Look at the results: Unsurprisingly, PetalWidth covaries with PetalLength, but
		SepalLength covaries with both PetalLength and PetalWidth. With which of these
		does SepalLength covary least strongly?
		""",
		"",
		x -> x=="PetalWidth"
	),
	Activity(
		"""
		This is what we might expect - after all, there seems no particular reason why SepalLength
		should covary strongly PetalWidth. But wait! Covariance is a measure that depends on the
		absolute size of the variables! Of PetalLength and PetalWidth, which has the greater
		mean value?
		""",
		"mean(iris[:,\"PetalLength\"]",
		x -> x=="PetalLength"
	),
	Activity(
		"""
		Hm. This is tricky. SepalLength covaries with PetalLength more strongly than with
		PetalWidth, however it may be that the smaller covariance with PetalWidth arises simply
		from the fact that PetalWidth values are much smaller than PetalLength. We can solve this
		question using correlation instead of covariance, because correlation is a relative
		measure rather than an absolute one.

		Write code to compare the two correlation values for SepalLength-PetalLength and for
		SepalLength-PetalWidth. Then find out the percentage difference between these two
		values:
		""",
		"100 * (cor(SL,PL)-cor(SL,PW))/cor(SL,PL)",
		x -> abs(x-6.173) < 0.1
	),
	Activity(
		"""
		We see that the correlation of PetalWidth is definitely major - almost as large as the
		correlation PetalLength to SepalLength. What biological function of sepals might explain
		why SepalLength correlates so highly with PetalWidth?
		""",
		"Both length and width of petals increases the bud volume that sepals must enclose.",
		x -> true
	),
	Activity(
		"""
		We can use the columns of iris as ranges for generating random values, for example:

		rand(iris[:,"SepalLength"])

		Create a Vector containing a random sample of 21 PetalWidths:
		""",
		"rand(range,10)",
		x -> length(x)==21 && all(map(x) do pw in(pw,Main.iris[:,"PetalWidth"]) end)
	),
	Activity(
		"""
		Let's create our own dataframe of the Group I chemical elements. It is a bad idea to
		clutter our global namespace with lots of trivial names, so we'll create the dataframe
		inside a function using local names:

		function group1()
			symbol = ["Li","Na","K","Rb","Cs","Fr"]
			protons = [3,11,19,37,55,87]
			nucleons = [7,23,39,85,133,223]
			electronegativity = [1.0,0.9,0.8,0.8,0.7,0.7]
			DataFrame(; symbol, protons, nucleons, electronegativity)
		end

		You may first need to add the DataFrames package and load it with the keyword "using".
		Now enter the function group1(), use it to create a dataframe named grp1, then extract
		from grp1 the subtable consisting of the 3rd and 4th rows:
		""",
		"",
		x -> size(x)==(2,4) && x[1:2,"symbol"] == ["K","Rb"]
	),
	Activity(
		"""
		Finally, we'll learn to save and load dataframes to and from a file. First we'll do it
		simply, using comma-separated variable (CSV) files. Add and load the CSV package, then
		write the group1 dataframe to a CSV file:

		CSV.write( "group1.csv", group1())

		Now give me the result of reading the csv file:

		str = read("group1.csv",String);
		""",
		"",
		x -> x[43:44] == "Li"
	),
	Activity(
		"""
		Sometimes we want to write to other file formats, such as Excel files, and Julia offers
		us a range of packages such as XLSX for doing this. However, the advantage of csv files
		for small projects is that we can read them into text strings and with a text editor.

		We can also read our csv file as a DataFrame. Give me the following dataframe:

		CSV.read("group1.csv",DataFrame)
		""",
		"",
		x -> typeof(x)==Main.DataFrame && size(x)==(6,4)
	),
]


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

module ttest
using ..tenedyn
using ..datastructs
using ..ode
using PyPlot
PyPlot.pygui(true)

struct OdeParams_PiPt1

    setpoint_amplitude::Float64
    setpoint_time_delay::Float64
    ctrl_kp::Float64
    ctrl_ki::Float64
    ctrl_integral_init_value::Float64
    plant_T::Float64
    plant_integral_init_value::Float64

    function OdeParams_PiPt1()
        setpoint_amplitude  = 2.0
        setpoint_time_delay = 0.5

        ctrl_kp = 10.0
        ctrl_ki = 5.0
        ctrl_integral_init_value = 0.0

        plant_T = 0.2
        plant_integral_init_value = 0.5

        return new(
            setpoint_amplitude,
            setpoint_time_delay,
            ctrl_kp,
            ctrl_ki,
            ctrl_integral_init_value,
            plant_T,
            plant_integral_init_value)
    end
end

function step(time, a, td)
    return time >= td ? a : zero(typeof(time))
end

function manual_ode_fcn(der_q, time, q, params::OdeParams_PiPt1)

    u2 = q[1]
    x  = q[2]

    e  = step(time, params.setpoint_amplitude, params.setpoint_time_delay) - x
    u1 = params.ctrl_kp * e
    u  = u1 + u2

    der_q[1] = params.ctrl_ki * e
    der_q[2] = (u - x) / params.plant_T

    return der_q
end

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

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

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

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

    sensor = tenedyn.Identity()

    model = tenedyn.Model()

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

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

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

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

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

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

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

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

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

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

    return model
end

function create_parametrized_model()

    # model parameters
    setpoint_amplitude  = 2.0
    setpoint_time_delay = 0.5

    ctrl_kp = 10.0
    ctrl_ki = 5.0
    ctrl_integral_init_value = 0.0

    plant_T = 0.2
    plant_integral_init_value = 0.5

    @time model = create_model_pi_controller_pt1(
        setpoint_amplitude,
        setpoint_time_delay,
        ctrl_kp,
        ctrl_ki,
        ctrl_integral_init_value,
        plant_T,
        plant_integral_init_value)

    return model
end

function create_odefcn_pi_controller_pt1()

    # model parameters
    setpoint_amplitude  = 2.0
    setpoint_time_delay = 0.5

    ctrl_kp = 10.0
    ctrl_ki = 5.0
    ctrl_integral_init_value = 0.0

    plant_T = 0.2
    plant_integral_init_value = 0.5

    @time model = create_model_pi_controller_pt1(
        setpoint_amplitude,
        setpoint_time_delay,
        ctrl_kp,
        ctrl_ki,
        ctrl_integral_init_value,
        plant_T,
        plant_integral_init_value)

    @time (forest, integrator_indices, tree_node_vecvec, evalulation_indices_vecvec) = tenedyn.compile(model)

    ode_fcn = tenedyn.create_ode_function(
        model,
        forest,
        integrator_indices,
        tree_node_vecvec,
        evalulation_indices_vecvec)

    return ode_fcn
end

function run_simulation(ode_fcn)

    time_interval = ode.TimeInterval(50.0)
    init_cond     = [0.0, 0.5]

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

    ode_solver = ode.Tsitouras45()

    basic_options = ode.BasicOdeOptions(
        ode_problem;
        abs_tol = 1e-5,
        rel_tol = 1e-5,
        max_iter = 1000
    )

    step_size_controller = ode.ClassicalStepSizeControl()

    stepper_options = ode.StepperOptions(;
        max_step_size = 5.0)

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

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

    return ode_res
end

function plot_ode_res(ode_res)

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

    figh = PyPlot.figure()
    a1 = PyPlot.subplot(2,1,1)

    PyPlot.grid()
    PyPlot.plot( t, q[1, :] )

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

    return figh
end

# after creation of a function string from the tree, only this is needed for more speed:
# fcn_str         = "fcn(t) = sin(t) + 3"
# parsed_expr     = Meta.parse(fcn_str)
# callable_object = eval(parsed_expr)

#=
ode_fcn_str_prefix  = "ode_fcn(der_q, time, q) = "
str_begin           = "begin "
str_end             = " end"
body                = "der_q[1] = - 0.2 * q[1] + sin(time) ;"

ode_fcn_str = ode_fcn_str_prefix * str_begin * body * str_end
=#

function create_callable(ode_fcn_str)

    parsed_expr = Meta.parse(ode_fcn_str)
    ode_fcn     = eval(parsed_expr)

    return ode_fcn
end

function sim2(ode_fcn)

    time_interval = ode.TimeInterval(5.0)
    init_cond     = [0.0, 0.5]

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

    ode_solver = ode.Tsitouras45()

    basic_options = ode.BasicOdeOptions(
        ode_problem;
        abs_tol = 1e-5,
        rel_tol = 1e-5,
        max_iter = 1000
    )

    step_size_controller = ode.ClassicalStepSizeControl()

    stepper_options = ode.StepperOptions(;
        max_step_size = 0.1)

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

    return ode_res
end

function plot_sim2(ode_res)

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

    figh = PyPlot.figure()
    PyPlot.grid()
    PyPlot.plot( t, q[1, :] )

    return figh
end

model = create_parametrized_model()

(forest,
integrator_indices,
tree_node_vecvec,
evaluation_indices_vecvec) = tenedyn.compile(model)

@time fcn_body = tenedyn.model_to_string(
    model,
    forest,
    integrator_indices,
    tree_node_vecvec,
    evaluation_indices_vecvec)

function create_callable_as_closure(ode_fcn_str, model)
    parsed_expr             = Meta.parse(ode_fcn_str)
    gen_ode_fcn             = eval(parsed_expr)
    ode_fcn(der_q, time, q) = gen_ode_fcn(der_q, time, q, model)

    return ode_fcn
end

ode_fcn = create_callable_as_closure(fcn_body, model)
# ode_res = sim2(ode_fcn);

end # module


# push!(LOAD_PATH, joinpath(@__DIR__, "..", ".."))
using JLD2
jlfile = jldopen("data/local.jld2", "a+")
# jlfile = jldopen("data/nonlocal.jld2", "a+")
# jlfile = jldopen("data/nonlocal_more_velocity.jld2", "a+")

κ¹¹ = jlfile["averagelocaldiffusivity"]["κ11"]
κ¹² = jlfile["averagelocaldiffusivity"]["κ12"]
κ²¹ = jlfile["averagelocaldiffusivity"]["κ21"]
κ²² = jlfile["averagelocaldiffusivity"]["κ22"]
z = jlfile["grid"]["z"][:]
x = jlfile["grid"]["x"][:]
##
# Contruct local diffusivity estimate from flow field and γ
γ = jlfile["parameters"]["γ"]
ω = jlfile["parameters"]["ω"]
u¹ = jlfile["velocities"]["u¹"]
u² = jlfile["velocities"]["u²"]
v¹ = jlfile["velocities"]["v¹"]
v² = jlfile["velocities"]["v²"]

# integrating out the first dimension amounts to assuming that the dominant mode
# is the zero'th.
N = length(x)
scale = (2 * (ω^2 + γ^2))
analytic_κ¹¹ = sum((γ * u¹ .* u¹ + γ * u² .* u²) ./ scale, dims = 1)[:] ./ N
analytic_κ¹² = sum((u¹ .* (γ * v¹ + ω * v²) + u² .* (γ * v² - ω * v¹)) ./ scale, dims = 1)[:] ./ N
analytic_κ²¹ = sum((v¹ .* (γ * u¹ + ω * u²) + v² .* (γ * u² - ω * u¹)) ./ scale, dims = 1)[:] ./ N
analytic_κ²² = sum((γ * v¹ .* v¹ + γ * v² .* v²) ./ scale, dims = 1)[:] ./ N

##
using GLMakie

options = (; ylabel = "z", ylabelsize = 32,
    xlabelsize = 32, xgridstyle = :dash, ygridstyle = :dash, xtickalign = 1,
    xticksize = 30, ytickalign = 1, yticksize = 30,
    xticklabelsize = 30, yticklabelsize = 30)

fig = Figure(resolution = (1800, 1300), title = "Local Operators")
titlestring = "Kˣˣ"
ax1 = Axis(fig[1, 1]; options..., title = titlestring, titlesize = 30)
titlestring = "Kˣᶻ"
ax2 = Axis(fig[1, 2]; options..., title = titlestring, titlesize = 30)
titlestring = "Kᶻˣ"
ax3 = Axis(fig[2, 1]; options..., title = titlestring, titlesize = 30)
titlestring = "Kᶻᶻ"
ax4 = Axis(fig[2, 2]; options..., title = titlestring, titlesize = 30)

plot_string_1 = "analytic"
plot_string_2 = "numerical"

ln1 = lines!(ax1, analytic_κ¹¹, z, color = :red)
sc1 = scatter!(ax1, κ¹¹, z, color = :blue)
axislegend(ax1, [ln1, sc1], [plot_string_1, plot_string_2], position = :rc, labelsize = 30)

ln2 = lines!(ax2, analytic_κ¹², z, color = :red)
sc2 = scatter!(ax2, κ¹², z, color = :blue)
# axislegend(ax2, [ln2, sc2], [plot_string_1, plot_string_2], position = :rt, labelsize = 30)
hideydecorations!(ax2)

ln3 = lines!(ax3, analytic_κ²¹, z, color = :red)
sc3 = scatter!(ax3, κ²¹, z, color = :blue)
# axislegend(ax3, [ln3, sc3], [plot_string_1, plot_string_2], position = :rb, labelsize = 30)

ln4 = lines!(ax4, analytic_κ²², z, color = :red)
sc4 = scatter!(ax4, κ²², z, color = :blue)
hideydecorations!(ax4)
# axislegend(ax4, [ln4, sc4], [plot_string_1, plot_string_2], position = :rt, labelsize = 30)
# colgap!(ax1, 10)
# rowgap!(ax1, 10)

for ax in [ax1, ax2, ax3, ax4]
    # xlims!(ax, (-0.6, 0.6))
end

display(fig)

##
# Check x-independence of local diffusivity estimate
κ¹¹ = jlfile["localdiffusivity"]["κ11"]
κ¹² = jlfile["localdiffusivity"]["κ12"]
κ²¹ = jlfile["localdiffusivity"]["κ21"]
κ²² = jlfile["localdiffusivity"]["κ22"]
z = jlfile["grid"]["z"][:]
x = jlfile["grid"]["x"][:]
fig = Figure()
ax1 = Axis(fig[1, 1])
ax2 = Axis(fig[2, 1])
ax3 = Axis(fig[1, 2])
ax4 = Axis(fig[2, 2])
cmap = :balance
heatmap!(ax1, x, z, κ¹¹, colormap = cmap, interpolate = true)
heatmap!(ax2, x, z, κ²¹, colormap = cmap, interpolate = true)
heatmap!(ax3, x, z, κ¹², colormap = cmap, interpolate = true)
heatmap!(ax4, x, z, κ²², colormap = cmap, interpolate = true)
display(fig)


using ILog2
using Test

# FIXME: Tests for other floating point types

@testset "ILog2" begin
    bitstypes = (Int8, Int16, Int32, Int64,
                 UInt8, UInt16, UInt32, UInt64, Int128, UInt128)
    largest_Float64_corresponding_to_unique_integer = 2^53 - 1
    for T in  bitstypes
        N = min(typemax(T) - 1, largest_Float64_corresponding_to_unique_integer)
        num_trials_per_type = min(N, 10^4)
        nums = rand(T(1):T(N), num_trials_per_type)
        for n in nums
            @test ilog2(T(n)) == ilog2(float(n))
        end
    end
end

@testset "BigInt" begin
    for expt in rand(10:10^4, 10^3)
        n = big(2)^expt
        np = n + rand(1:100)
        nm = n - rand(1:100)
        @test ilog2(n) == expt
        @test ilog2(np) == expt
        @test ilog2(nm) == expt - 1
        @test ilog2(n) == ilog2(float(n))
    end
end

@testset "exceptions" begin
    @test_throws ArgumentError ILog2.msbindex(BigInt)
    @test_throws InexactError  ilog2(float(typemax(Int)-2^8))
    @test_throws DomainError ilog2(0)
    @test_throws DomainError ilog2(-1)
end


# z.jl
# Various forms of z-tests
#
# Copyright (C) 2016   John Myles White
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

export OneSampleZTest, TwoSampleZTest, EqualVarianceZTest,
    UnequalVarianceZTest

abstract type ZTest <: HypothesisTest end
abstract type TwoSampleZTest <: ZTest end

pvalue(x::ZTest; tail=:both) = pvalue(Normal(0.0, 1.0), x.z; tail=tail)

default_tail(test::ZTest) = :both

# confidence interval by inversion
function StatsBase.confint(x::ZTest, alpha::Float64=0.05; tail=:both)
    check_alpha(alpha)

    if tail == :left
        (-Inf, StatsBase.confint(x, alpha*2)[2])
    elseif tail == :right
        (StatsBase.confint(x, alpha*2)[1], Inf)
    elseif tail == :both
        q = cquantile(Normal(0.0, 1.0), alpha/2)
        (x.xbar-q*x.stderr, x.xbar+q*x.stderr)
    else
        throw(ArgumentError("tail=$(tail) is invalid"))
    end
end

## ONE SAMPLE Z-TEST

struct OneSampleZTest <: ZTest
    n::Int       # number of observations
    xbar::Real   # estimated mean
    stderr::Real # population standard error
    z::Real      # t-statistic
    μ0::Real     # mean under h_0
end

testname(::OneSampleZTest) = "One sample z-test"
population_param_of_interest(x::OneSampleZTest) = ("Mean", x.μ0, x.xbar) # parameter of interest: name, value under h0, point estimate

function show_params(io::IO, x::OneSampleZTest, ident="")
    println(io, ident, "number of observations:   $(x.n)")
    println(io, ident, "z-statistic:              $(x.z)")
    println(io, ident, "population standard error: $(x.stderr)")
end

"""
    OneSampleZTest(xbar::Real, stddev::Real, n::Int, μ0::Real = 0)

Perform a one sample z-test of the null hypothesis that `n` values with mean `xbar` and
population standard deviation `stddev`  come from a distribution with mean `μ0` against the
alternative hypothesis that the distribution does not have mean `μ0`.

Implements: [`pvalue`](@ref), [`confint`](@ref)
"""
function OneSampleZTest(xbar::Real, stddev::Real, n::Int, μ0::Real=0)
    stderr = stddev/sqrt(n)
    z = (xbar-μ0)/stderr
    OneSampleZTest(n, xbar, stderr, z, μ0)
end

"""
    OneSampleZTest(v::AbstractVector{T<:Real}, μ0::Real = 0)

Perform a one sample z-test of the null hypothesis that the data in vector `v` comes from
a distribution with mean `μ0` against the alternative hypothesis that the distribution
does not have mean `μ0`.

Implements: [`pvalue`](@ref), [`confint`](@ref)
"""
OneSampleZTest(v::AbstractVector{T}, μ0::Real=0) where {T<:Real} = OneSampleZTest(mean(v), std(v), length(v), μ0)

"""
    OneSampleZTest(x::AbstractVector{T<:Real}, y::AbstractVector{T<:Real}, μ0::Real = 0)

Perform a paired sample z-test of the null hypothesis that the differences between pairs of
values in vectors `x` and `y` come from a distribution with mean `μ0` against the
alternative hypothesis that the distribution does not have mean `μ0`.

Implements: [`pvalue`](@ref), [`confint`](@ref)
"""
function OneSampleZTest(x::AbstractVector{T}, y::AbstractVector{S}, μ0::Real=0) where {T<:Real, S<:Real}
    check_same_length(x, y)

    OneSampleZTest(x - y, μ0)
end

## TWO SAMPLE Z-TEST (EQUAL VARIANCE)

struct EqualVarianceZTest <: TwoSampleZTest
    n_x::Int     # number of observations
    n_y::Int     # number of observations
    xbar::Real   # estimated mean difference
    stderr::Real # population standard error
    z::Real      # z-statistic
    μ0::Real     # mean difference under h_0
end

function show_params(io::IO, x::TwoSampleZTest, ident="")
    println(io, ident, "number of observations:   [$(x.n_x),$(x.n_y)]")
    println(io, ident, "z-statistic:              $(x.z)")
    println(io, ident, "population standard error: $(x.stderr)")
end

testname(::EqualVarianceZTest) = "Two sample z-test (equal variance)"
population_param_of_interest(x::TwoSampleZTest) = ("Mean difference", x.μ0, x.xbar) # parameter of interest: name, value under h0, point estimate

"""
    EqualVarianceZTest(x::AbstractVector{T<:Real}, y::AbstractVector{T<:Real})

Perform a two-sample z-test of the null hypothesis that `x` and `y` come from distributions
with equal means and variances against the alternative hypothesis that the distributions
have different means but equal variances.

Implements: [`pvalue`](@ref), [`confint`](@ref)
"""
function EqualVarianceZTest(x::AbstractVector{T}, y::AbstractVector{S}, μ0::Real=0) where {T<:Real,S<:Real}
    nx, ny = length(x), length(y)
    xbar = mean(x) - mean(y)
    stddev = sqrt(((nx - 1) * var(x) + (ny - 1) * var(y)) / (nx + ny - 2))
    stderr = stddev * sqrt(1/nx + 1/ny)
    z = (xbar - μ0) / stderr
    EqualVarianceZTest(nx, ny, xbar, stderr, z, μ0)
end

## TWO SAMPLE Z-TEST (UNEQUAL VARIANCE)

struct UnequalVarianceZTest <: TwoSampleZTest
    n_x::Int     # number of observations
    n_y::Int     # number of observations
    xbar::Real   # estimated mean
    stderr::Real # empirical standard error
    z::Real      # z-statistic
    μ0::Real     # mean under h_0
end

testname(::UnequalVarianceZTest) = "Two sample z-test (unequal variance)"

"""
    UnequalVarianceZTest(x::AbstractVector{T<:Real}, y::AbstractVector{T<:Real})

Perform an unequal variance two-sample z-test of the null hypothesis that `x` and `y` come
from distributions with equal means against the alternative hypothesis that the
distributions have different means.

Implements: [`pvalue`](@ref), [`confint`](@ref)
"""
function UnequalVarianceZTest(x::AbstractVector{T}, y::AbstractVector{S}, μ0::Real=0) where {T<:Real,S<:Real}
    nx, ny = length(x), length(y)
    xbar = mean(x)-mean(y)
    varx, vary = var(x), var(y)
    stderr = sqrt(varx/nx + vary/ny)
    z = (xbar-μ0)/stderr
    UnequalVarianceZTest(nx, ny, xbar, stderr, z, μ0)
end


# A module that collects all necessary helper functions
module JuTools
export compute_accuracy, scale_data, scale_data!, shuffle_data, shuffle_data!, split_data
export data_generate_linear_2d

import Statistics
import Random

"""
Compute accuracy given prediction and real values
"""
function compute_accuracy(y_pred::Array, y_real::Array)::AbstractFloat
    @assert ndims(y_pred) == ndims(y_real) == 1
    @assert size(y_pred) == size(y_real)
    acc = Statistics.mean(y_pred .== y_real)
    return acc
end

"""
Scale a 2d data, to unit variance
"""
function scale_data(X::Array{T} where T<:Number)::Array
    @assert ndims(X) == 2
    u = Statistics.mean(X, dims=1)
    s = Statistics.std(X, dims=1)
    res = (X .- u) ./ s
    return res
end

"""
Scale a 2d data in place, to unit variance
"""
function scale_data!(X::Array{T} where T<:Number)
    @assert ndims(X) == 2
    u = Statistics.mean(X, dims=1)
    s = Statistics.std(X, dims=1)
    X .= (X .- u) ./ s
    return nothing
end

"""
Shuffle X and y data\\
`X` is 2 dim, `y` is 1 dim\\
Returns shuffled `X` and `y` in Tuple
"""
function shuffle_data(X::Array{T} where T<:Number, y::Array{T} where T<:Number)::Tuple{Array,Array}
    @assert ndims(X) == 2
    @assert ndims(y) == 1
    @assert size(X)[1] == size(y)[1]
    data = hcat(X, y)
    data .= data[Random.randperm(size(data)[1]), :]
    X_shuffled = data[:, 1:end-1]
    y_shuffled = data[:, end]
    return (X_shuffled, y_shuffled)
end

"""
Shuffle X and y data in place\\
`X` is 2 dim, `y` is 1 dim\\
Returns shuffled `X` and `y` in Tuple
"""
function shuffle_data!(X::Array{T} where T<:Number, y::Array{T} where T<:Number)
    @assert ndims(X) == 2
    @assert ndims(y) == 1
    @assert size(X)[1] == size(y)[1]
    data = hcat(X, y)
    data .= data[Random.randperm(size(data)[1]), :]
    X .= data[:, 1:end-1]
    y .= data[:, end]
    return nothing
end

"""
Split X and y data to training set and test set\\
`X` is 2 dim, `y` is 1 dim\\
`ratio` should be in range (0, 1), defines the test data percentage\\
Returns `X_train`, `X_test`, `y_train`, `y_test`
"""
function split_data(X::Array{T} where T<:Number, y::Array{T} where T<:Number;
        shuffle::Bool=true, ratio::AbstractFloat=0.3)::Tuple{Array,Array,Array,Array}
    @assert ndims(X) == 2
    @assert ndims(y) == 1
    @assert size(X)[1] == size(y)[1]
    @assert 0.0 < ratio < 1.0
    if shuffle
        X, y = shuffle_data(X, y)
    end
    m = size(X)[1]
    n = floor(Int, float(m) * ratio)
    # make sure at least one value in the arrays
    n = min(n, m-1)
    n = max(1, n)
    X_test = X[1:n, :]
    y_test = y[1:n]
    X_train = X[n+1:end, :]
    y_train = y[n+1:end]
    return (X_train, X_test, y_train, y_test)
end

# functions for data generation

"""
Generate 2 dimensional data for testing\\
`linear_func` should define a linear function\\
e.g. x1 -> x1 * 2 + 1\\
Returns a Tuple for X and y
"""
function data_generate_linear_2d(;linear_func::Function=k -> k * 2 + 1, data_size::Integer=1000,
        range_min::AbstractFloat=0.0, range_max::AbstractFloat=100.0, range_step::AbstractFloat=0.1,
        random_scale::AbstractFloat=20.0)::Tuple{Array, Array}
    @assert data_size > 0
    @assert range_max > range_min
    @assert range_step > 0
    @assert range_step < (range_max - range_min)
    X_data = Random.rand(range_min:range_step:range_max, (data_size, 2))
    Y_data = Array{Int64}(undef, data_size)
    for i = 1:size(X_data)[1]
        if X_data[i, 2] > linear_func(X_data[i, 1]) + Random.randn() * random_scale
            Y_data[i] = 0
        else
            Y_data[i] = 1
        end
    end
    shuffle_data!(X_data, Y_data)
    return (X_data, Y_data)
end

"""
Generate 2 dimensional data for testing\\
`pos1` and `pos2` define the cluster center of 2 targets\\
`radius1` and `radius2` define the cluster radius\\
`random_scale` defines the scale of randomness\\
Returns a Tuple for X and y, where 2 targets are balanced
"""
function data_generate_cluster_2d(;pos1::Tuple{AbstractFloat, AbstractFloat}=(10.0, 10.0), pos2::Tuple{AbstractFloat, AbstractFloat}=(30.0, 30.0),
        radius1::AbstractFloat=5.0, radius2::AbstractFloat=5.0, random_scale::AbstractFloat=10.0, data_size::Integer=1000)::Tuple{Array, Array}
    @assert radius1 > 0
    @assert radius2 > 0
    @assert data_size >= 2
    data_size_1 = div(data_size, 2)
    data_size_2 = data_size - data_size_1
    step_scale = min(radius1 / 10.0, radius2 / 10.0)
    step_scale = 10.0 ^ (trunc(Int32, step_scale) - 2)
    X_data_1 = Random.rand(-radius1:step_scale:radius1, (data_size_1, 2))
    X_data_2 = Random.rand(-radius2:step_scale:radius2, (data_size_2, 2))
    X_data_1[:, 1] .= X_data_1[:, 1] .+ pos1[1]
    X_data_1[:, 2] .= X_data_1[:, 2] .+ pos1[2]
    X_data_2[:, 1] .= X_data_2[:, 1] .+ pos2[1]
    X_data_2[:, 2] .= X_data_2[:, 2] .+ pos2[2]
    X_data_1 .= X_data_1 .+ (Random.randn(size(X_data_1)) .* random_scale)
    X_data_2 .= X_data_2 .+ (Random.randn(size(X_data_2)) .* random_scale)
    Y_data_1 = ones(data_size_1)
    Y_data_2 = zeros(data_size_2)
    X_data = cat(X_data_1, X_data_2, dims=1)
    Y_data = cat(Y_data_1, Y_data_2, dims=1)
    shuffle_data!(X_data, Y_data)
    return (X_data, Y_data)
end

end


"""
    se_kernel_base(λ, δ)

Squared exonential GP kernel (~Gaussian). Infinitely mean square differentiable
(a.k.a. very smooth).

# Arguments
- `λ::Number`: The kernel lengthscale
- `δ::Number`: The difference between the inputs (e.g. `t1 - t2`)
"""
function se_kernel_base(λ::Number, δ::Number)
    return exp(-δ * δ / (2 * λ * λ))
end

# """
# Periodic kernel (for random cyclic functions)
# SE kernel where δ^2 -> 4 sin(π δ/P)^2
# """
# function periodic_kernel_base(hyperparameters::Vector{<:Number}, δ::Number)
#
#     @assert length(hyperparameters) == 2 "incompatible amount of hyperparameters passed"
#     P, λ = hyperparameters
#
#     sin_τ = sin(π * δ / P)
#     return exp(-2 * sin_τ * sin_τ / (λ * λ))
# end

# "Quasi-periodic kernel"
# function quasi_periodic_kernel_base(hyperparameters::Vector{<:Number}, δ::Number)
#
#     @assert length(hyperparameters) == 3 "incompatible amount of hyperparameters passed"
#     SE_λ, P_P, P_λ = hyperparameters
#
#     return se_kernel_base(SE_λ, δ) * periodic_kernel_base([P_P, P_λ], δ)
# end

# "Ornstein–Uhlenbeck (Exponential) kernel"
# function exp_kernel_base(λ::Number, δ::Number)
#     return exp(-abs(δ) / λ)
# end

# "Exponential-periodic kernel"
# function exp_periodic_kernel_base(hyperparameters::Vector{<:Number}, δ::Number)
#
#     @assert length(hyperparameters) == 3 "incompatible amount of hyperparameters passed"
#     OU_λ, P_P, P_λ = hyperparameters
#
#     return ou_kernel_base(OU_λ, δ) * periodic_kernel_base([P_P, P_λ], δ)
# end

# "general Matern kernel"
# function matern_kernel_base(λ::Number, δ::Number, nu::Number)
#
#     #limit of the function as it apporaches 0 (see https://en.wikipedia.org/wiki/Mat%C3%A9rn_covariance_function)
#     if δ == 0
#         return kernel_amplitude * kernel_amplitude
#     else
#         x = (sqrt(2 * nu) * abs(δ)) / λ
#         return ((2 ^ (1 - nu)) / (gamma(nu))) * x ^ nu * besselk(nu, x)
#     end
# end

# "Matern 3/2 kernel"
# function matern32_kernel_base(λ::Number, δ::Number)
#     x = sqrt(3) * abs(δ) / λ
#     return (1 + x) * exp(-x)
# end

"""
    matern52_kernel_base(λ, δ)

Matern 5/2 kernel. Twice mean square differentiable

# Arguments
- `λ::Number`: The kernel lengthscale
- `δ::Number`: The difference between the inputs (e.g. `t1 - t2`)
"""
function matern52_kernel_base(λ::Number, δ::Number)
    x = sqrt(5) * abs(δ) / λ
    return (1 + x * (1 + x / 3)) * exp(-x)
end

# "Matern 7/2 kernel"
# function matern72_kernel_base(λ::Number, δ::Number)
#     x = sqrt(7) * abs(δ) / λ
#     return (1 + x * (1 + x * (2 / 5 + x / 15))) * exp(-x)
# end

# "Matern 9/2 kernel"
# function matern92_kernel_base(λ::Number, δ::Number)
#     x = 3 * abs(δ) / λ
#     return (1 + x * (1 + x * (3 / 7 + x * (2 / 21 + x / 105)))) * exp(-x)
# end

"""
    pp_kernel_base(λ, δ)

Piecewise polynomial kernel. Twice mean square differentiable. Equation 4.21 in
Rasmussen and Williams.

# Arguments
- `λ::Number`: The kernel lengthscale, and cutoff variable
- `δ::Number`: The difference between the inputs (e.g. `t1 - t2`)
"""
function pp_kernel_base(λ::Number, δ::Number)
    # D = 1  # 1 dimension
    # q = 2  # twice MS differentiable
    # j = q + 1 + floor(D / 2) = 3
    r = abs(δ) / λ
    return (1 - r) ^ 5 * (8 * r * r + 5 * r + 1)
end

# """
# Rational Quadratic kernel
# Equivalent to adding together SE kernels with the inverse square of the
# lengthscales (τ = SE_λ^-2) are distributed as a Gamma distribution of p(τ|k,θ)
# where k (sometimes written as α) is the shape parameter and θ is the scale
# parameter. When α→∞, the RQ is identical to the SE with λ = (k θ)^-1/2.
# """
# function rq_kernel_base(hyperparameters::Vector{<:Number}, δ::Number)
#
#     @assert length(hyperparameters) == 2 "incompatible amount of hyperparameters passed"
#     k, θ = hyperparameters
#
#     return (1 + δ * δ * θ / 2) ^ -k
#     # return (1 / θ + δ * δ / 2) ^ -k
# end
# """
# Rational Quadratic kernel
# Equivalent to adding together SE kernels with the inverse square of the
# lengthscales (τ = SE_λ^-2) are distributed as a Gamma distribution of p(τ|α,β)
# where α (sometimes written as k) is the shape parameter and β is the rate
# parameter. When α→∞, the RQ is identical to the SE with λ = (α / β)^-1/2.
# """
# function rq_kernel_base(hyperparameters::Vector{<:Number}, δ::Number)
#
#     @assert length(hyperparameters) == 2 "incompatible amount of hyperparameters passed"
#     α, β = hyperparameters
#
#     # return (1 + δ * δ / (2 * β)) ^ -α
#     return (β + δ * δ / 2) ^ -α
# end
"""
    rq_kernel_base(hyperparameters, δ)

Rational Quadratic kernel. Equivalent to adding together SE kernels with the
inverse square of the lengthscales (τ = SE_λ^-2) are distributed as a Gamma
distribution of p(τ|α,μ) where α (sometimes written as k) is the shape parameter
and μ is the mean of the distribution. When α→∞, the RQ is identical to the SE
with λ = μ^-1/2.

# Arguments
- `hyperparameters::Vector`: The kernel shape parameter and mean (i.e. `[α, μ]`)
- `δ::Number`: The difference between the inputs (e.g. `t1 - t2`)
"""
function rq_kernel_base(hyperparameters::Vector{<:Number}, δ::Number)

    @assert length(hyperparameters) == 2 "incompatible amount of hyperparameters passed"
    α, μ = hyperparameters

    return (1 + δ * δ * μ / (2 * α)) ^ -α
    # return (α / μ + δ * δ / 2) ^ -α
end

"""
    rq_kernel_base(hyperparameters, δ)

Rational Matern 5/2 kernel. Equivalent to adding together Matern 5/2 kernels
with the inverse of the lengthscale (τ = M52_λ^-1) are distributed as a Gamma
distribution of p(τ|α,μ) where α (sometimes written as k) is the shape parameter
and μ is the mean of the distribution.

# Arguments
- `hyperparameters::Vector`: The kernel shape parameter and mean (i.e. `[α, μ]`)
- `δ::Number`: The difference between the inputs (e.g. `t1 - t2`)
"""
function rm52_kernel_base(hyperparameters::Vector{<:Number}, δ::Number)
    @assert length(hyperparameters) == 2 "incompatible amount of hyperparameters passed"
    α, μ = hyperparameters

    x = sqrt(5) * abs(δ)
    y = x + α / μ
    return y ^ -α * (α * (α + x * (2 + α) * μ) + x * x * (1 + α) * (3 + α) * μ * μ / 3) / (y * y * μ * μ)
end

# """
# Periodic kernel (for random cyclic functions)
# RQ kernel where δ^2 -> 4 sin(π δ/P)^2
# """
# function periodic_rq_kernel_base(hyperparameters::Vector{<:Number}, δ::Number)
#
#     @assert length(hyperparameters) == 3 "incompatible amount of hyperparameters passed"
#     P, λ, α = hyperparameters
#
#     sin_τ = sin(π * δ / P)
#     return (1 + 2 * sin_τ * sin_τ / (α * λ * λ)) ^ -α
# end

# """
# Bessel (function of the first kind) kernel
# Bessel functions of the first kind, denoted as Jα(x), are solutions of Bessel's
# differential equation that are finite at the origin (x = 0) for integer or positive α
# http://crsouza.com/2010/03/17/kernel-functions-for-machine-learning-applications/#bessel
# """
# function bessel_kernel_base(hyperparameters::Vector{<:Number}, δ::Number; nu=0)
#
#     @assert length(hyperparameters) == 2 "incompatible amount of hyperparameters passed"
#     λ, n = hyperparameters
#
#     @assert nu >= 0 "nu must be >= 0"
#
#     return besselj(nu + 1, λ * δ) / (δ ^ (-n * (nu + 1)))
# end


using IntervalArithmetic
using Test
using StaticArrays

@testset "rand tests" begin

    X = 3..4
    for i in 1:100
        @test rand(X) ∈ X
    end

    Y = IntervalBox(3..4, 5..6)
    for i in 1:100
        @test rand(Y) ∈ Y
    end

    for T in (Float32, Float64, BigFloat)
        X = Interval{T}(3, 4)
        @test rand(X) isa T

        Y = IntervalBox(X, X)
        @test rand(Y) isa SVector{2,T}
    end

end


using DualNumbers, SpecialFunctions
using Test
using LinearAlgebra
import DualNumbers: value
import NaNMath

x = Dual(2, 1)
y = x^3

@test value(y) ≈ 2.0^3
@test epsilon(y) ≈ 3.0*2^2

y = x^3.0

@test value(y) ≈ 2.0^3
@test epsilon(y) ≈ 3.0*2^2

# taking care with divides by zero where there shouldn't be any on paper
for (y, n) ∈ Iterators.product((float(x), Dual(0.0, 1)), (0, 0.0))
  z = y^n
  @test value(z) == 1
  @test !isnan(epsilon(z))
  @test epsilon(z) == 0
end

# acting on floats works as expected
for (y, n) ∈ ((float(x), Dual(0.0, 1)), -1:1)
  @test float(y)^n == float(y)^float(n)
end

@test !isnan(epsilon(Dual(0, 1)^1))
@test Dual(0, 1)^1 == Dual(0, 1)

# power_by_squaring error for integers
# needs to be wrapped to make n a literal
powwrap(z, n, epspart=0) = Dual(z, epspart)^n
@test_throws DomainError powwrap(0, -1)
@test_throws DomainError powwrap(2, -1)
@test_throws DomainError powwrap(123, -1) # etc
# these ones don't DomainError
@test powwrap(0, 0, 0) == Dual(1, 0) # special case is handled
@test powwrap(0, 0, 1) == Dual(1, 0) # special case is handled
@test powwrap(1, -1) == powwrap(1.0, -1) # special case is handled
@test powwrap(1, -2) == powwrap(1.0, -2) # special case is handled
@test powwrap(1, -123) == powwrap(1.0, -123) # special case is handled
@test powwrap(1, 0) == Dual(1, 1)
@test powwrap(123, 0) == Dual(1, 1)
for i ∈ -3:3
  @test powwrap(1, i) == Dual(1, i)
end

# this no longer throws 1/0 DomainError
@test powwrap(0, Dual(0, 1)) == Dual(1, 0)
# this never did DomainError because it starts off with a float
@test 0.0^Dual(0, 1) == Dual(1.0, NaN)
# and Dual^Dual uses a log and is now type stable
# because the log promotes ints to floats for all values
@test typeof(value(powwrap(0, Dual(0, 1)))) == Float64
@test Dual(0, 1)^Dual(0, 1) == Dual(1, 0)

y = Dual(2.0, 1)^UInt64(0)
@test !isnan(epsilon(y))
@test epsilon(y) == 0

y = sin(x)+exp(x)
@test value(y) ≈ sin(2)+exp(2)
@test epsilon(y) ≈ cos(2)+exp(2)

@test x > 1
@test dual(1) < dual(2.0)
@test dual(1.0) < dual(2.0)
y = abs(-x)
@test value(y) ≈ 2.0
@test epsilon(y) ≈ 1.0

@test isequal(1.0,Dual(1.0))
@test Dual{Float32}(3) === Dual{Float32}(3.0f0, 0.0f0)

y = 1/x
@test value(y) ≈ 1/2
@test epsilon(y) ≈ -1/2^2

Q = [1.0 0.1; 0.1 1.0]
x = dual.([1.0,2.0])
x[1] = Dual(1.0,1.0)
y = (1/2)*dot(x,Q*x)
@test value(y) ≈ 2.7
@test epsilon(y) ≈ 1.2

function squareroot(x)
    it = x
    while abs(it*it - x) > 1e-13
        it = (it+x/it)/2
    end
    return it
end

@test epsilon(squareroot(Dual(10000.0,1.0))) ≈ 0.005

@test epsilon(exp(1)^Dual(1.0,1.0)) ≈ exp(1)
@test epsilon(NaNMath.pow(exp(1),Dual(1.0,1.0))) ≈ exp(1)
@test epsilon(NaNMath.sin(Dual(1.0,1.0))) ≈ cos(1)

@test Dual(1.0,3) == Dual(1.0,3.0)
x = Dual(1.0,1.0)
@test eps(x) == eps(1.0)
@test eps(Dual{Float64}) == eps(Float64)
@test one(x) == Dual(1.0,0.0)
@test one(Dual{Float64}) == Dual(1.0,0.0)
@test convert(Dual{Float64}, Inf) == convert(Float64, Inf)
@test isnan(convert(Dual{Float64}, NaN))

@test convert(Dual{Float64},Dual(1,2)) == Dual(1.0,2.0)
@test convert(Float64, Dual(10.0,0.0)) == 10.0
@test convert(Dual{Int}, Dual(10.0,0.0)) == Dual(10,0)

x = Dual(1.2,1.0)
@test floor(x) === 1.0
@test ceil(x)  === 2.0
@test trunc(x) === 1.0
@test round(x) === 1.0
@test floor(Int, x) === 1
@test ceil(Int, x)  === 2
@test trunc(Int, x) === 1
@test round(Int, x) === 1

# test Dual{Complex}

z = Dual(1.0+1.0im,1.0)
f = exp(z)
@test value(f) == exp(value(z))
@test epsilon(f) == epsilon(z)*exp(value(z))

g = sinpi(z)
@test value(g) == sinpi(value(z))
@test epsilon(g) == epsilon(z)*cospi(value(z))*π

h = z^4
@test value(h) == value(z)^4
@test epsilon(h) == 4epsilon(z)*value(z)^3

a = abs2(z)
@test value(a) == abs2(value(z))
@test epsilon(a) == conj(epsilon(z))*value(z)+conj(value(z))*epsilon(z)

l = log(z)
@test value(l) == log(value(z))
@test epsilon(l) == epsilon(z)/value(z)

s = sign(z)
@test value(s) == value(z)/abs(value(z))

a = angle(z)
@test value(a) == angle(value(z))

@test angle(Dual(0.0+im,0.0+im)) == π/2

# check bug in inv
@test inv(dual(1.0+1.0im,1.0)) == 1/dual(1.0+1.0im,1.0) == dual(1.0+1.0im,1.0)^(-1)

#
# Tests limit definition. Let z = a + b ɛ, where a and b ∈ C.
#
# The dual of |z| is lim_{h→0} (|a + bɛh| - |a|)/h
#
# and it depends on the direction (i.e. the complex value of epsilon(z)).
#

z = Dual(1.0+1.0im,1.0)
@test abs(z) ≡ sqrt(2) + 1/sqrt(2)*ɛ
z = Dual(1.0+1.0im,cis(π/4))
@test abs(z) ≡ sqrt(2) + 2/sqrt(2)^2*ɛ
z = Dual(1.0+1.0im,cis(π/2))
@test abs(z) ≡ sqrt(2) + 1/sqrt(2)*ɛ

# tests vectorized methods
const zv = dual.(collect(1.0:10.0), ones(10))

f = exp.(zv)
@test all(value.(f) .== exp.(value.(zv)))
@test all(epsilon.(f) .== epsilon.(zv) .* exp.(value.(zv)))

# tests norms and inequalities
@test norm(f,Inf) ≤ norm(f) ≤ norm(f,1)

# tests for constant ɛ
@test epsilon(1.0 + ɛ) == 1.0
@test epsilon(1.0 + 0.0ɛ) == 0.0
test(x, y) = x^2 + y
@test test(1.0 + ɛ, 1.0) == 2.0 + 2.0ɛ
@test test(1.0, 1.0 + ɛ) == 2.0 + 1.0ɛ

@test ɛ*im == Dual(Complex(false,false),Complex(false,true))

@test value(mod(Dual(15.23, 1), 10)) == 5.23
@test epsilon(mod(Dual(15.23, 1), 10)) == 1

@test epsilon(Dual(-2.0,1.0)^2.0) == -4
@test epsilon(Dual(-2.0,1.0)^Dual(2.0,0.0)) == -4

# test for flipsign
flipsign(Dual(1.0,1.0),2.0) == Dual(1.0,1.0)
flipsign(Dual(1.0,1.0),-2.0) == Dual(-1.0,-1.0)
flipsign(Dual(1.0,1.0),Dual(1.0,1.0)) == Dual(1.0,1.0)
flipsign(Dual(1.0,1.0),Dual(0.0,-1.0)) == Dual(-1.0,-1.0)
flipsign(-1.0,Dual(1.0,1.0)) == -1.0

# test SpecialFunctions
@test erf(dual(1.0,1.0)) == dual(erf(1.0), 2exp(-1.0^2)/sqrt(π))
@test gamma(dual(1.,1)) == dual(gamma(1.0),polygamma(0,1.0))

let x = exp10(Dual(2, 0.01))
    @test value(x) ≈ 100.0
    @test epsilon(x) ≈ log(10)
end

@test value(3) == 3
@test epsilon(44.0) ≈ 0.0


function form_lcp(J, v, body, physics)
    μ = body.μ
    M = body.M
    Mi = body.M_inv
    ϵ = body.ϵ
    dt = physics.dt
    g = physics.g

    Jₙ = J[:,2]
    Jₜ = J[:,1]

    V = zeros((4,4))
    V[1,1] = Jₙ'* Mi * (dt*Jₙ)
    V[1,2] = -Jₙ'* Mi * (dt*Jₜ)
    V[1,3] = Jₙ'* Mi * (dt*Jₜ)
    V[2,1] = -Jₜ'* Mi * (dt*Jₙ)
    V[2,2] = Jₜ'* Mi * (dt*Jₜ)
    V[2,3] = -Jₜ'* Mi * (dt*Jₜ)
    V[2,4] = 1.
    V[3,1] = Jₜ'* Mi * (dt*Jₙ)
    V[3,2] = -Jₜ'* Mi * (dt*Jₜ)
    V[3,3] = Jₜ'* Mi * (dt*Jₜ)
    V[3,4] = 1.
    V[4,1] = μ
    V[4,2] = -1.
    V[4,3] = -1.
    fₑ = M*g
    p = zeros(4)
    p[1] = Jₙ' * ((1+ϵ)*v + dt*Mi*fₑ)
    p[2] = -Jₜ' * (v + dt*Mi*fₑ)
    p[3] = Jₜ' * (v + dt*Mi*fₑ)

    return V, p
end

function solve_lcp(V, p)
    lcp = Model(Ipopt.Optimizer)
    N = length(p)
    @variable(lcp, x[1:N])
    @constraint(lcp,  V*x + p .>= 0.)
    @constraint(lcp, x .>= 0.)
    @objective(lcp, Min, x'*(V*x + p))
    optimize!(lcp)

    return value.(x)

    # result = solve!(LCP(M, q))
    # return result.sol
end


export TFIDF

"""
    TFIDF(
        data::DataAccessor,
        tf::AbstractMatrix,
        idf::AbstractMatrix
    )

Content-based recommendation using TF-IDF scoring. TF and IDF matrix are respectively specified as `tf` and `idf`.

More concretely, each item is represented as a set of words, and the items are modeled by TF-IDF weighting of the words. The technique was initially used in information retrieval to model a document-word matrix. In case of our item-word matrices, for a given item ``i``, term frequency (TF) for a term ``t`` is defined as:

```math
\\mathrm{tf}(t, i) = \\frac{n_{t,i}}{N_i},
```

where ``n_{t,i}`` denotes an ``(i, t)`` element in ``I``, and ``N_i`` is the total number of words that an item ``i`` contains. So, ``\\mathrm{tf}(t, i)`` characterizes an item-term pair by normalized frequency of terms. Meanwhile, inverse document frequency (IDF) is computed over ``M`` items as:

```math
\\mathrm{idf}(t) = \\log \\frac{M}{\\mathrm{df}(t)} + 1,
```

where ``\\mathrm{df}(t)`` counts the number of items which associate with a term ``t``. What ``\\mathrm{idf}(t)`` does is to decrease the effect of commonly used terms, because general words cannot characterize a specific item.

Finally, each item-term pair is weighted by ``\\mathrm{tf}(t, i) \\cdot \\mathrm{idf}(t)`` in the TF-IDF scheme. For instance, if we like to recommend web pages to users, we first need to parse sentences on a page and then construct a vector based on the frequency of each term as follows. Similarly, in case that a recommender is running on a folksonomic (i.e. social tagging) service, the frequency of tags corresponds to each dimension of a vector, and ``\\mathrm{tf}(t, i)`` and ``\\mathrm{idf}(t)`` are finally calculated for each item-tag pair.

![tfidf](./assets/images/tfidf.png)
"""
struct TFIDF <: Recommender
    data::DataAccessor # document x attribute
    tf::AbstractMatrix # 1 x attribute
    idf::AbstractMatrix # 1 x attribute

    function TFIDF(data::DataAccessor, tf::AbstractMatrix, idf::AbstractMatrix)
        # instanciate with dummy status (i.e., always true)
        new(data, tf, idf)
    end
end

function predict(recommender::TFIDF, u::Int, i::Int)
    uv = recommender.data.user_attributes[u]
    profile = recommender.data.R' * uv # attribute x 1
    ((recommender.data.R .* recommender.idf) * profile)[i]
end


using Base.Iterators, PolygonOps

abstract type Edge end
struct EdgeX <: Edge
    range :: Tuple{Number, Number} # [a, b]
    y :: Function # y(x)
    a :: Number # ∂y
end

struct EdgeY <: Edge
    range :: Tuple{Number, Number} # [a, b]
    x :: Function # x(y)
    a :: Number # ∂x
end

function Edge(A, B)::Edge
    xRange, yRange = minmax.(A, B)
    (xa, ya), (xb, yb) = A, B
    if xa != xb
        a = (ya - yb) // (xa - xb)
        b = ya - a * xa
        EdgeX(xRange, x -> a * x + b, a)
    else
        a = (xa - xb) // (ya - yb)
        b = xa - a * ya
        EdgeY(yRange, y -> a * y + b, a)
    end
end

wrap1(xs) = flatten((rest(xs, 2), [xs[1]]))

onEdge((x, y), edge :: EdgeX) = (edge.range[1] <= x <= edge.range[2]) && y ≈ edge.y(x)
onEdge((x, y), edge :: EdgeY) = (edge.range[1] <= y <= edge.range[2]) && x ≈ edge.x(y)
onEdge(point, edges :: Vector{Edge}) = any([onEdge(point, edge) for edge in edges])

function prepareEdge(shape, Γ) :: Tuple{Any, Vector{Edge}}
    Γn, Γd = [], []
    for (edge, f) ∈ zip(Edge.(shape, wrap1(shape)), Γ)
        if f != 0 push!(Γn, (f, edge))
        else push!(Γd, edge) end
    end
    Γn, Γd
end


@testset "Univariate leading coefficient" begin
    R, (x, y, z) = Nemo.PolynomialRing(Nemo.QQ, ["x", "y", "z"])
    p = x^2 * y + x^2 * (z + z^3) + y - 5
    @test lc_univariate(p, x) == y + z + z^3
    @test lc_univariate(p, z) == x^2
    @test lc_univariate(p, y) == 1 + x^2
    @test lc_univariate(x + y, z) == x + y
end


# module PengRobinson

using PolynomialRoots

struct PengRobinsonData{T} <: AbstractCubicEOS where T <: AbstractFloat
    CompoundName::String
    MW::T
    Pc::T
    Tc::T
    a::T
    b::T
    ω::T
    κ::T
end

function α(eos::PengRobinsonData, T)
    Tr = T/eos.Tc
    return (1+eos.κ*(1-√(Tr)))^2
end
function A(eos::PengRobinsonData, P, T)
    return eos.a * α(eos, T) * P / (R() ^2 * T^2)
end
function B(eos::PengRobinsonData, P, T)
    return eos.b*P/(T*R())
end
function c1(eos::PengRobinsonData, P, T)
    return (B(eos, P, T) - 1)
end
function c2(eos::PengRobinsonData, P, T)
    return A(eos, P, T) - 2B(eos, P,T) - 3B(eos, P,T)^2
end
function c3(eos::PengRobinson, p, T)
    return B(eos, P,T)^3 + B(eos, P,T)^2 - A(eos, P,T)*B(eos, P,T)
end

function PengRobinsonData{T}(CompoundName::String, MW::T, Pc::T, Tc::T, ω::T) where T<: AbstractFloat
    a = 0.45724 * R() ^2 * Tc^2 /pc
    b = 0.0778*R() * Tc /pc
    κ = 0.37464 + 1.54226*ω - 0.26992*ω^2
    return PengRobinsonData(CompoundName, MW, Pc, Tc, a,b,ω,κ)
end

function get_the_real(vec::Vector{Complex{T}}) where {T<:Number}
    for i in vec
        if abs(imag(i)) < 2*eps(T)
            return real(i)
        end
    end
end

function volume_roots(eos::PengRobinsonData, P, T)
    c1_ = c1(eos, P, T)
    c2_ = c2(eos, P, T)
    c3_ = c3(eos, P, T)
    roots_ = roots([c3_, c2_, c1_, 1.0])
    return roots_
end

# end # ends module PengRobinson


using PolyChaos, GaussQuadrature, Test, LinearAlgebra

mytol = 1e-10
N = 5:10:100

names = ["hermite", "legendre"]
funs = [hermite, legendre, laguerre]

@testset "Compare $(names...) against GaussQuadrature" begin
    for i=1:length(names), n in N
        opq = OrthoPolyQ(names[i],n)
        mynw = nw(opq)
        n, w = funs[i](n)
        @test isapprox(norm(n-mynw[:,1],Inf),0;atol=mytol)
        @test isapprox(norm(w-mynw[:,2],Inf),0;atol=mytol)
    end
end

a = -0.5:0.1:2.0
b = copy(a)

@testset "Compare Jacobi against GaussQuadrature" begin
    for aa in a, bb in b, n in N
        opq = OrthoPolyQ("jacobi",n,Dict(:shape_a=>aa,:shape_b=>bb))
        mynw = nw(opq)
        n, w = jacobi(n,aa,bb)
        @test isapprox(norm(n-mynw[:,1],Inf),0;atol=mytol)
        @test isapprox(norm(w-mynw[:,2],Inf),0;atol=mytol)
    end
end

@testset "Compare Laguerre against GaussQuadrature" begin
    for n in N
        opq = OrthoPolyQ("laguerre",n,Dict(:shape=>0.,:rate=>1.))
        mynw = nw(opq)
        n, w = laguerre(n,0.)
        @test isapprox(norm(n-mynw[:,1],Inf),0;atol=mytol)
        @test isapprox(norm(w-mynw[:,2],Inf),0;atol=mytol)
    end
end


function get_nbins(alg, data)

    n = length(data)

    if alg == :sqrt
        nbins = ceil(Int, sqrt(n))
    elseif alg == :sturges
        nbins = ceil(Int, log(2,n)) + 1
    elseif alg == :rice
        nbins = ceil(Int, 2*cbrt(n))
    elseif alg == :doane
        g₁ = moment(data, 3)
        σ = sqrt((6*(n-2))/((n+1)*(n+3)))
        nbins = ceil(Int, 1 + log(2,n) + log(2, 1+abs(g₁)/σ))
    elseif alg == :scott
        σ = std(data)
        binwidth = 3.5*σ/cbrt(n)
        lo, hi = extrema(data)
        nbins = ceil(Int, (hi - lo)/binwidth)
    elseif alg == :fd
        binwidth = 2*iqr(data)/cbrt(n)
        lo, hi = extrema(data)
        nbins = ceil((hi - lo)/binwidth)
    else
        binwidth = 2*iqr(data)/cbrt(n)
        lo, hi = extrema(data)
        nbins_fd = ceil(Int, (hi - lo)/binwidth)
        nbins_sturges = ceil(Int, log(2,n)) + 1
        nbins = max(nbins_fd, nbins_sturges)
    end

    return(round(nbins))
end

# M: data
# ndim: number of dimensions
# k: number of eigenvalues to keep
# d: discretizer used per principal components: :bb for bayesian blocks :uw for uniform width :km for kmeans
# bintype: type of binning, only used if disc=:uw, select :manual for manual setting of number of bins
# bins: number of bins, only used if bintype=:manual
function CPD(M, ndim::Int64, npoints::Int64, k::Int64, d=:km, bintype=:manual, bins=Int(round(ndim)),processing=:none)
    results = DataFrame()

    #calculate correlation matrix
    C=cor(M');

    #select principal components
    K = size(C, 1)
    ef=eigen(C)
    ef.values[K-k+1:K]
    S=ef.vectors[:, K-k+1:K]
    S=Matrix(S)

    #project onto principal components
    D=S'*M

    #determine cutpoints
    B = []
    for m in 1:k
        if d == :km
            R = kmeans(copy(convert(Array,D[m,:])'),bins)
            cutpoints=zeros(bins+1)
            cutpoints[1]=minimum(D[m,:])
            cutpoints[bins+1]=maximum(D[m,:])
            centers = sort(R.centers,dims=2)
            [cutpoints[i] = (centers[i-1]+centers[i])/2 for i in 2:bins]
            push!(B,cutpoints)
        else
            if d == :bb
                disc = LinearDiscretizer(binedges(DiscretizeBayesianBlocks(), D[m,:]))
            else
                if bintype==:manual
                    nbins=bins
                else
                    nbins=get_nbins(bintype, D[m,:])
                end

                disc = LinearDiscretizer(binedges(DiscretizeUniformWidth(nbins), D[m,:]))
            end
            cutpoints = zeros(nlabels(disc)+1)
            cutpoints[1] = extrema(disc,1)[1]
            #cutpoints = zeros(nlabels(disc))
            [cutpoints[i+1]=extrema(disc,i)[2] for i in 1:nlabels(disc)]
            push!(B,cutpoints)
        end
    end

    #reprojection the cutpoints
    cp_proj = []
    for i in B
        for j in i
            for l in 1:k
                push!(cp_proj, j*S[:,l])
            end
        end
    end

    projections = zeros(ndim,size(cp_proj)[1])

    for i in 1:ndim
        for j in 1:size(cp_proj)[1]
            projections[i,j]=cp_proj[j][i]
        end
        lindisc = LinearDiscretizer(sort(projections[i,:]))
        results[!,i] = [encode(lindisc,one) for one in M[i,:]]
    end

    if processing==:Integer
        results = postprocessing_integer(results,ndim,npoints)
    elseif processing==:Cutpoints
        results = postprocessing_cutpoints(results,ndim,npoints,projections)
    end

    return(results)
end

function CPD_clustered(M, ndim::Int64, npoints::Int64, c::Int64, p::Float64, d=:km)
    results = zeros(size(M))
    SPC = 1/2*(I-corspearman(M'))
    clust = hclust(SPC,linkage=:ward_presquared)
    clustered_data = cutree(clust;k=c)

    for i in 1:c
        selector = clustered_data .== i
        data = M[selector,:]
        k = Int(ceil(size(data)[1]/p))
        bins = get_nbins(:doane, data)
        res = CPD(data,size(data)[1],npoints,k,d,:manual,bins)
        #print("\n")
        #print(typeof(res))
        results[selector,:] .= Array(res)'
    end

    return(DataFrame(results))
end


#Structs used for Forest-Photosynthesis-Hydraulic model

#Constants
mutable struct Constants{T<:Float64}
    M_H2O::T #Molar mass water (Kg mol⁻¹)
    M_C::T #Molar mass Carbon (Kg mol⁻¹
    ρ_H2O::T #Density water (Kg m⁻³)
    g::T #gravitational acceleration (m s⁻²)
    r::T #ratio water conductance:co2 conductance (-)
end
function Constants(;M_H2O::T=0.018,M_C::T=0.012,ρ_H2O::T=997.0,g::T=9.82,r::T=1.6) where {T<:Float64}
    Constants(M_H2O,M_C,ρ_H2O,g,r)
end

#Struct containing environmental variables
mutable struct EnvironmentStruct{T<:Float64}
    I₀::T #Above canopy irradiance (mol m⁻² s⁻¹)
    Cₐ::T #Ambient carbon dioxide partial pressure (Pa)
    P::T #Atmospheric pressure (Pa)
    Tₐ::T #Ambient air tempreture (°C)
    VPD::T #Vapour-pressure deficit
    θₛ::T #Soil volumetric water content (-)
end
#Standard vaules
EnvironmentStruct(;I₀::T=820.0*10^-6,Cₐ::T=400.0/10.0,P::T=1.0*10^5,
Tₐ::T=25.0,VPD::T=800.0,θₛ::T = 0.15) where {T<:Float64} =  EnvironmentStruct(I₀,Cₐ,P,Tₐ,VPD,θₛ)

mutable struct ArrheniusKineticRate{T<:Float64}
    K_ref::T #Kinetic rate at referance temperature
    T_ref::T #Reference temperature (°C)
    Eₐ::T #Activation energy (J mol⁻¹)
end
#Calcualte Kinetic rate at temperature Temp (°C)
function (K::ArrheniusKineticRate)(T::Float64)
    T += 273.15 #Temperature in Kelvin
    T_ref = K.T_ref+273.15 #Reference temperature in Kelvin
    R = 8.3145 #Gas constant (J K⁻¹ mol⁻¹)
    return K.K_ref*exp(K.Eₐ*(1/T_ref-1/T)/R)
end

mutable struct EnzymeKineticRate{T<:Float64}
    Kₒₚₜ::T #Kinetic rate at optimal temperature
    Tₒₚₜ::T #Optimal temperature (°C)
    E_a::T #Activation energy (J mol⁻¹)
    E_d::T #Deactivation energy (J mol⁻¹)
end
#Calcualte Kinetic rate at temperature Temp (°C)
function (K::EnzymeKineticRate)(T::Float64)
    T += 273.15 #Temperature in Kelvin
    Tₒₚₜ = K.Tₒₚₜ+273.15 #Optimal temperature in Kelvin
    R = 8.3145 #Gas constant (J K⁻¹ mol⁻¹)
    return K.Kₒₚₜ*K.E_d*exp(K.E_a*(T-Tₒₚₜ)/(T*R*Tₒₚₜ))/(K.E_d-K.E_a*(1-exp(K.E_d*(T-Tₒₚₜ)/(T*R*Tₒₚₜ))))
end

mutable struct PhotoKineticRates{T<:ArrheniusKineticRate}
    K_C::T #Carboxylation Michaelis-Menten constant
    K_O::T #Oxygenation Michaelis-Menten constant
    Γ::T #The CO2 compensation point in the absence of mitochondrial (day) respiration (Pa)
    b_r::T #The rate of dark respiration to Vmax ratio (Pa)
    Jmax::EnzymeKineticRate #
    O::Float64 #Intercellular partial pressure of oxygen (Pa)
end
function PhotoKineticRates(;Temp_ref::T=25.0, #Reference temperature (°C)
    K_C_ref::T = 40.40, K_C_E_a::T = 59.36e3, #Reference value (Pa) and activation energy (J mol⁻¹) for carboxylation Michaelis-Menten constant
    K_O_ref::T = 248.0e2, K_O_E_a::T = 35.94e3, #Reference value (Pa) and activation energy (J mol⁻¹) for oxygenation Michaelis-Menten constant
    Γ_ref::T = 4.17, Γ_E_a::T = 23.42e3, #Reference value (Pa) and activation energy (J mol⁻¹) for CO2 compensation point
    b_r_ref::T = 0.02, b_r_E_a::T = 7.88e3, #Reference value (-) and activation energy (J mol⁻¹) for dark respiration to Vmax ratio
    Jmaxₒₚₜ::T = 184.0e-6, Jmax_Tempₒₚₜ::T = 31.85, Jmax_E_a::T = 47.4e3, Jmax_E_d::T = 200.0e3,
    #Optimal value (mol m⁻² s⁻¹), optimal temperature (°C), activation (J mol⁻¹) and deactivaiton (J mol⁻¹) energy for Jamx
    O::T = 20.5e3) where {T<:Float64}

    K_C = ArrheniusKineticRate(K_C_ref,Temp_ref,K_C_E_a)
    K_O = ArrheniusKineticRate(K_O_ref,Temp_ref,K_O_E_a)
    Γ = ArrheniusKineticRate(Γ_ref,Temp_ref,Γ_E_a)
    b_r = ArrheniusKineticRate(b_r_ref,Temp_ref,b_r_E_a)
    Jmax = EnzymeKineticRate(Jmaxₒₚₜ,Jmax_Tempₒₚₜ,Jmax_E_a,Jmax_E_d)

    PhotoKineticRates(K_C,K_O,Γ,b_r,Jmax,O)
end

#Parameters values used in the Farquhar photosynthesis model
mutable struct PhotoPar{T<:Float64}
    K::T #Combined Michaelis-Menten constant (Pa)
    b_r::T #Ratio between rate of dark respiration and Vmax (Pa)
    Γ::T #The CO2 compensation point in the absence of mitochondrial (day) respiration (Pa)
    α::T #quantum yield of electron tansport
    θ::T #Curvature of the light response curve
    b_Jmax::T #The ratio between Jmax at tempreatur Temp and Jmax at optimal tempreture (Jmaxₒₚₜ)
end
#Reference values at temperature = 25°C
PhotoPar(;K::T=73.8,b_r::T=0.02,Γ::T=4.17,α::T=0.36,θ::T=0.7,b_Jmax::T=0.812) where {T<:Float64} =
PhotoPar(K,b_r,Γ,α,θ,b_Jmax)
#Function for initiate PhotoPar object at a tempreture of Temp °C
function PhotoPar(kinetic::PhotoKineticRates,Temp::T;α::T=0.36,θ::T=0.7) where {T<:Float64}
    K = kinetic.K_C(Temp)*(1+kinetic.O/kinetic.K_O(Temp))
    b_r = kinetic.b_r(Temp)
    Γ = kinetic.Γ(Temp)
    b_Jmax = kinetic.Jmax(Temp)/kinetic.Jmax.Kₒₚₜ

    PhotoPar(K,b_r,Γ,α,θ,b_Jmax)
end
#Calc PhotoPar values at temperature Temp (⁰C)
function PhotoPar!(photo::PhotoPar,kinetic::PhotoKineticRates,Temp::Float64)
    K = kinetic.K_C(Temp)*(1+kinetic.O/kinetic.K_O(Temp))
    b_r = kinetic.b_r(Temp)
    Γ = kinetic.Γ(Temp)
    b_Jmax = kinetic.Jmax(Temp)/kinetic.Jmax.Kₒₚₜ

    photo.K = K
    photo.b_r = b_r
    photo.Γ = Γ
    photo.b_Jmax = b_Jmax

    return nothing
end

#Struct containing variables which defines the size of a mean tree in the stand
mutable struct TreeSize{T<:Float64}
    Wf::T #Foliage weight (kg)
    Ww::T #Foliage try weight (kg)
    H::T #Tree height (m)
    Hs::T #Height to crown base (m)
    As::T #sapwood area at crown base (m²)
    B::T #Basal area (single tree) (m²)
    N::T #Stem density (# trees m⁻² ground area)
end

#Struct containing tree and stand parameters
mutable struct TreePar{T<:Float64}
    αf::T #Foliage weight to Sapwood area ratio (Kg m⁻²)
    ρw::T #Sapwood density (Kg m³)
    β₁::T #Parameter for estimating the average length of an active pipe (β₁H+β₂Hs)
    β₂::T #Parameter for estimating the average length of an active pipe (β₁H+β₂Hs)
    Tf::T #Average longevity of foliage (time⁻¹)
    Tr::T #Average longevity of fine root (time⁻¹)
    y::T #Conversion efficiency of C to biomass (includes growth respiration)
    z::T #Scaling exponent As∝(H-Hs)ᶻ
    Nₛ::T #Maximum N uptake per fine root mass (Kg N Kg⁻¹ Wr)
    rₘ::T #Specific Nitrogen maintenance respiration rate (Kg⁻¹ N year⁻¹)
    k::T #Light extinction coefficient (-)
    m::T #Average leaf transmittance (-)
    a_Jmax::T #Slope of the Nitrogen per leaf area (Nₐ)-Jmaxₒₚₜ line (mol m⁻² leaf s⁻¹ Nₐ⁻¹)
    b_Jmax::T #Intercept of the Nitrogen per leaf area (Nₐ)-Jmaxₒₚₜ line (mol m⁻² leaf s⁻¹)
    LMA::T #Leaf mass area (Kg foliage m⁻² leaf)
    Kr::T #Maximum stand fine root mass wich result in 1/2 of maximum N uptake (Kg C m⁻²)
    rW::T #Sapwood to foliage nitrogen concentration (mass) (-)
    rR::T #Fine root to foliage nitrogen concentration (mass) (-)
    r_gₛ::T #total leaf conductance (stomatal+mesophyll) to stomatal conductance (gₜ/gₛ)
    Xₜ::T #Factor [0,1] accounting for the delayed effect of temperature on gross primary production (-)
end
#Standard values
TreePar(;αf::T=460.0,ρw::T=400.0,β₁::T=1.27,β₂::T=-0.27,Tf::T=3.33,Tr::T=1.25,y::T=1.54,z::T=1.86,
Nₛ::T=0.04,rₘ::T=24.0,k::T=0.52,m::T=0.05,a_Jmax::T=0.033,b_Jmax::T=-1.1e-5,LMA::T=0.256
,Kr::T = 0.35,rW::T = 0.07,rR::T = 0.6,r_gₛ::T = 0.42,Xₜ::T = 1.0) where {T<:Float64}  =
TreePar(αf,ρw,β₁,β₂,Tf,Tr,y,z,Nₛ,rₘ,k,m,a_Jmax,b_Jmax,LMA,Kr,rW,rR,r_gₛ,Xₜ)

#Parameteres used in the Hydraulics model
mutable struct HydraulicsPar{T<:Float64}
    ψ₅₀::T #water potential which causes 50% loss in leaf conductance (MPa)
    b::T #Sensitivity of leaf conductance to water potential (-)
    i::T #Sensitivity to hydraulic failure
    Kₓₗ₀::T #Maximum xylem-leaf hydraulic conductance (mol m⁻² leaf s⁻¹ MPa⁻¹)
    Kₛᵣ₀::T #Maximum soil hydraulic conductance
end
#Standard values
HydraulicsPar(;ψ₅₀::T=-2.0,b::T=2.0,i::T=1.0,Kₓₗ₀::T=0.01,Kₛᵣ₀::T=0.1) where {T<:Float64} =
HydraulicsPar(ψ₅₀,b,i,Kₓₗ₀,Kₛᵣ₀)

#Collection of structs used for the Photosynthesis and Hydraulic model
mutable struct CCPHStruct
    cons::Constants
    env::EnvironmentStruct
    treepar::TreePar
    treesize::TreeSize
    photopar::PhotoPar
    hydPar::HydraulicsPar
end

#Struct collecting output from the Coupled Canopy Photosynthesis and Hydraulic model
mutable struct CCPHOutput
    P::Float64
    αr::Float64
    ψ_c::Float64
    Kₓₗ::Float64
    K_cost::Float64
    Gain::Float64
end

#Struct collecting times series results from growth simulation
mutable struct CCPHTS{T<:Float64}
    Wf::Array{T,1}
    Ww::Array{T,1}
    H::Array{T,1}
    Hs::Array{T,1}
    As::Array{T,1}
    B::Array{T,1}
    N::Array{T,1}
    P::Array{T,1}
    αr::Array{T,1}
    ψ_c::Array{T,1}
    Kₓₗ::Array{T,1}
    K_cost::Array{T,1}
    Gain::Array{T,1}
    gₛ::Array{T,1}
    Nₘ_f::Array{T,1}
    gₛ_crit::Array{T,1}
end
CCPHTS() = CCPHTS(Float64[],Float64[],Float64[],Float64[],Float64[],Float64[],Float64[],Float64[],
Float64[],Float64[],Float64[],Float64[],Float64[],Float64[],Float64[],Float64[])
function CCPHTS!(ccphts::CCPHTS,treesize::TreeSize)
    push!(ccphts.Wf,treesize.Wf)
    push!(ccphts.Ww,treesize.Ww)
    push!(ccphts.H,treesize.H)
    push!(ccphts.Hs,treesize.Hs)
    push!(ccphts.As,treesize.As)
    push!(ccphts.B,treesize.B)
    push!(ccphts.N,treesize.N)
end
function CCPHTS!(ccphts::CCPHTS,modeloutput::CCPHOutput,gₛ::T,Nₘ_f::T,gₛ_crit::T) where {T<:Float64}
    push!(ccphts.P,modeloutput.P)
    push!(ccphts.αr,modeloutput.αr)
    push!(ccphts.ψ_c,modeloutput.ψ_c)
    push!(ccphts.Kₓₗ,modeloutput.Kₓₗ)
    push!(ccphts.K_cost,modeloutput.K_cost)
    push!(ccphts.Gain,modeloutput.Gain)
    push!(ccphts.gₛ,gₛ)
    push!(ccphts.Nₘ_f,Nₘ_f)
    push!(ccphts.gₛ_crit,gₛ_crit)
end

#Containter for weather related time series
mutable struct WeatherTS{T<:Float64}
    date::Array{Dates.DateTime,1} #Dates
    daylight::Array{T,1} #Daylight lenght, for each timestep, given in seconds
    tot_annual_daylight::Array{T,1} #Total annual daylight lenght during growth period, given in seconds
    PAR::Array{T,1} #mol s⁻¹ m⁻²
    temp::Array{T,1} #Mean air temperature (⁰C)
    VPD::Array{T,1} #Pa
    θₛ::Array{T,1} #Volumetric soil water content
    acclimation_fac::Array{T,1} #Factor [0,1] accounting for annual acclimation cycle, see Mäkelä 2004 & 2008
end