function maximal_excursion(r::AbstractMatrix, τ::Integer, k::Integer)
    @argcheck (k >= 1) && (τ >= 0)
    @argcheck size(r, 1) >= 2
    T = size(r, 1)
    m = displacement(r, 1)
    @inbounds for δt = 1:τ
        mₜ = displacement(r, δt)
        if mₜ > m
            m = mₜ
        end
    end
    m^k
end

function mme(
    df::DataFrame,
    id::Symbol = :TrackID,
    x::Symbol = :POSITION_X,
    y::Symbol = :POSITION_Y,
)
    result = DataFrame(n = Int[], τ = Int[], m1 = Float64[])
    @inbounds for n = 1:maximum(df[!, id])
        m = extract(df, n, id, [x, y])
        @simd for τ = 1:size(m, 1)-1
            m1 = maximal_excursion(m, τ, 1)
            push!(result, [n, τ, m1])
        end
    end
    result
end

function StatsBase.moment(
    df::DataFrame,
    idlabel::Symbol = :TrackID,
    xlabel::Symbol = :POSITION_X,
    ylabel::Symbol = :POSITION_Y,
)
    result = DataFrame(
        TrackID = Int64[],
        fourth_moment = Float64[],
        second_moment = Float64[],
        delta_t = Int64[],
        n = Int64[],
    )
    @inbounds for n in sort(collect(Set(df[!, idlabel])))
        m = extract(df, n, id, [xlabel, ylabel])
        T = size(m, 1)
        for δ = 1:T-1
            c⁴ = 0.0
            c² = 0.0
            @simd for t = 1:(T-δ)
                c⁴ += abs2(abs2(displacement(m, t, δ)))
                c² += squared_displacement(m, t, δ)
            end
            c⁴ /= (T - δ)
            c² /= (T - δ)
            push!(result, [n, c⁴, c², δ, T - δ])
        end
    end
    result
end


plotstuff_linear(args...) = println("Install package Plots.jl (and call using Plots) to plot results in the end of demo_linear")

function demo_linear(;kwargs...)
    println("Running linear demo function for DifferentialDynamicProgramming.jl")

    # make stable linear dynamics
    h = .01         # time step
    n = 10          # state dimension
    m = 2           # control dimension
    A = randn(n,n)
    A = A-A'        # skew-symmetric = pure imaginary eigenvalues
    A = exp(h*A)   # discrete time
    B = h*randn(n,m)

    # quadratic costs
    Q    = h*eye(n)
    R    = .1*h*eye(m)

    # control limits
    lims = []            #ones(m,1)*[-1 1]*.6

    T    = 1000          # horizon
    x0   = ones(n,1)     # initial state
    u0   = .1*randn(m,T) # initial controls

    # optimization problem
    N    = T+1
    fx   = A
    fu   = B
    cxx  = Q
    cxu  = zeros(size(B))
    cuu  = R
    function lin_dyn_df(x,u,Q,R)
        u[isnan.(u)] .= 0
        cx  = Q*x
        cu  = R*u
        fxx=fxu=fuu = []
        return fx,fu,fxx,fxu,fuu,cx,cu,cxx,cxu,cuu
    end
    function lin_dyn_f(x,u,A,B,Q,R)
        u[isnan.(u)] .= 0
        xnew = A*x + B*u
        return xnew
    end
    lin_dyn_fT(x,Q) = 0.5*sum(x.*(Q*x))
    f(x,u,i)        = lin_dyn_f(x,u,A,B,Q,R)
    costfun(x,u)    = 0.5*sum(x.*(Q*x)) + 0.5*sum(u.*(R*u))
    df(x,u)         = lin_dyn_df(x,u,Q,R)
    # plotFn(x)  = plot(squeeze(x,2)')

    # run the optimization
    @time x, u, traj_new, Vx, Vxx, cost, otrace = iLQG(f,costfun,df, x0, u0; lims=lims,kwargs...);

    totalcost = get(otrace, :cost)[2]

    plotstuff_linear(x,u,[cost],totalcost)
    x, u, traj_new, Vx, Vxx, cost, otrace
end

function demo_linear_kl(;kwargs...)
    println("Running linear demo function with KL-divergence constraint for DifferentialDynamicProgramming.jl")

    # make stable linear dynamics
    h    = .01           # time step
    n    = 10            # state dimension
    m    = 2             # control dimension
    A    = randn(n,n)
    A    = A-A'          # skew-symmetric = pure imaginary eigenvalues
    A    = exp(h*A)     # discrete time
    B    = h*randn(n,m)

    # quadratic costs
    Q    = h*eye(n)
    R    = .1*h*eye(m)

    # control limits
    lims = []            #ones(m,1)*[-1 1]*.6

    T    = 1000          # horizon
    x0   = ones(n)       # initial state
    u    = .1*randn(m,T) # initial controls

    # optimization problem
    N    = T+1
    fx   = repeat(A,1,1,T)
    fu   = repeat(B,1,1,T)
    cxx  = repeat(Q,1,1,T)
    cxu  = repeat(zeros(size(B)),1,1,T)
    cuu  = repeat(R,1,1,T)
    function lin_dyn_df(x,u,Q,R)
        u[isnan.(u)] .= 0
        cx  = Q*x
        cu  = R*u
        fxx=fxu=fuu = []
        return fx,fu,fxx,fxu,fuu,cx,cu,cxx,cxu,cuu
    end
    function lin_dyn_f(x,u,A,B,Q,R)
        u[isnan.(u)] .= 0
        xnew = A*x + B*u
        return xnew
    end
    dyn = (x,u,i)   -> lin_dyn_f(x,u,A,B,Q,R)
    costf = (x,u) -> 0.5*(sum(x.*(Q*x),dims=1) + sum(u.*(R*u),dims=1))[:]
    diffdyn = (x,u)  -> lin_dyn_df(x,u,Q,R)

    function rollout(u)
        x = zeros(n,T)
        x[:,1] = x0
        for t = 1:T-1
            x[:,t+1] = dyn(x[:,t],u[:,t],t)
        end
        x
    end
    x = rollout(u)
    model = LinearTimeVaryingModelsBase.SimpleLTVModel(repeat(A,1,1,N),repeat(B,1,1,N),false)
    # plotFn(x)  = plot(squeeze(x,2)')
    traj = GaussianPolicy(Float64,T,n,m)
    # run the optimization
    local Vx, Vxx, cost, otrace, totalcost
    outercosts = zeros(5)
    @time for iter = 1:5
        cost0 = 0.5*sum(x.*(Q*x)) + 0.5*sum(u.*(R*u))
        x, u, traj, Vx, Vxx, cost, otrace = iLQGkl(dyn,costf,diffdyn, x, traj, model; cost=cost0, lims=lims,kwargs...);
        totalcost = get(otrace, :cost)[2]
        outercosts[iter] = sum(totalcost)
        println("Outer loop: Cost = ", sum(cost))
    end

    totalcost = get(otrace, :cost)[2]
    plotstuff_linear(x,u,[cost],min.(totalcost,400))
    # plotstuff_linear(x,u,totalcost,outercosts)
    x, u, traj, Vx, Vxx, cost, otrace
end


"""
    GP{Tm<:MeanFunction, Tk<:Kernel}

A Gaussian Process (GP) with known `mean` and `kernel`. See e.g. [1] for an introduction.

# Zero Mean
If only one argument is provided, assume the mean to be zero everywhere:

```jldoctest
julia> f = GP(Matern32Kernel());

julia> x = randn(5);

julia> mean(f(x)) == zeros(5)
true

julia> cov(f(x)) == kernelmatrix(Matern32Kernel(), x)
true
```

### Constant Mean

If a `Real` is provided as the first argument, assume the mean function is
constant with that value.

```jldoctest
julia> f = GP(5.0, Matern32Kernel());

julia> x = randn(5);

julia> mean(f(x)) == 5.0 .* ones(5)
true

julia> cov(f(x)) == kernelmatrix(Matern32Kernel(), x)
true
```

### Custom Mean

Provide an arbitrary function to compute the mean:

```jldoctest
julia> f = GP(x -> sin(x) + cos(x / 2), Matern32Kernel());

julia> x = randn(5);

julia> mean(f(x)) == sin.(x) .+ cos.(x ./ 2)
true

julia> cov(f(x)) == kernelmatrix(Matern32Kernel(), x)
true
```

[1] - C. E. Rasmussen and C. Williams. "Gaussian processes for machine learning".
MIT Press. 2006.
"""
struct GP{Tm<:MeanFunction,Tk<:Kernel} <: AbstractGP
    mean::Tm
    kernel::Tk
end

GP(mean, kernel::Kernel) = GP(CustomMean(mean), kernel)
GP(mean::Real, kernel::Kernel) = GP(ConstMean(mean), kernel)
GP(kernel::Kernel) = GP(ZeroMean(), kernel)

# AbstractGP interface implementation.

Statistics.mean(f::GP, x::AbstractVector) = _map_meanfunction(f.mean, x)

Statistics.cov(f::GP, x::AbstractVector) = kernelmatrix(f.kernel, x)

Statistics.var(f::GP, x::AbstractVector) = kernelmatrix_diag(f.kernel, x)

Statistics.cov(f::GP, x::AbstractVector, x′::AbstractVector) = kernelmatrix(f.kernel, x, x′)


# Adapted from 'Ordinary Differential Equations' test/units_tests.jl
using Test
using MechGlueDiffEqBase
using MechanicalUnits: @import_expand, dimension, NoDims, ∙
using OrdinaryDiffEq
@import_expand(N, s, m, km, kg)

#using OrdinaryDiffEq, Unitfu
algs = [Euler(),Midpoint(),Heun(),Ralston(),RK4(),SSPRK104(),SSPRK22(),SSPRK33(),
        SSPRK43(),SSPRK432(),BS3(),BS5(),DP5(),DP8(),Feagin10(),Feagin12(),
        Feagin14(),TanYam7(),Tsit5(),TsitPap8(),Vern6(),Vern7(),Vern8(),Vern9()]

@testset "Unitful time with unitless state" begin
    u0 = 30.0
    tspan = (0.0, 10.0)s
    prob1 = ODEProblem((du,u,t,p) -> (du[1] = -0.2∙s⁻¹ * u[1]), [u0], tspan)
    prob2 = ODEProblem((u,t,p)    -> (-0.2∙s⁻¹ * u[1]), u0, tspan)
    prob3 = ODEProblem((u,t,p)    -> [-0.2∙s⁻¹* u[1]], [u0],tspan)
    for prob in [prob1, prob2, prob3]
        @test solve(prob, Tsit5()).retcode === :Success
    end
end

@testset "Scalar units" begin
    f(y,p,t) = 0.5*y / 3.0s
    u0 = 1.0∙N
    prob = ODEProblem(f,u0,(0.0,1.0)s)

    for alg in algs
        @show alg
        @test solve(prob,alg,dt=1∙s/10).retcode === :Success
    end
end

@testset "2D units" begin
    f(dy,y,p,t) = (dy .= 0.5.*y ./ 3.0s)
    u0 = [1.0 2.0
          3.0 1.0]N
    prob = ODEProblem(f,u0,(0.0, 1.0)s)

    for alg in algs
        @show alg
        sol = solve(prob,alg,dt=1∙s/10)
    end
    sol = solve(prob,ExplicitRK())
end

@testset "Without ArrayPartition - broken" begin
    # coordinate: u = [position, momentum]
    # parameters: p = [mass, force constanst]
    function f_harmonic!(du,u,p,t)
        du[1] = u[2]/p[1]
        du[2] = -p[2]*u[1]
    end

    mass = 1.0∙kg
    k = 1.0∙N/m
    p = [mass, k]

    u0 = [1.0m, 0.0∙kg∙m/s] # initial values (position, momentum)
    tspan = (0.0∙s, 10.0∙s)
    prob = ODEProblem(f_harmonic!, u0, tspan, p)
    @test_broken solve(prob, Tsit5()).retcode == :Success
end


module Day09

import ..data_dir # from parent module
input = read(joinpath(data_dir, "day09"), String)

export part1, part2

"""
Horrendous logic for converting this string of integers to a matrix.
There must be a better way, but I can't find it.
"""
function string_block_to_matrix(string_block::AbstractString)
    as_arrays = split.(split(string_block, "\n"), "")
    as_int_arrays = [parse.(Int64, array) for array in as_arrays]
    # Transposing changes the type!
    Matrix(transpose(hcat(as_int_arrays...)))
end

function out_of_bounds(heatmap::Matrix{Int64}, row::Int64, column::Int64)
    heatmap_height, heatmap_width = size(heatmap)
    row < 1 || row > heatmap_height || column < 1 || column > heatmap_width
end

function identify_neighbours(heatmap::Matrix{Int64}, row::Int64, column::Int64)
    candidates = [
        (row - 1, column), (row + 1, column),
        (row, column - 1), (row, column + 1)
    ]
    filter(candidate -> !out_of_bounds(heatmap, candidate...), candidates)
end

function risk_level(heatmap::Matrix{Int64}, row::Int64, column::Int64)
    value = heatmap[row, column] # matrices are indexed row then column
    if value == 9
        return (0)
    end
    neighbours = identify_neighbours(heatmap, row, column)
    is_minimum = all(value < heatmap[neighbour...] for neighbour in neighbours)
    is_minimum * (1 + value) # non-minima don't get a risk value
end

function add_to_basin(
    heatmap::Matrix{Int64},
    unclaimed::Vector{Tuple{Int64,Int64}},
    row::Int64,
    column::Int64
)
    # can't pop by value, so I need to get the index
    index_in_unclaimed = findfirst(x -> x == (row, column), unclaimed)
    if isnothing(index_in_unclaimed) # already claimed by another path
        return Set{Tuple{Int64,Int64}}()
    end
    in_basin = Set([popat!(unclaimed, index_in_unclaimed)])
    neighbours = [neighbour for neighbour in identify_neighbours(heatmap, row, column)
                  if neighbour in unclaimed]
    if length(neighbours) == 0
        return (in_basin)
    end
    for neighbour in neighbours
        in_basin = in_basin ∪ add_to_basin(heatmap, unclaimed, neighbour...)
    end
    in_basin
end

function part1(input = input)
    heatmap = string_block_to_matrix(input)
    n_rows, n_cols = size(heatmap)
    total_risk_level = 0
    for row = 1:n_rows, column = 1:n_cols
        total_risk_level += risk_level(heatmap, row, column)
    end
    total_risk_level
end

"""
Two things make this second part manageable:

* I didn't need to consider the actual values of the heatmap,
    as long as I knew which ones were 9 and which ones aren't
* Every non-9 element of the heatmap is in exactly one basin
"""
function part2(input = input)
    heatmap = string_block_to_matrix(input)
    n_rows, n_cols = size(heatmap)
    unclaimed = [(row, column) for row in 1:n_rows, column in 1:n_cols if heatmap[row, column] != 9]
    basins = Vector{Set{Tuple{Int64,Int64}}}()
    while length(unclaimed) > 0
        push!(basins, add_to_basin(heatmap, unclaimed, unclaimed[1]...))
    end
    basin_sizes = map(x -> length(x), basins)
    last(sort(basin_sizes), 3) |> prod
end

end #module


module Test_cfid

using DescriptorSystems
using LinearAlgebra
using Polynomials
using Test

println("Test_cfid")
@testset "grcfid and glcfid" begin

@testset "grcfid" begin

sys = rdss(0,0,0);
@time sysni, sysmi = grcfid(sys);
@test gnrank(sys*sysmi-sysni) == 0   &&   #  G(s)*Mi(s)-Ni(s) = 0
      gnrank(sysmi'*sysmi-I) == 0  && # conj(Mi(s))*Mi(s)-I = 0
      isproper(sysmi) & isproper(sysni) && # checking properness of factors
      isstable(sysmi,atol=1.e-7) &&
      isstable(sysni,atol=1.e-7)  &&
      isempty(gzero(gminreal([sysni;sysmi],atol=1.e-7)))

a = -(1. +2im)*ones(1,1); e = (1. -im)*ones(1,1); b = (2. +im)*ones(1,1);
c = (-1. +im)*ones(1,1); d = complex(0)*ones(1,1);
sys = dss(a,e,b,c,d);
@time sysni, sysmi = grcfid(sys,atol=1.e-7,atol3=1.e-7);
@test gnrank(sys*sysmi-sysni,atol=1.e-7) == 0 &&
      gnrank(sysmi'*sysmi-I,atol=1.e-7) == 0  &&
      isproper(sysmi) && isproper(sysni) &&
      isstable(sysmi,atol=1.e-7) &&
      isstable(sysni,atol=1.e-7)  &&
      isempty(gzero(gminreal([sysni;sysmi],atol=1.e-7)))

a = -(1. +2im)*ones(1,1); e = (1. -im)*ones(1,1); b = (2. +im)*ones(1,1);
c = (-1. +im)*ones(1,1); d = complex(0)*ones(1,1);
sys = dss(a,e,b,c,d,Ts=1);
@time sysni, sysmi = grcfid(sys,atol=1.e-7,atol3=1.e-7);
@test gnrank(sys*sysmi-sysni,atol=1.e-7) == 0 &&
      gnrank(sysmi'*sysmi-I,atol=1.e-7) == 0  &&
      isproper(sysmi) && isproper(sysni) &&
      isstable(sysmi,atol=1.e-7) &&
      isstable(sysni,atol=1.e-7)  &&
      isempty(gzero(gminreal([sysni;sysmi],atol=1.e-7)))

s = Polynomial([0, 1],'s'); z = Polynomial([0, 1],'z');     # define the complex variables s and z

# Gd = [z^2 z/(z-2); 0 1/z]     # define the 2-by-2 improper Gd(z)
Nd = [z^2 z; 0 1]; Dd = [1 z-2; 1 z];

# build LTI (nonminimal) descriptor realizations of Gc(s) and Gd(z)
sysd = dss(Nd,Dd,Ts = 1);      # build discrete-time descriptor system realization

sysni, sysmi = grcfid(sysd);
@test gnrank(sysd*sysmi-sysni,atol=1.e-7) == 0  &&    #  Gd(z)*M(z)-N(z) = 0
      iszero(sysmi'-inv(sysmi),atol=1.e-7)  && # conj(M(z))*M(z)-I = 0
      isproper(sysmi) & isproper(sysni)  &&
      isstable(sysmi,atol=1.e-7) &&
      isstable(sysni,atol=1.e-7)  &&
      isempty(gzero(gminreal([sysni;sysmi],atol=1.e-7)))

s = Polynomial([0, 1],'s');
gn = [(s-1) s 1;
    0 (s-2) (s-2);
    (s-1) (s^2+2*s-2) (2*s-1)];
gd = [(s+2) (s+2) (s+2);
    1 (s+1)^2 (s+1)^2;
    (s+2) (s+1)*(s+2) (s+1)*(s+2)];

sys = dss(gn,gd,minimal = true, atol = 1.e-7);
sysni, sysmi = grcfid(sys,mindeg=true);
@test gnrank(sys*sysmi-sysni,atol=1.e-7) == 0  &&    #  Gd(z)*M(z)-N(z) = 0
      iszero(sysmi'-inv(sysmi),atol=1.e-7)  && # conj(M(z))*M(z)-I = 0
      isproper(sysmi) & isproper(sysni)  &&
      isstable(sysmi,atol=1.e-7) &&
      isstable(sysni,atol=1.e-7)  &&
      isempty(gzero(gminreal([sysni;sysmi],atol=1.e-7))) &&
      order(sysmi) == 0

sys = sys'
sysni, sysmi = grcfid(sys);
@test gnrank(sys*sysmi-sysni,atol=1.e-7) == 0  &&    #  Gd(z)*M(z)-N(z) = 0
      iszero(sysmi'-inv(sysmi),atol=1.e-7)  && # conj(M(z))*M(z)-I = 0
      isproper(sysmi) & isproper(sysni)  &&
      isstable(sysmi,atol=1.e-7) &&
      isstable(sysni,atol=1.e-7)  &&
      isempty(gzero(gminreal([sysni;sysmi],atol=1.e-7))) &&
      order(sysmi) == 4

s = Polynomial([0, 1],'s');
gn = [(s-1) s 1;
    0 (s-2) (s-2);
    (s-1) (s^2+2*s-2) (2*s-1)];
gd = [(s-2) (s+2) (s+2);
    1 (s+1)^2 (s+1)^2;
    (s+2) (s-1)*(s+2) (s+1)*(s-2)];

sys = dss(gn,gd,minimal = true, atol = 1.e-7);
sysni, sysmi = grcfid(sys,mindeg = true);
@test gnrank(sys*sysmi-sysni,atol=1.e-7) == 0  &&    #  Gd(z)*M(z)-N(z) = 0
      iszero(sysmi'-inv(sysmi),atol=1.e-7)  && # conj(M(z))*M(z)-I = 0
      isproper(sysmi) & isproper(sysni)  &&
      isstable(sysmi,atol=1.e-7) &&
      isstable(sysni,atol=1.e-7)  &&
      isempty(gzero(gminreal([sysni;sysmi],atol=1.e-7))) &&
      order(sysmi) == 3

z = Polynomial([0, 1],'z')
g = [z^2+z+1 4*z^2+3*z+2 2*z^2-2;
    z 4*z-1 2*z-2;
    z^2 4*z^2-z 2*z^2-2*z];

sys = dss(g,minimal = true, atol = 1.e-7,Ts = 1);
sysni, sysmi = grcfid(sys,mindeg = true,atol=1.e-7,atol3=1.e-7);
@test gnrank(sys*sysmi-sysni,atol=1.e-7) == 0  &&    #  Gd(z)*M(z)-N(z) = 0
      iszero(sysmi'-inv(sysmi),atol=1.e-7)  && # conj(M(z))*M(z)-I = 0
      isproper(sysmi) & isproper(sysni)  &&
      isstable(sysmi,atol=1.e-7) &&
      isstable(sysni,atol=1.e-7)  &&
      isempty(gzero(gminreal([sysni;sysmi],atol=1.e-7)))

z = Polynomial([0, 1],'z')
# Eigenvalue(s) on the unit circle
gn = 1
gd = z-1
sys = dss(gn,gd,Ts = 1)
try
   sysni, sysmi = grcfid(sys)
   @test false
catch
   @test true
end

fast = true; Ty = Complex{Float64}; #Ty = Float64
n = 5; p = 3; m = 2;

for fast in (true, false)
# random examples
for Ty in (Float64, Complex{Float64})

# continuous, standard
sys = rss(n,p,m,T = Ty,disc=false);
@time sysni, sysmi = grcfid(sys, fast = fast,atol=1.e-7,atol3=1.e-7);
@test gnrank(sys*sysmi-sysni,atol=1.e-7) == 0   &&
      iszero(sysmi'-inv(sysmi),atol=1.e-7)   &&
      isproper(sysmi) && isproper(sysni) &&
      isstable(sysmi,atol=1.e-7) &&
      isstable(sysni,atol=1.e-7)  &&
      isempty(gzero(gminreal([sysni;sysmi],atol=1.e-7),atol=1.e-7))

# discrete, standard
sys = rss(n,p,m,T = Ty,disc=true);
@time sysni, sysmi = grcfid(sys, fast = fast,atol=1.e-7,atol3=1.e-7);
@test gnrank(sys*sysmi-sysni,atol=1.e-7) == 0   &&
      iszero(sysmi'-inv(sysmi),atol=1.e-7)   &&
      isproper(sysmi) && isproper(sysni) &&
      isstable(sysmi,atol=1.e-7) &&
      isstable(sysni,atol=1.e-7)  &&
      isempty(gzero(gminreal([sysni;sysmi],atol=1.e-7),atol=1.e-7))

# continuous, standard, uncontrollable
sys = rss(n,p,m,T = Ty,disc=false, nuc = 5);
@time sysni, sysmi = grcfid(sys, fast = fast,atol=1.e-7,atol3=1.e-7);
@test gnrank(sys*sysmi-sysni,atol=1.e-7) == 0   &&
      iszero(sysmi'-inv(sysmi),atol=1.e-7)   &&
      isproper(sysmi) && isproper(sysni) &&
      isstable(sysmi,atol=1.e-7) &&
      isstable(sysni,atol=1.e-7)  &&
      isempty(gzero(gminreal([sysni;sysmi],atol=1.e-7),atol=1.e-7))

# discrete, standard, uncontrollable
sys = rss(n,p,m,T = Ty,disc=true, nuc = 5);
@time sysni, sysmi = grcfid(sys, fast = fast,atol=1.e-7,atol3=1.e-7);
@test gnrank(sys*sysmi-sysni,atol=1.e-7) == 0   &&
      iszero(sysmi'-inv(sysmi),atol=1.e-7)   &&
      isproper(sysmi) && isproper(sysni) &&
      isstable(sysmi,atol=1.e-7) &&
      isstable(sysni,atol=1.e-7)  &&
      isempty(gzero(gminreal([sysni;sysmi],atol=1.e-7),atol=1.e-7))

#failure
# continuous, descriptor, no infinite eigenvalues
sys = rdss(n,p,m,T = Ty,disc=false);
@time sysni, sysmi = grcfid(sys, fast = fast,atol=1.e-7,atol3=1.e-7);
@test gnrank(sys*sysmi-sysni,atol=1.e-7) == 0   &&
      gnrank(sysmi'*sysmi-I,atol=1.e-7) == 0  &&
      isproper(sysmi) && isproper(sysni) &&
      isstable(sysmi,atol=1.e-7) &&
      isstable(sysni,atol=1.e-7)  &&
      isempty(gzero(gminreal([sysni;sysmi],atol=1.e-7)))

# discrete, descriptor, no infinite eigenvalues
sys = rdss(n,p,m,T = Ty, disc=true);
@time sysni, sysmi = grcfid(sys, fast = fast,atol=1.e-7,atol3=1.e-7);
@test gnrank(sys*sysmi-sysni,atol=1.e-7) == 0   &&
      gnrank(sysmi'*sysmi-I,atol=1.e-7) == 0  &&
      isproper(sysmi) && isproper(sysni) &&
      isstable(sysmi,atol=1.e-7) &&
      isstable(sysni,atol=1.e-7)  &&
      isempty(gzero(gminreal([sysni;sysmi],atol=1.e-7)))

# continuous, descriptor, proper
sys = rdss(n,p,m,T = Ty,disc=false,id=ones(Int,3));
@time sysni, sysmi = grcfid(sys, fast = fast,atol=1.e-7,atol3=1.e-7);
@test gnrank(sys*sysmi-sysni,atol=1.e-7) == 0   &&
      gnrank(sysmi'*sysmi-I,atol=1.e-7) == 0  &&
      isproper(sysmi) && isproper(sysni) &&
      isstable(sysmi,atol=1.e-7) &&
      isstable(sysni,atol=1.e-7)  &&
      isempty(gzero(gminreal([sysni;sysmi],atol=1.e-7)))

# discrete, descriptor, proper
sys = rdss(n,p,m,T = Ty, disc=true);
@time sysni, sysmi = grcfid(sys, fast = fast,atol=1.e-7,atol3=1.e-7);
@test gnrank(sys*sysmi-sysni,atol=1.e-7) == 0   &&
      gnrank(sysmi'*sysmi-I,atol=1.e-7) == 0  &&
      isproper(sysmi) && isproper(sysni) &&
      isstable(sysmi,atol=1.e-7) &&
      isstable(sysni,atol=1.e-7)  &&
      isempty(gzero(gminreal([sysni;sysmi],atol=1.e-7)))

# continuous, descriptor, proper
sys = rdss(n,p,m,T = Ty,disc=false,id=ones(Int,3));
@time sysni, sysmi = grcfid(sys, fast = fast, mindeg = true, mininf = true, atol=1.e-7,atol3=1.e-7);
@test gnrank(sys*sysmi-sysni,atol=1.e-7) == 0   &&
      gnrank(sysmi'*sysmi-I,atol=1.e-7) == 0  &&
      isproper(sysmi) && isproper(sysni) &&
      isstable(sysmi,atol=1.e-7) &&
      isstable(sysni,atol=1.e-7)  &&
      isempty(gzero(gminreal([sysni;sysmi],atol=1.e-7)))

# discrete, descriptor, proper
sys = rdss(n,p,m,T = Ty, disc=true,id=ones(Int,3));
@time sysni, sysmi = grcfid(sys, fast = fast,mindeg = true, mininf = true, atol=1.e-7,atol3=1.e-7);
@test gnrank(sys*sysmi-sysni,atol=1.e-7) == 0   &&
      gnrank(sysmi'*sysmi-I,atol=1.e-7) == 0  &&
      isproper(sysmi) && isproper(sysni) &&
      isstable(sysmi,atol=1.e-7) &&
      isstable(sysni,atol=1.e-7)  &&
      isempty(gzero(gminreal([sysni;sysmi],atol=1.e-7)))

# continuous, descriptor, infinite poles -> error
sys = rdss(n,p,m,T = Ty,disc=false,id=[3*ones(Int,1);2*ones(Int,1)]);
try
   @time sysni, sysmi = grcfid(sys, fast = fast, mindeg = true, mininf = true, atol=1.e-7,atol3=1.e-7);
   @test false
catch
   @test true
end

# discrete, descriptor, infinite poles
sys = rdss(n,p,m,T = Ty, disc=true,id=[3*ones(Int,1);2*ones(Int,1)]);
@time sysni, sysmi = grcfid(sys, fast = fast,mindeg = true, mininf = true, atol=1.e-7,atol3=1.e-7);
@test gnrank(sys*sysmi-sysni,atol=1.e-7) == 0   &&
      gnrank(sysmi'*sysmi-I,atol=1.e-7) == 0  &&
      isproper(sysmi) && isproper(sysni) &&
      isstable(sysmi,atol=1.e-7) &&
      isstable(sysni,atol=1.e-7)  &&
      isempty(gzero(gminreal([sysni;sysmi],atol=1.e-7)))

# continuous, descriptor, proper, uncontrollable infinite eigenvalues
sys = rdss(n,p,m,T = Ty,disc=false,iduc=[3*ones(Int,1);2*ones(Int,1)]);
@time sysni, sysmi = grcfid(sys, fast = fast, mindeg = true, mininf = true, atol=1.e-7,atol3=1.e-7);
@test gnrank(sys*sysmi-sysni,atol=1.e-7) == 0   &&
      gnrank(sysmi'*sysmi-I,atol=1.e-7) == 0  &&
      isproper(sysmi) && isproper(sysni) &&
      isstable(sysmi,atol=1.e-7) &&
      isstable(sysni,atol=1.e-7)  &&
      isempty(gzero(gminreal([sysni;sysmi],atol=1.e-7)))

# discrete, descriptor, proper, uncontrollable infinite eigenvalues
sys = rdss(n,p,m,T = Ty,disc=true,iduc=[3*ones(Int,1);2*ones(Int,1)]);
@time sysni, sysmi = grcfid(sys, fast = fast, mindeg = true, mininf = true, atol=1.e-7,atol3=1.e-7);
@test gnrank(sys*sysmi-sysni,atol=1.e-7) == 0   &&
      gnrank(sysmi'*sysmi-I,atol=1.e-7) == 0  &&
      isproper(sysmi) && isproper(sysni) &&
      isstable(sysmi,atol=1.e-7) &&
      isstable(sysni,atol=1.e-7)  &&
      isempty(gzero(gminreal([sysni;sysmi],atol=1.e-7)))

# continuous, descriptor, proper, uncontrollable finite eigenvalues
sys = rdss(n,p,m,T = Ty,disc=false, nfuc = 5);
@time sysni, sysmi = grcfid(sys, fast = fast, mindeg = true, mininf = true, atol=1.e-7,atol3=1.e-7);
@test gnrank(sys*sysmi-sysni,atol=1.e-7) == 0   &&
      gnrank(sysmi'*sysmi-I,atol=1.e-7) == 0  &&
      isproper(sysmi) && isproper(sysni) &&
      isstable(sysmi,atol=1.e-7) &&
      isstable(sysni,atol=1.e-7)  &&
      isempty(gzero(gminreal([sysni;sysmi],atol=1.e-7)))

# discrete, descriptor, proper, uncontrollable finite eigenvalues
sys = rdss(n,p,m,T = Ty,disc=true, nfuc = 5);
@time sysni, sysmi = grcfid(sys, fast = fast, mindeg = true, mininf = true, atol=1.e-7,atol3=1.e-7);
@test gnrank(sys*sysmi-sysni,atol=1.e-7) == 0   &&
      gnrank(sysmi'*sysmi-I,atol=1.e-7) == 0  &&
      isproper(sysmi) && isproper(sysni) &&
      isstable(sysmi,atol=1.e-7) &&
      isstable(sysni,atol=1.e-7)  &&
      isempty(gzero(gminreal([sysni;sysmi],atol=1.e-7)))

end
end

end # grcfid

@testset "glcfid" begin

sys = rdss(0,0,0);
@time sysni, sysmi = glcfid(sys);
@test gnrank(sysmi*sys-sysni) == 0   &&   #  Mi(s)*G(s)-Ni(s) = 0
      gnrank(sysmi*sysmi'-I) == 0  && # Mi(s)*conj(Mi(s))-I = 0
      isproper(sysmi) & isproper(sysni) && # checking properness of factors
      isstable(sysmi,atol=1.e-7) &&
      isstable(sysni,atol=1.e-7)  &&
      isempty(gzero(gminreal([sysni sysmi])))

fast = true; Ty = Complex{Float64}; #Ty = Float64
n = 5; p = 3; m = 2;
for fast in (true, false)
# random examples
for Ty in (Float64, Complex{Float64})

# continuous, standard
sys = rss(n,p,m,T = Ty,disc=false);
@time sysni, sysmi = glcfid(sys, fast = fast,atol=1.e-7,atol3=1.e-7);
@test gnrank(sysmi*sys-sysni,atol=1.e-7) == 0   &&
      iszero(sysmi'-inv(sysmi),atol=1.e-7)   &&
      isproper(sysmi) && isproper(sysni) &&
      isstable(sysmi,atol=1.e-7) &&
      isstable(sysni,atol=1.e-7)  &&
      isempty(gzero(gminreal([sysni sysmi],atol=1.e-7),atol=1.e-7))

# discrete, standard
sys = rss(n,p,m,T = Ty,disc=true);
@time sysni, sysmi = glcfid(sys, fast = fast,atol=1.e-7,atol3=1.e-7);
@test gnrank(sysmi*sys-sysni,atol=1.e-7) == 0   &&
      iszero(sysmi'-inv(sysmi),atol=1.e-7)   &&
      isproper(sysmi) && isproper(sysni) &&
      isstable(sysmi,atol=1.e-7) &&
      isstable(sysni,atol=1.e-7)  &&
      isempty(gzero(gminreal([sysni sysmi],atol=1.e-7),atol=1.e-7))

# continuous, standard, unobservable
sys = rss(n,p,m,T = Ty,disc=false, nuo = 5);
@time sysni, sysmi = glcfid(sys, fast = fast,atol=1.e-7,atol3=1.e-7);
@test gnrank(sysmi*sys-sysni,atol=1.e-7) == 0   &&
      iszero(sysmi'-inv(sysmi),atol=1.e-7)   &&
      isproper(sysmi) && isproper(sysni) &&
      isstable(sysmi,atol=1.e-7) &&
      isstable(sysni,atol=1.e-7)  &&
      isempty(gzero(gminreal([sysni sysmi],atol=1.e-7),atol=1.e-7))

# discrete, standard, unobservable
sys = rss(n,p,m,T = Ty,disc=true, nuo = 5);
@time sysni, sysmi = glcfid(sys, fast = fast,atol=1.e-7,atol3=1.e-7);
@test gnrank(sysmi*sys-sysni,atol=1.e-7) == 0   &&
      iszero(sysmi'-inv(sysmi),atol=1.e-7)   &&
      isproper(sysmi) && isproper(sysni) &&
      isstable(sysmi,atol=1.e-7) &&
      isstable(sysni,atol=1.e-7)  &&
      isempty(gzero(gminreal([sysni sysmi],atol=1.e-7),atol=1.e-7))

# continuous, descriptor, proper, unobservable infinite eigenvalues
sys = rdss(n,p,m,T = Ty,disc=false,iduo=[3*ones(Int,1);2*ones(Int,1)],randlt=true,randrt=false);
#sys = rdss(n,p,m,T = Ty,disc=false,randlt=true,randrt=false);
@time sysni, sysmi = glcfid(sys, fast = fast, mindeg = true, mininf = true, atol=1.e-7,atol3=1.e-7);
#@time sysni, sysmi = glcfid(sys, fast = false, mindeg = false, mininf = false, atol=1.e-7,atol3=1.e-7);
@test gnrank(sysmi*sys-sysni,atol=1.e-7) == 0   &&
      gnrank(sysmi'*sysmi-I,atol=1.e-7) == 0  &&
      isproper(sysmi) && isproper(sysni) &&
      isstable(sysmi,atol=1.e-7) &&
      isstable(sysni,atol=1.e-7)  &&
      isempty(gzero(gminreal([sysni sysmi])))

# discrete, descriptor, proper, unobservable infinite eigenvalues
sys = rdss(n,p,m,T = Ty,disc=true,iduo=[3*ones(Int,1);2*ones(Int,1)]);
@time sysni, sysmi = glcfid(sys, fast = fast, mindeg = true, mininf = true, atol=1.e-7,atol3=1.e-7);
@test gnrank(sysmi*sys-sysni,atol=1.e-7) == 0   &&
      gnrank(sysmi'*sysmi-I,atol=1.e-7) == 0  &&
      isproper(sysmi) && isproper(sysni) &&
      isstable(sysmi,atol=1.e-7) &&
      isstable(sysni,atol=1.e-7)  &&
      isempty(gzero(gminreal([sysni sysmi])))

# continuous, descriptor, proper, unobservable finite eigenvalues
sys = rdss(n,p,m,T = Ty,disc=false, nfuo = 5);
@time sysni, sysmi = glcfid(sys, fast = fast, mindeg = true, mininf = true, atol=1.e-7,atol3=1.e-7);
@test gnrank(sysmi*sys-sysni,atol=1.e-7) == 0   &&
      gnrank(sysmi'*sysmi-I,atol=1.e-7) == 0  &&
      isproper(sysmi) && isproper(sysni) &&
      isstable(sysmi,atol=1.e-7) &&
      isstable(sysni,atol=1.e-7)  &&
      isempty(gzero(gminreal([sysni sysmi])))

# discrete, descriptor, proper, unobservable finite eigenvalues
sys = rdss(n,p,m,T = Ty,disc=true, nfuo = 5);
@time sysni, sysmi = glcfid(sys, fast = fast, mindeg = true, mininf = true, atol=1.e-7,atol3=1.e-7);
@test gnrank(sysmi*sys-sysni,atol=1.e-7) == 0   &&
      gnrank(sysmi'*sysmi-I,atol=1.e-7) == 0  &&
      isproper(sysmi) && isproper(sysni) &&
      isstable(sysmi,atol=1.e-7) &&
      isstable(sysni,atol=1.e-7)  &&
      isempty(gzero(gminreal([sysni sysmi])))

end
end

end # glcfid

end #test

end #module


using Test
using PolynomialBasis
# using Revise
using CutCellDG
include("../useful_routines.jl")

L, W = 1.0, 1.0
levelsetbasis = LagrangeTensorProductBasis(2, 1)
dim, nf = size(interpolation_points(levelsetbasis))

mesh = CutCellDG.CGMesh([0.0, 0.0], [L, W], [1, 1], nf)
levelset = CutCellDG.LevelSet(
    x -> plane_distance_function(x, [1.0, 0.0], [0.75, 0.0]),
    mesh,
    levelsetbasis,
)
CutCellDG.load_coefficients!(levelset, 1)
cellmap = CutCellDG.cell_map(mesh, 1)

xq = [0.0, 0.0]
x0 = [0.75, 0.5]
X0 = CutCellDG.inverse(cellmap, x0)
invjac = CutCellDG.inverse_jacobian(mesh)
func = CutCellDG.interpolater(levelset)

grad(X) = CutCellDG.spatial_gradient(func, X, invjac)
hess(X) = CutCellDG.spatial_hessian(func, X, invjac)
invfunc(x) = func(CutCellDG.inverse(cellmap, x))

g0 = grad(X0)
l0 = (xq - x0)' * g0 / (g0' * g0)

p0 = func(X0)
∇p0 = grad(X0)
∇2p0 = hess(X0)
gf = CutCellDG.saye_newton_gradient(x0, l0, xq, p0, ∇p0)
hf = CutCellDG.saye_newton_hessian(l0, ∇p0, ∇2p0)

x1, l1 = CutCellDG.step_saye_newton_iterate(x0, l0, gf, hf, 2.0)
testcp = [0.75, 0.0]
@test allapprox(x1, testcp)

X1 = CutCellDG.inverse(cellmap, x1)
p1 = func(X1)
∇p1 = grad(X1)
∇2p1 = hess(X1)
gf = CutCellDG.saye_newton_gradient(x1, l1, xq, p1, ∇p1)
hf = CutCellDG.saye_newton_hessian(l1, ∇p1, ∇2p1)

x2, l2 = CutCellDG.step_saye_newton_iterate(x1, l1, gf, hf, 2.0)
@test allapprox(x2, testcp)
################################################################################

x0 = [0.75, 0.5]
X0 = CutCellDG.inverse(cellmap, x0)
p0 = func(X0)
∇p0 = grad(X0)
∇2p0 = hess(X0)

x1, l1 = CutCellDG.step_chopp_iterate(x0, xq, p0, ∇p0, 0.5)
@test allapprox(x1, testcp)

X1 = CutCellDG.inverse(cellmap, x1)
p1 = func(X1)
∇p1 = grad(X1)
∇2p0 = hess(X1)

x2, l2 = CutCellDG.step_chopp_iterate(x1, xq, p1, ∇p1, 0.5)
@test allapprox(x2, testcp)

################################################################################
tol = 1e3eps()
x1, flag = CutCellDG.spatial_closest_point(
    xq,
    x0,
    func,
    cellmap,
    tol,
    5.0,
    20,
    1e4eps(),
)
@test allapprox(x1, testcp)
@test flag

################################################################################
mesh = CutCellDG.CGMesh([0.0, 0.0], [L, W], [2, 3], nf)
normal = [1.0, 0.0]
xI = [0.75, 0.0]
levelset = CutCellDG.LevelSet(
    x -> plane_distance_function(x, normal, xI),
    mesh,
    levelsetbasis,
)
cutmesh = CutCellDG.CutMesh(mesh, levelset)

refseedpoints, seedcellids = CutCellDG.seed_zero_levelset(2, levelset, cutmesh)
seedpoints = CutCellDG.map_to_spatial(refseedpoints, seedcellids, mesh)

querypoints = [
    0.0 0.5 0.8 0.9 1.2 -0.8
    0.1 0.8 0.3 0.5 0.4 0.2
]

dx = CutCellDG.element_size(mesh)
tol = 1e3eps()
boundingradius = 1.5maximum(dx)
closestpoints, closestcellids, flags =
    CutCellDG.closest_points_on_zero_levelset(
        querypoints,
        seedpoints,
        seedcellids,
        levelset,
        tol,
        boundingradius,
    )

testcp = closest_point_on_plane(querypoints, normal, xI)
testcellids = [4, 6, 4, 5, 5, 4]
@test all(flags)
@test allapprox(testcp, closestpoints)
@test allequal(closestcellids, testcellids)
################################################################################

################################################################################
L, W = 1.0, 1.0
levelsetbasis = HermiteTensorProductBasis(2)

dim, nf = size(interpolation_points(levelsetbasis))

mesh = CutCellDG.CGMesh([0.0, 0.0], [L, W], [1, 1], nf)
levelset = CutCellDG.LevelSet(
    x -> plane_distance_function(x, [1.0, 0.0], [0.75, 0.0]),
    mesh,
    levelsetbasis,
)
CutCellDG.load_coefficients!(levelset, 1)
cellmap = CutCellDG.cell_map(mesh, 1)

xq = [0.0, 0.0]
x0 = [0.75, 0.5]
X0 = CutCellDG.inverse(cellmap, x0)
invjac = CutCellDG.inverse_jacobian(mesh)
func = CutCellDG.interpolater(levelset)

grad(X) = CutCellDG.spatial_gradient(func, X, invjac)
hess(X) = CutCellDG.spatial_hessian(func, X, invjac)
invfunc(x) = func(CutCellDG.inverse(cellmap, x))

g0 = grad(X0)
l0 = (xq - x0)' * g0 / (g0' * g0)

p0 = func(X0)
∇p0 = grad(X0)
∇2p0 = hess(X0)
gf = CutCellDG.saye_newton_gradient(x0, l0, xq, p0, ∇p0)
hf = CutCellDG.saye_newton_hessian(l0, ∇p0, ∇2p0)

x1, l1 = CutCellDG.step_saye_newton_iterate(x0, l0, gf, hf, 2.0)
testcp = [0.75, 0.0]
@test allapprox(x1, testcp, 1e3eps())

X1 = CutCellDG.inverse(cellmap, x1)
p1 = func(X1)
∇p1 = grad(X1)
∇2p1 = hess(X1)
gf = CutCellDG.saye_newton_gradient(x1, l1, xq, p1, ∇p1)
hf = CutCellDG.saye_newton_hessian(l1, ∇p1, ∇2p1)

x2, l2 = CutCellDG.step_saye_newton_iterate(x1, l1, gf, hf, 2.0)
@test allapprox(x2, testcp, 1e4eps())
################################################################################

################################################################################
x0 = [0.75, 0.5]
X0 = CutCellDG.inverse(cellmap, x0)
p0 = func(X0)
∇p0 = grad(X0)
∇2p0 = hess(X0)

x1, l1 = CutCellDG.step_chopp_iterate(x0, xq, p0, ∇p0, 0.5)
@test allapprox(x1, testcp, 1e4eps())

X1 = CutCellDG.inverse(cellmap, x1)
p1 = func(X1)
∇p1 = grad(X1)
∇2p0 = hess(X1)

x2, l2 = CutCellDG.step_chopp_iterate(x1, xq, p1, ∇p1, 0.5)
@test allapprox(x2, testcp, 1e4eps())
################################################################################

################################################################################
tol = 1e3eps()
x1, flag = CutCellDG.spatial_closest_point(
    xq,
    x0,
    func,
    cellmap,
    tol,
    5.0,
    20,
    1e4eps(),
)
@test allapprox(x1, testcp, 1e4eps())
@test flag
################################################################################

################################################################################
mesh = CutCellDG.CGMesh([0.0, 0.0], [L, W], [2, 3], nf)
normal = [1.0, 0.0]
xI = [0.75, 0.0]
levelset = CutCellDG.LevelSet(
    x -> plane_distance_function(x, normal, xI),
    mesh,
    levelsetbasis,
)
cutmesh = CutCellDG.CutMesh(mesh, levelset)

refseedpoints, seedcellids = CutCellDG.seed_zero_levelset(2, levelset, cutmesh)
seedpoints = CutCellDG.map_to_spatial(refseedpoints, seedcellids, mesh)

querypoints = [
    0.0 0.5 0.8 0.9 1.2 -0.8
    0.1 0.8 0.3 0.5 0.4 0.2
]

dx = CutCellDG.element_size(mesh)
tol = 1e3eps()
boundingradius = 1.5maximum(dx)
closestpoints, closestcellids, flags =
    CutCellDG.closest_points_on_zero_levelset(
        querypoints,
        seedpoints,
        seedcellids,
        levelset,
        tol,
        boundingradius,
    )

testcp = closest_point_on_plane(querypoints, normal, xI)
testcellids = [4, 6, 4, 5, 5, 4]
@test all(flags)
@test allapprox(testcp, closestpoints)
@test allequal(closestcellids, testcellids)


#using Distributed
#@everywhere using MambaModels
using MambaModels

globe_toss = Dict{Symbol, Any}(
  :w => [6, 7, 5, 6, 6],
  :n => [9, 9, 9, 9, 9]
)
globe_toss[:N] = length(globe_toss[:w]);

model = Model(
  w = Stochastic(1,
    (n, p, N) ->
      UnivariateDistribution[Binomial(n[i], p) for i in 1:N],
    false
  ),
  p = Stochastic(() -> Beta(1, 1))
);

inits = [
  Dict(:w => globe_toss[:w], :n => globe_toss[:n], :p => 0.5),
  Dict(:w => globe_toss[:w], :n => globe_toss[:n], :p => rand(Beta(1, 1)))
];

scheme = [NUTS(:p)]
setsamplers!(model, scheme);

chn = mcmc(model, globe_toss, inits, 10000, burnin=2500, thin=1, chains=2);

describe(chn)

chn2 = MCMCChains.Chains(chn.value, String.(chn.names))

MCMCChains.describe(chn2)

MCMCChains.plot(chn2)

# This file was generated using Literate.jl, https://github.com/fredrikekre/Literate.jl


using Test
using DFTK
include("testcases.jl")

@testset "Test reduced / cartesian conversion" begin
    Ecut = 3
    fft_size = [13, 15, 14]
    model = Model(silicon.lattice, silicon.atoms, silicon.positions)

    rred = randn(3)  # reduced "position"
    fred = randn(3)  # reduced "force"
    qred = randn(3)  # reduced "reciprocal vector"
    @test(  dot(DFTK.covector_red_to_cart(model, fred), DFTK.vector_red_to_cart(model, rred))
          ≈ dot(fred, rred))
    @test(dot(DFTK.vector_red_to_cart(model, rred), DFTK.recip_vector_red_to_cart(model, qred))
          ≈ 2π * dot(rred, qred))

    @test DFTK.vector_cart_to_red(model, DFTK.vector_red_to_cart(model, rred)) ≈ rred
    @test DFTK.covector_cart_to_red(model, DFTK.covector_red_to_cart(model, fred)) ≈ fred
    @test DFTK.recip_vector_cart_to_red(model, DFTK.recip_vector_red_to_cart(model, qred)) ≈ qred

    rcart = randn(3)
    fcart = randn(3)
    @test dot(DFTK.covector_cart_to_red(model, fcart),
              DFTK.vector_cart_to_red(model, rcart))   ≈ dot(fcart, rcart)

    Ared = randn(3, 3)  # real-space       "symmetry"
    Bred = randn(3, 3)  # reciprocal-space "symmetry"
    @test(  dot(DFTK.comatrix_red_to_cart(model, Bred) * DFTK.covector_red_to_cart(model, fred),
                DFTK.matrix_red_to_cart(model, Ared) * DFTK.vector_red_to_cart(model, rred))
          ≈ dot(Bred * fred, Ared * rred))

    @test DFTK.comatrix_cart_to_red(model, DFTK.comatrix_red_to_cart(model, Bred)) ≈ Bred
    @test DFTK.matrix_cart_to_red(model,   DFTK.matrix_red_to_cart(model,   Ared)) ≈ Ared
end


using Test
using SparseIR

include("_util.jl")

@testset "bessels.jl" begin
    @test typestable(SparseIR.sphericalbesselj, [Int, Float64])

    @testset "small_x" begin
        n = 11
        x = 1e0
        # Mathematica
        ref = 3.099551854790080034457495747083911933213405593516888829346e-12
        @test SparseIR.sphericalbesselj(n, x) ≈ ref
    end

    @testset "large_x" begin
        n = 11
        x = 2e9
        # Mathematica
        ref = 2.020549136012222873136295240464724116017069579717738807316e-10
        @test SparseIR.sphericalbesselj(n, x) ≈ ref
    end
end


using Surrogates
using Distributions
#1D
lb = 0.0
ub = 5.0
n = 5
d = 1
Surrogates.sample(n,lb,ub,GridSample(0.1))
Surrogates.sample(n,lb,ub,UniformSample())
Surrogates.sample(n,lb,ub,SobolSample())
Surrogates.sample(n,lb,ub,LatinHypercubeSample())
Surrogates.sample(20,lb,ub,LowDiscrepancySample(10))
Surrogates.sample(5,d,Cauchy())
Surrogates.sample(5,d,Normal(0,4))

#ND
lb = [0.1,-0.5]
ub = [1.0,20.0]
n = 5
d = 2
s = Surrogates.sample(n,lb,ub,GridSample([0.1,0.5]))
Surrogates.sample(n,lb,ub,UniformSample())
Surrogates.sample(n,lb,ub,SobolSample())
Surrogates.sample(n,lb,ub,LatinHypercubeSample())
Surrogates.sample(n,lb,ub,LowDiscrepancySample([10,3]))
Surrogates.sample(n,d,Cauchy())
Surrogates.sample(n,d,Normal(3,5))


immutable Multinomial <: DiscreteMultivariateDistribution
    n::Int
    prob::Vector{Float64}

    function Multinomial(n::Integer, p::Vector{Float64})
        if n <= 0
            throw(ArgumentError("n must be a positive integer."))
        end
        if !isprobvec(p)
            throw(ArgumentError("p = $p is not a probability vector."))
        end
        new(int(n), p)
    end
end

immutable MultinomialSampler <: DiscreteMultivariateDistribution
    d::Multinomial
    alias::AliasTable
    function MultinomialSampler(d::Multinomial)
        new(d, AliasTable(d.prob))
    end
end

sampler(d::Multinomial) = MultinomialSampler(d)

function Multinomial(n::Integer, d::Integer)
    if d < 1
        error("d must be greater than 0")
    end
    prob = Array(Float64, d)
    fill!(prob, 1.0 / d)
    Multinomial(n, prob)
end

# TODO: Debate removing this
Multinomial(d::Integer) = Multinomial(1, d)

dim(d::Multinomial) = length(d.prob)
dim(s::MultinomialSampler) = length(s.d.prob)

entropy(d::Multinomial) = NumericExtensions.entropy(d.prob)

function insupport{T <: Real}(d::Multinomial, x::Vector{T})
    n = length(x)
    if length(d.prob) != n
        return false
    end
    s = 0.0
    for i in 1:n
        if x[i] < 0.0 || !isinteger(x[i])
            return false
        end
        s += x[i]
    end
    if abs(s - d.n) > 1e-8
        return false
    end
    return true
end

mean(d::Multinomial) = d.n .* d.prob

function var(d::Multinomial)
    p = d.prob
    k = length(p)
    v = Array(Float64, k)
    n = d.n
    for i = 1:k
        pi = p[i]
        v[i] = n * pi * (1.0 - pi)
    end
    v
end

function cov(d::Multinomial)
    p = d.prob
    k = length(p)
    C = Array(Float64, k, k)
    n = d.n

    for j = 1:k
        pj = p[j]
        for i = 1:j-1
            C[i,j] = - n * p[i] * pj
        end

        C[j,j] = n * pj * (1.0-pj)
    end

    for j = 1:k-1
        for i = j+1:k
            C[i,j] = C[j,i]
        end
    end
    C
end

function mgf(d::Multinomial, t::AbstractVector)
    p, n = d.prob, d.n
    k = length(p)
    s = 0.0
    for i in 1:k
        s += p[i] * exp(t[i])
    end
    return s^n
end

function cf(d::Multinomial, t::AbstractVector)
    p, n = d.prob, d.n
    k = length(p)
    s = 0.0 + 0.0im
    for i in 1:k
        s += p[i] * exp(im * t[i])
    end
    return s^n
end

pdf{T <: Real}(d::Multinomial, x::Vector{T}) = exp(logpdf(d, x))

function logpdf{T <: Real}(d::Multinomial, x::Vector{T})
    if !insupport(d, x)
        return -Inf
    else
        s = lgamma(d.n + 1.0)
        for i in 1:length(x)
            s -= lgamma(x[i] + 1.0)
            s += x[i] * log(d.prob[i])
        end
        return s
    end
end

# TODO: Debate making T <: Integer
function rand!{T <: Real}(d::Multinomial, x::Vector{T})
    n, k = d.n, dim(d)
    fill!(x, 0)
    psum = 1.0
    for j in 1:(k - 1)
        tmp = rand(Binomial(n, d.prob[j] / psum))
        x[j] = tmp
        n -= tmp
        if n == 0
            break
        end
        psum -= d.prob[j]
    end
    x[k] = n
    return x
end

function rand!{T <: Real}(s::MultinomialSampler, x::Vector{T})
    d = s.d
    n, k = d.n, dim(d)
    fill!(x, 0)
    if n^2 > k
        # Use sequential binomial sampling
        # TODO: Refactor this code to make it DRYer
        psum = 1.0
        for j in 1:(k - 1)
            tmp = rand(Binomial(n, d.prob[j] / psum))
            x[j] = tmp
            n -= tmp
            if n == 0
                break
            end
            psum -= d.prob[j]
        end
        x[k] = n
    else
        # Use an alias table
        for itr in 1:n
            x[rand(s.alias)] += 1
        end
    end
    return x
end

function rand(d::Multinomial)
    x = zeros(Int, dim(d))
    return rand!(d, x)
end

function rand(s::MultinomialSampler)
    x = zeros(Int, dim(s.d))
    return rand!(s, x)
end

## Fit model

immutable MultinomialStats
    n::Int  # number of trials in each experiment
    scnts::Vector{Float64}  # sum of counts
    tw::Float64  # total sample weight

    MultinomialStats(n::Int, scnts::Vector{Float64}, tw::Real) = new(n, scnts, float64(tw))
end

function suffstats{T<:Real}(::Type{Multinomial}, x::Matrix{T})
    K = size(x, 1)
    n::T = zero(T)
    scnts = zeros(K)

    for j = 1:size(x,2)
        nj = zero(T)
        for i = 1:K
            xi = x[i,j]
            nj += xi

            scnts[i] += xi
        end

        if j == 1
            n = nj
        elseif nj != n
            error("Each sample in X should sum to the same value.")
        end
    end
    MultinomialStats(n, scnts, size(x,2))
end

function suffstats{T<:Real}(::Type{Multinomial}, x::Matrix{T}, w::Array{Float64})
    if length(w) != size(x, 2)
        throw(ArgumentError("Inconsistent argument dimensions."))
    end

    K = size(x, 1)
    n::T = zero(T)
    scnts = zeros(K)
    tw = 0.

    for j = 1:size(x,2)
        nj = zero(T)
        wj = w[j]
        tw += wj
        for i = 1:K
            xi = x[i,j]
            nj += xi

            scnts[i] += xi * wj
        end

        if j == 1
            n = nj
        elseif nj != n
            error("Each sample in X should sum to the same value.")
        end
    end
    MultinomialStats(n, scnts, tw)
end

fit_mle(::Type{Multinomial}, ss::MultinomialStats) = Multinomial(ss.n, ss.scnts * inv(ss.tw * ss.n))


__precompile__()

module ArbFloats

import Base: STDOUT,
    hash, convert, promote_rule, isa,
    string, show, showcompact, showall, parse,
    finalizer, decompose, precision, setprecision,
    serialize, deserialize,
    typemin, typemax, realmin, realmax,
    copy, deepcopy,
    zero, one, isinteger,
    ldexp, frexp, eps,
    isequal, isless, (==),(!=),(<),(<=),(>=),(>), contains,
    min, max, minmax,
    typemax, typemin, realmax, realmin,
    float, nextfloat, prevfloat,
    isnan, isinf, isfinite, issubnormal,
    signbit, sign, flipsign, copysign, abs, abs2,
    (+),(-),(*),(/),(\),(%),(^), inv, sqrt, hypot,
    (.+),(.-),(.*),(./),
    trunc, round, ceil, floor,
    fld, cld, div, mod, rem, divrem, fldmod,
    muladd, fma,
    exp, expm1, log, log1p, log2, log10,
    sin, cos, tan, csc, sec, cot, asin, acos, atan, atan2,
    sinh, cosh, tanh, csch, sech, coth, asinh, acosh, atanh,
    sinc, gamma, lgamma, digamma, zeta, factorial,
    in, union, intersect,
    BigInt, BigFloat, Float64, Float32, Int128, Int64, Int32, Rational,
    Cint

export ArbFloat,      # co-matched decimal rounding, n | round(hi,n,10) == round(lo,n,10)
       @ArbFloat,     # converts string form of argument, precision is optional first arg
       setprecisionAugmented,
       simeq, nsime, prec, preceq, succ, succeq, # non-strict total ordering comparisons
       (≃), (≄), (≺), (⪯), (≻), (⪰),           #    matched binary operators
       upperbound, lowerbound, bounds,
       midpoint, radius, midpoint_radius,
       bounding_midpoint, bounding_radius, bounding_midpoint_radius,
       stringsmall, stringcompact, stringmedium, stringlarge, stringall,
       stringsmall_pm, stringcompact_pm, string_pm,
       stringlarge_pm, stringall_pm,
       showsmall, showcompact, showlarge, showallcompact, showpretty,
       showsmall_pm, showcompact_pm, show_pm,
       showlarge_pm, showall_pm,
       stringpretty, smartvalue, smartstring, showsmart,
       two, three, four, copymidpoint, copyradius, deepcopyradius,
       get_emax, get_emin, bounded, boundedrange,
       decompose, isexact, notexact,
       isposinf, isneginf,
       notnan, notinf, notposinf, notneginf, notfinite,
       iszero, notzero, nonzero, isone, notone, notinteger,
       ispositive, notpositive, isnegative, notnegative,
       includesAnInteger, excludesIntegers, includesZero, excludesZero,
       includesPositive, excludesPositive, includesNegative, excludesNegative,
       includesNonpositive,  includesNonnegative,
       areequal, notequal, approxeq, (≊),
       narrow, overlap, donotoverlap,
       contains, iscontainedby, doesnotcontain, isnotcontainedby,
       absz, invsqrt, pow, root, tanpi, cotpi, logbase, sincos, sincospi, sinhcosh,
       doublefactorial, risingfactorial, rgamma, agm, polylog,
       relativeError, relativeAccuracy, midpointPrecision, trimmed,
       PI,SQRTPI,LOG2,LOG10,EXP1,EULER,CATALAN,KHINCHIN,GLAISHER,APERY, # constants
       get_midpoint_digits_shown, get_radius_digits_shown,  # some interface control
       set_midpoint_digits_shown, set_radius_digits_shown,
       isolate_nonnegative_content, isolate_positive_content, # for interval algorithms
       force_nonnegative_content, force_positive_conent

NotImplemented(info::AbstractString="") = error(string("this is not implemented\n\t",info,"\n"))

include("support/NemoLibs.jl")                 # for precompiled libraries
include("support/ReadableNumbers.jl")          # digit subsequence separators

include("type/ArbCstructs.jl")
include("type/MagFloat.jl")
include("type/ArfFloat.jl")
include("type/ArbFloat.jl")
include("type/ArbInterval.jl")

include("basics/primitive.jl")
include("basics/IEEEfp.jl")

include("basics/predicates.jl")
include("basics/convert.jl")
include("basics/compare.jl")

include("basics/string.jl")
include("basics/smartstring.jl")
include("basics/show.jl")
include("basics/serialize.jl")

include("math/arith.jl")
include("math/round.jl")
include("math/elementary.jl")
include("math/constants.jl")
include("math/special.jl")

include("math/rounding.jl")

include("math/arrayops.jl")

#=
# precision is significand precision, significand_bits(FloatNN) + 1, for the hidden bit
typealias ArbFloat16  ArbFloat{ 11}  # read   2 ? 3 or fewer decimal digits to write the same digits ( 16bit Float)
typealias ArbFloat32  ArbFloat{ 24}  # read   6 ? 7 or fewer decimal digits to write the same digits ( 32bit Float)
typealias ArbFloat64  ArbFloat{ 53}  # read  15 ?15 or fewer decimal digits to write the same digits ( 64bit Float)
typealias ArbFloat128 ArbFloat{113}  # read  33 ?34 or fewer decimal digits to write the same digits (128bit Float)
typealias ArbFloat256 ArbFloat{237}  # read  71 ?71 or fewer decimal digits to write the same digits (256bit Float)
typealias ArbFloat512 ArbFloat{496}  # read 148?149 or fewer decimal digits to write the same digits (512bit Float)
=#

end # ArbFloats


"""
    get_spatial_profile_single_mode_and_fit(problem, mode_index)

return (data = ( x=DCM, y=ψ² ), fit=(x=x_fit, y=y_fit) )
"""
function get_spatial_profile_single_mode_and_fit(problem, mode_index)
    DCM, ψ² = get_spatial_profile_single_mode(problem, mode_index)
    x_fit, y_fit = get_decay_fit(problem, mode_index)
    return (DCM = DCM, ψ² = ψ², fit = (x = x_fit, y = y_fit))
end

function get_decay_fit(problem, mode_index)
    DCM, ψ² = get_spatial_profile_single_mode(problem, mode_index)

    x_use, y_use = select_points(DCM, ψ²)
    A_L₁, ξ_L₁, R1, y_fit_L₁ = get_coeffs_L1(x_use, y_use)

    if is_R1_localized(R1)
        return x_use, y_fit_L₁
    else
        A_L₂, ξ_L₂, R2, y_fit_L₂ = get_coeffs_L2(x_use, y_use)
        return x_use, y_fit_L₂
    end
end


module BDDSolver

    using Laplacians
    using StaticArrays
    using Arpack
    using Statistics

    using SparseArrays
    using LinearAlgebra

    using Random

    include("BlockCSC.jl")
    include("BDDTypes.jl")
    include("acBDD.jl")
    include("approxPQ.jl")
    include("generators.jl")
    include("solvers.jl")

    export approxCholBDD
    export BlockCSC, BlockVec, FlatVec
    export randOrthWt
end # module


#################################################################################################
#                               Load Packages
#################################################################################################
cd(@__DIR__)
using Pkg
Pkg.activate("..")
using Revise, PAAV, Plots, DataFrames, Statistics, StatsPlots
using GLM, Random
include("simulation ratio effect.jl")
Random.seed!(524184)
#################################################################################################
#                               Conjunctive Search
#################################################################################################
set_sizes = [1,2,5,10,15]

conj_results = run_simulation(set_sizes; fun=conjunctive_ratio,
    Δτ=.39, topdown_weight=.66)

pyplot()
@df conj_results plot(:distractors, :hit_rate, grid=false,
    ylims=(.5,1), leg=false,ylabel="Accuracy", xlabel="Set Size",
    color=:grey, linewidth=2, xaxis=font(10), yaxis=font(10), size=(600,400))

#savefig("Conjunctive_hit_rate.eps")

@df conj_results plot(:distractors, :rt_mean, group=(:target_present,:response), grid=false,
     ylims=(0,3.0), linewidth=2, leg=true, ylabel="Mean RT (seconds)", xlabel="Set Size",
     size=(600,400))

#savefig("Conjunctive_set_size.eps")

df_present = filter(x->x[:target_present] ==:present && x[:response] ==:present, conj_results)
conj_ols_present = lm(@formula(rt_mean ~ distractors), df_present)

df_absent = filter(x->x[:target_present] ==:absent && x[:response] ==:absent, conj_results)
conj_ols_absent = lm(@formula(rt_mean ~ distractors), df_absent)


# # Tilted bottom boundary layer example
#
# This example simulates a two-dimensional (x-z) tilted oceanic bottom boundary layer with a
# constant background alongslope (y-direction) velocity. It demonstrates how to simulate a domain
# tilt by
#
#   * Changing the direction of the buoyancy acceleration
#   * Changing the axis of rotation for Coriolis
#
# ## Install dependencies
#
# First let's make sure we have all required packages installed.

# ```julia
# using Pkg
# pkg"add Oceananigans, Plots"
# ```

# ## Load `Oceananigans.jl` and define problem constants
#
# We set a named tuple that defines the necessary simulation parameters:

using Oceananigans
using Oceananigans.Units
using Printf
using CUDA

params = (f₀ = 1e-4, #1/s
          V∞ = 0.1, # m/s
          N²∞ = 1e-5, # 1/s²
          θ_rad = 0.05,
          ν = 5e-4, # m²/s
          z₀ = 0.1, # m (roughness length)
          Lx = 1000, # m
          Lz = 100, # m
          )

# Here `f₀` is the Coriolis frequency, `V∞` in the constant interior `v`-velocity, `N²∞` is the
# interior stratification, `θ_rad` is the bottom slope in radians, `ν` is the eddy viscosity, and
# `z₀` is the roughness length (needed for the drag at the bottom). `Lx` and `Lz` are the domain
# length and height.
#
#
# ## Creating the grid
#
# Since we anticipate most of the energy in the flow to be located close to the bottom, we
# can save computational resources by creating a grid that has finer spacings near the bottom
# which gets progressively coarser near the top. Such a grid can be achieved with the following
# equations:

Nx = 128
Nz = 32

refinement = 1.8 # controls spacing near surface (higher means finer spaced)
stretching = 10   # controls rate of stretching at bottom

h₁(k) = ((-k+Nz)+1) / Nz

## Linear near-surface generator
ζ₁(k) = 1 + (h₁(k) - 1) / refinement

## Bottom-intensified stretching function
Σ₁(k) = (1 - exp(-stretching * h₁(k))) / (1 - exp(-stretching))

## Generating function
z_faces(k) = -params.Lz * (ζ₁(k) * Σ₁(k) - 1)

grid = RectilinearGrid(topology=(Periodic, Flat, Bounded),
                       size=(Nx, Nz),
                       x=(0, params.Lx), z=z_faces,
                       halo=(3,3),
                       )

# We plot vertical spacing versus depth to inspect the prescribed grid stretching. Note that
# the spacing near the bottom is relatively uniform with height, which is a desired property from
# a numerical perspective.
#

using Plots

plot(grid.Δzᵃᵃᶜ[1:Nz], grid.zᵃᵃᶜ[1:Nz],
     marker = :circle,
     ylabel = "Depth (m)",
     xlabel = "Vertical spacing (m)",
     legend = nothing)

# ## Setting up a buoyancy model in a tilted domain
#
# We set-up our domain in a way that the coordinates align with the tilted bottom. That means that,
# from the coordinate's point of view, gravity is tilted by `θ_rad` radians, which we can simulate
# by passing the `gravity_unit_vector` parameter to `Buoyancy()`:

ĝ = [sin(params.θ_rad), 0, cos(params.θ_rad)]
buoyancy = Buoyancy(model=BuoyancyTracer(), gravity_unit_vector=ĝ)

# ## Background fields
#
# Since we are simulating a period domain that is tilted, we need to set-up the background
# stratification as a `BackgroundField` and solve for the buoyancy perturbation. This avoids mean
# horizontal buoyant gradients, which cannot exist in a horizontally-periodic domain:

b∞(x, y, z, t, p) = p.N²∞ * (x * sin(p.θ_rad) + z * cos(p.θ_rad))
B_field = BackgroundField(b∞, parameters=(; params.N²∞, params.θ_rad))

# We also set the constant interior velocity `V∞` as a background field in order to avoid inertial
# oscillations due to an unbalanced resolved flow:
#

V_bg(x, y, z, t, p) = p.V∞
V_field = BackgroundField(V_bg, parameters=(; V∞=params.V∞))

# ## Bottom drag
#
# We set-up a bottom drag that follows Monin-Obukhov theory. Note that we need to include`V∞`
# in the velocity, since we will model it as a background field.
#

const κ = 0.4 # von Karman constant
z₁ = CUDA.@allowscalar znodes(Center, grid)[1] # Closest grid center to the bottom
cᴰ = (κ / log(z₁/params.z₀))^2 # Drag coefficient

@inline drag_u(x, y, t, u, v, p) = - p.cᴰ * √(u^2 + (v + p.V∞)^2) * u
@inline drag_v(x, y, t, u, v, p) = - p.cᴰ * √(u^2 + (v + p.V∞)^2) * (v + p.V∞)

drag_bc_u = FluxBoundaryCondition(drag_u, field_dependencies=(:u, :v), parameters=(cᴰ=cᴰ, V∞=params.V∞))
drag_bc_v = FluxBoundaryCondition(drag_v, field_dependencies=(:u, :v), parameters=(cᴰ=cᴰ, V∞=params.V∞))

u_bcs = FieldBoundaryConditions(bottom = drag_bc_u)
v_bcs = FieldBoundaryConditions(bottom = drag_bc_v)

# ## Create the `NonhydrostaticModel`
#
# Since the coordinate system is aligned with the tilted domain, we can use a traditional f-plane
# approximation (only keep the rotation component that's aligned with gravity) by also tilting the
# rotation from the coordinate system's perpective by `θ_rad` radians using
# `ConstantCartesianCoriolis` and passing the `rotation_axis` argument:
#

coriolis = ConstantCartesianCoriolis(f=params.f₀, rotation_axis=ĝ)

#
# We are now ready to create the model. We create a `NonhydrostaticModel` with an
# `UpwindBiasedFifthOrder` advection scheme and a `RungeKutta3` timestepper.  For simplicity, we use
# a constant-diffusivity turbulence closure:

model = NonhydrostaticModel(grid = grid, timestepper = :RungeKutta3,
                            advection = UpwindBiasedFifthOrder(),
                            buoyancy = buoyancy,
                            coriolis = coriolis,
                            tracers = :b,
                            closure = ScalarDiffusivity(ν=params.ν, κ=params.ν),
                            boundary_conditions = (u=u_bcs, v=v_bcs,),
                            background_fields = (b=B_field, v=V_field,),
                           )

# ## Create and run a simulation
#
# We are now ready to create the simulation. We begin by setting the initial time step
# conservatively, based on the smallest grid size of our domain and set-up a

using Oceananigans.Grids: min_Δz
simulation = Simulation(model,
                        Δt=0.5*min_Δz(grid)/params.V∞,
                        stop_time=2days)

# We now add callbacks to adjust the time-step and to display the simulation progress:

wizard = TimeStepWizard(max_change=1.1, cfl=0.7)
simulation.callbacks[:wizard] = Callback(wizard, IterationInterval(4))

start_time = time_ns() # so we can print the total elapsed wall time
progress_message(sim) =
    @printf("i: %04d, t: %s, Δt: %s, wmax = %.1e ms⁻¹, wall time: %s\n",
            iteration(sim), prettytime(time(sim)),
            prettytime(sim.Δt), maximum(abs, sim.model.velocities.w),
            prettytime((time_ns() - start_time) * 1e-9))

simulation.callbacks[:progress] = Callback(progress_message, IterationInterval(100))

# ## Add outputs to the simulation
#
# We add outputs to our model using the `NetCDFOutputWriter`

u, v, w = model.velocities

b_tot = Field(model.tracers.b + model.background_fields.tracers.b)
v_tot = Field(v + model.background_fields.velocities.v)
ω_y = Field(∂z(u)-∂x(w))

fields = (; u, v_tot, w, b_tot, ω_y)

simulation.output_writers[:fields] = NetCDFOutputWriter(model, fields,
                                                        filename = joinpath(@__DIR__, "out.tilted_bbl.nc"),
                                                        schedule = TimeInterval(20minutes),
                                                        overwrite_existing = true)

# Now we just run it!

run!(simulation)

# ## Visualize the results
#
# First we load the required package to load NetCDF output files and define the coordinates for
# plotting using existing objects:
#

using NCDatasets
xᶠ, zᶠ = xnodes(ω_y), znodes(ω_y);
xᶜ, zᶜ = xnodes(v_tot), znodes(v_tot) ;

# Define keyword arguments for plotting function
#
kwargs = (xlabel = "x",
          zlabel = "z",
          fill = true,
          levels = 30,
          linewidth = 0,
          color = :balance,
          colorbar = true,
          xlim = (0, params.Lx),
          zlim = (0, params.Lz)
         )

# Read in the output_writer for the two-dimensional fields and then create an animation showing the
# vorticity.

ds = NCDataset(simulation.output_writers[:fields].filepath, "r")

anim = @animate for (iter, t) in enumerate(ds["time"])
    ω_y = ds["ω_y"][:,1,:, iter]
    v_tot = ds["v_tot"][:,1,:,iter]

    ω_max = maximum(abs, ω_y)
    v_max = maximum(abs, v_tot)

    plot_ω = contour(xᶠ, zᶠ, ω_y',
                     clim = (-0.015, +0.015),
                     title = @sprintf("y-vorticity, ω_y, at t = %.1f", t); kwargs...)

    plot_v = contour(xᶜ, zᶜ, v_tot',
                     clim = (-params.V∞, +params.V∞),
                     title = @sprintf("Total along-slope velocity, at t = %.1f", t); kwargs...)

    plot(plot_ω, plot_v, layout = (2, 1), size = (800, 440))
end

close(ds)

mp4(anim, "tilted_bottom_boundary_layer.mp4", fps=15)


using MicrostructureNoise, Distributions
using Plots

using Statistics, DelimitedFiles, Dates
# uncomment if you do not mind to create this large file
# Base.download("https://www.truefx.com/dev/data//2015/MARCH-2015/EURUSD-2015-03.zip","./data/EURUSD-2015-03.zip")
# run(`unzip ./data/EURUSD-2015-03.zip -d ./data`)
dat = readdlm("./data/EURUSD-2015-03.csv", ',')
times = map(a -> DateTime(a, "yyyymmdd H:M:S.s"), dat[1:10:130260,2])
tt = Float64[1/1000*(times[i].instant.periods.value-times[1].instant.periods.value) for i in 1:length(times)]
n = length(tt)-1
T = tt[end]
y = Float64.(dat[1:10:130260, 3])

prior = MicrostructureNoise.Prior(
N = 40,

α1 = 0.0,
β1 = 0.0,

αη = 0.3,
βη = 0.3,

Πα = LogNormal(1., 0.5),
μ0 = 0.0,
C0 = 5.0
)

α = 0.3
σα = 0.1
td, θs, ηs, αs, p = MicrostructureNoise.MCMC(prior, tt, y, α, σα, 1500)

posterior = MicrostructureNoise.posterior_volatility(td, θs)

plot(MicrostructureNoise.piecewise(posterior.post_t, posterior.post_mean_root)..., label="Volatility estimate")


#=
  solve Upper Trianglar Matrices
  solve Lower Triangular Matrices
  Solve via Augmented Guassian Elimination
  Inverse via Augmented Guassian Elimination
  Solve via Inverse Linear Transformation Matrix
  Linear Least Squares Fitting
  Polynomial Least Squares Fitting
=#

function upTri(A, b)
  # accepts upper triangular matrix and vector b
  #TODO: use newtons

function lowTri(A)
  # accepts lower triangular matrix and vector b

function GuassSolve(A, b)
  # square matrix A, vector b
  return upTri(Guass([A b]))

function inverse(A)
  # try all
  r = remotecall(1, ,A)
  s = remotecall(2, ,A)
  t = remotecall(3, ,A)

function InverseSolve(A, b)
  return inverse(A) * b

function augGuassInverse()
  for i = 1:size(A)[1]

function solveviaCramers(A,b)
  B = copy(A)
  x =
  a = determinant(B)
  for i = 1:length(B)[1]
    C = copy(B)
    for j = 1:length(b)
      C[j,i] = b[j]
    end
    x[i] = determinant(C)
  end
  return a / x

function pseudo(A)
  B = transpose(A)
  try
    C = transpose(A) * (A * transpose(A))
    return C
  try
    C = (transpose(A) * A) * transpose(A)
    return C
  catch e
    println e
  end

function linearLS(A,b)
  # accepts 2 by n matrix
  T = [1 transpose(A[2,:])]
  # solve notal equations ATA = ATb
  return pseudo(T) * b

function polyLS(A)
  #TODO: complete


for f in (:erf, :erfc)

    @eval function($f)(x::BigFloat, r::RoundingMode)
        setrounding(BigFloat, r) do
            ($f)(x)
        end
    end

    @eval ($f)(a::Interval{Float64}) = convert(Interval{Float64}, ($f)(big53(a)))

end

function erf(a::Interval{T}) where T
    isempty(a) && return a
    @round( erf(a.lo), erf(a.hi) )
end

function erfc(a::Interval{T}) where T
    isempty(a) && return a
    @round( erfc(a.hi), erfc(a.lo) )
end

erfinv(x, r::RoundingMode{:Down}) = _erfinv(x).lo
erfinv(x, r::RoundingMode{:Up}) = _erfinv(x).hi
erfcinv(x, r::RoundingMode{:Down}) = _erfcinv(x).hi
erfcinv(x, r::RoundingMode{:Up}) = _erfcinv(x).lo

erfinv(a::BigFloat) = mid(_erfinv(a))
erfcinv(a::BigFloat) = mid(_erfcinv(a))

function _erfinv(a::T) where T
    domain = Interval{T}(-1, 1)
    a ∉ domain && return DomainError("$a is not in [-1, 1]")
    f = x -> erf(x) - a
    fp = x->2/sqrt(pi_interval(T)) * exp(-x^2)
    rts = roots(f, Interval{T}(-Inf, Inf), 1e-20, deriv=fp)
    @assert length(rts) == 1 # && rts[1].status == :unique

    rts[1].interval
end

function erfinv(a::Interval{T}) where T
    domain = Interval{T}(-1, 1)
    a = a ∩ domain

    isempty(a) && return a
    @round(erfinv(a.lo), erfinv(a.hi))
end

function _erfcinv(a::T) where T
    domain = Interval{T}(0, 2)
    a ∉ domain && return DomainError("$a is not in [0, 2]")
    f = x -> erfc(x) - a
    fp = x -> -2/sqrt(pi_interval(T)) * exp(-x^2)
    rts = roots(f, Interval{T}(-Inf, Inf), 1e-20, deriv=fp)
    @assert length(rts) == 1 # && rts[1].status == :unique

    rts[1].interval
end

function erfcinv(a::Interval{T}) where T
    domain = Interval{T}(0, 2)
    a = a ∩ domain

    isempty(a) && return a
    @round(erfcinv(a.hi), erfcinv(a.lo))
end


# +begin_src jupyter-julia
using LightGraphs
using GraphPlot
using GraphRecipes
using Plots
using Printf
using LaTeXStrings
println("Loading complete")
# +end_src

# +begin_src jupyter-julia
push!(LOAD_PATH, "../dev")
using Dice
# +end_src

# +begin_src jupyter-julia
G8 = SimpleGraph(8)
add_edge!(G8, 1, 2)
add_edge!(G8, 2, 3)
add_edge!(G8, 3, 4)
add_edge!(G8, 4, 5)
add_edge!(G8, 5, 6)
add_edge!(G8, 6, 7)
add_edge!(G8, 7, 8)
add_edge!(G8, 1, 8)
add_edge!(G8, 1, 5)
add_edge!(G8, 2, 6)
add_edge!(G8, 3, 7)
add_edge!(G8, 4, 8)

p = graphplot(G8, nodesize=0.05, nodeshape=:circle, curves=false, nodecolor=:black)
# +end_src

# +begin_src jupyter-julia
# The parameters below may differ from those used for the paper,
# therefore, the exact profile may differ
G = G8
mtds = sine
scale = 0.15 / nv(G)
tmax = 350

Ksstart = 0
Ksmax = 7
Ksstep = 0.1
Ksran = collect(Ksstart:Ksstep:Ksmax)
MM = length(Ksran)

model = Model(G, mtds, scale, Ksstart)

Nrep = 10000
probs = zeros(MM)
for j in 1:MM
    model.Ks = Ksran[j]
    count_best = 0

    for i in 1:Nrep
        V3 = get_initial(nv(G), (-0.15, 0.15));
        V3t = propagate(model, tmax, V3);
        state = roundup(V3t[:, end])
        curcut = cut(G, extract_configuration(state, 0))
        if curcut == 10
            count_best += 1
        end
    end
    println("For Ks = $(model.Ks):$Ksmax - success $count_best out of $Nrep")
    probs[j] = count_best / Nrep
end
plot(Ksran, probs)
# +end_src


function get_Ψ(S, Λ, Ω, tol, k=0)
    E = sum(Λ)
    Eᵣ = 0
    if k == 0
        k = 1
        while 1 - (Eᵣ + Λ[k])/E > tol
            Eᵣ += Λ[k]
            k+=1
        end
    end
    Ψ = S*Ω[:,1:k]
    for i in 1:k
        Ψ[:,i] ./= sqrt(abs.(Λ[i]))
    end
    return k, Ψ
end


export diageye

import LinearAlgebra: Diagonal

xT_A_x(x::AbstractVector, A::AbstractMatrix) = dot(x, A * x)

diageye(::Type{T}, n::Int) where { T <: Real } = Matrix{T}(I, n, n)
diageye(n::Int)                                = diageye(Float64, n)


using Pkg
Pkg.activate(".")

using Serialization

filenames = (train_image="train-images-idx3-ubyte",
             train_label="train-labels-idx1-ubyte",
             test_image="t10k-images-idx3-ubyte",
             test_label="t10k-labels-idx1-ubyte")

function read_labels(fname)
    f = open(fname)
    magic = bswap(read(f, UInt32))
    @assert magic == 2049
    nobs = bswap(read(f, UInt32))
    @assert filesize(f) == 8 + nobs
    labels = read(f)
    @assert length(labels) == nobs
    close(f)
    return Int.(labels)
end

function read_images(fname)
    f = open(fname)
    magic = bswap(read(f, UInt32))
    @assert magic == 2051
    nobs = bswap(read(f, UInt32))
    @assert bswap(read(f, UInt32)) == 28
    @assert bswap(read(f, UInt32)) == 28
    @assert filesize(f) == 16 + nobs*28*28
    images = Matrix{Float64}[]
    for i in 1:nobs
        data = read(f, 28*28)
        mx = permutedims(reshape(data, 28, 28)) / 255
        push!(images, mx)
    end
    close(f)
    return images
end

train_images = read_images(filenames.train_image)
train_labels = read_labels(filenames.train_label)
test_images = read_images(filenames.test_image)
test_labels = read_labels(filenames.test_label)

serialize("mnist.data", (train_images, train_labels,
                         test_images, test_labels))


#=
# Tests vector unknown capability with a simple Poisson-like problem.
=#

### If the Finch package has already been added, use this line #########
using Finch # Note: to add the package, first do: ]add "https://github.com/paralab/Finch.git"

### If not, use these four lines (working from the examples directory) ###
# if !@isdefined(Finch)
#     include("../Finch.jl");
#     using .Finch
# end
##########################################################################

init_finch("vector");

# Try making an optional log
useLog("vectorlog", level=3)

# Set up the configuration
domain(2)
functionSpace(order = 3)

# Specify the problem
mesh(QUADMESH, elsperdim=20, bids=3)

u = variable("u", type=VECTOR)
p = variable("p")

testSymbol("v", type=VECTOR)
testSymbol("w")

# boundary(u, 2, DIRICHLET, [0.1, 0.1])
# boundary(p, 2, DIRICHLET, 0.1)
boundary(u, 1, NEUMANN, ["-pi*sin(2*pi*y)", "-2*pi*sin(3*pi*y)"])
boundary(p, 1, NEUMANN, "-pi*sin(pi*y)")
boundary(u, 2, DIRICHLET, [0, 0])
boundary(p, 2, DIRICHLET, 0)
boundary(u, 3, DIRICHLET, [0, 0])
boundary(p, 3, DIRICHLET, 0)

# Write the weak form
coefficient("f", ["-25*pi*pi*sin(pi*x)*sin(2*pi*y)", "-65*pi*pi*sin(2*pi*x)*sin(3*pi*y)"], type=VECTOR)
coefficient("g", "-2*pi*pi*sin(pi*x)*sin(pi*y)")
coefficient("a", 5)

weakForm([u,p], ["-a*inner(grad(u), grad(v)) - dot(f,v)", "-dot(grad(p), grad(w)) - g*w"])

solve([u,p]);

# # exact solution is [sin(pi*x)*sin(2*pi*y), sin(2*pi*x)*sin(3*pi*y)]
# # check error
erroru = zeros(size(u.values));
maxerru = 0
maxerrp = 0
exactu1(x,y) = sin(pi*x)*sin(2*pi*y);
exactu2(x,y) = sin(2*pi*x)*sin(3*pi*y);
exactp(x,y) = sin(pi*x)*sin(pi*y);

for i=1:size(Finch.grid_data.allnodes,2)
    x = Finch.grid_data.allnodes[1,i];
    y = Finch.grid_data.allnodes[2,i];
    exac = [exactu1(x,y), exactu2(x,y)];
    for j=1:size(Finch.grid_data.allnodes,1)
        erroru[j,i] = u.values[j,i] - exac[j];
        global maxerru;
        maxerru = max(abs(erroru[j,i]),maxerru);
    end
    global maxerrp;
    maxerrp = max(abs(p.values[i] - exactp(x,y)),maxerrp);
end
println("u max error = "*string(maxerru));
println("p max error = "*string(maxerrp));

using Plots
pyplot();
display(plot(Finch.grid_data.allnodes[1,:], Finch.grid_data.allnodes[2,:], erroru[1,:], st=:surface))

# check
# log_dump_config();
# log_dump_prob();

output_values([u,p], "vector2d", format="vtk");

finalize_finch();


# Day 17: Trick Shot
function load(::Day{17}, path)
    m = match(r"target area: x=(-?\d*)\.\.(-?\d*), y=(-?\d*)\.\.(-?\d*)", readline(path))
    return parse.(Int, m.captures)
end

# Part 1 – highest shot
# For this task, the trajectory in x doesn't matter as the probe will always fall
# vertically into the target area. We just brute-force compute all trajectories
# that end in the target area and return the highest point across all of these trajectories.
function solve1(::Day{17}, data; vmax=150)
    _, _, ymin, ymax = data

    ytrajectories = filter(
        trajectory -> in(last(trajectory), ymin:ymax),
        [cumsum(ẏ₀:-1:ymin) for ẏ₀ in 1:vmax],
    )
    return maximum(maximum.(ytrajectories))
end

# Part 2 – count all viable trajectories
function solve2(::Day{17}, data; vmax=150)
    xmin, xmax, ymin, ymax = data
    xrange = xmin:xmax
    yrange = ymin:ymax
    count = 0

    # List of viable trajectories in x
    xtrajectories = filter(
        trajectory -> any(in(xrange), trajectory), [cumsum(ẋ₀:-1:0) for ẋ₀ in 1:xmax]
    )

    for xtraj in xtrajectories
        # There are two types of xtrajectories:
        # a) those that have zero velocity in x when reaching the target
        # b) those that pass through the target

        steps_until_target = findall(in(xrange), xtraj)
        if length(xtraj) >= 2 && in(xtraj[end], xrange) && xtraj[end] == xtraj[end - 1] # case a
            ytrajectories = filter(
                trajectory -> any(in(yrange), trajectory[first(steps_until_target):end]),
                [cumsum(ẏ₀:-1:ymin) for ẏ₀ in ymin:vmax],
            )
            count += length(ytrajectories)
        else # case b
            vels = Vector{Int}()
            for step in steps_until_target
                ytrajectories = filter(
                    trajectory -> in(last(trajectory), yrange),
                    [cumsum(range(; start=ẏ₀, step=-1, length=step)) for ẏ₀ in ymin:vmax],
                )
                append!(vels, first.(ytrajectories))
            end
            count += length(unique!(vels))
        end
    end
    return count
end

testresult1(::Day{17}) = 45
testresult2(::Day{17}) = 112


### A Pluto.jl notebook ###
# v0.12.21

using Markdown
using InteractiveUtils

# This Pluto notebook uses @bind for interactivity. When running this notebook outside of Pluto, the following 'mock version' of @bind gives bound variables a default value (instead of an error).
macro bind(def, element)
    quote
        local el = $(esc(element))
        global $(esc(def)) = Core.applicable(Base.get, el) ? Base.get(el) : missing
        el
    end
end

# ╔═╡ 07ca21ee-80d8-11eb-26f5-3927b8d5af66
begin
    using PlutoUI
    using Zygote
    using Plots
    using VisualOptim
    plotly()
end

# ╔═╡ 4419ca0c-80ea-11eb-38e0-9dc5d25fa2c2
md"Add Library for Dark mode"

# ╔═╡ dc72ce94-8758-11eb-0846-3b66ba7f962c
html"<button onclick=present()> present </button>"

# ╔═╡ b7d9dd1e-80d7-11eb-0fc1-2d52414a9e70
begin
    using DarkMode: DarkMode
    DarkMode.enable()
    # OR DarkMode.Toolbox(theme="default")
end

# ╔═╡ 0008da96-80eb-11eb-1b44-15ac852eae50
md"
These sliders control the learning rate $\eta$ and the initial value of $x$. Please play around with them to get a sense of how the optimizer works
"

# ╔═╡ b74929b4-80d8-11eb-08d6-f52ce81e4da4
@bind η Slider(0.1:0.05:1; show_value=true, default=0.2)

# ╔═╡ 62e0efe8-8981-11eb-03db-9da240116edb

# ╔═╡ Cell order:
# ╟─4419ca0c-80ea-11eb-38e0-9dc5d25fa2c2
# ╠═dc72ce94-8758-11eb-0846-3b66ba7f962c
# ╠═b7d9dd1e-80d7-11eb-0fc1-2d52414a9e70
# ╠═07ca21ee-80d8-11eb-26f5-3927b8d5af66
# ╟─0008da96-80eb-11eb-1b44-15ac852eae50
# ╠═b74929b4-80d8-11eb-08d6-f52ce81e4da4
# ╠═62e0efe8-8981-11eb-03db-9da240116edb


"""
Hypergraph configuration model for fixed degree and edge-dimension sequences using
a Markov Chain Monte Carlo scheme.

Based on "Configuration Models of Random Hypergraphs" by Philip S. Chodrow.
The original paper may be found at https://arxiv.org/abs/1902.09302.
"""

"""
`stub_matching`
===============

Produce a random (likely degenerate) hypergraph with given degree and edge
dimension sequences via stub matching. (pp. 2,6)

Arguments
---------
   - `D::Vector{Int64}`: The desired degree sequence
   - `K::Vector{Int64}`: The desired edge dimension sequence
   - `vals::Any (=ones())`: The values located at each node

Examples
-------
~~~~
stub_matching([3,2,2,2], [4,3,2])
stub_matching([1], [2], ["foo"])
~~~~
"""
function stub_matching(D::Vector{Int64}, K::Vector{Int64}; vals=ones(length(D)))
   hypergraph = Hypergraph(Vector{Vector{Float64}}(), 0, 0) # Blank hypergraph
   degs = copy(D)
   valid_nodes = 1:length(D)

   for k = 1:size(K,1) # Iterate over edge dimension sequence
      edge = zeros(K[k]) # Empty edge of length K[k]
      for d = 1:K[k] # Choose K[k] random stubs for the edge
         node = rand(1:length(degs))
         edge[d] = node
         degs[node] -= 1 # Remove stub
         if degs[node] == 0
            setdiff!(valid_nodes, [node])
         end
      end
      push!(hypergraph.edges, sort(edge, by=x -> D[x], rev=true))
   end
   hypergraph.n = size(D,1) # Set hypergraph fields
   hypergraph.m = size(K,1)
   hypergraph.D = D
   hypergraph.K = K
   hypergraph.vals = vals
   return hypergraph
end

"""
`pairwise_reshuffle!`
=====================

Performs a pairwise reshuffle on the edges numbered `e1` and `e2`.

Arguments
---------
   - `H::Hypergraph`: The hypergraph containing the edges to shuffle
   - `e1::Int64`: The index of the first edge
   - `e2::Int64`: The index of the second edge

Preconditions
-------------
   - `e1, e2 ∈ (0,m]` where `m` is the number of edges in the hypergraph
   - `e1 ≠ e2`

A pairwise reshuffle is an operation that randomly swaps nodes between
two edges while leaving their intersection intact. Stubs within the
intersection may be exchanged. (pp. 6-7)

Example
-------
~~~~
pairwise_reshuffle(H, 1, 2) # Shuffles edges 1 and 2
~~~~
"""
function pairwise_reshuffle!(H::Hypergraph, e1::Int64, e2::Int64)
   @assert 0 < e1 <= H.m && 0 < e2 <= H.m && e1 != e2 # Edges to be reshuffled are valid

   inter = collect(edge_intersect(h, e1, e2)) # edge1 ⋂ edge2
   exclu = setdiff([h.edges[e1]; h.edges[e2]], inter) # (edge1 ⋃ edge2) \ inter

   h.edges[e1] = copy(inter) # Initialize e1 as intersection
   for i = 1:h.K[e1] - size(inter,1) # Randomly assign |e1 \ e2| nodes to e1
      new_n = rand(exclu)
      push!(h.edges[e1], new_n)
      setdiff!(exclu, [new_n])
   end

   h.edges[e2] = [inter; exclu] # Assign the rest to e2

   sort!(h.edges[e1], by=x -> H.D[x], rev=true)
   sort!(h.edges[e2], by=x -> H.D[x], rev=true)
end

"""
`MCMC`
======

Randomly explore the space of hypergraphs (vertex- or stub-labeled) with the degree
and edge-dimension sequences of a given hypergraph via random pairwise shuffling. (pp. 7-9)

Arguments
---------
   - `intiial::Hypergraph`: The hypergraph from which exploration begins
   - `samples::Int64 (=1000)`: The number of pairwise reshuffles to perform
   - `type::String (="vertex")`: Which distribution to draw from. Choices are
                                 "vertex" or "stub".

Preconditions
-------------
   - `samples > 0`
"""
function MCMC(initial::Hypergraph; samples::Int64=1000, type::String="vertex")
   @assert samples > 0 # Positive number of samples
   if type == "stub"
      return MCMC_s(initial; samples=samples)
   elseif type == "vertex"
      return MCMC_v(initial; samples=samples)
   else
      error("Invalid type argument. Choices are 'vertex' and 'stub'.")
   end
end

"""
`MCMC_s`
========

See `MCMC`.

We generate a random stub-labeled hypergraph by performing a swap at every
iteration of the loop.
"""
function MCMC_s(initial::Hypergraph; samples=1000)
   c = copy(initial)
   for t = 1:samples
      sample_edges = random_edge_indices(c, 2) # Random pair of edges
      pairwise_reshuffle!(c, sample_edges[1], sample_edges[2])
   end
   return c
end

"""
`MCMC_v`
========

See `MCMC`.

To generate a random vertex-labeled hypergraph, we can't swap at every iteration;
otherwise, we effectively perform the stub-labeled process, with the effect of
biasing the end result towards vertex-labeled hypergraphs that have more equivalent
realizations as stub-labeled hypergraphs. Thus, we must sample from the below
distribution.
"""
function MCMC_v(initial::Hypergraph; samples=1000)
   k = 0 # Number of iterations performed
   n_rejected = 0 # Number of iterations in which a swap was rejected

   c = copy(initial)
   parallels = Dict() # Keeps track of the number of hyperedges parallel (multiset equal) to each distinct hyperedge
   for i = 1:c.m
      if !haskey(parallels, c.edges[i])
         parallels[c.edges[i]] = 0
      end
      parallels[c.edges[i]] += 1
   end

   while k - n_rejected < samples # Number of performed swaps < desired
      n_rand = 20000

      n_ = 1
      p_ = 1
      inds = zeros(Int64, n_rand) # Random edge indices
      for i = 0:2:n_rand-1
         pair = sample(1:initial.m, 2, replace=false) # Prohibit swaps between an edge and itself
         inds[i+1], inds[i+2] = pair[1], pair[2]
      end
      probs = rand(Float64, Int(n_rand / 2)) # Generate many random floats at once

      while true
         if n_ >= n_rand / 2 # Refresh random values
            n_ = 1
            p_ = 1
            inds = zeros(Int64, n_rand)
            for i = 0:2:n_rand-1
               pair = sample(1:initial.m, 2, replace=false)
               inds[i+1], inds[i+2] = pair[1], pair[2]
            end
            probs = rand(Float64, n_rand)
         end

         i, j = inds[n_], inds[n_+1]
         ei, ej = c.edges[i], c.edges[j]

         n_ += 2 # Go to next pair of edges
         p_ += 1 # Next probability
         inter = 2.0^-length(intersect(ei, ej)) # Larger intersection = less likely to swap

         if probs[p_] > inter / (parallels[ei] * parallels[ej]) # Randomly decide whether to shuffle
            n_rejected += 1
            k += 1
         else
            if haskey(parallels, ei)
               parallels[ei] -= 1
               if parallels[ei] <= 0
                  delete!(parallels, ei)
               end
            end

            if haskey(parallels, ej)
               parallels[ej] -= 1
               if parallels[ej] <= 0
                  delete!(parallels, ej)
               end
            end

            pairwise_reshuffle!(c, i, j) # Perform shuffle

            new_ei = c.edges[i]
            new_ej = c.edges[j]

            if !haskey(parallels, new_ei)
               parallels[new_ei] = 0
            end
            if !haskey(parallels, new_ej)
               parallels[new_ej] = 0
            end
            parallels[new_ei] += 1 # Update parallels
            parallels[new_ej] += 1

            k += 1
            break
         end
      end
   end

   return c
end


function get_neighbours(layout, ix, jy)
    nr_rows, nr_cols = size(layout)
    neighbours = Char[]
    for dx in -1:1, dy in -1:1
        if dx != 0 || dy != 0
            x = ix + dx
            y = jy + dy
            if 1 <= x <= nr_rows && 1 <= y <= nr_cols
                push!(neighbours, layout[x, y])
            end
        end
    end
    neighbours
end

function single_step(layout)
    updated = Array{Char, 2}(undef, size(layout))
    nr_changed = 0
    for jy in 1:size(layout, 2)
        for ix in 1:size(layout, 1)
            item = layout[ix, jy]
            if item == '.'
                updated[ix, jy] = '.'
            else
                neighbours = get_neighbours(layout, ix, jy)
                if item == 'L' && all(neighbour != '#' for neighbour in neighbours)
                    updated[ix, jy] = '#'
                    nr_changed += 1
                elseif item == '#' && count(n -> n == '#', neighbours) >= 4
                    updated[ix, jy] = 'L'
                    nr_changed += 1
                else
                    updated[ix, jy] = item
                end
            end
        end
    end
    updated, nr_changed
end

function seating_system(seat_layout)
    while true
        seat_layout, nr_changed_seats = single_step(seat_layout)
        if nr_changed_seats == 0
            return count(x -> x == '#', seat_layout)
        end
    end
end

function main()
    lines = readlines("../resources/input_11.txt")
    seat_layout = permutedims(hcat(map(collect, lines)...))
    result = seating_system(seat_layout)
    println(result)
end

main()


#    Copyright (c) 2021 J.A. Duffek
#    Copyright (c) 2000 D.M. Spink
#
#    This program is free software; you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation; either version 2 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program; if not, see <http://www.gnu.org/licenses/>.

"""
    basisfun(i::I,u::F,p::I,U::Vector{F}
            )::Vector{F} where {I<:Integer,F<:AbstractFloat}

Calculates the B-Spline basis function at `u`.

# Arguments:
- `i`: knot span from [`findspan`](@ref)
- `u`: parametric point
- `p`: spline degree
- `U`: knot sequence

# Output:
- `N`: basis function vector of lenght `p+1`

# Examples:
```julia
julia> N = basisfun(i,u,p,U)
```

# Reference:
- Algorithm A2.2 from 'The NURBS BOOK' pg70 ([1997Piegl](@cite))
"""
function basisfun(i::I,u::F,p::I,U::Vector{F}
                 )::Vector{F} where {I<:Integer,F<:AbstractFloat}
# TODO: the original C function got U and N passed as reference
# TODO: this function has optimization potential

i = i + 1;

# allocate result and work space
N     = zeros(F,p+1);
left  = zeros(F,p+1);
right = zeros(F,p+1);

N[1] = 1;
for j in 1:p
  left[j+1]  = u - U[i+1-j];
  right[j+1] = U[i+j] - u;
  saved = 0;

  for r in 0:j-1
    temp = N[r+1] / (right[r+2] + left[j-r+1]);
    N[r+1] = saved + right[r+2] * temp;
    saved = left[j-r+1] * temp;
  end # for r

  N[j+1] = saved;
end # for j
return N;
end # basisfun


################################################################################
# forestGrowthFitting Problem
#
# How fast are French forests growing? How much timber can they provide in one hectare?
# Surely forests provide multiple ecosystem services, but in this (simplified) exercise we focus on the above questions by looking at the so-called "raw data" from the French National Forest Inventory, both in terms of individual trees and in terms of inventoried plots, to fit a generic growth model of the forest stands in terms of volumes with respect to the age of the trees.
#

# ### Environment set-up and data loading

# 1) Start by setting the working directory to the directory of this file and activate it. If you have the provided `Manifest.toml` file in the directory, just run `Pkg.instantiate()`, otherwise manually add the packages Pipe, HTTP, CSV, DataFrames, LsqFit, StatsPlots.

# 2) Load the packages Pipe, HTTP, CSV, DataFrames, LsqFit, StatsPlots

# 3) Load from internet or from local files the following data:

ltURL     = "https://github.com/sylvaticus/IntroSPMLJuliaCourse/blob/main/lessonsMaterial/02_JULIA2/forestExercise/data/arbres_foret_2012.csv?raw=true" # live individual trees data
dtURL     = "https://github.com/sylvaticus/IntroSPMLJuliaCourse/blob/main/lessonsMaterial/02_JULIA2/forestExercise/data/arbres_morts_foret_2012.csv?raw=true" # dead individual trees data
pointsURL = "https://github.com/sylvaticus/IntroSPMLJuliaCourse/blob/main/lessonsMaterial/02_JULIA2/forestExercise/data/placettes_foret_2012.csv?raw=true" # plot level data
docURL    = "https://github.com/sylvaticus/IntroSPMLJuliaCourse/blob/main/lessonsMaterial/02_JULIA2/forestExercise/data/documentation_2012.csv?raw=true" # optional, needed for the species label

# If you choosen to download the data from internet, you can make for each of the dataset a `@pipe` macro starting with `HTTP.get(URL).body`, continuing the pipe with `CSV.File(_)` and end the pipe with a DataFrame object.

# 4) These datasets have many variable we are not using in this exercise.
# Out of all the variables, select only for the `lt` and `dt` dataframes the columns "idp" (pixel id), "c13" (circumference at 1.30 meters) and "v" (tree's volume). Then vertical concatenate the two dataset in an overall `trees` dataset.
# For the `points` dataset, select only the variables "idp" (pixel id), "esspre" (code of the main forest species in the stand) and "cac" (age class).

# 5) As the French inventory system is based on a concentric sample method (small trees are sampled on a small area (6 metres radius), intermediate trees on a concentric area of 9 metres and only large trees (with a circonference larger than 117.5 cm) are sampled on a concentric area of 15 metres of radius), define the following function to compute the contribution of each tree to the volume per hectare:

"""
    vHaContribution(volume,circonference)

Return the contribution in terms of m³/ha of the tree.

The French inventory system is based on a concentric sample method: small trees are sampled on a small area (6 metres radius), intermediate trees on a concentric area of 9 metres and only large trees (with a circonference larger than 117.5 cm) are sampled on a concentric area of 15 metres of radius.
This function normalise the contribution of each tree to m³/ha.
"""
function vHaContribution(v,c13)
    if c13 < 70.5
        return v/(6^2*pi/(100*100))
    elseif c13 < 117.5
        return v/(9^2*pi/(100*100))
    else
        return v/(15^2*pi/(100*100))
    end
end
# Use the above function to compute `trees.vHa` based on `trees.v` and `trees.c13`.

# 6) Aggregate the `trees` dataframe by the `idp` column to retrieve the sum of `vHa` and the number of trees for each point, calling these two columns `vHa` and `ntrees`.

# 7) Join the output of step 6 (the trees dataframe aggregated "by point") with the original points dataframe using the column `idp`.

# 8) Use boolean selection to apply the following filters:
filter_nTrees           = points.ntrees .> 5
filter_IHaveAgeClass    = .! in.(points.cac,Ref(["AA","NR"]))
filter_IHaveMainSpecies = .! ismissing.(points.esspre)
filter_overall          = filter_nTrees .&& filter_IHaveAgeClass .&& filter_IHaveMainSpecies

# 9) Run the following line to parse the age class (originally as a string indicating the 5-ages group) to an integer and compute the mid-range of the class in years. For example, class "02" will become 7.5 years.
points.cac              = (parse.(Int64,points.cac) .- 1 ) .* 5 .+ 2.5

# 10) Define the following logistic model of the growth relation with respect to the age with 3 parameters and make its vectorised form:
logisticModel(age,parameters) = parameters[1]/(1+exp(-parameters[2] * (age-parameters[3]) ))
logisticModelVec(age,parameters) = # .... complete

# 11) Set `initialParameters` to 1000,0.05 and 50 respectivelly

# 12) Perform the fittin of the model using the function `curve_fit(model,X,Y,initial parameters)` and obtain the fitted parameter fitobject.param

# 13) Compute the standard error for each estimated parameter and the confidence interval at 10% significance level

# 14) Plot a chart of fitted (y) by stand age (x) (i.e. the logisticModel with the given parameters)

# 15) Add to the plot a scatter chart of the actual observed VHa

# 16) [OPTIONAL] Look at the growth curves of individual species.
# Try to perform the above analysis for individual species, for example plot the fitted curves for the 5 most common species


"""
  Define a point type
"""
struct MyPoint
        x::Float64
        y::Float64
end

Base.:+(p1::MyPoint,p2::MyPoint) = MyPoint( p1.x+p2.x, p1.y+p2.y )

Base.:-(p1::MyPoint,p2::MyPoint) = MyPoint( p1.x-p2.x, p1.y-p2.y )

function MyPoint(p::MyPoint)
        MyPoint(p.x, p.y)
end