export  PlaneStressPlasticityLawBased, getStress

mutable struct PlaneStressPlasticityLawBased
    H::Array{Float64} # tangent matrix for plane stress
    E::Float64 # Young's modulus
    ν::Float64 # Poisson's ratio
    ρ::Float64 # density
    a::Array{Float64}  # explicit hardening law parameters
    σ0::Array{Float64} # stress at last time step
    σ0_::Array{Float64} # σ0 to be updated in `commitHistory`
    ε0::Array{Float64} # stress at last time step
    ε0_::Array{Float64} # σ0 to be updated in `commitHistory`
end

@doc """
    The yield function
        \bar{σ} - σY(\bar{\epsilon} ,  a)

    σY is described by a hardening law, for example
    The power-law:
            σY = a[1] + a[2] ε_equiv^a[3]

    To modify the law, you need to modify both function f(σ, ε, a) and fε(ε, a)

""" ->
function f(σ, ε, a)
    # von Mises equivalent strain
    ε_equiv = sqrt(4.0/9.0*(ε[1]^2 + ε[2]^2 - ε[1]*ε[2]) + 1.0/3.0*ε[3]^2)
    σY = a[1] + a[2]*ε_equiv^a[3]
    return sqrt(σ[1]^2-σ[1]*σ[2]+σ[2]^2+3*σ[3]^2)-σY
end

function fσ(σ)
    σ1, σ2, σ3 = σ[1], σ[2], σ[3]
    J2 = sqrt(σ1^2-σ1*σ2+σ2^2+3*σ3^2)
    z = [(σ1 - σ2/2)/J2;
        (-σ1/2 + σ2)/J2;
        3*σ3/J2]
end

function fε(ε, a)
    ε1, ε2, ε3 = ε[1], ε[2], ε[3]
    ε_equiv = sqrt(4.0/9.0*(ε[1]^2 + ε[2]^2 - ε[1]*ε[2]) + 1.0/3.0*ε[3]^2)

    z = [(4.0/9.0 * ε1 - 2.0/9.0 * ε2)/ε_equiv;
         (4.0/9.0 * ε2 - 2.0/9.0 * ε1)/ε_equiv;
         1.0/3.0 * ε3/ε_equiv] * (a[2]*a[3]*ε_equiv^(a[3] - 1.0))

    return z
end

function fσσ(σ)
    σ1, σ2, σ3 = σ[1], σ[2], σ[3]
    J2 = sqrt(σ1^2-σ1*σ2+σ2^2+3*σ3^2)
    [     (-σ1 + σ2/2)*(σ1 - σ2/2)/J2^3 + 1/J2 (σ1/2 - σ2)*(σ1 - σ2/2)/J2^3 - 1/(2*J2)                                   -3*σ3*(σ1 - σ2/2)/J2^3;
    (-σ1 + σ2/2)*(-σ1/2 + σ2)/J2^3 - 1/(2*J2)    (-σ1/2 + σ2)*(σ1/2 - σ2)/J2^3 + 1/J2                                  -3*σ3*(-σ1/2 + σ2)/J2^3;
    3*σ3*(-σ1 + σ2/2)/J2^3                                                        3*σ3*(σ1/2 - σ2)/J2^3 -9*σ3^2/J2^3 + 3/J2]
end

function PlaneStressPlasticityLawBased(prop::Dict{String, Any})
    E = prop["E"]; ν = prop["nu"]; ρ = prop["rho"]; a = prop["args"]
    H = zeros(3,3)
    H[1,1] = E/(1. -ν*ν)
    H[1,2] = H[1,1]*ν
    H[2,1] = H[1,2]
    H[2,2] = H[1,1]
    H[3,3] = E/(2.0*(1.0+ν))
    σ0 = zeros(3); σ0_ = zeros(3); ε0 = zeros(3); ε0_ = zeros(3)
    PlaneStressPlasticityLawBased(H, E, ν, ρ, a, σ0, σ0_, ε0, ε0_)
end

@doc """
    For debugging pathdependent materials, we an build arbitrary pathdependent material law

    sigma = sigma0 + (eps - eps0)**2

""" ->
function getStress(self::PlaneStressPlasticityLawBased,  strain::Array{Float64},  Dstrain::Array{Float64}, Δt::Float64 = 0.0)
    Newton_maxiter = 10
    Newton_Abs_Err = 1e-8
    Newton_Rel_Err = 1e-5

    local dΔσdΔε
    ε = strain
    ε0 = Dstrain
    σ0 = self.σ0

    E = self.E
    H = self.H
    a = self.a

    Δγ = 0.0

    σ = σ0 + H*(ε-ε0)
    r2 = f(σ, ε, a)
    #@show r2, α, K, σY
    if r2 < 0
        #@show "Elasticity"
        σ = σ0 + H*(ε-ε0)
        dΔσdΔε = H
        #@show "elastic", α, α0, σ, σ0,ε, ε0, σ0 + H*(ε-ε0) , H*ε
    else
        # solve for Δγ
        function compute(σ, Δγ)
            r1 = σ - (σ0 + H*(ε - ε0) - Δγ * H* fσ(σ))
            r2 = f(σ, ε, a)
            J = [UniformScaling(1.0)+Δγ*H*fσσ(σ)  H*fσ(σ)
                 reshape(fσ(σ),1,3)                  0]
            return [r1;r2], J
        end

        function compute_sensitivity(σ, Δγ)
            J = [UniformScaling(1.0)+Δγ*H*fσσ(σ)  H*fσ(σ)
                 reshape(fσ(σ),1,3)                  0]

            δ = J\[H ;reshape(fε(ε, a),1,3)]

            return δ[1:3,:]
        end
        res0, _ = compute(σ, Δγ)
        for i = 1:Newton_maxiter
            res, J = compute(σ, Δγ)
            δ = -J\res
            σ += δ[1:3]; Δγ += δ[4]
            # #@show norm(res)/norm(res0)
            if norm(res)/norm(res0) < Newton_Rel_Err || norm(res) < Newton_Abs_Err
                break
            end
            if i == Newton_maxiter
                function f(∂∂u)
                    res, J = compute(∂∂u[1:3],∂∂u[4])
                end
                gradtest(f, [σ;Δγ])
                error("Newton in GetStress does not converge ", res, res0, norm(res)/norm(res0))
            end
        end

        # if f(σ, α, σY, K)>0
        #     @show sqrt(σ[1]^2-σ[1]*σ[2]+σ[2]^2+3*σ[3]^2), α, K
        # end

        dΔσdΔε = compute_sensitivity(σ, Δγ)

    end
    # #@show Δγ
    self.σ0_ = σ[:]
    self.ε0_ = ε
    # self.σ0_ = self.σ0
    #@show σ, dΔσdΔε

    return σ, dΔσdΔε
end

function getTangent(self::PlaneStressPlasticityLawBased)
    error("Not implemented")
end

function commitHistory(self::PlaneStressPlasticityLawBased)
    self.σ0 = self.σ0_
    self.ε0 = self.ε0_
end


using ProbNumODE
using DifferentialEquations
using Plots
using ProgressMeter
using LinearAlgebra

FILEDIR = dirname(@__FILE__)

prob = ProbNumODE.remake_prob_with_jac(van_der_pol(p=[1e6]))

# The EKF1-IWP3 is able to solve this very stiff problem
println("Timed solve of the VdP problem with EKS1-IBM3:")
@time sol = solve(prob, EKF1(), q=3, smooth=true); sol.destats

# Visualizations
# 1. Solution
p1 = plot(sol.t,
          max.(min.(ProbNumODE.stack(sol.u), 7.0), -7.0),
          xlabel="t", ylabel="y(t)", label="",
          );
savefig(p1, joinpath(FILEDIR, "vdp_solution.png"))

# 2. Step sizes
p2 = plot(sol.t[1:end-1], sol.t[2:end]-sol.t[1:end-1],
          yscale=:log10, xlabel="t", ylabel="dt", label="");
savefig(p2, joinpath(FILEDIR, "vdp_stepsizes.png"))

# 3. Errors
println("Timed very-low-tolerance-solve of the VdP problem with RadauIIA5:")
@time appxsol = solve(
    prob, RadauIIA5(), abstol=1e-14, reltol=1e-14, maxiters=1e7); appxsol.destats
errs = sol.u - appxsol.(sol.t)
ProbNumODE.stack(errs) ./ ProbNumODE.stack(appxsol.(sol.t))

p3 = plot(sol.t, replace(abs.(ProbNumODE.stack(errs)), 0=>missing),
          color=[1 2], label="", yscale=:log10, xlabel="t", ylabel="Error");
plot!(p3, sol.t, replace(sqrt.(ProbNumODE.stack(diag.(sol.pu.Σ))), 0=>missing),
      color=[1 2], linestyle=:dash, label="");
savefig(p3, joinpath(FILEDIR, "vdp_errors.png"))

# Additional plot: Phase plot
u = ProbNumODE.stack(sol.u)
p4 = plot(u[:, 1], u[:, 2], xlabel="y_1(t)", ylabel="y_2(t)", label="");
savefig(p4, joinpath(FILEDIR, "vdp_solution_phase.png"))

println("Plots saved to $FILEDIR")


"""

The exported names are
$(EXPORTS)
"""
module ReferenceFEs

using Test
using DocStringExtensions
using LinearAlgebra
using Combinatorics
using FillArrays

using Gridap.Helpers
using Gridap.Arrays
using Gridap.TensorValues
using Gridap.Fields
using Gridap.Polynomials

using QuadGK: gauss
using FastGaussQuadrature: gaussjacobi
using FastGaussQuadrature: gausslegendre

import Gridap.Arrays: return_cache
import Gridap.Arrays: evaluate!
import Gridap.Arrays: return_type
import Gridap.Fields: evaluate
import Gridap.Fields: lazy_map
import Gridap.Polynomials: MonomialBasis

import Gridap.Polynomials: get_order
import Gridap.Polynomials: get_orders

import Gridap.Io: to_dict
import Gridap.Io: from_dict

import Base: ==

export Polytope
export ExtrusionPolytope
export get_extrusion
export get_faces
export get_dimranges
export get_dimrange
export get_vertex_coordinates
export get_facet_normal
export get_facet_orientations
export get_edge_tangent
export get_vertex_permutations
export get_face_dimranges
export get_face_coordinates
export is_simplex
export is_n_cube
export simplexify
export num_dims
export num_cell_dims
export num_point_dims
export num_vertices
export num_faces
export num_facets
export num_edges
export num_dims
export num_point_dims
export get_facedims
export get_offsets
export get_offset
export get_face_vertices
export get_reffaces
export get_face_type
export get_bounding_box
export get_face_vertex_permutations
export get_order
export get_orders
export test_polytope
export VERTEX
export SEGMENT
export TRI
export QUAD
export TET
export HEX
export WEDGE
export PYRAMID
export HEX_AXIS
export TET_AXIS
export INVALID_PERM

export ContraVariantPiolaMap

export Dof
export get_nodes
export get_face_moments
export get_face_nodes_dofs
export get_nodes
export evaluate!
export evaluate_dof
export return_cache
export return_type
export test_dof
export test_dof_array
# export evaluate_dof_array

export ReferenceFE
export ReferenceFEName
export GenericRefFE
export get_polytope
export get_prebasis
export get_dof_basis
export Conformity
export get_face_own_dofs
export get_face_own_dofs_permutations
export get_face_dofs
export get_own_dofs_permutations
export get_shapefuns
export compute_shapefuns
export test_reference_fe
export num_dofs
export expand_cell_data
export compress_cell_data

export LagrangianRefFE
export GenericLagrangianRefFE
export get_face_own_nodes_permutations
export get_own_nodes_permutations
export get_node_coordinates
export get_dof_to_node
export get_dof_to_comp
export get_node_and_comp_to_dof
export get_vertex_node
export num_nodes
export test_lagrangian_reference_fe

export LagrangianDofBasis
export compute_monomial_basis
export compute_own_nodes
export compute_face_orders
export compute_nodes
export compute_own_nodes_permutations
export compute_lagrangian_reffaces
export is_first_order
export is_Q
export is_P
export is_S

export MomentBasedDofBasis
export get_face_own_nodes
export get_face_nodes

export VERTEX1
export SEG2
export TRI3
export QUAD4
export TET4
export HEX8

export CONT
export DISC

export Conformity
export L2Conformity
export H1Conformity
export GradConformity
export CurlConformity
export DivConformity
export CDConformity

export SerendipityRefFE
export RaviartThomasRefFE
export NedelecRefFE
export BezierRefFE

export Lagrangian
export RaviartThomas
export Nedelec
export Bezier

export lagrangian
export raviart_thomas
export nedelec
export bezier

export Quadrature
export QuadratureName
export GenericQuadrature
export num_points
export get_coordinates
export get_weights
export get_name
export num_dims
export num_point_dims
export test_quadrature
export tensor_product
export duffy
export strang

include("Polytopes.jl")

include("ExtrusionPolytopes.jl")

include("Dofs.jl")

include("LagrangianDofBases.jl")

include("ReferenceFEInterfaces.jl")

include("LagrangianRefFEs.jl")

include("CLagrangianRefFEs.jl")

include("SerendipityRefFEs.jl")

include("PDiscRefFEs.jl")

include("CDLagrangianRefFEs.jl")

include("Quadratures.jl")

include("TensorProductQuadratures.jl")

include("DuffyQuadratures.jl")

include("StrangQuadratures.jl")

include("RaviartThomasRefFEs.jl")

include("NedelecRefFEs.jl")

include("MockDofs.jl")

include("BezierRefFEs.jl")

end # module


using naumer_Dimensionality_2022
using Random: MersenneTwister
using LinearAlgebra: I, tr, norm, svd
using CSV

dim_lst = 1:50
μ = 1.0
σ² = 1.0
τ = 0.05
stepcount = 1000
T = 10

stepsize = 1e4

crlb_trace = zeros(length(dim_lst))

Threads.@threads for i in 1:length(dim_lst)
    global crlb_trace

    # Initialize system
    system = VanDerPolSystem_expanded(μ,dim_lst[i])
    crlb = I(dimension(system))

    actionseed = MersenneTwister(1234)

    state = ones(dimension(system))

    # Run 1D approximate optimal sampling procedure
    for j in 1:stepcount

        _jacobian = flowJacobian(state, T, system)
        ~, ~, V = svd(_jacobian)
        limitvector = Array(V[:,1])

        # Decide action
        action = limitvector + .01 * randn(length(limitvector))
        action ./= norm(action)
        for step in 1:1000
            action .= actiongradientstep_1DApprox(action, limitvector, crlb, σ², stepsize*dimension(system) / step^(2.0/3.0))
        end

        # Update System
        jacobian = flowJacobian(state, τ, system)
        crlb = updateCRLB(crlb, action, jacobian, σ²)
        state = flow(state, τ, system)
    end

    # Now record final crlb trace
    crlb_trace[i] = tr(crlb)
end

# Now random smapling for comparison
random_crlb_trace = zeros(length(crlb_trace))
for i in 1:length(dim_lst)
    @info(i)
    global random_crlb_trace

    # Initialize system
    system = VanDerPolSystem_expanded(μ,dim_lst[i])
    SystemMat = -1*I(dim_lst[i] + 1)
    SystemMat[1,1] = 0
    system = LinearSystem(SystemMat)
    crlb = I(dimension(system))

    state = 2*ones(dimension(system))

    # Run 1D approximate optimal sampling procedure
    for j in 1:stepcount
        # Decide action
        action = randn(dimension(system))
        action ./= norm(action)

        # Update System
        jacobian = flowJacobian(state, τ, system)
        crlb = updateCRLB(crlb, action, jacobian, σ²)
        state = flow(state, τ, system)
    end

    # Now record final crlb trace
    random_crlb_trace[i] = tr(crlb)
end

println(random_crlb_trace ./ crlb_trace)

CSV.write("DimensionScaling.csv", ( crlb_trace = crlb_trace,
                                    random_crlb_trace = random_crlb_trace))


#=
Created on Monday 18th of January 2021
Last update: Tuesday 19th of January 2021

@author: Bram De Jaegher
bram.de.jaegher@gmail.com
=#

#=______________________
|                       |
|         Day 1         |
|_______________________|
=#

#= Notebook 1: basics =#
### Clipping
function clip(x)
  if x ≤ 0 && return 0.
  elseif x ≥ 1 && return 1.
  else
  return x
  end
end

### Stirling
stirling(n) = sqrt(2*π*n)*(n/exp(1))^n

### Time is relative
function since_epoch(t)
  now = t
  seconds = now % 60 # 60 seconds in a minute
  now -= seconds
  # to minutes
  now = div(now, 60)
  minutes = now % 60 # 60 minutes in an hour
  now -= minutes
  # to hours
  now = div(now, 60)
  hours = now % 24 # 24 hours in a day
  now -= hours
  # to days
  now = div(now, 24)
  days = now % 365 # ± 365 days in a year
  return days, hours, minutes, seconds
end

### Fermat
function checkfermat(a::Int, b::Int, c::Int, n::Int)
  if a^n + b^n == c^n
      println("Holy smokes, Fermat was wrong!")
  else
      println("No, that doesn’t work.")
  end
end

### Justify
rightjustify(s) = println(" "^(70-length(s)) * s)

### Grid print
function printgrid()
  dashed = " -"^4*" "
  plusdash = "+"*dashed*"+"*dashed*"+"
  verticals = ("|"*" "^9)^2*"|"
  s = (plusdash * "\n" * (verticals * "\n")^4)^2*plusdash * "\n"
  println(s)
end

### Molecular mass spectrometry
f1(s) = replace(s, "-CH3"=> "-COOH") |> s -> replace(s, "CH3-"=> "COOH-")
f2(s) = replace(s, "-CO-COOH"=> "") |> s -> replace(s, "COOH-CO-"=> "")

function f3(molecule)
  if length(molecule) < 4
    molecule = "CH4"
    return molecule
  end

  if molecule[1:4] == "CH2-"
    molecule = "CH3-" * molecule[5:end]
  end
  if molecule[end-3:end] == "-CH2"
    molecule = molecule[1:end-4] * "-CH3"
  end
  return molecule
end

f4(s) = replace(s, "-CH2-CH2-"=> "-CO-")

function f5(molecule)
  if molecule[1:3] == "CO-"
    molecule = "COOH-" * molecule[4:end]
  end

  if molecule[end-2:end] == "-CO"
    molecule = molecule[1:end-3] * "-COOH"
  end
  return molecule
end

f6(s) = replace(s, "CO-CO-CO" => "CO-CH2-CO")

function cycle(molecule)
  molecule |> f1 |> f2 |> f3 |> f4 |> f5 |> f6
end

function degrade(molecule)
  mol_prev = ""
  while mol_prev !== molecule
    mol_prev = molecule
    molecule = cycle(molecule)
  end
  return molecule
end

function molecular_weight(molecule)
	return count("CH3", molecule)* 15.0 +
			count("CH4", molecule) * 16.0 +
			count("CH2", molecule) * 14.0 +
			count("CO", molecule) * 28.0 +
			count("OH", molecule) * 17
end

function simulate_path(space, offset, down=1)
  x, y = 1, 1
  n, m = size(space)
  n_trees = 0
  for y in 1:down:n
    n_trees += space[y,x]
    x += offset
    while x > m
      x -= m
    end
  end
  return n_trees
end

# n_trees = simulate_path(map, 3)
n_trees = 254
# product_all_trees = simulate_path(space, 1) * simulate_path(space, 3) * simulate_path(space, 5) * simulate_path(space, 7) * simulate_path(space, 1, 2)
product_all_trees = 1666768320
# answers to advanced solution:
#goingdowntheslope(3, 1, slope)
#slopestyles = (
#    (1, 1),
#    (3, 1),
#    (5, 1),
#    (7, 1),
#    (1, 2),
#)
#prod([goingdowntheslope(direction..., slope) for direction in slopestyles])

# WIP bigprint

#= Notebook 2: collections =#
### Riemann sum

function riemannsum(f, a, b; n=100)
  dx = (b - a) / n
  return sum(f.(a:dx:b)) * dx
end

### Markdown table
function markdowntable(table, header)
  n, m = size(table)
  table_string = ""
  # add header
  table_string *= "| " * join(header, " | ") * " |\n"
  # horizontal lines for separating the columns
  table_string *= "| " * repeat(":--|", m) * "\n"
  for i in 1:n
      table_string *= "| " * join(table[i,:], " | ") * " |\n"
  end
  #clipboard(table_string)
  return table_string
end

### Determinant
# WIP

### Pi Time
function estimatepi(n)
  hits = 0
  for i in 1:n
      x = rand()
      y = rand()
      if x^2 + y^2 ≤ 1.0
          hits += 1
      end
  end
  return 4hits/n
end

estimatepi2(n) = 4count(x-> x ≤ 1.0, sum(rand(n, 2).^2, dims=2)) / n

### Vandermonde
vandermonde(α, n) = [αᵢ^j for αᵢ in α, j in 0:n-1]

#= Notebook 3: project 1 =#
### Mean
mean(x) = sum(x)/length(x)

weighted_mean(x, w) = sum(w.*x)

### 1D convolution
function convolve_1d(x::Vector, w::Vector)
	@assert length(w) % 2 == 1 "length of `w` has to be odd!"
	@assert length(w) < length(x) "length of `w` should be smaller than `x`"
	n = length(x)
	m = length(w) ÷ 2
	y = zeros(n)

	fill!(y, 0.0)
	for (i, xj) in enumerate(x)
		for (j, wj) in enumerate(w)
			k = clamp(i + j - m, 1, n)
			y[i] += w[j] * x[k]
		end
	end
	return y
end

### Weight vectors
function uniform_weights(m)
	return ones(2m+1) / (2m+1)
end

function triangle_weights(m)
	w = zeros(2m+1)
	for i in 1:(m+1)
		w[i] = i
		w[end-i+1] = i
	end
	w ./= sum(w)
	return w
end

function gaussian_weights(m; σ=4)
	w = exp.(-(-m:m).^2 / 2σ^2)
	return w ./ sum(w)
end

### Protein sliding window
function protein_sliding_window(sequence, m, zscales)
	n = length(sequence)
	y = zeros(n)
	for i in (m+1):(n-m)
		for k in -m:m
			y[i] += zscales[sequence[i+k]]
		end
	end
	return y / (2m + 1)
end

### Decimate
decimate(image, ratio=5) = image[1:ratio:end, 1:ratio:end]

### 2D convolution
function convolve_2d(M::Matrix, K::Matrix)
	out = similar(M)
	n_rows, n_cols = size(M)
	fill!(out, 0.0)
	m = div(size(K, 1), 2)
	for i in 1:n_rows
		for j in 1:n_cols
			for k in -m:m
				for l in -m:m
					out[i,j] += M[clamp(i+k, 1, n_rows), clamp(j+l, 1, n_cols)] * K[k+m+1,l+m+1]
				end
			end
		end
	end
	return out
end

function gaussian_kernel(m; σ=4)
  K = [exp(-(x^2 + y^2) / 2σ^2) for x in -m:m, y in -m:m]
  K ./= sum(K)
  return K
end

function convolve_image(M::Matrix{<:AbstractRGB}, K::Matrix)
	Mred = convolve_2d(red.(M) .|> Float32, K)
	Mgreen = convolve_2d(green.(M) .|> Float32, K)
	Mblue = convolve_2d(blue.(M) .|> Float32, K)
	return RGB.(Mred, Mgreen, Mblue)
end

### Edge detection
Gx = [1 0 -1; 2 0 -2; 1 0 -1]
Gy = [1 2 1; 0 0 0; -1 -2 -1]
function edge_detection(M)
	M = M .|> Gray .|> Float64
	return sqrt.(Solutions.convolve_2d(M, Gx).^2 + Solutions.convolve_2d(M, Gy).^2) .|> Gray
end

### Get index of bitstring
getbinarydigit(rule, i) = isodd(rule >> i)

### Next state
nextstate(l::Bool, s::Bool, r::Bool, rule::Int) = nextstate(l, s, r, UInt8(rule))

function nextstate(l::Bool, s::Bool, r::Bool, rule::UInt8)
  return getbinarydigit(rule, 4l+2s+1r)
end

### Update 1 step of CA
function update1dca!(xnew, x, rule::Integer)
	n = length(x)
	xnew[1] = nextstate(x[end], x[1], x[2], rule)
	xnew[end] = nextstate(x[end-1], x[end], x[1], rule)
	for i in 2:n-1
		xnew[i] = nextstate(x[i-1], x[i], x[i+1], rule)
	end
	return xnew
end

update1dca(x, rule::Integer) = update1dca!(similar(x), x, rule)

### simulating the CA
"""
    simulate(x0, rule; nsteps=100)

Simulate `nsteps` time steps according to `rule` with `X0` as the initial condition.
Returns a matrix X, where the rows are the state vectors at different time steps.
"""
function simulate(x0, rule::UInt8; nsteps=100)
	n = length(x0)
    X = zeros(Bool, nsteps+1, n)
	X[1,:] = x0
	for t in 1:nsteps
		x = @view X[t,:]
		xnew = @view X[t+1,:]
		update1dca!(xnew, x, rule)
	end
    return X
end

### plotting CA
#plot(ca_image(X), size=(1000, 1000))

#=______________________
|                       |
|         Day 2         |
|_______________________|
=#

#= Notebook 1: types =#
### String parsing
bunchofnumbers_parser(bunchofnumbers) = parse.(Float64, split(rstrip(bunchofnumbers), ", ")) |> sum

#= Notebook 2: composite types =#
### Wizarding currency
struct WizCur
  galleons::Int
  sickles::Int
  knuts::Int
  function WizCur(galleons::Int, sickles::Int, knuts::Int)
      sickles += knuts ÷ 29
      knuts %= 29
      galleons += sickles ÷ 17
      sickles %= 17
      return new(galleons, sickles, knuts)
  end
end

galleons(money::WizCur) = money.galleons
sickles(money::WizCur) = money.sickles
knuts(money::WizCur) = money.knuts

moneyinknuts(money::WizCur) = 29*17galleons(money) + 29sickles(money) + knuts(money)

function Base.show(io::IO, money::WizCur)
  print(io, "$(galleons(money))G, $(sickles(money))S, $(knuts(money))K")
end

Base.isless(m1::WizCur, m2::WizCur) = moneyinknuts(m1) < moneyinknuts(m2)
Base.isgreater(m1::WizCur, m2::WizCur) = moneyinknuts(m1) > moneyinknuts(m2)
Base.isequal(m1::WizCur, m2::WizCur) = moneyinknuts(m1) == moneyinknuts(m2)

Base.:+(m1::WizCur, m2::WizCur) = WizCur(galleons(m1)+galleons(m2),
                                          sickles(m1)+sickles(m2),
                                          knuts(m1)+knuts(m2))

money_ron = WizCur(0, 19, 732)
money_harry = WizCur(3, 1, 7)

dungbomb_fund = money_ron + money_harry

### Vandermonde
struct Vandermonde{T,VT} <: AbstractMatrix{T}
  α::VT
  m::Int
  Vandermonde(α::AbstractVector{T}, m) where {T} = new{T,typeof(α)}(α,m)
end

Vandermonde(α::Vector{<:Number}) = Vandermonde(α, length(α))

Base.size(V::Vandermonde) = (length(V.α), V.m)
Base.getindex(V::Vandermonde, i, j) = V.α[i]^(j-1)

α = [1, 2, 3, 4]

V = Vandermonde(α, 4)

determinant(V::Vandermonde) =
  ((xi-xj) for (i,xi) in enumerate(V.α), (j, xj) in enumerate(V.α) if i < j) |> prod


using Random

function parse_instance_haberman(MyFileName::String)
  path = "../data/" * MyFileName
  # Si le fichier path existe
  if isfile(path)
    # L’ouvrir
    myFile = open(path)
    n = 306
    X = Array{Float64,2}(zeros(n,3))
    Y = Array{Int64,1}(zeros(n))
    for i in 1:n
      line =  split(readline(myFile), ",")
      Y[i] = parse(Int64,line[4])
      for j in 1:3
        X[i,j] = parse(Float64,line[j])
      end
    end
    # normalisation de X
    for i in 1:3
      X[:,i] .-= minimum(X[:,i])
      X[:,i] ./= maximum(X[:,i])
    end

    # ecriture du fichier correct
    fout = open("../data/haberman.txt", "w")
    println(fout, "X = ")
    println(fout, X)
    println(fout, "Y = ")
    println(fout, Y)
    close(fout)

  end

end

parse_instance_haberman("haberman.data")

function parse_instance_banknote(MyFileName::String)
  path = "../data/" * MyFileName
  # Si le fichier path existe
  if isfile(path)
    # L’ouvrir
    myFile = open(path)
    n = 1372
    X = Array{Float64,2}(zeros(n,4))
    Y = Array{Int64,1}(zeros(n))
    for i in 1:n
      line =  split(readline(myFile), ",")
      Y[i] = parse(Int64,line[5]) + 1
      for j in 1:4
        X[i,j] = parse(Float64,line[j])
      end
    end
    # normalisation de X
    for i in 1:4
      X[:,i] .-= minimum(X[:,i])
      X[:,i] ./= maximum(X[:,i])
    end

    # ecriture du fichier correct
    fout = open("../data/banknote.txt", "w")
    println(fout, "X = ")
    println(fout, X)
    println(fout, "Y = ")
    println(fout, Y)
    close(fout)
  end
end

parse_instance_banknote("data_banknote_authentication.txt")


struct SystemMatrix
    rows::Any
    cols::Any
    vals::Any
    function SystemMatrix(rows, cols, vals)
        @assert length(rows) == length(cols) == length(vals)
        new(rows, cols, vals)
    end
end

function SystemMatrix()
    rows = Int[]
    cols = Int[]
    vals = zeros(0)
    SystemMatrix(rows, cols, vals)
end

function Base.show(io::IO, sysmatrix::SystemMatrix)
    numvals = length(sysmatrix.rows)
    str = "SystemMatrix with $numvals entries"
    print(io, str)
end

function assemble!(matrix, rows, cols, vals)
    @assert length(rows) == length(cols) == length(vals)
    append!(matrix.rows, rows)
    append!(matrix.cols, cols)
    append!(matrix.vals, vals)
end

struct SystemRHS
    rows::Any
    vals::Any
    function SystemRHS(rows, vals)
        @assert length(rows) == length(vals)
        new(rows, vals)
    end
end

function SystemRHS()
    SystemRHS(Int[], zeros(0))
end

function Base.show(io::IO, sysrhs::SystemRHS)
    numvals = length(sysrhs.rows)
    str = "SystemRHS with $numvals entries"
    print(io, str)
end

function assemble!(systemrhs, rows, vals)
    @assert length(rows) == length(vals)
    append!(systemrhs.rows, rows)
    append!(systemrhs.vals, vals)
end

function node_to_dof_id(nodeid, dofs, dofspernode)
    return (nodeid - 1) * dofspernode + dofs
end

function element_dofs(nodeids, dofs, dofspernode)
    numnodes = length(nodeids)
    numdofs = length(dofs)

    extnodeids = repeat(nodeids, inner = numdofs)
    extdofs = repeat(dofs, outer = numnodes)

    edofs = [
        node_to_dof_id(n, d, dofspernode) for
        (n, d) in zip(extnodeids, extdofs)
    ]
    return edofs
end

function element_dofs(nodeids, dofspernode)
    dofs = 1:dofspernode
    return element_dofs(nodeids, dofs, dofspernode)
end

function element_dofs_to_operator_dofs(rowdofs, coldofs)
    nr = length(rowdofs)
    nc = length(coldofs)
    rows = repeat(rowdofs, outer = nc)
    cols = repeat(coldofs, inner = nr)
    return rows, cols
end

function assemble_couple_cell_matrix!(
    sysmatrix,
    nodeids1,
    dofs1,
    nodeids2,
    dofs2,
    dofspernode,
    vals,
)

    edofs1 = element_dofs(nodeids1, dofs1, dofspernode)
    edofs2 = element_dofs(nodeids2, dofs2, dofspernode)

    rows, cols = element_dofs_to_operator_dofs(edofs1, edofs2)
    assemble!(sysmatrix, rows, cols, vals)
end

function assemble_couple_cell_matrix!(
    sysmatrix,
    nodeids1,
    nodeids2,
    dofspernode,
    vals,
)
    edofs1 = element_dofs(nodeids1, dofspernode)
    edofs2 = element_dofs(nodeids2, dofspernode)

    rows, cols = element_dofs_to_operator_dofs(edofs1, edofs2)
    assemble!(sysmatrix, rows, cols, vals)
end

function assemble_cell_matrix!(sysmatrix, nodeids, dofspernode, vals)
    assemble_couple_cell_matrix!(sysmatrix, nodeids, nodeids, dofspernode, vals)
end

function assemble_cell_rhs!(sysrhs, nodeids, dofs, dofspernode, vals)
    rows = element_dofs(nodeids, dofs, dofspernode)
    assemble!(sysrhs, rows, vals)
end

function assemble_cell_rhs!(sysrhs, nodeids, dofspernode, vals)
    dofs = 1:dofspernode
    assemble_cell_rhs!(sysrhs,nodeids,dofs,dofspernode,vals)
end

function sparse_operator(sysmatrix, ndofs)
    return dropzeros!(
        sparse(sysmatrix.rows, sysmatrix.cols, sysmatrix.vals, ndofs, ndofs),
    )
end

function sparse_operator(sysmatrix, mesh, dofspernode)
    numnodes = number_of_nodes(mesh)
    totaldofs = dofspernode * numnodes
    return sparse_operator(sysmatrix, totaldofs)
end

function sparse_displacement_operator(sysmatrix, mesh)
    dim = dimension(mesh)
    return sparse_operator(sysmatrix, mesh, dim)
end

function rhs_vector(sysrhs, ndofs)
    return Array(sparsevec(sysrhs.rows, sysrhs.vals, ndofs))
end

function rhs_vector(sysrhs, mesh, dofspernode)
    numnodes = number_of_nodes(mesh)
    totaldofs = dofspernode * numnodes
    return rhs_vector(sysrhs, totaldofs)
end

function displacement_rhs_vector(sysrhs, mesh)
    dim = dimension(mesh)
    return rhs_vector(sysrhs, mesh, dim)
end


using PhysicalOceanography
using Test, Documenter

@testset "PhysicalOceanography.jl" begin

    @test isapprox(f(45),1.03125867182e-4)
    @test isapprox(β(45),1.61687441686e-11)

end

@testset "doctests" begin
    doctest(PhysicalOceanography; manual = false)
end


g = FactorGraph(3, 4)
@test nv(g) == 7
@test ne(g) == 0
@test_throws ArgumentError add_edge!(g, 1, 2)
@test_throws ArgumentError add_edge!(g, 6, 7)
add_edge!(g, 1, 4)
@test ne(g) == 1

g = FactorGraph(3, 4, 8)
@test g.nvars == 3
@test g.nfacts == 4
@test nv(g) == 7
@test ne(g) == 8

g = BeliefPropagation.random_bipartite_regular_graph(5, 6, 2)
degs = degree(g)
@test all(degs[1:5] .== 2)

g = BeliefPropagation.random_bipartite_regular_graph(5, 6, 2, first_regular=false)
degs = degree(g)
@test all(degs[6:11] .== 2)


abstract type StartValueGenerator end

const two_pi = convert(Float64, 2π)

struct TorusStartValueGenerator{N} <: StartValueGenerator
end

startvalue(::TorusStartValueGenerator{2}, w=nothing) = SVector(rand() * two_pi, rand() * two_pi)
startvalue(::TorusStartValueGenerator{3}, w=nothing) = SVector(rand() * two_pi, rand() * two_pi, rand() * two_pi)

struct DomainStartValueGenerator2D <: StartValueGenerator
    xmin::Float64
    ymin::Float64

    dx::Float64
    dy::Float64
end

function DomainStartValueGenerator2D(lims::Tuple{<:Real, <:Real, <:Real, <:Real})
    xmin, xmax, ymin, ymax = convert.(Float64, lims)
    DomainStartValueGenerator2D(xmin, ymin, xmax - xmin, ymax - ymin)
end
DomainStartValueGenerator2D(xlims, ylims) = DomainStartValueGenerator2D((xlims..., ylims...))

# function DomainStartValueGenerator2D(f::MP.AbstractPolynomial; apply_exp=false, kwargs...)
#     xmin, xmax, ymin, ymax = amoeba_carcase_domain_heuristic(f; kwargs...)
#     DomainStartValueGenerator2D(xmin, ymin, xmax - xmin, xmax - ymin)
# end

function startvalue(c::DomainStartValueGenerator2D, y=nothing)
    w1 = rand() * c.dx + c.xmin
    w2 = rand() * c.dy + c.ymin

    SVector(w1, w2)
end

struct DomainStartValueGenerator3D <: StartValueGenerator
    xmin::Float64
    ymin::Float64
    zmin::Float64

    dx::Float64
    dy::Float64
    dz::Float64
end

function DomainStartValueGenerator3D(lims::Tuple{<:Real, <:Real, <:Real, <:Real, <:Real, <:Real})
    xmin, xmax, ymin, ymax, zmin, zmax = convert.(Float64, lims)
    DomainStartValueGenerator3D(
        xmin, ymin, zmin,
        xmax - xmin, ymax - ymin, zmax - zmin)
end
DomainStartValueGenerator3D(xlims, ylims, zlims) = DomainStartValueGenerator3D(xlims..., ylims..., zlims...)

function startvalue(c::DomainStartValueGenerator3D, w=nothing)
    w1 = rand() * c.dx + c.xmin
    w2 = rand() * c.dy + c.ymin
    w3 = rand() * c.dz + c.zmin

    SVector(w1, w2, w3)
end

startvalue_generator(::AmoebaFiber2D) = TorusStartValueGenerator{2}()
startvalue_generator(::AmoebaFiber3D) = TorusStartValueGenerator{3}()


# AbstractDiscretizer
abstract type AbstractDiscretizer{N,D} end
    # N indicates the decoded, or natural type
    # D indicates the encoded, or discrete type

encoded_type(::AbstractDiscretizer{N,D}) where {N,D} = D
decoded_type(::AbstractDiscretizer{N,D}) where {N,D} = N

# DiscretizationAlgorithm
abstract type DiscretizationAlgorithm end

function get_discretization_counts(disc::AbstractDiscretizer, data::AbstractArray)
    counts = zeros(Int, nlabels(disc))
    for v in data
        counts[encode(disc, v)] += 1
    end
    counts
end

function get_discretization_counts(discs::Vector{T}, data::AbstractMatrix{N}) where {N, D, T<:AbstractDiscretizer{N, D}}
    nobservations, ndimensions = size(data)
    @assert length(discs) == ndimensions

    nbins = Tuple(nlabels.(discs))
    counts = zeros(Int, nbins)
    for i in 1:nobservations  # for each data point
        binaddress = zeros(D, ndimensions)
        for j in 1:ndimensions  # for each dimension
            binaddress[j] = encode(discs[j], data[i, j])
        end
        counts[binaddress...] += 1
    end

    counts
end

function get_histogram_plot_arrays(binedges::Vector{R}, counts::AbstractVector{I}) where {R<:Real, I<:Integer}
    n = length(binedges)
    n == length(counts)+1 || error("binedges must have exactly one more entry than counts!")
    arr_x = [binedges[1]; binedges]
    arr_y = [zero(I); counts; zero(I)]
    (arr_x, arr_y)
end
function get_histogram_plot_arrays(binedges::Vector{R}, pdfs::AbstractVector{F}) where {R<:Real, F<:Real}
    n = length(binedges)
    n == length(pdfs)+1 || error("binedges must have exactly one more entry than pdfs!")
    arr_x = [binedges[1]; binedges]
    arr_y = [zero(F); pdfs; zero(F)]
    (arr_x, arr_y)
end


module FeatureTransform

using ..ImageMorphology: SplitAxis, SplitAxes
using ..ImageCore

export feature_transform, distance_transform

"""
    feature_transform(img::AbstractArray{Bool, N};
                      weights=nothing, nthreads=Threads.nthreads()) -> F

Compute the feature transform of a binary image `I`, finding the
closest "feature" (positions where `I` is `true`) for each location in
`I`.  Specifically, `F[i]` is a `CartesianIndex` encoding the position
closest to `i` for which `I[F[i]]` is `true`.  In cases where two or
more features in `I` have the same distance from `i`, an arbitrary
feature is chosen. If `I` has no `true` values, then all locations are
mapped to an index where each coordinate is `typemin(Int)`.

Optionally specify the weight `w` assigned to each coordinate.  For
example, if `I` corresponds to an image where voxels are anisotropic,
`w` could be the voxel spacing along each coordinate axis. The default
value of `nothing` is equivalent to `w=(1,1,...)`.

See also: [`distance_transform`](@ref).

# Citation

'A Linear Time Algorithm for Computing Exact Euclidean Distance
Transforms of Binary Images in Arbitrary Dimensions' [Maurer et al.,
2003] (DOI: 10.1109/TPAMI.2003.1177156)
"""
function feature_transform(img::AbstractArray{<:Union{Bool,AbstractGray{Bool}},N};
                           weights::Union{Nothing,NTuple{N}}=nothing,
                           nthreads::Int = length(img) < 1000 ? 1 : Threads.nthreads()) where N
    nthreads > 0 || error("the number of threads must be positive, got $nthreads")
    N == 0 && return reshape([CartesianIndex()])
    # Allocate the output
    F = similar(img, CartesianIndex{N})
    axsimg = axes(img)
    # To allocate temporary storage for voronoift!, compute one
    # element (so we have the proper type)
    fi = first(CartesianIndices(axsimg))
    drft = DistRFT(fi, weights, (), Base.tail(fi.I))
    if nthreads == 1 || N == 1
        tmp = typeof(drft)[]
        computeft!(F, img, axsimg, CartesianIndex(), weights, tmp)
        # Finish the last dimension (for multithreading, we avoid doing it in computeft!)
        finishft!(F, img, axsimg, CartesianIndex(), weights, tmp)
    else
        tmps = [typeof(drft)[] for _ = 1:nthreads] # temporary storage (one per thread)
        saxs = SplitAxes(axsimg, nthreads - 0.2)   # give main thread less work since it also schedules the others
        tasks = [Threads.@spawn computeft!(F, img, saxs[i], CartesianIndex(), weights, tmps[i]) for i = 2:nthreads]
        computeft!(F, img, saxs[1], CartesianIndex(), weights, tmps[1])
        foreach(wait, tasks)
        # Finish the last dimension
        saxs1 = SplitAxes(axsimg[1:N-1], nthreads - 0.2)
        tasks = [Threads.@spawn finishft!(F, img, (saxs1[i]..., axsimg[end]), CartesianIndex(), weights, tmps[i]) for i = 2:nthreads]
        finishft!(F, img, (saxs1[1]..., axsimg[end]), CartesianIndex(), weights, tmps[1])
        foreach(wait, tasks)
    end
    return F
end

"""
    distance_transform(F::AbstractArray{CartesianIndex}, [w=nothing]) -> D

Compute the distance transform of `F`, where each element `F[i]`
represents a "target" or "feature" location assigned to `i`.
Specifically, `D[i]` is the distance between `i` and `F[i]`.
Optionally specify the weight `w` assigned to each coordinate; the
default value of `nothing` is equivalent to `w=(1,1,...)`.

See also: [`feature_transform`](@ref).
"""
function distance_transform(F::AbstractArray{CartesianIndex{N},N}, w::Union{Nothing,NTuple{N}}=nothing) where N
    # To allocate the proper output type, compute the distance for one element
    R = CartesianIndices(axes(F))
    dst = wnorm2(zero(eltype(R)), w)
    D = similar(F, typeof(sqrt(dst)))

    ∅ = nullindex(F)
    @inbounds for i in R
        fi = F[i]
        D[i] = fi == ∅ ? Inf : sqrt(wnorm2(fi - i, w))
    end

    D
end

# This recursive implementation computes the feature transform, other than for finishing the
# work along the final axis (axis `N` for an `N` dimensional array).
# Omission of the final axis makes it easy to implement multithreading.
# You can finish the final axis with a call to `finishft!` with `jpost = CartesianIndex()`.
function computeft!(F, img, axsimg, jpost::CartesianIndex{K}, pixelspacing, tmp) where K
    # tmp is workspace for voronoift!
    ∅ = nullindex(F)                             # sentinel position
    if K == ndims(img)-1                           # innermost loop (d=1 case, line 1)
        # Fig. 2, lines 2-8
        @inbounds @simd for i1 in axes(img, 1)
            F[i1, jpost] = gray(img[i1, jpost]) ? CartesianIndex(i1, jpost) : ∅
        end
    else                                         # recursively handle trailing dimensions
        # Fig. 2, lines 10-12
        for i1 in axsimg[ndims(img) - K]
            computeft!(F, img, axsimg, CartesianIndex(i1, jpost), pixelspacing, tmp)
        end
    end
    K == 0 && return F                           # defer the final axis, where threads will be split across next-to-last axis
    return finishft!(F, img, axsimg, jpost, pixelspacing, tmp)
end

function finishft!(F, img, axsimg, jpost, pixelspacing, tmp)
    # Fig. 2, lines 14-20
    axespre = truncatet(axsimg, jpost)          # first N-K-1 axes (these are "finished" within each K+1-dimensional slice)
    for jpre in CartesianIndices(axespre)
        voronoift!(F, img, jpre, jpost, pixelspacing, tmp)     # finish axis N-K in K-dimensional slice `jpost`
    end
    return F
end

function voronoift!(F, img, jpre, jpost, pixelspacing, tmp)
    d = length(jpre)+1   # axis to work along
    ∅ = nullindex(F)
    empty!(tmp)
    for i in axes(img, d)
        # Fig 3, lines 3-13
        xi = CartesianIndex(jpre, i, jpost)
        @inbounds fi = F[xi]
        if fi != ∅
            fidist = DistRFT(fi, pixelspacing, jpre, jpost)
            if length(tmp) < 2
                push!(tmp, fidist)
            else
                @inbounds while length(tmp) >= 2 && removeft(tmp[end-1], tmp[end], fidist)
                    pop!(tmp)
                end
                push!(tmp, fidist)
            end
        end
    end
    nS = length(tmp)
    nS == 0 && return F
    # Fig 3, lines 18-24
    l = 1
    @inbounds fthis = tmp[l].fi
    for i in axes(img, d)
        xi = CartesianIndex(jpre, i, jpost)
        d2this = wnorm2(xi-fthis, pixelspacing)
        while l < nS
            @inbounds fnext = tmp[l+1].fi
            d2next = wnorm2(xi-fnext, pixelspacing)
            if d2this > d2next
                d2this, fthis = d2next, fnext
                l += 1
            else
                break
            end
        end
        @inbounds F[xi] = fthis
    end
    F
end

## Utilities

# Stores a feature location and its distance from the hyperplane Rd
struct DistRFT{N,T}
    fi::CartesianIndex{N}
    dist2::T
    d::Int  # the coordinate in dimension d
end

"""
    DistRFT(fi::CartesianIndex, w, jpre, jpost)

Bundles a feature `fi` together with its distance from the line Rd,
where Rd is specified by `(jpre..., :, jpost...)`. `w` is the
weighting applied to each coordinate, and must be `nothing` or be a
tuple with the same number of coordiantes as `fi`.

"""
function DistRFT(fi::CartesianIndex, w, jpre::CartesianIndex, jpost::CartesianIndex)
    d2pre, ipost, wpost = dist2pre(fi.I, w, jpre.I)
    d2post = wnorm2(CartesianIndex(ipost) - jpost, wpost)
    @inbounds fid = fi[length(jpre)+1]
    DistRFT(fi, d2pre + d2post, fid)
end
DistRFT(fi::CartesianIndex, w, jpre::Tuple, jpost::Tuple) =
    DistRFT(fi, w, CartesianIndex(jpre), CartesianIndex(jpost))

@inline function removeft(u, v, w)
    a, b, c = v.d-u.d, w.d-v.d, w.d-u.d
    c*v.dist2 - b*u.dist2 - a*w.dist2 > a*b*c
end

"""
    truncatet(inds, j::CartesianIndex{K})

Discard the last `K+1` elements of the tuple `inds`.
"""
truncatet(inds, j::CartesianIndex) = _truncatet((), inds, j)
_truncatet(out, inds::NTuple{N}, j::CartesianIndex{N}) where {N} = Base.front(out)
@inline _truncatet(out, inds, j) = _truncatet((out..., inds[1]), Base.tail(inds), j)

if VERSION < v"1.1"
    nullindex(A::AbstractArray{T,N}) where {T,N} = typemin(Int)*one(CartesianIndex{N})
else
    # This is the proper implementation
    nullindex(A::AbstractArray{T,N}) where {T,N} = typemin(Int)*oneunit(CartesianIndex{N})
end

"""
    wnorm2(x::CartesianIndex, w)

Compute `∑ (w[i]*x[i])^2`.  Specifying `nothing` for `w` is equivalent to `w = (1,1,...)`.
"""
wnorm2(x::CartesianIndex, w) = _wnorm2(0, x.I, w)
_wnorm2(s, ::Tuple{}, ::Nothing)    = s
_wnorm2(s, ::Tuple{}, ::Tuple{}) = s
@inline _wnorm2(s, x, w::Nothing) = _wnorm2(s + sqr(x[1]), Base.tail(x), w)
@inline _wnorm2(s, x, w) = _wnorm2(s + sqr(w[1]*x[1]), Base.tail(x), Base.tail(w))

"""
    dist2pre(x, w, jpre) -> s, xpost, wpost

`s` is equivalent to `wnorm2(x[1:length(jpre)]-jpre, w)`. `xpost` and
`wpost` contain the trailing indices of `x` and `w` (skipping the
element `length(jpre)+1`).
"""
dist2pre(x::Tuple, w, jpre) = _dist2pre(0, x, w, jpre)
_dist2pre(s, x, w::Nothing, ::Tuple{}) = s, Base.tail(x), w
_dist2pre(s, x, w,       ::Tuple{}) = s, Base.tail(x), Base.tail(w)
@inline _dist2pre(s, x, w::Nothing, jpre) = _dist2pre(s + sqr(x[1]-jpre[1]), Base.tail(x), w, Base.tail(jpre))
@inline _dist2pre(s, x, w, jpre) = _dist2pre(s + sqr(w[1]*(x[1]-jpre[1])), Base.tail(x), Base.tail(w), Base.tail(jpre))

@inline sqr(x) = x*x

end # module


@testset "667.beautiful-arrangement-ii.jl" begin
    @test beautiful_arrangement(3, 1) == [1, 2, 3]
    @test beautiful_arrangement(3, 2) == [1, 3, 2]
    @test beautiful_arrangement(6, 3) == [1, 2, 3, 6, 4, 5]
    res1 = beautiful_arrangement(60, 32)
    @test res1 ==
          [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22,
           23, 24, 25, 26, 27, 28, 60, 29, 59, 30, 58, 31, 57, 32, 56, 33, 55, 34, 54, 35,
           53, 36, 52, 37, 51, 38, 50, 39, 49, 40, 48, 41, 47, 42, 46, 43, 45, 44]
    @test length(Set(abs(res1[i] - res1[i + 1]) for i in 1:(length(res1) - 1))) == 32
end


# implementation of 5 stage, 4th order Low Storage Explicit Runge Kutta
# from Carpenter and Kennedy's Fourth-Order 2N-Storage Runge Kutta Schemes
# NASA Technical Memorandum 109112

# this code borrows some infrastructure (pde_pre_func, pde_post_func) from
# the classical rk4 file
# Also borrows the RK$CheckpointData

"""
  This function implements the 5 stage, 4th order Low Storage Explicit Runge Kutta scheme of
  Carpenter and Kennedy

  Arguments:
    f: a function that evalutes du/dt = f(q, t)
    delta_t: the time step
    t_max: the maximum time value
    q_vec: vector (of length numDof) containing initial solution.  Will contain final solution
           at exit
    res_vec: vector to store the residual in during evaluation of f.  The contents of this vector
             at exit is undefined
    pre_func: function to call after new values are written into q_vec but before f is called
    post_func: function to call after f is called but before res_vec is accessed
    ctx: tuple arguments of f (ie. f = f(ctx...))
    opts: options dictionary
    timing: a Timings object

  Keyword Arguments:
    majorIterationCallback: function to call after first function evaluation of each time step, ie.
                            when q_vec and res_vec have been updated.  Useful for logging.  Defaults
                            to no-op
    res_tol: stopping tolerance for residual (useful for pseudo-timestepping), default -1.0
    real_time: whether or not to advance time (ie. pseudo timestepping or not) default faulse

  See the documentation for rk4.
"""

function lserk54(f::Function, delta_t::AbstractFloat, t_max::AbstractFloat,
             q_vec::AbstractVector, res_vec::AbstractVector, pre_func,
             post_func, ctx, opts, timing::Timings=Timings();
             majorIterationCallback=((a...) -> (a...)),
             res_tol = -1.0, real_time=false)

  # LSERK coefficients
  const a_coeffs = [0;
                    -567301805773.0/1357537059087.0;
                    -2404267990393.0/2016746695238.0;
                    -3550918686646.0/2091501179385.0;
                    -1275806237668.0/842570457699.0]

  const b_coeffs = [1432997174477.0/9575080441755.0;
                    5161836677717.0/13612068292357.0;
                    1720146321549.0/2090206949498.0;
                    3134564353537.0/4481467310338.0;
                    2277821191437.0/14882151754819.0]

  const c_coeffs = [0;
                    1432997174477/9575080441755;
                    2526269341429/6820363962896;
                    2006345519317/3224310063776;
                    2802321613138/2924317926251]

  myrank = MPI.Comm_rank(MPI.COMM_WORLD)
#  MPI.Barrier(MPI.COMM_WORLD)
  if myrank == 0
    println(BSTDOUT, "\nEntered lserk54")
    println(BSTDOUT, "res_tol = ", res_tol)
  end
#  flush(BSTDOUT)
#  MPI.Barrier(MPI.COMM_WORLD)
# res_tol is alternative stopping criteria

  # unpack options
  output_freq = opts["output_freq"]::Int
  use_itermax = opts["use_itermax"]::Bool
  if use_itermax
    itermax = opts["itermax"]
  end

  use_checkpointing = opts["use_checkpointing"]::Bool
  chkpoint_freq = opts["checkpoint_freq"]::Int
  ncheckpoints = opts["ncheckpoints"]::Int

  t = 0.0  # timestepper time
  treal = 0.0  # real time (as opposed to pseudo-time)
  t_steps = round(Int, t_max/delta_t)
  println(BSTDOUT, "t_steps: ",t_steps)
  println(BSTDOUT, "delta_t = ", delta_t)

  (m,) = size(q_vec)

  # allocate storage
  # this is actually a 3N scheme because the function f cannot overwrite its
  # input
  dq_vec = zeros(q_vec)

  if myrank == 0
    _f1 = open("convergence.dat", "a")
    f1 = BufferedIO(_f1)
  end

  # setup all the checkpointing related data
  chkpointer, chkpointdata, skip_checkpoint = explicit_checkpoint_setup(opts, myrank)
  istart = chkpointdata.i

  flush(BSTDOUT)
  #-----------------------------------------------------
  # Main timestepping loop
  timing.t_timemarch += @elapsed for i=istart:(t_steps + 1)

    t = (i-2)*delta_t

    @mpi_master if i % output_freq == 0
       println(BSTDOUT, "\ntimestep ",i)
       if i % output_freq == 0
         flush(BSTDOUT)
       end
    end

    if use_checkpointing && i % chkpoint_freq == 0
      if skip_checkpoint    # skip only the first checkpoint
        skip_checkpoint = false
      else
        @mpi_master println(BSTDOUT, "Saving checkpoint at timestep ", i)
        skip_checkpoint = false
        # save all needed variables to the chkpointdata
        chkpointdata.i = i

        if countFreeCheckpoints(chkpointer) == 0
          freeOldestCheckpoint(chkpointer)  # make room for a new checkpoint
        end

        # save the checkpoint
        saveNextFreeCheckpoint(chkpointer, ctx..., opts, chkpointdata)
      end   # end of if skip_checkpoint check
    end   # end of if use_checkpointing check

    #--------------------------------------------------------------------------
    # stage 1
#    f(params, u, F_vals, t_i)

    pre_func(ctx..., opts)
    if real_time treal = t end
    timing.t_func += @elapsed f(ctx..., opts, treal)
    sol_norm = post_func(ctx..., opts)

    #--------------------------------------------------------------------------
    # callback and logging
    timing.t_callback += @elapsed majorIterationCallback(i, ctx..., opts, BSTDOUT)

    # logging
    @mpi_master if i % 1 == 0
      println(f1, i, " ", sol_norm)
    end

    @mpi_master if i % output_freq == 0
      println(BSTDOUT, "flushing convergence.dat to disk")
      flush(f1)
    end

    # check stopping conditions
    if (sol_norm < res_tol) && !real_time
      if myrank == 0
        println(BSTDOUT, "breaking due to res_tol, res norm = $sol_norm")
        close(f1)
        flush(BSTDOUT)
      end
      break
    end

    if use_itermax && i > itermax
      if myrank == 0
        println(BSTDOUT, "breaking due to itermax")
        close(f1)
        flush(BSTDOUT)
      end
      break
    end

    #--------------------------------------------------------------------------
    # remaining stages

    # stage 1 update

    fac = b_coeffs[1]
    for j=1:length(q_vec)
      dq_vec[j] = delta_t*res_vec[j]
      q_vec[j] += fac*dq_vec[j]
    end

    # loop over remaining stages
    for stage=2:5
      pre_func(ctx..., opts)
      if real_time
        treal = t + c_coeffs[stage]*delta_t
      end
      timing.t_func += @elapsed f( ctx..., opts, treal)
      post_func(ctx..., opts, calc_norm=false)

      # update
      fac = a_coeffs[stage]
      fac2 = b_coeffs[stage]
      for j=1:length(q_vec)
        dq_vec[j] = fac*dq_vec[j] + delta_t*res_vec[j]
        q_vec[j] += fac2*dq_vec[j]
      end
    end  # end loop over stages

  end  # end loop over timesteps

  # final update
  t += delta_t

  @mpi_master println("---------------------------------------------")
  @mpi_master println("   LSERK: final time step reached. t = $t")
  @mpi_master println("---------------------------------------------")

  if myrank == 0
    close(f1)
  end

  flush(BSTDOUT)

  return t
end  # end lserk54

"""
  See rk4 method with same signature
"""
function lserk54(f::Function, h::AbstractFloat, t_max::AbstractFloat,
             q_vec::AbstractVector, res_vec::AbstractVector, ctx, opts,
             timing::Timings=Timings();
             majorIterationCallback=((a...) -> (a...)), res_tol=-1.0,
             real_time=false)

  t = lserk54(f::Function, h::AbstractFloat, t_max::AbstractFloat,
              q_vec::AbstractVector, res_vec::AbstractVector,
              pde_pre_func, pde_post_func, ctx, opts, timing;
              majorIterationCallback=majorIterationCallback, res_tol=res_tol,
              real_time=real_time)

        return t
end


# A confusion matrix counts the number of times that the values
# of two (discrete-valued) sequences co-occur.
abstract ConfusionMatrix

type IntConfusionMatrix <: ConfusionMatrix
  xvalues::Vector{Int64}
  yvalues::Vector{Int64}
  counts::Array{Int64, 2}

  function IntConfusionMatrix(x::Vector{Int64}, y::Vector{Int64})
    xvalues = unique(x)
    yvalues = unique(y)
    cm = new(xvalues, yvalues,
      zeros(Int64, length(xvalues), length(yvalues)))
    count_pairs(cm, x, y)
    return(cm)
  end
end

xvalues(cm::ConfusionMatrix) = cm.xvalues
yvalues(cm::ConfusionMatrix) = cm.yvalues

function xindex(cm::ConfusionMatrix, x)
  index = findfirst(cm.xvalues, x)
  if index == 0
    cm.xvalues = [xvalues, x]
    cm.counts[end+1,:] = zeros(Int64, length(cm.yvalues))
    return length(xm.xvalues)
  else
    return index
  end
end

function yindex(cm::ConfusionMatrix, y)
  index = findfirst(cm.yvalues, y)
  if index == 0
    cm.yvalues = [yvalues, y]
    cm.counts[:,end+1] = zeros(Int64, length(cm.xvalues))
    return length(xm.yvalues)
  else
    return index
  end
end

function count_pairs(cm::ConfusionMatrix, x, y)
  for i in 1:min(length(x), length(y))
    count_pair(cm, x[i], y[i])
  end
end

count_pair(cm::ConfusionMatrix, x, y) = cm.counts[xindex(cm, x), yindex(cm, y)] += 1
count(cm::ConfusionMatrix, x, y) = cm.counts[xindex(cm, x), yindex(cm, y)]


"""
**Coefficient of variation**

Corrected for the sample size.

"""
function coefficient_of_variation(x)
    cv = std(x) / mean(x)
    norm = 1 + 1 / (4 * length(x))
    return norm * cv
end

"""
**Number of surviving species**

Number of species with a biomass larger than the `threshold`. The threshold is
by default set at `eps()`, which should be close to 10^-16.
"""
function species_richness(parameters; threshold::Float64=eps(), last::Int64=1000)
    @assert last <= size(parameters[:B], 1)
    measure_on = parameters[:B][end-(last-1):end,:]
    if sum(measure_on) == 0
        return NaN
    end
    richness = vec(sum(measure_on .> threshold, dims = 2))
    return mean(richness)
end

"""
**Proportion of surviving species**

Proportion of species with a biomass larger than the `threshold`. The threshold is
by default set at `eps()`, which should be close to 10^-16.
"""
function species_persistence(parameters; threshold::Float64=eps(), last::Int64=1000)
    r = species_richness(parameters, threshold=threshold, last=last)
    m = size(parameters[:B], 2) # Number of species is the number of columns in the biomass matrix
    return r/m
end

"""
**Population stability**

Population stability is measured as the mean of the negative coefficient
of variations of all species with an abundance higher than `threshold`. By
default, the stability is measured over the last `last=1000` timesteps.
"""
function population_stability(parameters; threshold::Float64=eps(), last=1000)
    @assert last <= size(parameters[:B], 1)
    non_extinct = parameters[:B][end,:] .> threshold
    measure_on = parameters[:B][end-(last-1):end,non_extinct]
    if sum(measure_on) == 0
        return NaN
    end
    stability = -mapslices(coefficient_of_variation, measure_on, dims = 1)
    return mean(stability)
end

"""
**Total biomass**

Returns the sum of biomass, averaged over the last `last` timesteps.

"""
function total_biomass(parameters; last=1000)
    @assert last <= size(parameters[:B], 1)
    measure_on = parameters[:B][end-(last-1):end,:]
    if sum(measure_on) == 0
        return NaN
    end
    biomass = vec(sum(measure_on, dims = 2))
    return mean(biomass)
end

"""
**Per species biomass**

Returns the average biomass of all species, over the last `last` timesteps.
"""
function population_biomass(parameters; last=1000)
    @assert last <= size(parameters[:B], 1)
    measure_on = parameters[:B][end-(last-1):end,:]
    if sum(measure_on) == 0
        return NaN
    end
    biomass = vec(mean(measure_on, dims = 1))
    return biomass
end

"""
**Shannon's entropy**

Corrected for the number of species, removes negative and null values, return
`NaN` in case of problem.
"""
function shannon(n)
    x = copy(n)
    x = filter((k) -> k > 0.0, x)
    try
        if length(x) > 1
            p = x ./ sum(x)
            corr = log.(length(x))
            p_ln_p = p .* log.(p)
            return -(sum(p_ln_p)/corr)
        else
            return NaN
        end
    catch
        return NaN
    end
end

"""
**Food web diversity**

Based on the average of Shannon's entropy (corrected for the number of
species) over the last `last` timesteps. Values close to 1 indicate that
all populations have equal biomasses.

"""
function foodweb_evenness(parameters; last=1000)
    @assert last <= size(parameters[:B], 1)
    measure_on = parameters[:B][end-(last-1):end,:]
    if sum(measure_on) == 0
        return NaN
    end
    shan = [shannon(vec(measure_on[i,:])) for i in 1:size(measure_on, 1)]
    return mean(shan)
end

# """
# **Save the output of a simulation**
#
# Takes a simulation output as a mandatory argument. The two keyword arguments
# are `as` (can be `:json` or `:jld`), defining the file format, and `filename`
# (without an extension, defaults to `NaN`). If `:jld` is used, the variable
# is named `befwm_simul` unless a `varname` is given.
#
# Called with the defaults, this function will write `befwm_xxxxxxxx.json`
# with the current simulation output, where `xxxxxxxx` is a hash of the `p`
# output (ensuring that all output files are unique).
#
# This function is *not* exported, so it must be called with `BioEnergeticFoodWebs.save`.
#
# """
# function save(parameters::Dict{Symbol,Any}; as::Symbol=:json, filename=nothing, varname=nothing)
#     if as == :JSON
#         as = :json
#     end
#     if as == :JLD
#         as = :jld
#     end
#     @assert as ∈ vec([:json :jld])
#     if filename == nothing
#         filename = "befwm_" * string(hash(parameters))
#     end
#     if varname == nothing
#         varname = "befwm_simul"
#     end
#     if as == :json
#         filename = filename * ".json"
#         f = open(filename, "w")
#         JSON.print(f, parameters)
#         close(f)
#     end
#     if as == :jld
#         filename = filename * ".jld"
#         JLD.save(filename, varname, parameters)
#     end
# end

"""
**Producers growth rate**

This function takes the simulation outputs from `simulate` and returns the producers
growth rates. Depending on the value given to the keyword `out_type`, it can return
more specifically:
- growth rates for each producer at each time step form end-last to last (`out_type = :all`)
- the mean growth rate for each producer over the last `last` time steps (`out_type = :mean`)
- the standard deviation of the growth rate for each producer over the last `last` time steps (`out_type = :std`)
"""
function producer_growth(out::Dict{Symbol,Any}; last::Int64 = 1000, out_type::Symbol = :all)
    parameters = out[:p] #extract parameters
    @assert last <= size(out[:B], 1)
    measure_on = out[:B][end-(last-1):end,:] #extract the biomasses that will be used
    measure_on_mat = [measure_on[i,:] for i = 1:last] #make it an array of array so we can use the map function
    if parameters[:productivity] == :nutrients #if the producers do NOT rely on nutrients for their growth
        c = out[:C][end-(last-1):end,:] #extract the timesteps of interest for the nutrients concentration
        c_mat = [c[i,:] for i = 1:last] #make it an array of array
        gr = map((x,y) -> get_growth(parameters,x,c=y), measure_on_mat, c_mat)
        growth = hcat(map(x -> x[1], gr)...)'
    else
        gr = map(x -> get_growth(parameters,x), measure_on_mat)
        growth = hcat(map(x -> x[1], gr)...)'
    end
    growth[:,.!parameters[:is_producer]] .= 0.0
    if out_type == :all #return all growth rates (each producer at each time step)
        return growth
    elseif out_type == :mean #return the producers mean growth rate over the last `last` time steps
        return mean(growth, dims = 1)
    elseif out_type == :std #return the growth rate standard deviation over the last `last` time steps (for each producer)
        return std(growth, dims = 1)
    else #if the keyword used is not one of :mean, :all or :std, print an error
        error("out_type should be one of :all, :mean or :std")
    end
end

"""
**Nutrients intake**

This function takes the simulation outputs from `simulate` and returns the producers
nutrient intake. Depending on the value given to the keyword `out_type`, it can return
more specifically:

- nutrient intake for each producer at each time step form end-last to last (`out_type = :all`)
- the mean nutrient intake for each producer over the last `last` time steps (`out_type = :mean`)
- the standard deviation of the nutrient intake for each producer over the last `last` time steps (`out_type = :std`)
"""
function nutrient_intake(out::Dict{Symbol,Any}; last::Int64 = 1000, out_type::Symbol = :all)
    parameters = out[:p] #extract parameters
    @assert last <= size(out[:B], 1)
    @assert parameters[:productivity] == :nutrients
    measure_on = out[:B][end-(last-1):end,:] #extract the biomasses that will be used
    measure_on_mat = [measure_on[i,:] for i = 1:last] #make it an array of array so we can use the map function
    c = out[:C][end-(last-1):end,:] #extract the timesteps of interest for the nutrients concentration
    c_mat = [c[i,:] for i = 1:last] #make it an array of array
    gr = map((x,y) -> get_growth(parameters,x,c=y), measure_on_mat, c_mat)
    intake = hcat(map(x -> x[2], gr)...)'
    if out_type == :all #return all growth rates (each producer at each time step)
        return intake
    elseif out_type == :mean #return the producers mean growth rate over the last `last` time steps
        return mean(intake, dims = 1)
    elseif out_type == :std #return the growth rate standard deviation over the last `last` time steps (for each producer)
        return std(intake, dims = 1)
    else #if the keyword used is not one of :mean, :all or :std, print an error
        error("out_type should be one of :all, :mean or :std")
    end
end

"""
**Consumers' biomass intake**

This function takes the simulation outputs from `simulate` and returns the consumers
biomass intake. Depending on the value given to the keyword `out_type`, it can return
more specifically:

- biomass intake for each species at each time step form end-last to last (`out_type = :all`)
- the mean biomass intake for each species over the last `last` time steps (`out_type = :mean`)
- the standard deviation of the biomass intake for each species over the last `last` time steps (`out_type = :std`)

"""
function consumer_intake(out::Dict{Symbol,Any}; last::Int64 = 1000, out_type::Symbol = :all)
    parameters = out[:p] #extract parameters
    @assert last <= size(out[:B], 1)
    measure_on = out[:B][end-(last-1):end,:] #extract the biomasses that will be used
    measure_on_mat = [measure_on[i,:] for i = 1:last] #make it an array of array so we can use the map function
    cons = map(x -> consumption(parameters, x), measure_on_mat)
    gains = hcat(map(x-> x[1], cons)...)'
    #losses = hcat(map(x-> x[:loss], cons)...)'
    if out_type == :all #return all growth rates (each producer at each time step)
        return gains
    elseif out_type == :mean #return the producers mean growth rate over the last `last` time steps
        return mean(gains, dims = 1)
    elseif out_type == :std #return the growth rate standard deviation over the last `last` time steps (for each producer)
        return std(gains, dims = 1)
    else #if the keyword used is not one of :mean, :all or :std, print an error
        error("out_type should be one of :all, :mean or :std")
    end
end

"""
**Metabolic loss**

This function takes the simulation outputs from `simulate` and returns the species
metabolic losses. Depending on the value given to the keyword `out_type`, it can return
more specifically:

- metabolic losses for each species at each time step form end-last to last (`out_type = :all`)
- the mean metabolic loss for each species over the last `last` time steps (`out_type = :mean`)
- the standard deviation of the metabolic losses for each species over the last `last` time steps (`out_type = :std`)
"""
function metabolism(out::Dict{Symbol,Any}; last::Int64 = 1000, out_type::Symbol = :all)
    parameters = out[:p] #extract parameters
    @assert last <= size(out[:B], 1)
    measure_on = out[:B][end-(last-1):end,:] #extract the biomasses that will be used
    measure_on_mat = [measure_on[i,:] for i = 1:last] #make it an array of array so we can use the map function
    function metab(b,p)
        if parameters[:productivity] == :nutrients
            m = parameters[:x] .* b
        else
            m = (parameters[:x] .* b) .* .!parameters[:is_producer]
        end
        return m
    end
    metabolic_losses = hcat(map(x -> metab(x, parameters), measure_on_mat)...)'
    if out_type == :all #return all growth rates (each producer at each time step)
        return metabolic_losses
    elseif out_type == :mean #return the producers mean growth rate over the last `last` time steps
        return mean(metabolic_losses, dims = 1)
    elseif out_type == :std #return the growth rate standard deviation over the last `last` time steps (for each producer)
        return std(metabolic_losses, dims = 1)
    else #if the keyword used is not one of :mean, :all or :std, print an error
        error("out_type should be one of :all, :mean or :std")
    end
end


using FourierGPE

##Set simulation parameters
    L=(16.,16.,16.);
    N=(64,64,64);
    sim = Sim(L,N);
    @unpack_Sim sim;

## Initialse sim
    # parameters
    μ = 25.0;
    γ = 0.05;
    tf = 4/γ;
    Nt = 200;
    t = LinRange(0.,tf,Nt);

## Run sim
    x,y,z = X;
    ψi = randn(N)+im*randn(N);
    ϕi = kspace(ψi,sim);

    @pack_Sim! sim;

## Evolve in k-space
    sol = runsim(sim); # will take a few minutes to run.

# size(xspace(sol[40], sim))

plot_iso(xspace(sol[150], sim))
psi_chaos1 = xspace(sol[10], sim)
psi_chaos2 = xspace(sol[16], sim)
psi_knots1 = xspace(sol[20], sim)
psi_knots2 = xspace(sol[25], sim)
psi_knots3 = xspace(sol[30], sim)
psi_knots4 = xspace(sol[40], sim)
psi_tubes1 = xspace(sol[50], sim)
psi_tubes2 = xspace(sol[90], sim)
psi_ringtube = xspace(sol[100], sim)

@save "box_vorts.jld2" psi_chaos1 psi_chaos2 psi_knots1 psi_knots2 psi_knots3 psi_knots4 psi_tubes1 psi_tubes2 psi_ringtube X

# function dense(i)
#     ψm = xspace(sol[i],sim)
#     density = abs2.(ψm)
#     pmax = maximum(density)
#     return density/pmax
# end

# function densityfilm(Nt,saveto="media/3dquenchiso.gif")
#     scene = Scene()
#     tindex = Node(1)

#     scene = volume(lift(i -> dense(i), tindex), algorithm = :iso,show_axis=false)

#     record(scene, saveto, 1:Nt-10) do i
#         tindex[] = i
#     end
# end

# densityfilm(Nt)


@testset "methods" begin
# Data
fn_phase = "data/small/Phase.nii"
fn_mag = "data/small/Mag.nii"
phase_nii = readphase(fn_phase)
mag_nii = readmag(fn_mag)
I = mag_nii .* exp.(1im .* phase_nii)
TEs = 4:4:12

# homodyne
h1 = homodyne(mag_nii, phase_nii)
h2 = homodyne(Float32.(mag_nii), Float32.(phase_nii))
h3 = homodyne(I)
h4 = homodyne(mag_nii, phase_nii; σ=[5,5])
h5 = homodyne(mag_nii, phase_nii; dims=1:3)

@test h1 == h2
@test h1 == h3
@test h1 != h4
@test h1 != h5
@test h4 != h5

# inplace test
I2 = copy(I)
@test I == I2
homodyne!(I2)
@test I != I2
@test h3 == I2

# calculateB0
B0 = calculateB0_unwrapped(romeo(phase_nii; TEs=TEs), mag_nii, TEs)
@test all(isfinite.(B0))

# make border of image noise
phase = Float32.(phase_nii)
phase[1:3,:,:,:] .= 2π .* rand.() .- π
phase[(end-2):end,:,:,:] .= 2π .* rand.() .- π
phase[:,1:3,:,:] .= 2π .* rand.() .- π
phase[:,(end-2):end,:,:] .= 2π .* rand.() .- π
phase[:,:,1:3,:] .= 2π .* rand.() .- π
phase[:,:,(end-2):end,:] .= 2π .* rand.() .- π

# getvoxelquality
vq = romeovoxelquality(phase; TEs=TEs)
@test all(isfinite.(vq))
@test size(vq) == size(phase_nii)[1:3]

# mask_from_voxelquality
mask = mask_from_voxelquality(vq, 0.5)
@test mask isa AbstractArray{<:Bool}
@test !all(mask .== true)
@test !all(mask .== false)

end


module TestDiversityMeasure
using Test
using LinearAlgebra

using Diversity
using Diversity.ShortNames
using DataFrames

pop = [3, 3, 4]
pop = pop / sum(pop)
oc = Onecommunity()
sim = [1.0 0 0; 1.0 1.0 0.0; 1.0 1.0 1.0]
ms = GeneralTypes(sim)
meta = Metacommunity(pop, ms, oc)
meta1 = Metacommunity(pop)

ab3 = [1.0 2; 3 0; 0 4]'
ab3 = ab3 / sum(ab3)
sc = Subcommunities(3)
sp = Species(2)
meta2 = Metacommunity(ab3, sp, sc)
nab = NormalisedAlpha(meta2)

@testset "Diversity measures" begin
    diversities = [RawAlpha, NormalisedAlpha, RawBeta, NormalisedBeta,
                   RawRho, NormalisedRho, Gamma]
    shortds = [α, ᾱ, β, β̄, ρ, ρ̄, Γ]
    chars = ["α", "ᾱ", "β", "β̄", "ρ", "ρ̄", "γ"]
    asciis = ["RawAlpha", "NormalisedAlpha",
              "RawBeta", "NormalisedBeta",
              "RawRho", "NormalisedRho", "Gamma"]
    fulls = ["raw alpha diversity", "normalised alpha diversity",
             "distinctiveness", "effective number of subcommunities",
             "redundancy", "representativeness", "gamma diversity"]
    for i in 1:length(diversities)
        @test diversities[i] == shortds[i]
        @test getName(diversities[i](meta)) == chars[i]
        @test getASCIIName(diversities[i](meta2)) == asciis[i]
        @test getFullName(diversities[i](meta1)) == fulls[i]
    end
end

numbers = [1., 2, 4, 8, 16];
numspecies = 100;
fragments = rand(numspecies);
weights = rand(numspecies);
weights /= sum(weights);
Z1 = ones(typeof(weights[1]), (length(weights), length(weights)));
numcommunities = 8;
manyweights = rand(numspecies, numcommunities);
manyweights *= Diagonal(reshape(mapslices(v -> 1. / sum(v), manyweights;
                                          dims=1), numcommunities));

@testset "inddiv / subdiv / metadiv" begin
    @test individualDiversity(nab, 0)[!,:diversity] ≈ inddiv(nab, 0)[!,:diversity]
    @test individualDiversity(nab)(1)[!,:diversity] ≈ inddiv(nab, 1)[!,:diversity]
    idnab = inddiv(nab, [2, 3])
    @test idnab[isapprox.(collect(idnab[!,:q]), 2), :diversity] ≈ inddiv(nab, 2)[!,:diversity]
    allid = inddiv(meta2, Inf)
    @test allid[allid[!,:measure] .== "RawAlpha", :diversity] ≈ inddiv(RawAlpha(meta2), Inf)[!,:diversity]

    @test subcommunityDiversity(nab, Inf)[!,:diversity] ≈ subdiv(nab, Inf)[!,:diversity]
    sdnab = subdiv(nab, [4, 5])
    @test sdnab[isapprox.(collect(sdnab[!,:q]), 4), :diversity] ≈ subdiv(nab, 4)[!,:diversity]
    allsd = subdiv(meta2, Inf)
    @test allsd[allsd[!,:measure] .== "NormalisedAlpha", :diversity] ≈ subdiv(nab, Inf)[!,:diversity]
    allmd = subdiv(meta1, Inf)
    @test allmd[allmd[!,:measure] .== "NormalisedAlpha", :diversity] ≈ metadiv(ᾱ(meta1), Inf)[!,:diversity]

    scg = subcommunityDiversity(Gamma(meta2))
    @test scg(1)[!,:diversity] ≈ scg(1.0)[!,:diversity]

    communities = rand(numspecies, numcommunities);
    communities /= sum(communities);
    @test subdiv(NormalisedAlpha(Metacommunity(communities)), 0)[!,:diversity] ≈ numspecies * ones(size(communities, 2))
    @test subdiv(NormalisedAlpha(Metacommunity(communities)), [0])[!,:diversity] ≈ numspecies * ones(size(communities, 2))
    qs = [0, 1, 2, Inf]
    sna = subdiv(NormalisedAlpha(Metacommunity(communities, Z1)), qs)
    @test nrow(sna) == length(qs) * size(communities, 2)
    for q in qs
        @test sna[isapprox.(sna[!,:q], q), :diversity] ≈ ones(size(communities, 2))
    end
    @test subdiv(RawAlpha(Metacommunity(communities)), 0)[!,:diversity] ≈ numspecies * vec(mapslices(v -> 1. / sum(v), communities; dims=1))

    even = ones((numspecies, numcommunities)) / (numspecies * numcommunities);
    qs = [0, 1, 2, 3, 4, 5, 6, Inf];
    @test metadiv(NormalisedAlpha(Metacommunity(even)), qs)[!,:diversity] ≈ numspecies * ones(length(qs))
    @test metadiv(RawAlpha(Metacommunity(even)), qs)[!,:diversity] ≈ numspecies * numcommunities * ones(length(qs))
    md2 = metadiv(meta2, Inf)
    @test md2[md2[!,:measure] .== "Gamma", :diversity] ≈ metadiv(Gamma(meta2), Inf)[!,:diversity]

    probs = reshape(mapslices(sum, communities; dims=2),
                    size(communities, 1));
    @test metadiv(Gamma(Metacommunity(communities)), qs)[!,:diversity] ≈ qD(probs, qs)
    @test metadiv(Gamma(Metacommunity(communities, Z1)), qs)[!,:diversity] ≈ qDZ(probs, qs, Z1)

    Z = rand(numspecies, numspecies)
    @test metadiv(Gamma(Metacommunity(communities, Z)), qs)[!,:diversity] ≈ qDZ(probs, qs, Z)

    colweights = rand(numcommunities)
    colweights /= sum(colweights)
    allthesame = probs * colweights'
    @test metadiv(RawBeta(Metacommunity(allthesame, Z)), qs)[!,:diversity] ≈ 1.0 ./ qD(colweights, 2 .- qs)
    @test metadiv(NormalisedBeta(Metacommunity(allthesame, Z)), qs)[!,:diversity] ≈ ones(length(qs))
    @test metadiv(NormalisedRho(Metacommunity(allthesame, Z)), qs)[!,:diversity] ≈ ones(length(qs))
    @test metadiv(RawRho(Metacommunity(allthesame, Z)), qs)[!,:diversity] ≈ qD(colweights, qs)

    communitylist = rand(1:numcommunities, numspecies)
    distinct = zeros(Float64, (numspecies, numcommunities))
    for i in 1:numspecies
        distinct[i, communitylist[i]] = weights[i]
    end

    @test metadiv(RawRho(Metacommunity(distinct)), qs)[!,:diversity] ≈ ones(length(qs))
    subnr = subdiv(NormalisedRho(Metacommunity(distinct)), qs)
    for q in qs
        @test subnr[isapprox.(subnr[!,:q], q), :diversity] ≈
              vec(sum(distinct; dims=1))
    end
    @test metadiv(NormalisedBeta(Metacommunity(distinct)), qs)[!,:diversity] ≈
        qD(reshape(sum(distinct; dims=1), numcommunities), qs)
    @test metadiv(RawBeta(Metacommunity(distinct)), qs)[!,:diversity] ≈ ones(length(qs))

    # many (unexported!) diversity levels not yet implemented
    @test_throws ErrorException Diversity.communityDiversity(nab)
end

end


## Aircraft analysis case
using AeroMDAO

## Wing
wing = Wing(foils     = Foil.(fill(naca4((0,0,1,2)), 3)),
            chords    = [1.0, 0.6, 0.2],
            twists    = [2.0, 0.0, -2.0],
            spans     = [4.0, 0.2],
            dihedrals = [5., 30.],
            sweep_LEs = [5., 30.]);
print_info(wing, "Wing")

# Horizontal tail
htail = Wing(foils     = Foil.(fill(naca4((0,0,1,2)), 2)),
             chords    = [0.7, 0.42],
             twists    = [0.0, 0.0],
             spans     = [1.25],
             dihedrals = [0.],
             sweep_LEs = [6.39])
print_info(htail, "Horizontal Tail")

# Vertical tail
vtail = HalfWing(foils     = Foil.(fill(naca4((0,0,0,9)), 2)),
                 chords    = [0.7, 0.42],
                 twists    = [0.0, 0.0],
                 spans     = [1.0],
                 dihedrals = [0.],
                 sweep_LEs = [7.97])
print_info(vtail, "Vertical Tail")

## Assembly
wing_panels  = panel_wing(wing,                 # Wing or HalfWing type
                          [20, 3],              # Number of spanwise panels for half-wing
                          10;                   # Chordwise panels
                        #   spacing = "cosine"  # Spacing distribution: Default works well for symmetric
                         )

htail_panels = panel_wing(htail, [6], 6;
                          position	= [4., 0, 0],
                          angle 	= deg2rad(-2.),
                          axis 	  	= [0., 1., 0.],
                          spacing   = "uniform"
                         )
vtail_panels = panel_wing(vtail, [6], 5;
                          position 	= [4., 0, 0],
                          angle 	= π/2,
                          axis 	 	= [1., 0., 0.],
                          spacing   = "uniform"
                         )

aircraft = Dict("Wing" 			  	=> wing_panels,
                "Horizontal Tail" 	=> htail_panels,
                "Vertical Tail"   	=> vtail_panels)

S, b, c = projected_area(wing), span(wing), mean_aerodynamic_chord(wing);

## Case
ac_name = "My Aircraft"
ρ       = 1.225
ref     = [0.25c, 0., 0.]
V, α, β = 1.0, 1.0, 0.0
Ω       = [0.0, 0.0, 0.0]
fs      = Freestream(V, α, β, Ω)

@time data =
    solve_case(aircraft, fs;
               rho_ref     = ρ, 		# Reference density
               r_ref       = ref, 		# Reference point for moments
               area_ref    = S, 		# Reference area
               span_ref    = b, 		# Reference span
               chord_ref   = c, 		# Reference chord
               name        = ac_name,	# Aircraft name
               print       = true,		# Prints the results for the entire aircraft
            #    print_components = true,	# Prints the results for each component
              );

## Data collection
comp_names = (collect ∘ keys)(data) # Gets aircraft component names from analysis
comp  = comp_names[1]			   # Pick your component
nf_coeffs, ff_coeffs, CFs, CMs, horseshoe_panels, normals, horseshoes, Γs = data[comp]; #  Get the nearfield, farfield, force and moment coefficients, and other data for post-processing
print_coefficients(nf_coeffs, ff_coeffs, comp)

## Stability case
@time dv_data =
    solve_stability_case(aircraft, fs;
                         rho_ref     = ρ,
                         r_ref       = ref,
                         area_ref    = S,
                         span_ref    = b,
                         chord_ref   = c,
                         name        = ac_name,
                         print       = true,
                         print_components = true,
                        );

## Data collection
names = (collect ∘ keys)(dv_data)
comp  = names[1]
nf, ff, dvs = dv_data[comp];
print_coefficients(nf, ff, comp)
print_derivatives(dvs, comp)

## Streamlines

# Chordwise distribution
# using StaticArrays

# num_points = 100
# max_z = 2
# y = span(wing) / 10
# seed = SVector.(fill(-0.1, num_points), fill(y, num_points), range(-max_z, stop = max_z, length = num_points))

# Spanwise distribution
span_points = 10
init        = chop_leading_edge(wing, span_points)
dx, dy, dz  = 0, 0, 1e-3
seed        = [ init .+ Ref([dx, dy, dz]) ;
                init .+ Ref([dx, dy,-dz]) ];

distance = 8
num_stream_points = 200
streams = plot_streams(fs, seed, horseshoes, Γs, distance, num_stream_points);

##
using Plots
gr(size = (200, 100), dpi = 300)

##
horseshoe_coords = plot_panels(horseshoe_panels[:]);

z_limit = b
plot(xaxis = "x", yaxis = "y", zaxis = "z",
     camera = (30, 60),
    #  xlim = (-z_limit/2, z_limit/2),
     aspect_ratio = 1,
     zlim = (-z_limit/2, z_limit/2),
     size = (1280, 720)
    )
plot!.(horseshoe_coords, color = :gray, label = :none)
plot!.(streams, color = :green, label = :none)
plot!()

## Exaggerated CF distribution for plot, only works with GR and not Plotly

# Forces
wind_CFs = body_to_wind_axes.(CFs, fs.alpha, fs.beta)
CDis     = @. getindex(wind_CFs, 1)
CYs	     = @. getindex(wind_CFs, 2)
CLs      = @. getindex(wind_CFs, 3)

hs_pts = bound_leg_center.(horseshoes)
hs_xs  = getindex.(hs_pts, 1)
hs_ys  = getindex.(hs_pts, 2)
hs_zs  = getindex.(hs_pts, 3)

quiver!(hs_xs[:], hs_ys[:], hs_zs[:], quiver=(CDis[:], CYs[:], CLs[:]) .* 500)
plot!()


export buildD, findSEA

struct DistanceMatrix
    atoms::Vector{Any}
    distances::AbstractArray
end

struct SEA
    sets::Tuple
end

function distance(A::Atom, B::Atom)
    a = A.xyz
    b = B.xyz
    return (((a[1]-b[1])^2) + ((a[2]-b[2])^2) + ((a[3]-b[3])^2))^0.5
end

function buildD(V)
    len = size(V)[1]
    atoms = []
    arr = zeros(Float64, (len,len))
    for i = 1:len
        push!(atoms, V[i].Z)
    end
    for i = 1:len, j = i+1:len
        arr[i,j] = distance(V[i],V[j])
        arr[j,i] = arr[i,j]
    end
    return DistanceMatrix(atoms, arr)
end

function checkSEA(A::Vector{Float64}, B::Vector{Float64}, δ)::Bool
    a = sort(A)
    b = sort(B)
    z = a - b
    for i in z
        if i < 10.0^(-δ)
            continue
        else
            return false
        end
    end
    return true
end

function findSEA(D::DistanceMatrix, δ::Int)
    len = size(D.distances)[1]
    out = Tuple[]
    for i = 1:len, j = i+1:len
        if cmp(D.atoms[i], D.atoms[j]) == 0
            c = checkSEA(D.distances[:,i], D.distances[:,j], δ)
            if c
                push!(out, (i,j))
            end
        end
    end
    nonos = Int[]
    biggerun = Vector{Int}[]
    for i = 1:len
        if i in nonos
            continue
        else
            biggun = Int[]
            push!(biggun,i)
        end

        for k in out
            if i in k
                if i == k[1]
                    push!(biggun, k[2])
                    push!(nonos, k[2])
                else
                    push!(biggun, k[1])
                    push!(nonos, k[1])
                end
            end
        end
    push!(biggerun, biggun)
    end

    return biggerun
end


include("SortSparse.jl")
include("KoLesky.jl")
include("Cholesky.jl")
include("CovFuncs.jl")
include("Utils.jl")
using LinearAlgebra
using Distributions
using Distances
using Random
using Match
using JLD
Random.seed!(123)

N = 10000
d = 2
l = 0.5
σList = [1e3, 1e2, 1e1, 1e0, 1e-1, 1e-2, 1e-3, 1e-4]
ρ = 3.0
λ = 1.5

# Creating uniform random data
x = rand(d, N)

# reordering points and forming skeletons
colptr, rowval, P, revP, distances = sortSparseRev(x, ρ, 1)
xOrd = x[:, P]
skeletons = construct_skeletons(colptr, rowval, distances, λ)

KLTrueNaive = zeros(3, length(σList))
KLNaiveTrue = zeros(3, length(σList))
KLTrueApp = zeros(3, length(σList))
KLAppTrue = zeros(3, length(σList))
KLTrueLarge = zeros(3, length(σList))
KLLargeTrue = zeros(3, length(σList))
KLTrueExact = zeros(3, length(σList))
KLExactTrue = zeros(3, length(σList))

σBase = exp.(randn(N))
σBase .= max.(σBase, eps(Float64))

covfunc12!(r) = matern12!(r,l)
covfunc32!(r) = matern32!(r,l)
covfunc52!(r) = matern52!(r,l)
for (iOrder, maternOrder) in enumerate(["12", "32", "52"])
  # Make sure to avoid NANs
  @match maternOrder begin
    "12" => (covfunc! = covfunc12!)
    "32" => (covfunc! = covfunc32!)
    "52" => (covfunc! = covfunc52!)
  end

  for (iScale, σScale) in enumerate(σList)
    σ = σScale * σBase
    # Creating reference solution
    Θ = covfunc!(pairwise(Euclidean(), xOrd, dims=2)) + Diagonal(σ)

    # Creating naive vecchia approximation
    LNaive = assembleL(skeletons, xOrd, covfunc!, σ)
    ΘNaive = inv(Matrix(LNaive)') * inv(Matrix(LNaive))

    # Creating "smart" vecchia approximation
    L = assembleL(skeletons, xOrd, covfunc!)
    U = squareSparse(L) + spdiagm(0 => 1 ./ σ)
    icholU_high_level!(U)
    noisecov = NoiseCov(L, U, σ)
    ΘExact = exactMatrix(noisecov)
    ΘApp = appMatrix(noisecov)

    # Creating smart vecchia approximation with larger stencil for IC0
    ULarge = triu(L * L' + spdiagm(0 => 1 ./ σ))
    icholU_high_level!(ULarge)
    noisecovLarge = NoiseCov(L, ULarge, σ)
    ΘLarge = appMatrix(noisecovLarge)

    KLTrueNaive[iOrder, iScale] = KL(Θ, ΘNaive)
    KLNaiveTrue[iOrder, iScale] = KL(ΘNaive, Θ)

    KLTrueApp[iOrder, iScale] = KL(Θ, ΘApp)
    KLAppTrue[iOrder, iScale] = KL(ΘApp, Θ)

    KLTrueLarge[iOrder, iScale] = KL(Θ, ΘLarge)
    KLLargeTrue[iOrder, iScale] = KL(ΘLarge, Θ)

    KLTrueExact[iOrder, iScale] = KL(Θ, ΘExact)
    KLExactTrue[iOrder, iScale] = KL(ΘExact, Θ)
  end
end

save("./out/jld/KLVarySigma.jld",
     "σList", σList,
     "KLTrueNaive", KLTrueNaive,
     "KLNaiveTrue", KLNaiveTrue,
     "KLTrueApp", KLTrueApp,
     "KLAppTrue", KLAppTrue,
     "KLTrueLarge", KLTrueLarge,
     "KLLargeTrue", KLLargeTrue,
     "KLTrueExact", KLTrueExact,
     "KLExactTrue", KLExactTrue)


pop1 = UncertainValue(
        [3.0, UncertainValue(Normal, 0, 1),
        UncertainValue(Gamma, 2, 3),
        UncertainValue(Uniform, rand(1000))],
        [0.5, 0.5, 0.5, 0.5]
    )

pop2 = UncertainValue(1:10 |> collect, rand(10))
pop3 = UncertainValue(1.0:10.0 |> collect, Weights(rand(10)))

# Uncertain population consisting of uncertain populations and other stuff
pop4 = UncertainValue([pop1, pop2], [0.1, 0.5])
pop5 = UncertainValue([pop1, pop2, 2, UncertainValue(Normal, -2, 3)], Weights(rand(4)));

uncertain_scalar_populations = [pop1, pop2, pop3, pop4, pop5];

T = Union{Nothing, ConstrainedUncertainScalarPopulation}

@test constrain(pop1, TruncateMinimum(0.2)) isa T
@test constrain(pop1, TruncateMaximum(3.0)) isa T
@test constrain(pop1, TruncateRange(0.0, 3.0)) isa T
@test constrain(pop1, TruncateLowerQuantile(0.2)) isa T
@test constrain(pop1, TruncateUpperQuantile(0.8)) isa T
@test constrain(pop1, TruncateQuantiles(0.2, 0.8)) isa T
@test constrain(pop1, TruncateStd(1)) isa T
@test constrain(pop1, TruncateStd(0.5)) isa T

# pop2.values = [1, 2, 3]
@test constrain(pop2, TruncateMinimum(0.2)) isa T
@test constrain(pop2, TruncateMaximum(2.5)) isa T
@test constrain(pop2, TruncateRange(0.5, 2.5)) isa T
@test constrain(pop2, TruncateLowerQuantile(0.2)) isa T
@test constrain(pop2, TruncateUpperQuantile(0.8)) isa T
@test constrain(pop2, TruncateQuantiles(0.2, 0.8)) isa T
@test constrain(pop2, TruncateStd(1)) isa T
@test constrain(pop2, TruncateStd(0.5)) isa T

# pop3.values = [1.0, 2.0, 3.0]
@test constrain(pop3, TruncateMinimum(0.2)) isa T
@test constrain(pop3, TruncateMaximum(2.5)) isa T
@test constrain(pop3, TruncateRange(0.5, 2.5)) isa T
@test constrain(pop3, TruncateLowerQuantile(0.2)) isa T
@test constrain(pop3, TruncateUpperQuantile(0.8)) isa T
@test constrain(pop3, TruncateQuantiles(0.2, 0.8)) isa T
@test constrain(pop3, TruncateStd(1)) isa T
@test constrain(pop3, TruncateStd(0.5)) isa T

@test constrain(pop4, TruncateMinimum(-10)) isa T
@test constrain(pop4, TruncateMaximum(10)) isa T
@test constrain(pop4, TruncateRange(-10, 10)) isa T
@test constrain(pop4, TruncateLowerQuantile(0.2)) isa T
@test constrain(pop4, TruncateUpperQuantile(0.8)) isa T
@test constrain(pop4, TruncateQuantiles(0.2, 0.8)) isa T
@test constrain(pop4, TruncateStd(1)) isa T
@test constrain(pop4, TruncateStd(0.5)) isa T

@test constrain(pop5, TruncateMinimum(-10)) isa T
@test constrain(pop5, TruncateMaximum(10)) isa T
@test constrain(pop5, TruncateRange(-10, 10)) isa T
@test constrain(pop5, TruncateLowerQuantile(0.2)) isa T
@test constrain(pop5, TruncateUpperQuantile(0.8)) isa T
@test constrain(pop5, TruncateQuantiles(0.2, 0.8)) isa T
@test constrain(pop5, TruncateStd(1)) isa T
@test constrain(pop5, TruncateStd(0.5)) isa T

# Some subpopulations consisting of both scalar values and distributions
subpop1_members = [UncertainValue(Normal, 0, 1), UncertainValue(Uniform, -2, 2), -5]
subpop2_members = [
    UncertainValue(Normal, -2, 1),
    UncertainValue(Uniform, -6, -1),
    -3,
    UncertainValue(Gamma, 1, 0.4)]

# Define the probabilities of sampling the different population members within the
# subpopulations. Weights are normalised, so we can input any numbers here indicating
# relative importance
subpop1_probs = [1, 2, 1]
subpop2_probs = [0.1, 0.2, 0.3, 0.1]

pop1 = UncertainValue(subpop1_members, subpop1_probs)
pop2 = UncertainValue(subpop2_members, subpop2_probs)

# Define the probabilities of sampling the two subpopulations in the overall population.
pop_probs = [0.3, 0.7]

# Construct overall population
pop_mixed = UncertainValue([pop1, pop2], pop_probs)

@test constrain(pop_mixed, TruncateMinimum(1)) isa T
@test constrain(pop_mixed, TruncateMaximum(2)) isa T
@test constrain(pop_mixed, TruncateRange(1, 2)) isa T
@test constrain(pop_mixed, TruncateQuantiles(0.1, 0.9)) isa T
@test constrain(pop_mixed, TruncateLowerQuantile(0.1)) isa T
@test constrain(pop_mixed, TruncateUpperQuantile(0.9)) isa T
@test constrain(pop_mixed, TruncateStd(1)) isa T
@test constrain(pop_mixed, TruncateStd(0.5)) isa T

# Truncation
#------------------------------------------
pop1 = UncertainValue(
        [3.0, UncertainValue(Normal, 0, 1),
        UncertainValue(Gamma, 2, 3),
        UncertainValue(Uniform, rand(1000))],
        [0.5, 0.5, 0.5, 0.5]
    )

pop2 = UncertainValue([1, 2, 3], rand(3))
pop3 = UncertainValue([1.0, 2.0, 3.0], Weights(rand(3)))

# Uncertain population consisting of uncertain populations and other stuff
pop4 = UncertainValue([pop1, pop2], [0.1, 0.5])
pop5 = UncertainValue([pop1, pop2, 2, UncertainValue(Normal, -2, 3)], Weights(rand(4)));

n = 2000
p = pop1
sb = resample(p, n)

v_min, v_max = -1, 1
r_min, r_max = -1, 1
c_max = TruncateMaximum(v_max)
c_min = TruncateMinimum(v_min)
c_range = TruncateRange(r_min, r_max)
ql, qh = 0.2, 0.8
c_ql = TruncateLowerQuantile(ql)
c_qh = TruncateUpperQuantile(qh)
c_qs = TruncateQuantiles(ql, qh)
c_std = TruncateStd(1.0)

@test truncate(p, c_max) isa T
@test truncate(p, c_min) isa T
@test truncate(p, c_range) isa T
@test truncate(p, c_ql) isa T
@test truncate(p, c_qh) isa T
@test truncate(p, c_qs) isa T
@test truncate(p, c_std) isa T

n = 2000
p = pop2
sb = resample(p, n)

v_min, v_max = -1, 1
r_min, r_max = -1, 1
c_max = TruncateMaximum(v_max)
c_min = TruncateMinimum(v_min)
c_range = TruncateRange(r_min, r_max)
ql, qh = 0.2, 0.8
c_ql = TruncateLowerQuantile(ql)
c_qh = TruncateUpperQuantile(qh)
c_qs = TruncateQuantiles(ql, qh)
c_std = TruncateStd(1.0)

@test truncate(p, c_max) isa T
@test truncate(p, c_min) isa T
@test truncate(p, c_range) isa T
@test truncate(p, c_ql) isa T
@test truncate(p, c_qh) isa T
@test truncate(p, c_qs) isa T
@test truncate(p, c_std) isa T

n = 2000
p = pop3
sb = resample(p, n)

v_min, v_max = -1, 1
r_min, r_max = -1, 1
c_max = TruncateMaximum(v_max)
c_min = TruncateMinimum(v_min)
c_range = TruncateRange(r_min, r_max)
ql, qh = 0.2, 0.8
c_ql = TruncateLowerQuantile(ql)
c_qh = TruncateUpperQuantile(qh)
c_qs = TruncateQuantiles(ql, qh)
c_std = TruncateStd(1.0)

@test truncate(p, c_max) isa T
@test truncate(p, c_min) isa T
@test truncate(p, c_range) isa T
@test truncate(p, c_ql) isa T
@test truncate(p, c_qh) isa T
@test truncate(p, c_qs) isa T
@test truncate(p, c_std) isa T

n = 2000
p = pop4
sb = resample(p, n)

v_min, v_max = -2, 2
r_min, r_max = -2, 2
c_max = TruncateMaximum(v_max)
c_min = TruncateMinimum(v_min)
c_range = TruncateRange(r_min, r_max)
ql, qh = 0.2, 0.8
c_ql = TruncateLowerQuantile(ql)
c_qh = TruncateUpperQuantile(qh)
c_qs = TruncateQuantiles(ql, qh)
c_std = TruncateStd(1.0)

@test truncate(p, c_max) isa T
@test truncate(p, c_min) isa T
@test truncate(p, c_range) isa T
@test truncate(p, c_ql) isa T
@test truncate(p, c_qh) isa T
@test truncate(p, c_qs) isa T
@test truncate(p, c_std) isa T

n = 2000
p = pop5
sb = resample(p, n)

v_min, v_max = -2, 2
r_min, r_max = -2, 2
c_max = TruncateMaximum(v_max)
c_min = TruncateMinimum(v_min)
c_range = TruncateRange(r_min, r_max)
ql, qh = 0.2, 0.8
c_ql = TruncateLowerQuantile(ql)
c_qh = TruncateUpperQuantile(qh)
c_qs = TruncateQuantiles(ql, qh)
c_std = TruncateStd(1.0)

@test truncate(p, c_max) isa T
@test truncate(p, c_min) isa T
@test truncate(p, c_range) isa T
@test truncate(p, c_ql) isa T
@test truncate(p, c_qh) isa T
@test truncate(p, c_qs) isa T
@test truncate(p, c_std) isa T

pop1 = UncertainValue(
        [3.0, UncertainValue(Normal, 0, 1),
        UncertainValue(Gamma, 2, 3),
        UncertainValue(Uniform, rand(10000))],
        [0.5, 0.5, 0.5, 0.5]
    )

pop2 = UncertainValue([1, 2, 3], rand(3))
pop3 = UncertainValue([1.0, 2.0, 3.0], Weights(rand(3)))

# Uncertain population consisting of uncertain populations and other stuff
pop4 = UncertainValue([pop1, pop2], [0.1, 0.5])
pop5 = UncertainValue([pop1, pop2, 2, UncertainValue(Normal, -2, 3)], Weights(rand(4)));

uncertain_scalar_populations = [pop1, pop2, pop3, pop4, pop5];

@test constrain(pop1, TruncateMinimum(0.2)) isa T
@test constrain(pop1, TruncateMaximum(3.0)) isa T
@test constrain(pop1, TruncateRange(0.0, 3.0)) isa T
@test constrain(pop1, TruncateLowerQuantile(0.2)) isa T
@test constrain(pop1, TruncateUpperQuantile(0.8)) isa T
@test constrain(pop1, TruncateQuantiles(0.2, 0.8)) isa T
@test constrain(pop1, TruncateStd(1)) isa T

# pop2.values = [1, 2, 3]
@test constrain(pop2, TruncateMinimum(0.2)) isa T
@test constrain(pop2, TruncateMaximum(2.5)) isa T
@test constrain(pop2, TruncateRange(0.5, 2.5)) isa T
@test constrain(pop2, TruncateLowerQuantile(0.2)) isa T
@test constrain(pop2, TruncateUpperQuantile(0.8)) isa T
@test constrain(pop2, TruncateQuantiles(0.2, 0.8)) isa T
@test constrain(pop2, TruncateStd(1)) isa T

# pop3.values = [1.0, 2.0, 3.0]
@test constrain(pop3, TruncateMinimum(0.2)) isa T
@test constrain(pop3, TruncateMaximum(2.5)) isa T
@test constrain(pop3, TruncateRange(0.5, 2.5)) isa T
@test constrain(pop3, TruncateLowerQuantile(0.2)) isa T
@test constrain(pop3, TruncateUpperQuantile(0.8)) isa T
@test constrain(pop3, TruncateQuantiles(0.2, 0.8)) isa T
@test constrain(pop3, TruncateStd(1)) isa T

@test constrain(pop4, TruncateMinimum(-10)) isa T
@test constrain(pop4, TruncateMaximum(10)) isa T
@test constrain(pop4, TruncateRange(-10, 10)) isa T
@test constrain(pop4, TruncateLowerQuantile(0.2)) isa T
@test constrain(pop4, TruncateUpperQuantile(0.8)) isa T
@test constrain(pop4, TruncateQuantiles(0.2, 0.8)) isa T
@test constrain(pop4, TruncateStd(1)) isa T

@test constrain(pop5, TruncateMinimum(-10)) isa T
@test constrain(pop5, TruncateMaximum(10)) isa T
@test constrain(pop5, TruncateRange(-10, 10)) isa T
@test constrain(pop5, TruncateLowerQuantile(0.2)) isa T
@test constrain(pop5, TruncateUpperQuantile(0.8)) isa T
@test constrain(pop5, TruncateQuantiles(0.2, 0.8)) isa T
@test constrain(pop5, TruncateStd(1)) isa T

# Some subpopulations consisting of both scalar values and distributions
subpop1_members = [UncertainValue(Normal, 0, 1), UncertainValue(Uniform, -2, 2), -5]
subpop2_members = [
    UncertainValue(Normal, -2, 1),
    UncertainValue(Uniform, -6, -1),
    -3,
    UncertainValue(Gamma, 1, 0.4)]

# Define the probabilities of sampling the different population members within the
# subpopulations. Weights are normalised, so we can input any numbers here indicating
# relative importance
subpop1_probs = [1, 2, 1]
subpop2_probs = [0.1, 0.2, 0.3, 0.1]
pop1 = UncertainValue(subpop1_members, subpop1_probs)
pop2 = UncertainValue(subpop2_members, subpop2_probs)

# Define the probabilities of sampling the two subpopulations in the overall population.
pop_probs = [0.3, 0.7]

# Construct overall population
pop_mixed = UncertainValue([pop1, pop2], pop_probs)

@test constrain(pop_mixed, TruncateMinimum(1)) isa T
@test constrain(pop_mixed, TruncateMaximum(2)) isa T
@test constrain(pop_mixed, TruncateRange(1, 2)) isa T
@test constrain(pop_mixed, TruncateQuantiles(0.1, 0.9)) isa T
@test constrain(pop_mixed, TruncateLowerQuantile(0.1)) isa T
@test constrain(pop_mixed, TruncateUpperQuantile(0.9)) isa T
@test constrain(pop_mixed, TruncateStd(1)) isa T


begin
#Linear Search

# Julia code to linearly search x in arr[].
# If x is present then return its location,
# otherwise return -1
import Base:length,show,AbstractArray
arr = [2, 3, 4, 10, 40]
x = 10
n = length(arr)

function linearsearch(arr::AbstractArray{},n,x)
	for i in 1:n
		if arr[i] == x
		 	return i
		else
				continue
		end
	end
	return -1
end
global result=-1
# Function call
# for i in 1:n
# 	if arr[i] == x
# 		global result=i
# 		break
# 	else
# 		continue
# 	end
# end
result=linearsearch(arr::AbstractArray{},n,x)

println("Result after function execution:",result)
if result == -1
    println("Element:",x," is not present in array",arr)
else
    println("Element:",x," is present at index:", result," in array",arr)
end
end


using Test
using FvCFD

include("test.jl")
include("testMeshes.jl")

@testset "Face Geometry" begin
    # Test for a triangle
    pt1 = [ 0.0, 0.0, 0.0 ]
    pt2 = [ 1.0, 0.0, 0.0 ]
    pt3 = [ 0.0, 1.0, 0.0 ]
    points = [ pt1, pt2, pt3 ]

    centroid = [ 0.3333333333333, 0.333333333333, 0.0 ]
    centroid2 = FvCFD.triangleCentroid(points)
    @test almostEqual(centroid, centroid2, 9)

    area = [ 0.0, 0.0, 0.5 ]
    area2 = FvCFD.triangleArea(points)
    @test almostEqual(area, area2)

    area2, centroid2 = FvCFD.faceAreaCentroid(points)
    @test almostEqual(centroid, centroid2, 9)
    @test almostEqual(area, area2)

    # Test for a square
    pt4 = [ 1.0, 1.0, 0.0 ]
    points = [ pt1, pt2, pt4, pt3 ]
    area = [ 0.0, 0.0, 1.0 ]
    centroid = [ 0.5, 0.5, 0.0 ]
    area2, centroid2 = FvCFD.faceAreaCentroid(points)
    @test almostEqual(centroid, centroid2, 9)
    @test almostEqual(area, area2)

    # Test mesh from Moukalled, pg. 247
    cells, cVols, cCenters, faces, fAVecs, fCenters, boundaryFaces = meshMoukalled813()

    # Moukalled pg. 249
    @test almostEqual(cVols[1], 8.625)
    # Calculated online (the one in book is incorrect)
    @test almostEqual(cCenters[1], [1.754, 1.92, 0.5], 2)

    # Moukalled pg. 249
    fAVecs2 = [ [2.5, 1.0, 0.0], [-1.0, 2.0, 0.0], [-2.0, 0.5, 0.0], [-1.0, -2.0, 0.0], [1.5, -1.5, 0.0 ] ]
    for i in 1:5
        @test almostEqual(fAVecs[i], fAVecs2[i])
    end

    # @test almostEqual(mag(fAVecs[6]), 8.625)
    @test almostEqual(FvCFD.mag(fAVecs[7]), 8.625)
    @test almostEqual(FvCFD.mag(fAVecs[6]), 8.625)
end;

@testset "Cell Geometry" begin
    # Test for a cube
    pt1 = [ 0.0, 0.0, 0.0 ]
    pt2 = [ 1.0, 0.0, 0.0 ]
    pt3 = [ 1.0, 1.0, 0.0 ]
    pt4 = [ 0.0, 1.0, 0.0 ]
    pt5 = [ 0.0, 0.0, 1.0 ]
    pt6 = [ 1.0, 0.0, 1.0 ]
    pt7 = [ 1.0, 1.0, 1.0 ]
    pt8 = [ 0.0, 1.0, 1.0 ]

    points = [ pt1, pt2, pt3, pt4, pt5, pt6, pt7, pt8 ]
    faceCentroids = [ [0.5, 0.5, 0.0], [0.0, 0.5, 0.5], [ 0.5, 0.0, 0.5], [0.5, 0.5, 1], [1.0, 0.5, 0.5], [0.5, 1, 0.5] ]
    fAVecs = [ [0.0, 0.0, -1.0], [ -1.0, 0.0, 0.0 ], [ 0.0, -1.0, 0.0 ], [ 0.0, 0.0, 1.0 ], [ 1.0, 0.0, 0.0 ], [ 0.0, 1.0, 0.0 ] ]

    centroid = [ 0.5, 0.5, 0.5 ]
    volume = 1.0
    volume2, centroid2 = FvCFD.cellVolCentroid(points, fAVecs, faceCentroids)
    @test almostEqual(volume, volume2)
    @test almostEqual(centroid, centroid2)

end;

@testset "Reading OF Meshes" begin
    faces = FvCFD.readOFFacesFile(pathPrefix*"OFshockTube_100/faces")
    @test length(faces) == 501
    @test faces[1] == [2, 103, 305, 204]

    points = FvCFD.readOFPointsFile(pathPrefix*"OFshockTube_100/points")
    @test length(points)/3 == 404
    @test points[1,:] == [0, -1, -1]
end

@testset "Getting cell points" begin
    points, cells = FvCFD.OpenFOAMMesh_findCellPts(pathPrefix*"OFshockTube_100")
    @test cells[1].pointIndices == [ 1, 102, 103, 2, 203, 304, 305, 204 ]
    @test cells[2].pointIndices == [ 2, 103, 305, 204, 3, 104, 306, 205 ]
end


module PerlaTonettiWaugh

# Dependencies.
using LinearAlgebra
using DifferentialEquations, Sundials, SimpleDifferentialOperators, DiffEqCallbacks
using DataFrames, DataFramesMeta, CSV, CSVFiles, JSON # results caching
using Interpolations, QuadGK # integration
using NLsolve # root-finding
using NamedTupleTools, Parameters # named tuples
using Roots

# General utilities files.
include("utils/quadrature.jl")
# Simple model.
include("simple/params.jl")
include("simple/dynamic.jl")
include("simple/stationary.jl")
# Full model.
include("full/params.jl")
include("full/static.jl")
include("full/stationary.jl")
include("full/dynamic.jl")

export parameter_defaults, settings_defaults, default_fixedpoint_x0, default_stationary_x0, model_cachename
export parameters_simple, settings_simple
export parameter_defaults_tests, settings_defaults_tests
export load_parameters
export solve_simple_transition
export stationary_algebraic, stationary_numerical, stationary_algebraic_simple, stationary_numerical_simple, steady_state_from_g
export ω_weights, solve_dynamics, welfare
export compare_steady_states
export solve_transition, prepare_results
export consumption_equivalent
export f!, f!_simple
export total_derivative, ACR

end # module


@testset "slfm" begin
    rng = MersenneTwister(123)
    FDM = FiniteDifferences.central_fdm(5, 1)
    N = 6
    in_dim = 3
    out_dim = 4
    x1IO = KernelFunctions.MOInputIsotopicByOutputs(
        [rand(rng, in_dim) for _ in 1:N], out_dim
    )
    x2IO = KernelFunctions.MOInputIsotopicByOutputs(
        [rand(rng, in_dim) for _ in 1:N], out_dim
    )
    x3IO = KernelFunctions.MOInputIsotopicByOutputs(
        [rand(rng, in_dim) for _ in 1:div(N, 2)], out_dim
    )
    x1IO = MOInput([rand(rng, in_dim) for _ in 1:N], out_dim)
    x2IO = MOInput([rand(rng, in_dim) for _ in 1:N], out_dim)

    k = LatentFactorMOKernel(
        [Matern32Kernel(), SqExponentialKernel(), FBMKernel()],
        IndependentMOKernel(GaussianKernel()),
        rand(rng, out_dim, 3),
    )
    @test k isa LatentFactorMOKernel
    @test k isa MOKernel
    @test k isa Kernel
    @test k(x1IO[1], x2IO[1]) isa Real

    @test kernelmatrix(k, x1IO, x2IO) ≈ kernelmatrix(k, collect(x1IO), collect(x2IO))

    TestUtils.test_interface(k, x1IO, x2IO, x3IO)

    x1IF = KernelFunctions.MOInputIsotopicByFeatures(x1IO.x, out_dim)
    x2IF = KernelFunctions.MOInputIsotopicByFeatures(x2IO.x, out_dim)
    x3IF = KernelFunctions.MOInputIsotopicByFeatures(x3IO.x, out_dim)

    TestUtils.test_interface(k, x1IF, x2IF, x3IF)

    @test string(k) == "Semi-parametric Latent Factor Multi-Output Kernel"
    @test repr("text/plain", k) == (
        "Semi-parametric Latent Factor Multi-Output Kernel\n" *
        "\tgᵢ: Matern 3/2 Kernel (metric = Euclidean(0.0))\n" *
        "\t\tSquared Exponential Kernel (metric = Euclidean(0.0))\n" *
        "\t\tFractional Brownian Motion Kernel (h = 0.5)\n" *
        "\teᵢ: Independent Multi-Output Kernel\n" *
        "\tSquared Exponential Kernel (metric = Euclidean(0.0))"
    )

    # AD test
    function test_slfm(A::AbstractMatrix, x1IO, x2IO)
        k = LatentFactorMOKernel(
            [Matern32Kernel(), SqExponentialKernel(), FBMKernel()],
            IndependentMOKernel(GaussianKernel()),
            A,
        )
        return k((x1IO, 1), (x2IO, 1))
    end

    k = LatentFactorMOKernel(
        [SqExponentialKernel(), SqExponentialKernel(), SqExponentialKernel()],
        IndependentMOKernel(GaussianKernel()),
        rand(rng, out_dim, 3),
    )

    test_ADs(k)
end