# # Decaying barotropic QG beta-plane turbulence
#
#md # This example can be viewed as a Jupyter notebook via [![](https://img.shields.io/badge/show-nbviewer-579ACA.svg)](@__NBVIEWER_ROOT_URL__/literated/singlelayerqg_betadecay.ipynb).
#
# An example of decaying barotropic quasi-geostrophic turbulence on a beta plane.
#
# ## Install dependencies
#
# First let's make sure we have all required packages installed.

# ```julia
# using Pkg
# pkg"add GeophysicalFlows, Plots, Printf, Statistics, Random"
# ```

# ## Let's begin
# Let's load `GeophysicalFlows.jl` and some other needed packages.
#
using GeophysicalFlows, Plots, Printf, Random

using Statistics: mean

# ## Choosing a device: CPU or GPU

dev = CPU()     # Device (CPU/GPU)
nothing # hide

# ## Numerical parameters and time-stepping parameters

      n = 128            # 2D resolution: n² grid points
stepper = "FilteredRK4"  # timestepper
     dt = 0.04           # timestep
 nsteps = 2000           # total number of time-steps
 nsubs  = 20             # number of time-steps for intermediate logging/plotting (nsteps must be multiple of nsubs)
nothing # hide

# ## Physical parameters

L = 2π        # domain size
β = 10.0      # planetary PV gradient
μ = 0.0       # bottom drag
nothing # hide

# ## Problem setup
# We initialize a `Problem` by providing a set of keyword arguments. Not providing a viscosity
# coefficient `ν` leads to the module's default value: `ν=0`. In this example numerical instability
# due to accumulation of enstrophy at high wavenumbers is taken care with the `FilteredTimestepper`
# we picked. Thus, we choose not to do any dealiasing by providing `aliased_fraction=0`.

prob = SingleLayerQG.Problem(dev; nx=n, Lx=L, β=β, μ=μ,
                                  dt=dt, stepper=stepper, aliased_fraction=0)
nothing # hide

# and define some shortcuts
sol, clock, vars, params, grid = prob.sol, prob.clock, prob.vars, prob.params, prob.grid
x, y = grid.x, grid.y
nothing # hide

# ## Setting initial conditions

# Our initial condition consist of a flow that has power only at wavenumbers with
# ``6 < \frac{L}{2\pi} \sqrt{k_x^2 + k_y^2} < 10`` and initial energy ``E_0``.
# `ArrayType()` function returns the array type appropriate for the device, i.e., `Array` for
# `dev = CPU()` and `CuArray` for `dev = GPU()`.

E₀ = 0.08 # energy of initial condition

K = @. sqrt(grid.Krsq)                          # a 2D array with the total wavenumber

Random.seed!(1234)
q₀h = ArrayType(dev)(randn(Complex{eltype(grid)}, size(sol)))
@. q₀h = ifelse(K < 6  * 2π/L, 0, q₀h)
@. q₀h = ifelse(K > 10 * 2π/L, 0, q₀h)
@. q₀h[1, :] = 0    # remove any power from zonal wavenumber k=0
q₀h *= sqrt(E₀ / SingleLayerQG.energy(q₀h, vars, params, grid)) # normalize q₀ to have energy E₀
q₀ = irfft(q₀h, grid.nx)

SingleLayerQG.set_q!(prob, q₀)
nothing # hide

# Let's plot the initial vorticity and streamfunction. Note that when plotting, we decorate
# the variable to be plotted with `Array()` to make sure it is brought back on the CPU when
# `vars` live on the GPU.

p1 = heatmap(x, y, Array(vars.q'),
         aspectratio = 1,
              c = :balance,
           clim = (-12, 12),
          xlims = (-grid.Lx/2, grid.Lx/2),
          ylims = (-grid.Ly/2, grid.Ly/2),
         xticks = -3:3,
         yticks = -3:3,
         xlabel = "x",
         ylabel = "y",
          title = "initial vorticity ∂v/∂x-∂u/∂y",
     framestyle = :box)

p2 = contourf(x, y, Array(vars.ψ'),
        aspectratio = 1,
             c = :viridis,
        levels = range(-0.7, stop=0.7, length=20),
          clim = (-0.35, 0.35),
         xlims = (-grid.Lx/2, grid.Lx/2),
         ylims = (-grid.Ly/2, grid.Ly/2),
        xticks = -3:3,
        yticks = -3:3,
        xlabel = "x",
        ylabel = "y",
         title = "initial streamfunction ψ",
    framestyle = :box)

layout = @layout Plots.grid(1, 2)
p = plot(p1, p2, layout = layout, size = (800, 360))

# ## Diagnostics

# Create Diagnostics -- `energy` and `enstrophy` functions are imported at the top.
E = Diagnostic(SingleLayerQG.energy, prob; nsteps=nsteps)
Z = Diagnostic(SingleLayerQG.enstrophy, prob; nsteps=nsteps)
diags = [E, Z] # A list of Diagnostics types passed to "stepforward!" will  be updated every timestep.
nothing # hide

# ## Output

# We choose folder for outputing `.jld2` files and snapshots (`.png` files).
filepath = "."
plotpath = "./plots_decayingbetaturb"
plotname = "snapshots"
filename = joinpath(filepath, "decayingbetaturb.jld2")
nothing # hide

# Do some basic file management,
if isfile(filename); rm(filename); end
if !isdir(plotpath); mkdir(plotpath); end
nothing # hide

# and then create Output.
get_sol(prob) = sol # extracts the Fourier-transformed solution
out = Output(prob, filename, (:sol, get_sol))
nothing # hide

# ## Visualizing the simulation

# We define a function that plots the vorticity and streamfunction and
# their corresponding zonal mean structure.

function plot_output(prob)
  q = Array(prob.vars.q)
  ψ = Array(prob.vars.ψ)
  q̄ = Array(mean(q, dims=1)')
  ū = Array(mean(prob.vars.u, dims=1)')

  pq = heatmap(x, y, q',
       aspectratio = 1,
            legend = false,
                 c = :balance,
              clim = (-12, 12),
             xlims = (-grid.Lx/2, grid.Lx/2),
             ylims = (-grid.Ly/2, grid.Ly/2),
            xticks = -3:3,
            yticks = -3:3,
            xlabel = "x",
            ylabel = "y",
             title = "vorticity ∂v/∂x-∂u/∂y",
        framestyle = :box)

  pψ = contourf(x, y, ψ',
       aspectratio = 1,
            legend = false,
                 c = :viridis,
            levels = range(-0.7, stop=0.7, length=20),
              clim = (-0.35, 0.35),
             xlims = (-grid.Lx/2, grid.Lx/2),
             ylims = (-grid.Ly/2, grid.Ly/2),
            xticks = -3:3,
            yticks = -3:3,
            xlabel = "x",
            ylabel = "y",
             title = "streamfunction ψ",
        framestyle = :box)

  pqm = plot(q̄, y,
            legend = false,
         linewidth = 2,
             alpha = 0.7,
            yticks = -3:3,
             xlims = (-2.2, 2.2),
            xlabel = "zonal mean q",
            ylabel = "y")
  plot!(pqm, 0*y, y, linestyle=:dash, linecolor=:black)

  pum = plot(ū, y,
            legend = false,
         linewidth = 2,
             alpha = 0.7,
            yticks = -3:3,
             xlims = (-0.55, 0.55),
            xlabel = "zonal mean u",
            ylabel = "y")
  plot!(pum, 0*y, y, linestyle=:dash, linecolor=:black)

  layout = @layout Plots.grid(2, 2)
  p = plot(pq, pqm, pψ, pum, layout = layout, size = (800, 720))

  return p
end
nothing # hide

# ## Time-stepping the `Problem` forward

# We time-step the `Problem` forward in time.

startwalltime = time()

p = plot_output(prob)

anim = @animate for j = 0:round(Int, nsteps/nsubs)

  if j % round(Int, nsteps/nsubs / 4) == 0
    cfl = clock.dt * maximum([maximum(vars.u) / grid.dx, maximum(vars.v) / grid.dy])

    log = @sprintf("step: %04d, t: %d, cfl: %.2f, E: %.4f, Q: %.4f, walltime: %.2f min",
      clock.step, clock.t, cfl, E.data[E.i], Z.data[Z.i], (time()-startwalltime)/60)

    println(log)
  end

  p[1][1][:z] = Array(vars.q)
  p[1][:title] = "vorticity, t="*@sprintf("%.2f", clock.t)
  p[3][1][:z] = Array(vars.ψ)
  p[2][1][:x] = Array(mean(vars.q, dims=1)')
  p[4][1][:x] = Array(mean(vars.u, dims=1)')

  stepforward!(prob, diags, nsubs)
  SingleLayerQG.updatevars!(prob)

end

mp4(anim, "singlelayerqg_betadecay.mp4", fps=8)

# ## Save

# Finally, we can save, e.g., the last snapshot via
# ```julia
# savename = @sprintf("%s_%09d.png", joinpath(plotpath, plotname), clock.step)
# savefig(savename)
# ```


#random numbers from the same distribution should have small ks dist
@test ApproxBayes.ksdist(rand(10^5), rand(10^5)) < 0.01


import AlgebraicMultigrid: Level, MultiLevel, GaussSeidel

function multigrid(A::TA; max_levels = 10, max_coarse = 10,
                    presmoother = GaussSeidel(), postsmoother = GaussSeidel()) where {T,V,TA<:SparseMatrixCSC{T,V}}

    levels = Vector{Level{TA,TA,TA}}()
    w = AlgebraicMultigrid.MultiLevelWorkspace(Val{1}, eltype(A))

    while length(levels) + 1 < max_levels && size(A, 1) > max_coarse
        AlgebraicMultigrid.residual!(w, size(A, 1))
        A = extend!(levels, A)
        AlgebraicMultigrid.coarse_x!(w, size(A, 1))
        AlgebraicMultigrid.coarse_b!(w, size(A, 1))
        #=if size(A, 1) <= max_coarse
            # push!(levels, Level(A,spzeros(T,0,0),spzeros(T,0,0)))
            break
        end=#
    end

    MultiLevel(levels, A, Pinv(A), presmoother, postsmoother, w)
end

function extend!(levels, A::SparseMatrixCSC{Ti,Tv}) where {Ti,Tv}

    size_F = size(A, 1)
    size_C = rem(size_F,2) == 0 ? div((size_F-1), 2) +1 : div((size_F-1), 2)
    total_len = size_C + 2 * (size_C - 1)

    I = Vector{Tv}(undef, total_len)
    J = Vector{Tv}(undef, total_len)
    V = Vector{Ti}(undef, total_len)

    l = 1
    for k = 1:size_C
        I[l], J[l], V[l] = 2k, k, 1
        l += 1
    end
    for k = 1:size_C - 1
        I[l], J[l], V[l] = 2k+1, k, 0.5
        I[l+1], J[l+1], V[l+1] = 2k+1, k+1, 0.5
        l += 2
    end

    P = sparse(I, J, V, size_F, size_C)

    R = AlgebraicMultigrid.adjoint(P)

    push!(levels, Level(A, P, R))

    R * A * P
end

Base.show(io::IO, level::Level) = print(io, "Level")


###############################################################################
#
# The Stopping structure eases the implementation of algorithms and the
# stopping criterion.
#
# The following examples illustrate a specialized Stopping for the
# LinearOperators type.
# https://github.com/JuliaSmoothOptimizers/LinearOperators.jl
#
using LinearOperators #v.1.0.1
using LinearAlgebra, SparseArrays, Main.Stopping, Test
#
# This tutorial illustrates the different step in preparing the resolution of a
# new problem.
# - This Stopping handles linear system
#  Ax = b with A an m x n matrix.
#  or linear Least Square problem
#  min_{x ∈ ℜⁿ} || Ax - b ||_p^2
# - evaluations are measured by the LinearOperator.
# - we will use the GenericState (a specialized State would keep track of evals)
#
###############################################################################
mutable struct LinearSystem
    A :: Union{AbstractLinearOperator, AbstractMatrix}
    b :: AbstractVector
end

function linear_system_check!(pb    :: LinearSystem,
                              state :: AbstractState;
                              pnorm :: Float64 = Inf,
                              kwargs...)
 return norm(pb.A * state.x - pb.b, pnorm)
end

"""
Type: LinearOperatorStopping (specialization of GenericStopping)
Methods: start!, stop!, update_and_start!, update_and_stop!, fill_in!, reinit!, status

Stopping structure for non-linear programming problems using NLPModels.
    Input :
       - pb         : a LinearSystem
       - optimality_check : a stopping criterion through an admissibility function
       - state      : The information relative to the problem, see GenericState
       - max_cntrs  : Dict contains the max number of evaluations
       - (opt) meta : Metadata relative to stopping criterion.
       - (opt) main_stp : Stopping of the main loop in case we consider a Stopping
                          of a subproblem.
                          If not a subproblem, then nothing.

 Note:
 * optimality_check : takes two inputs (LinearSystem, AbstractState)
 and returns a Float64 to be compared at 0.
 * Not specific State targeted
 * State don't necessarily keep track of evals
 * Evals are checked only for pb.A being a LinearOperator

 Warning:
 * optimality_check does not necessarily fill in the State.
 """
mutable struct LinearOperatorStopping <: AbstractStopping

    # problem
    pb :: LinearSystem
    # stopping criterion
    optimality_check :: Function
    # Common parameters
    meta      :: AbstractStoppingMeta
    # Contains the max number of evaluations
    max_cntrs :: Dict
    # current state of the problem
    current_state :: AbstractState
    # Stopping of the main problem, or nothing
    main_stp :: Union{AbstractStopping, Nothing}

    function LinearOperatorStopping(pb             :: LinearSystem,
                                    admissible     :: Function,
                                    current_state  :: AbstractState;
                                    meta           :: AbstractStoppingMeta = StoppingMeta(),
                                    max_cntrs      :: Dict = _init_max_counters_linear_operators(),
                                    main_stp       :: Union{AbstractStopping, Nothing} = nothing,
                                    kwargs...)

        if !(isempty(kwargs))
           meta = StoppingMeta(;kwargs...)
        end

        return new(pb, admissible, meta, max_cntrs, current_state, main_stp)
    end
end

"""
_init_max_counters_linear_operators():
"""
function _init_max_counters_linear_operators(;nprod   :: Int64 = 20000,
                                              ntprod  :: Int64 = 20000,
                                              nctprod :: Int64 = 20000,
                                              sum     :: Int64 = 20000*11)

  cntrs = Dict([(:nprod,   nprod),   (:ntprod, ntprod),
                (:nctprod, nctprod), (:neval_sum,    sum)])

 return cntrs
end

import Main.Stopping: _optimality_check, _resources_check!, start!, stop!, update_and_start!, update_and_stop!, fill_in!, reinit!, status

function _optimality_check(stp  :: LinearOperatorStopping; kwargs...)

 optimality = stp.optimality_check(stp.pb, stp.current_state; kwargs...)
 stp.current_state.current_score = optimality

 return optimality
end

"""
_resources_check!: check if the optimization algorithm has exhausted the resources.
                   This is the LinearOperator specialized version that takes
                   into account the evaluation of the functions.

Note:
* function does _not_ keep track of the evals in the State
* check :nprod, :ntprod, :nctprod in the LinearOperator entries
"""
function _resources_check!(stp    :: LinearOperatorStopping,
                           x      :: Union{Vector, Nothing})

  max_cntrs = stp.max_cntrs

  # check all the entries in the counter
  max_f = false
  sum   = 0
 if typeof(stp.pb.A) <: AbstractLinearOperator
  for f in [:nprod, :ntprod, :nctprod]
   max_f = max_f || (getfield(stp.pb.A, f) > max_cntrs[f])
   sum  += getfield(stp.pb.A, f)
  end
 end

 # Maximum number of function and derivative(s) computation
 max_evals = sum > max_cntrs[:neval_sum]

 # global user limit diagnostic
 stp.meta.resources = max_evals || max_f

 return stp
end

##############################################################
#TEST

n = 5
A = rand(n,n)
xref = 100 * rand(n)
x0 = zeros(n)
b    = A * xref
sA  = sparse(A)
opA = LinearOperator(A)

mLO = LinearSystem(A, b)
sLO = LinearSystem(sA, b)
opLO = LinearSystem(opA, b)
mLOstp = LinearOperatorStopping(mLO, linear_system_check!, GenericState(x0))
sLOstp = LinearOperatorStopping(sLO, linear_system_check!, GenericState(x0))
maxcn = _init_max_counters_linear_operators(nprod = 1)
opLOstp = LinearOperatorStopping(opLO, linear_system_check!, GenericState(x0), max_cntrs = maxcn)
@test start!(mLOstp) == false
@test start!(sLOstp) == false
@test start!(opLOstp) == false

update_and_stop!(mLOstp, x = xref)
@test status(mLOstp) == :Optimal
@test mLOstp.meta.resources == false
update_and_stop!(sLOstp, x = xref)
@test status(sLOstp) == :Optimal
@test sLOstp.meta.resources == false
update_and_stop!(opLOstp, x = xref)
@test status(opLOstp) == :Optimal
@test opLOstp.meta.resources == true

"""
Randomized block Kaczmarz
"""
function RandomizedBlockKaczmarz(stp :: AbstractStopping; kwargs...)

    #A,b = stp.current_state.Jx, stp.current_state.cx
    A,b = stp.pb.A, stp.pb.b
    x0  = stp.current_state.x

    m,n = size(A)
    xk  = x0

    OK = start!(stp)

    while !OK

     i  = Int(floor(rand() * m)+1) #rand a number between 1 and m
     Ai = A[i,:]
     xk  = Ai == 0 ? x0 : x0 - (dot(Ai,x0)-b[i])/dot(Ai,Ai) * Ai
     #xk  = Ai == 0 ? x0 : x0 - (Ai' * x0-b[i])/(Ai' * Ai) * Ai

     OK = update_and_stop!(stp, x = xk)
     x0  = xk

    end

 return stp
end

m, n = 400, 200 #size of A: m x n
A    = 100 * rand(m, n)
xref = 100 * rand(n)
b    = A * xref

#Our initial guess
x0 = zeros(n)

la_stop = LinearOperatorStopping(LinearSystem(A, b), linear_system_check!, GenericState(x0), max_iter = 150000, rtol = 1e-6)
#Be careful using GenericState(x0) would not work here without forcing convert = true
#in the update function. As the iterate will be an SparseVector to the contrary of initial guess.
#Tangi: maybe start! should send a Warning for such problem !?
sa_stop = LinearOperatorStopping(LinearSystem(sparse(A), b), linear_system_check!, GenericState(sparse(x0)), max_iter = 150000, rtol = 1e-6)
op_stop = LinearOperatorStopping(LinearSystem(LinearOperator(A), b), linear_system_check!, GenericState(x0), max_iter = 150000, rtol = 1e-6)

@time RandomizedBlockKaczmarz(la_stop)
@test status(la_stop) == :Optimal
@time RandomizedBlockKaczmarz(sa_stop)
@test status(sa_stop) == :Optimal
#No compatible dot product with LinearOperators.
#@time RandomizedBlockKaczmarz(op_stop)
#@test status(op_stop) == :Optimal


using LinearAlgebra

function create_board(nrow = 3, ncol = 3)

    board = Array{String}(undef, nrow, ncol)
    board[:] .= ""

    return board

end

function whos_turn(board)

    if sum(board .!= "") == 0
        turn = "X"
    elseif sum(board .!= "") % 2 == 0
        turn = "X"
    else
        turn = "O"
    end

    return turn

end

function allsame(x)

    return all(y -> y == first(x), x)

end

function checkwinner(board)

    n, m = size(board)
    winner = ""

    if sum(board .== "") == 0
        winner = "Tie"
    end

    # horizontal
    for i in 1:n
        if allsame(board[i, :]) && board[i, 1] != ""
            winner = board[i, 1]
        end
    end
    # vertical
    for j in 1:m
        if allsame(board[:, j]) && board[1, j] != ""
            winner = board[1, j]
        end
    end
    # diagonal
    straightdiag = board[diagind(board)]
    if allsame(straightdiag) && straightdiag[1] != ""
        winner = straightdiag[1]
    end
    revboard = board[:, end:-1:1]
    revdiag = revboard[diagind(revboard)]
    if allsame(revdiag) && revdiag[1] != ""
        winner = revdiag[1]
    end

    return winner
end

function isgameover(board)

    over = false
    if sum(board .== "") == 0
        over = true
    elseif checkwinner(board) != ""
        over = true
    end

    return over

end

function parseinput(input)

    #==
    parse input properly
    ==#

    inputs = split(input, ",")
    inputs_array = Tuple([parse(Int, a) for a in inputs])

    return inputs_array

end

function islegal(boardspot)

    legal = boardspot == ""

    return legal

end

function legal_moves(board)

    legalmoves = Tuple.(findall(i->(i==""), board))

    return legalmoves

end

function play_legal_move!(board)

    movelegal = false
    while movelegal == false

        player = whos_turn(board)
        println("Enter input:")
        input = readline()
        input = parseinput(input)

        if input in legal_moves(board)
            row, col = input
            board[row, col] = player
            movelegal = true
        else
            println("Made illegal move. Please try again")
        end
    end

end

function playgame(nrow = 3, ncol = 3)

    board = create_board(nrow, ncol)
    nmaxturns = nrow * ncol
    gameoutcome = "Tie"

    # start game
    for i in 1:nmaxturns

        println("==========================")
        println("Turn number: $i")
        writedlm(stdout, board,  "\t|\t")
        play_legal_move!(board)
        gameover = isgameover(board)
        println(gameover)
        if gameover
            winner = checkwinner(board)

            if winner == ""
                gameoutcome = "Tie!"
            else
                gameoutcome = "$winner wins!"
            end

            break
        end

    end

    println("==========================")
    writedlm(stdout, board,  "\t|\t")
    return gameoutcome

end


# Simply supported square plate with center concentrated load
module bent_strip_examples

using FinEtools
using FinEtoolsDeforLinear
using FinEtoolsFlexStructures.FESetShellT3Module: FESetShellT3
using FinEtoolsFlexStructures.FESetShellQ4Module: FESetShellQ4
using FinEtoolsFlexStructures.FEMMShellT3FFModule
using FinEtoolsFlexStructures.RotUtilModule: initial_Rfield, update_rotation_field!
using VisualStructures: plot_nodes, plot_midline, render, plot_space_box, plot_midsurface, space_aspectratio, save_to_json
using FinEtools.MeshExportModule.VTKWrite: vtkwrite

    # Generate a graphical display of resultants
function   cartesian!(csmatout::FFltMat, XYZ::FFltMat, tangents::FFltMat, fe_label::FInt)
    csmatout[:, 1] .= (1.0, 0.0, 0.0)
    csmatout[:, 2] .= (0.0, 1.0, 0.0)
    csmatout[:, 3] .= (0.0, 0.0, 1.0)using .Main.tippling_examples; Main.tippling_examples.allrun()
f
    return csmatout
end

function _execute_dsg_model(formul, n = 2, visualize = true)
    E = 1e3;
    nu = 0.3;
    thickness = 0.1
    W = 1.0
    L = 10.0;
    q = 1.0 * thickness^3;
    # analytical solution for the vertical deflection under the load
    analyt_sol=-1.5875;

    tolerance = L/n/1000
    fens, fes = T3block(L, W, 5*n, n);
    fens.xyz = xyz3(fens)

    mater = MatDeforElastIso(DeforModelRed3D, E, nu)

    sfes = FESetShellT3()
    accepttodelegate(fes, sfes)
    femm = formul.make(IntegDomain(fes, TriRule(1), thickness), mater)
    stiffness = formul.stiffness
    associategeometry! = formul.associategeometry!

    # Construct the requisite fields, geometry and displacement
    # Initialize configuration variables
    geom0 = NodalField(fens.xyz)
    u0 = NodalField(zeros(size(fens.xyz,1), 3))
    Rfield0 = initial_Rfield(fens)
    dchi = NodalField(zeros(size(fens.xyz,1), 6))

    # Apply EBC's
    l1 = selectnode(fens; box = Float64[0 0 -Inf Inf -Inf Inf], inflate = tolerance)
    for i in [1, 2, 3, 6]
        setebc!(dchi, l1, true, i)
    end
    l1 = selectnode(fens; box = Float64[L L -Inf Inf -Inf Inf], inflate = tolerance)
    for i in [1, 2, 3, 6]
        setebc!(dchi, l1, true, i)
    end
    applyebc!(dchi)
    numberdofs!(dchi);

    # Assemble the system matrix
    associategeometry!(femm, geom0)
    K = stiffness(femm, geom0, u0, Rfield0, dchi);

    # Load
    lfemm = FEMMBase(IntegDomain(fes, TriRule(3)))
    fi = ForceIntensity(FFlt[0, 0, -q, 0, 0, 0]);
    F = distribloads(lfemm, geom0, dchi, fi, 2);

    # Solve
    U = K\F
    scattersysvec!(dchi, U[:])
    @show minimum(dchi.values[:, 3])/analyt_sol*100

    # Visualization
    if visualize

    ocsys = CSys(3, 3, cartesian!)
    scalars = []
    for nc in 1:3
        fld = fieldfromintegpoints(femm, geom0, dchi, :moment, nc, outputcsys = ocsys)
        # fld = elemfieldfromintegpoints(femm, geom0, dchi, :moment, nc)
        push!(scalars, ("m$nc", fld.values))
    end
    vtkwrite("bent_strip_examples-m.vtu", fens, fes; scalars = scalars)
        # Generate a graphical display of resultants
    scalars = []
    for nc in 1:2
        fld = fieldfromintegpoints(femm, geom0, dchi, :shear, nc, outputcsys = ocsys)
        push!(scalars, ("q$nc", fld.values))
        fld = elemfieldfromintegpoints(femm, geom0, dchi, :shear, nc, outputcsys = ocsys)
        push!(scalars, ("eq$nc", fld.values))
    end
    vtkwrite("bent_strip_examples-q.vtu", fens, fes; scalars = scalars)

    scattersysvec!(dchi, (L/4)/maximum(abs.(U)).*U)
    update_rotation_field!(Rfield0, dchi)
    plots = cat(plot_space_box([[0 0 -L/2]; [L/2 L/2 L/2]]),
        #plot_nodes(fens),
        plot_midsurface(fens, fes; x = geom0.values, u = dchi.values[:, 1:3], R = Rfield0.values);
        dims = 1)
    pl = render(plots)
    return true
end
end

function test_convergence()
    formul = FEMMShellT3FFModule
    @info "Simply supported square plated with concentrated force,"
    @info " formulation=$(formul)"

    for n in [2, 4, 8, ]
        _execute_dsg_model(formul, n, true)
    end
    return true
end

function allrun()
    println("#####################################################")
    println("# test_convergence ")
    test_convergence()
    return true
end # function allrun

@info "All examples may be executed with "
println("using .$(@__MODULE__); $(@__MODULE__).allrun()")

end # module
nothing


using PyPlot
using Animations

function easingplots()

    efuncs = [noease, stepease, linear, sineio, saccadic, expin, expout, polyin, polyout]
    params = [[nothing], [0.25, 0.5, 0.75], [nothing], [nothing], [1, 2, 4], [0.2, 2, 8], [0.2, 2, 8], [2, 3, 6], [2, 3, 6]]

    n = length(efuncs)
    cols = 3
    rows = Int(ceil(n / cols))
    fig, axes = subplots(rows, cols, figsize=(5, rows/3*4), constrained_layout=true)

    xx = 0:0.005:1

    for (i, (ef, ps)) in enumerate(zip(efuncs, params))

        a = axes[(divrem(i - 1, cols) .+ (1, 1))...]
        a.axis("off")

        a.plot([-0.25, 0], [0, 0], color=(0.5, 0.5, 0.5), linestyle="dotted")
        a.plot([1, 1.25], [1, 1], color=(0.5, 0.5, 0.5), linestyle="dotted")
        a.set_title("$(Symbol(ef))", fontweight=600)
        for p in ps
            easing = isnothing(p) ? ef() : ef(p)
            anim = Animation([0, 1], [0.0, 1.0], easing)
            yy = at.(anim, xx)
            if isnothing(p)
                a.plot(xx, yy)
            else
                a.plot(xx, yy, label="$p")
            end
        end
        if ps != [nothing]
            a.legend(loc="center", bbox_to_anchor=(1, 0.5), frameon=false, borderpad=0, borderaxespad=0, handlelength=0.5)
        end
    end

    fig.savefig(joinpath(dirname(pathof(Animations)), "../misc/easingplots.svg"))

end
easingplots()


#      acedgfb cdfbe gcdfa fbcad dab cefabd cdfgeb eafb cagedb ab | cdfeb fcadb cdfeb cdbaf
# srt: abcdefg bcdef acdfg abcdf abd abcdef bcdefg abef abcdeg ab | bcdef abcdf bcdef abcdf
# len:    7      5    5      5    3    6      6     4     6    2      5     5     5     5
# num:    8     235  235    235   7   069    069    4    069   1     235   235   235   235
# Because ab is 1, then a 5group containing ab must be 3,
#                   and a 6group missing a or b must be 6.
# num:    8     25   25      3    7    09     09    4     6    1     25     3    25    3

# All the following definitions shall be equivalent
# normalize(x) = join(sort(collect(x)))
# normalize = join ∘ sort ∘ collect
normalize(x) = x |> collect |> sort |> join

num2seg = Dict(0 => "abcefg", 1 => "cf", 2 => "acdeg", 3 => "acdfg", 4 => "bcdf",
               5 => "abdfg", 6 => "abdefg", 7 => "acf", 8 => "abcdefg", 9 => "abcdfg")

function parse_7segments(io)
    signals = []
    quartets = []
    for line in eachline(io)
        left, right = split(line,'|')
        push!(signals, normalize.(split(left)))
        push!(quartets, normalize.(split(right)))
    end
    return signals, quartets
end

unique_patterns = Set([2, 4, 3, 7])  # 1,4,7,8 use a unique number of segments
is_unique(p) = p in unique_patterns

function day08a(quartets)
    return sum(count(is_unique, length.(nums)) for nums in quartets)
end

function invert(d::Dict)
    out = Dict()
    for (k, v) in d
        out[v] = k
    end
    return out
end

function find_encoding(signals)
    sigs = Dict()
    sigs[1] = first(s for s in signals if length(s) == 2)
    sigs[4] = first(s for s in signals if length(s) == 4)
    sigs[7] = first(s for s in signals if length(s) == 3)
    sigs[8] = first(s for s in signals if length(s) == 7)
    # Groups of 5 (3, 2, 5):
    len5 = [s for s in signals if length(s) == 5]
    # Because ab is 1, then a 5group containing ab must be 3,
    sigs[3] = first(s for s in len5 if length(s ∩ sigs[1]) == 2)
    # Decide between 2 and 5: 2 has 2 common elements with 4, 5 has 3 common elements with 4
    sigs[2] = first(s for s in len5 if s != sigs[3] && length(s ∩ sigs[4]) == 2)
    sigs[5] = first(s for s in len5 if s != sigs[3] && length(s ∩ sigs[4]) == 3)
    # Groups of 6 (0, 6, 9):
    len6 = [s for s in signals if length(s) == 6]
    # Because ab is 1, then a 6group missing a or b must be 6.
    sigs[6] = first(s for s in len6 if length(s ∩ sigs[1]) == 1)
    # Deciding between 0 and 9: 0 has 3 common elements with 4, 9 has 4 common elements with 4.
    sigs[0] = first(s for s in len6 if s != sigs[6] && length(s ∩ sigs[4]) == 3)
    sigs[9] = first(s for s in len6 if s != sigs[6] && length(s ∩ sigs[4]) == 4)

    return invert(sigs)
end

function decode(sig2num, sig)
    total = 0
    for (i, s) in enumerate(sig)
        ex = length(sig)-i
        total += sig2num[s] * 10^ex
    end
    return total
end

function day08b(signals, quartets)
    total = 0
    for (sig, q) in zip(signals, quartets)
        sig2num = find_encoding(sig)
        value = decode(sig2num, q)
        total += value
    end
    return total
end

function day08(io)
    signals, quartets = parse_7segments(io)
    p1 = day08a(quartets)
    p2 = day08b(signals, quartets)
    return (p1, p2)
end


export snr, smooth, smooth!, nextpow2, abs_max!, standardize!, mad, savitsky_golay,
     movingaverage, pws, std_threshold

"""
    snr(A,fs)

Signal to noise ratio of cross-correlations in matrix `A` with `fs'.

Follows method of Clarke et. al, 2011. Measures SNR at each point.
"""
function snr(A::AbstractArray,fs::Real)
    t,N = size(A)
    A_mean = mean(A,dims=2)

    # calculate noise and envelope functions
    sigma = mean(A.^2,dims=2) .- A_mean.^2
    sigma = sqrt.(sigma./ (N-1))
    return sigma
end
snr(C::CorrData) = snr(C.corr,C.fs)

"""
    smooth(A, half_win)

Smooth array `A` with half-window `half_win` (defaults to 3).
"""
function smooth!(A::AbstractArray, half_win::Int=3)
    if ndims(A) == 1
        return movingaverage(A,half_win)
    end

    Nrows, Ncols = size(A)

    for ii = 1:Ncols
        A[:,ii] .= movingaverage(A[:,ii],half_win)
    end
    return nothing
end
smooth(A::AbstractArray,half_win::Int=3) =
      (U = deepcopy(A);smooth!(U,half_win);return U)

"""
  movingaverage(A,half_win)

Smooth array `A` with moving average of half-window `half-win` (defaults to 3.)
"""
function movingaverage(A::AbstractArray, half_win::Int=3)
    prepend!(A,A[1:half_win])
    append!(A,A[end-half_win:end])
    N = length(A)
    B = zeros(eltype(A),N)
    window_len = 2 * half_win + 1
    s = sum(A[1:window_len])
    B[half_win+1] = s
    for ii = half_win+2:N-half_win
        s = s - A[ii-half_win] + A[ii+half_win]
        B[ii] = s
    end
    B ./= window_len
    return B[half_win+1:end-half_win-1]
end

"""
    nextpow2(x)

Returns the next power of 2 of real, positive number `x`.
"""
function nextpow2(x::Real)
    ceil(Int,log2(abs(x)))
end

"""
    abs_max!(A)

Returns array `A` divided by its absolute maximum value.
"""
function abs_max!(A::AbstractArray)
    A .= A ./ maximum(abs.(A),dims=1)
    return nothing
end
abs_max!(C::CorrData) = abs_max!(C.corr)

"""
    standardize!(A)

Demean and standardize array `A` to unit std.
"""
function standardize!(A::AbstractArray)
    A .= (A .- mean(A,dims=1)) ./ std(A,dims=1)
    return nothing
end
standardize!(C::CorrData) = standardize!(C.corr)

"""
    mad(A)

Median Absolute Deviation of array `A`.
MAD = median(|Xi- median(A)|)
"""
function mad(A::AbstractArray)
    return median(abs.(A .- median(A,dims=1)),dims=1)
end
mad(C::CorrData) = mad(C.corr)

"""
    savitsky_golay(x, window, polyOrder, [deriv])

Polynomial smoothing of vector `x` with Savitsky Golay filter.
Polynomial order `polyOrder' must be less than window length `N`.
Modified from https://github.com/BBN-Q/Qlab.jl/blob/master/src/SavitskyGolay.jl
"""
function savitsky_golay(x::Vector, N::Int, polyOrder::Int; deriv::Int=0)
    #Some error checking
    @assert isodd(N) "Window size must be an odd integer."
    @assert polyOrder < N "Polynomial order must me less than window size."

    halfWindow = Int((N-1)/2)

    #Setup the S matrix of basis vectors.
    S = zeros(N, polyOrder+1)
    for ct = 0:polyOrder
        S[:,ct+1] = Array(-halfWindow:halfWindow).^ct
    end

    #Compute the filter coefficients for all orders
    G = S*pinv(S'*S)

    #Slice out the derivative order we want
    filterCoeffs = G[:,deriv+1] * factorial(deriv);

    #Pad the signal with the endpoints and convolve with filter
    paddedX = vcat(x[1]*ones(halfWindow), x, x[end]*ones(halfWindow))
    y = conv(filterCoeffs[end:-1:1], paddedX)

    #Return the valid midsection
    return y[2*halfWindow+1:end-2*halfWindow]
end

"""

  std_threshold(A,thresh)

Returns indices of cols of `A` where max(abs.A[:,col]))/std(A[:,col]) < `max_std`.
"""
function std_threshold(A::AbstractArray, max_std::Float64)
    stds = std(A,dims=1)[1,:]
    maxs = maximum(abs.(A),dims=1)[1,:]
    threshs = maxs ./ stds
    ind = []
    for ii = 1:length(threshs)
        if threshs[ii] < max_std
            append!(ind,ii)
        end
    end
    return ind
end


using DoubleDouble
using Base.Test

x = sqrt(2.0)
bx = big(x)
sx = Single(x)
dx = Double(x)

y = 0.1
by = big(y)
sy = Single(y)
dy = Double(y)

@test x == sx == dx
@test y == sy == dy

dxy = dx*dy
bxy = bx*by
@test sx*sy == dxy
@test x*y == Float64(dxy)
@test dxy == Double(bxy)

@test x+y == Float64(dx+dy)
@test dx+dy == Double(bx+by)

@test x-y == Float64(dx-dy)
@test dx-dy == Double(bx-by)

@test x/y == Float64(dx/dy)
@test dx/dy == Double(bx/by)

@test sqrt(y) == Float64(sqrt(dy))
@test sqrt(dy) == Double(sqrt(by))

#@test rem(dxy,1.0) == Double(rem(bxy,1.0))

## New
@test Double(pi) == Double{Float64}(3.141592653589793, 1.2246467991473532e-16)
@test Double(3.5) == Double{Float64}(3.5, 0.0)
@test Double(3.5) == Double{Float64}(3.5, 0.0)

a = Double(big"3.1")
@test a == Double(3.1, -8.881784197001253e-17)
@test BigFloat(a) == big"3.099999999999999999999999999999995069619342368676216176696466982586064542459781"

@test Double(3) == Double(3.0, 0.0)
@test Double(big(3)) == Double(3.0, 0.0)


## Conversion to and from Matrix objects ##

function Graph(m::Matrix{T}; kwargs...) where {T<:Number}
    n, n_ = size(m)
    @assert(n == n_, "adjacency matrix should be square")
    g = Graph(n; kwargs...)
    for i = 1:n, j = 1:n
        if m[i, j] > zero(T)
            addedge!(g, i, j)
        end
    end
    return g
end

function Matrix(g::Graph)
    n = nodecount(g)
    m = zeros(n, n)
    for i = nodes(g), j = outneighbors(g, i)
        m[i, j] += 1
    end
    return m
end


using RvSpectraKitLearn

# Likely need to update this to point to directory containing the spectra you wish to analyze
include(joinpath(Pkg.dir("RvSpectraKitLearn"),"path_to_spectra.jl"))
cd(path_to_spectra)

(datafiles, phases) = get_filenames();
(lambda, obs) = read_filelist(datafiles);

function find_important_lamba{T}(lambda::Array{T,1}, obs::Array{T,2}, phases::Array{T,1};
                                 chunk_size::Integer=1500, frac_to_keep = [0.2,0.5] )
  Y = reshape(-10.0*cos.(phases*2pi),(length(phases),1))
 beta = ones(length(lambda)) # coefficients
 new_beta = copy(beta)
 for i in 1:length(frac_to_keep)
  idx_active = find(beta)
  println("i=",i," len=",length(idx_active))
  num_chunks = ceil(Int64,length(idx_active)/chunk_size)
  for c in 1:num_chunks
    idx_min = 1+(c-1)*chunk_size
    idx_max = min(c*chunk_size,length(idx_active))
    lambda_range = idx_active[idx_min:idx_max]
    #println("idx=",idx_min,"-",idx_max,"  active=",idx_active[idx_min],"-",idx_active[idx_max]," lambda=",lambda[idx_active[idx_min]],"-",lambda[idx_active[idx_max]])
    X = obs[lambda_range,:]'
    (gamma_list, Z_list)  = fit_arp_slr_2d(X,Y,stepsize=1.01,gamma_init=0.001, max_itterations=1000, min_active=floor(Int64,size(X,2)*size(Y,2)*frac_to_keep[i]) )
    new_beta[lambda_range] = vec(Z_list[size(Z_list,1),:,1])
    #println("size(Z_list)=", size(Z_list,1), " nactive=",length(find(new_beta[lambda_range])))
  end
  beta[:] = new_beta
 end
 return (lambda_range, beta)
end

(lambda_range, beta) = find_important_lamba(lambda,obs,phases,chunk_size=1500,frac_to_keep=[0.25,0.4])
#Pkg.add("JLD")  # If you don't already have JLD installed
using JLD        # Since this takes a while to compute, we'll save the results to disk
save("important_lambda_0.25_0.4.jld", "idx", lambda_range, "lambda", lambda[lambda_range], "beta", beta)

#=
# Plot which wavelengths are being used from the last itteration of the algorithm regularization path algorithm
=#
using PyPlot

zmax = maximum(abs(beta))
  ymax = maximum(vec(obs[:,1]))
  ymin = minimum(vec(obs[:,1]))
  yrange = ymax-ymin
  #plot(lambda,vec(obs[:,1]),"r.")
  min_lambda = lambda[lambda_range][1]
  max_lambda = lambda[lambda_range][end]
  idx_around = find(x->min_lambda<=x<=max_lambda,lambda)
  plot(lambda[idx_around],vec(obs[idx_around,1]),"r-")
  plot(lambda[lambda_range],ymin+yrange/2*(1+1/zmax*(beta[lambda_range])),"b+")
  plot(lambda[lambda_range],ymin+yrange/2*(ones(length(lambda_range))),"b-")

#=
=#

#=
# Check that the predictions are good
pred = X*reshape(Z,(p,q))
 plot(phases,Y,"r.")
 plot(phases,Y,"r+")
 plot(phases,pred,"b+")
=#

#=
# Random other diagnostic plots
plot(L0,chisq,"r-")
 plot(L0,chisq,"r.")
 plot(L0,chisq_refit,"b-")
 plot(L0,chisq_refit,"b.")
 #plot(L0,chisq_cv,"g-")
 #plot(L0,chisq_cv,"g.")

plt[:loglog](L0,gamma_list,"g.")
 plot(L0,L1./L0,"g-")
 plot(L0,L1./L0,"g.")

plot(L1,chisq,"r-")
 plot(L1,chisq,"r.")
 plot(L1,chisq_refit,"b-")
 plot(L1,chisq_refit,"b.")

plot(L0,chisq,"r-")
 plot(L0,chisq,"r.")

plt[:loglog](gamma_list,L0,"r-")
 plt[:loglog](gamma_list,L1/(p*q),"b-")
 plt[:loglog](gamma_list,sqrt(L2),"g-")

 plt[:loglog](gamma_list,sqrt(L2/(p*q)),"g+")
 plt[:loglog](gamma_list,sqrt(chisq/(n*q)),"r+")
 plt[:loglog](gamma_list,rmsdelta,"g+")

=#


#Function to check Wigner-6j conditions
#23/07/2017

function Wigner6jcheck(j1,j2,j3,J1,J2,J3)

  Triad = [j1 j2 j3;j1 J2 J3;J1 j2 J3;J1 J2 j3]

  tfvec = zeros(8)

  #Check integer condition
  for kk = 1:4
    tfvec[kk] = floor(sum(Triad[kk,:]))==sum(Triad[kk,:])
  end

  #Check triangle inequalities
  for kk = 1:4
    if abs(Triad[kk,1]-Triad[kk,2])<=Triad[kk,3]&&Triad[kk,3]<=(Triad[kk,1]+Triad[kk,2])
      tfvec[kk+4] = 1
    end
  end

  tfsum = sum(tfvec)

  if tfsum==8
    return check = 1
  else
    return check = 0
  end
end


##############
#  Polygons  #
##############

struct ConvexPolygon{T} <: Surface
    vertices::Vector{Point{T}}
end

ConvexPolygon(v::Vector{Tuple{T, T}}) where T = ConvexPolygon{T}([SVector(i1, i2) for (i1, i2) in v])

nb_vertices(p::ConvexPolygon) = length(p.vertices)
nb_sides(p::ConvexPolygon) = length(p.vertices)

==(p1::ConvexPolygon, p2::ConvexPolygon) = nb_sides(p1) == nb_sides(p2) && all(p1.vertices .== p2.vertices)

vertices(p::ConvexPolygon) = p.vertices

isempty(p::ConvexPolygon) = length(p.vertices) <= 2

translate(c::ConvexPolygon{T}, v) where T = ConvexPolygon{T}(map(x -> translate(x, v), c.vertices))
rotate(c::ConvexPolygon{T}, ϕ; kw...) where T = ConvexPolygon{T}(map(x -> rotate(x, ϕ; kw...), c.vertices))
scale(c::ConvexPolygon{T}, λ; center=Point(0.0, 0.0)) where T = ConvexPolygon{T}(map(x -> scale(x, λ; center), c.vertices))

function area(c::ConvexPolygon{T}) where T
	if isempty(c)
        return zero(T)*zero(T)
	else
        # Shoelace formula
        a = zero(T)*zero(T)
        last_vertex = c.vertices[end]
        for vertex in c.vertices
            a += last_vertex[1] * vertex[2] - last_vertex[2] * vertex[1]
            last_vertex = vertex
        end
        return abs(a)/2
	end
end

# REPRESENTATION AS INTERSECTION OF HALF-PLANES

function convert(::Type{Intersection{HalfPlane{T}}}, p::ConvexPolygon{U}) where {T, U}
    if isempty(p); return Intersection{HalfPlane{T}}(HalfPlane{T}[]); end
    half_planes = Intersection{HalfPlane{T}}(Vector{HalfPlane{T}}(undef, nb_sides(p)))
    last_i = nb_vertices(p)
    for i in 1:nb_vertices(p)
      half_planes.content[i] = HalfPlane{T}(convert(Point{T}, p.vertices[last_i]), convert(Point{T}, p.vertices[i]), :right)
        last_i = i
    end
    return half_planes
end
convert(::Type{Reunion{Intersection{HalfPlane{T}}}}, c::ConvexPolygon{U}) where {T, U} = convert(Reunion{Intersection{HalfPlane{T}}}, convert(Intersection{HalfPlane{T}}, c))

convert(::Type{ConvexPolygon{T}}, p::ConvexPolygon{U}) where {T, U} = ConvexPolygon{T}(convert.(Point{T}, p.vertices))
promote_rule(::Type{ConvexPolygon{T}}, ::Type{ConvexPolygon{U}}) where {T, U} = ConvexPolygon{promote_type(T, U)}

in(p, c::ConvexPolygon{T}) where T = in(p, convert(Intersection{HalfPlane{T}}, c))
complement(c::ConvexPolygon{T}) where T = complement(convert(Intersection{HalfPlane{T}}, c))

# INTERSECTIONS BETWEEN CONVEX POLYGONS

# Basically Sutherland-Hodgeman algorithm
function intersect(c::ConvexPolygon{T}, h::HalfPlane{U}) where {T, U}
    if isempty(c); return c; end
    S = promote_type(T, U)
    poly_in = ConvexPolygon{S}(Point{S}[])
    previous_corner = c.vertices[end]
    for corner in c.vertices
        if corner in h
            if previous_corner in h
                # Stay in
                push!(poly_in.vertices, corner)
            else
                # Entering
                push!(poly_in.vertices, corner_point(HalfPlane{T}(previous_corner, corner, :right), h))
                push!(poly_in.vertices, corner)
            end
        elseif previous_corner in h
                # Exiting
                push!(poly_in.vertices, corner_point(HalfPlane{T}(previous_corner, corner, :right), h))
        end
        previous_corner = corner
    end
    return poly_in
end
intersect(h::HalfPlane, c::ConvexPolygon) = intersect(c, h)

function intersect(c::ConvexPolygon, ih::Intersection{HalfPlane{T}}) where T
	for h in ih.content
		c = c ∩ h
		if isempty(c)
			break
		end
	end
    return c
end
intersect(ih::Intersection{HalfPlane{T}}, c::ConvexPolygon) where T = intersect(c, ih)

intersect(c1::ConvexPolygon{U}, c2::ConvexPolygon{T})  where {U, T} = intersect(c1, convert(Intersection{HalfPlane{promote_type(U, T)}}, c2))

# REUNION OF CONVEX POLYGONS

convert(::Type{Reunion{ConvexPolygon{T}}}, c::ConvexPolygon{U}) where {T, U} = Reunion{ConvexPolygon{promote_type(T, U)}}([c])
function convert(::Type{Reunion{Intersection{HalfPlane{T}}}}, u::Reunion{ConvexPolygon{U}}) where {T, U}
    S = promote_type(T, U)
    Reunion{Intersection{HalfPlane{S}}}(map(c -> convert(Intersection{HalfPlane{S}}, c), u.content))
end

promote_rule(::Type{ConvexPolygon{T}}, ::Type{Reunion{ConvexPolygon{U}}}) where {T, U} = Reunion{ConvexPolygon{promote_type(T, U)}}

complement(c::Reunion{ConvexPolygon{T}}) where T = complement(convert(Reunion{Intersection{HalfPlane{T}}}, c))

# _cut(c::ConvexPolygon{T}, h::HalfPlane{T}) where T = (c ∩ h, c ∩ complement(h))
# function _cut(c::ConvexPolygon{T}, ih::Intersection{HalfPlane{T}}) where T
# 	for h in ih.content
#         c, rest = _cut(c, h)
# 		if isempty(c)
# 			break
# 		end
# 	end
#     return c
#     (c ∩ h, c ∩ complement(h))
# end

function intersect(c::ConvexPolygon{T}, uh::Reunion{HalfPlane{U}}) where {T, U}
    S = promote_type(T, U)
    u_poly = Reunion{ConvexPolygon{S}}(ConvexPolygon{S}[])
    rest = c
	for h in uh.content
        ci = rest ∩ h
        rest = rest ∩ complement(h)
        if !isempty(ci)
            push!(u_poly.content, ci)
        end
	end
    return u_poly
end
intersect(uh::Reunion{HalfPlane{T}}, c::ConvexPolygon{U}) where {T, U} = intersect(c, uh)

function intersect(c::ConvexPolygon{T}, uih::Reunion{Intersection{HalfPlane{U}}}) where {T, U}
    S = promote_type(T, U)
    u_poly = Reunion{ConvexPolygon{S}}(ConvexPolygon{S}[])
    rest = c
	for ih in uih.content
        ci = rest ∩ ih
        rest = rest ∩ complement(ih)
        if !isempty(ci)
            u_poly = u_poly ∪ ci
        end
	end
    return u_poly
end
intersect(uih::Reunion{Intersection{HalfPlane{T}}}, c::ConvexPolygon{U}) where {T, U} = intersect(c, uih)

function intersect(uc::Reunion{ConvexPolygon{T}}, s::Surface) where T
    u_poly = Reunion{ConvexPolygon{T}}(ConvexPolygon{T}[])
    for ci in uc.content
        cii = ci ∩ s
		if !isempty(cii)
            u_poly = u_poly ∪ cii
		end
    end
    return u_poly
end
intersect(s::Surface, uc::Reunion{ConvexPolygon{T}}) where T = intersect(uc, s)
intersect(uc1::Reunion{ConvexPolygon{T}}, uc2::Reunion{ConvexPolygon{U}}) where {T, U} = intersect(uc1, convert(Reunion{Intersection{HalfPlane{T}}}, uc2))

union(c1::ConvexPolygon{T}, c2::ConvexPolygon{U}) where {T, U} = Reunion{ConvexPolygon{promote_type(T, U)}}([c1, c2])
union(u1::Reunion{ConvexPolygon{T}}, u2::Reunion{ConvexPolygon{U}}) where {T, U} = Reunion{ConvexPolygon{promote_type(T, U)}}(vcat(u1.content, u2.content))

# AREA OF REUNION OF CONVEX POLYGON
disjoint(c::ConvexPolygon) = c
function disjoint(u::Reunion{ConvexPolygon{T}}) where T
    isempty(u) && return u
    l = Reunion{ConvexPolygon{T}}(ConvexPolygon{T}[u.content[1]])
    for c in u.content[2:end]
        rest = c \ l
        if !isempty(rest)
            l = l ∪ rest
        end
    end
    return l
end
area(u::Reunion{ConvexPolygon{T}}) where T = isempty(u) ? zero(T)*zero(T) : sum(area(c) for c in u.content)


struct SecondaryLimbDark{LD1<:AbstractLimbDark,LD2<:AbstractLimbDark,T} <: AbstractLimbDark
    primary_driver::LD1
    secondary_driver::LD2
    brightness_ratio::T
end

@doc raw"""
    SecondaryLimbDark(primary::AbstractLimbDark,
                      secondary::AbstractLimbDark;
                      brightness_ratio=1)
    SecondaryLimbDark(u_p::AbstractVector, u_s=u_p; kwargs...)

Compose two limb darkening laws together to add a secondary eclipse. If vectors of coefficients are provided, laws will automatically be constructed using [`PolynomialLimbDark`](@ref). The surface brightness ratio is given in terms of the host; e.g., if the companion is half as bright as the host, the ratio would be 0.5.

!!! note "Interface"
    [`SecondaryLimbDark`](@ref) only works with an orbit, since the companion's reference frame needs to be calculated. This means you can't call it using an impact parameter like `ld(b, r)` directly.

# Mathematical form
```math
f(t, r) = \frac{2f_p(t, r) + \eta r^2 f_s(t', r')}{1 + f_p(t, r) + \eta r^2 f_s(t', r')}
```
where $f_p$ is to the primary flux, $f_s$ is to the secondary flux, and $\eta$ is the surface brightness ratio. $t'$ and $r'$ correspond to the time and radius ratio from the companion's reference frame.

# Examples
```jldoctest second
# equal size and limb darkening
r = 1.0
u = [0.4, 0.26]
# companion is 1/10 as bright
brightness_ratio = 0.1
ld = SecondaryLimbDark(u; brightness_ratio)
orbit = SimpleOrbit(period=2, duration=0.5)
fp = ld(orbit, 0, r) # primary egress
fs = ld(orbit, 1, r) # secondary egress

fp ≈ brightness_ratio * fs

# output
true
```
"""
function SecondaryLimbDark(driver1::AbstractLimbDark, driver2::AbstractLimbDark; brightness_ratio=1)
    return SecondaryLimbDark(driver1, driver2, brightness_ratio)
end

function SecondaryLimbDark(u1::AbstractVector, u2=u1; kwargs...)
    driver1 = PolynomialLimbDark(u1)
    driver2 = PolynomialLimbDark(u2)
    return SecondaryLimbDark(driver1, driver2; kwargs...)
end

function compute(ld::SecondaryLimbDark, orbit::AbstractOrbit, t, r; kwargs...)
    f1 = ld.primary_driver(orbit, t, r; kwargs...)
    orbit2 = Orbits.flip(orbit, r * orbit.r_star)
    f2 = ld.secondary_driver(orbit2, t, 1/r; kwargs...)
    flux_ratio = ld.brightness_ratio * r^2
    return (f1 + flux_ratio * f2) / (1 + flux_ratio)
end


#region BNRPosteriors
struct BNRPosteriors
    Gammas::Array{Array{Float64,1},1}
    Xis::Array{Array{Int64,1},1}
    us::Array{Array{Float64,2},1}
end

#Gammas(posterior::BNRPosteriors) = posterior.Gammas
#Xis(posterior::BNRPosteriors) = posterior.Xis
#us(posterior::BNRPosteriors) = posterior.us
#endregion

#region custom sampling
"""
    sample_u(ξ, R, M)

Sample rows of the u matrix, either from MVN with mean 0 and covariance matrix M or a row of 0s

# Arguments
- `ξ` : ξ value sampled from Binomial distribution. Set to 0 to return a row of 0s, 1 to sample from MVN
- `R` : dimension of u vectors, length of return vector
- `M` : R×R covariance matrix for MVN samples
"""
function sample_u(ξ::Int64, R::Int64, M::Array{Float64,2})
    if (ξ == 1)
        return rand(MultivariateNormal(zeros(R), M))
    else
        return zeros(R)
    end
end

"""
    sample_rgig(a,b)

Sample from the GeneralizedInverseGaussian distribution with p=1/2, b=b, a=a

# Arguments
- `a` : shape and scale parameter a, sometimes also called ψ
- `b` : shape and scale parameter b, sometimes also called χ

# Returns
one sample from the GIG distribution with p=1/2, b=b, a=a
"""
function sample_rgig(a::Float64,b::Float64)::Float64
    return rand(GeneralizedInverseGaussian(a,b,1/2))
end

"""
    sample_Beta(a,b)

Sample from the Beta distribution, with handling for a=0 and/or b=0

#Arguments
- `a` : shape parameter a ≥ 0
- `b` : shape parameter b ≥ 0
"""
function sample_Beta(a::Float64,b::Float64)
    Δ = 0.0
    if a > 0.0 && b > 0.0
        Δ = rand(Beta(a, b))
    elseif a > 0.0
        Δ = 1.0
    elseif b > 0.0
        Δ = 0.0
    else
        Δ = sample([0.0,1.0])
    end
    return Δ
end
#endregion

#region helper functions

"""
    sample_π_dirichlet(r,η,λ)

Sample from the 3-variable doirichlet distribution with weights
[r^η,1,1] + [#{λ[r] == 0}, #{λ[r] == 1}, #{λ[r] = -1}]

# Arguments
- `r` : integer, base term for the first weight and index for λ vector
- `η` : real number, power term for the first weight
- `λ` : 1d array of -1,0,1s, used to determine which weight is added to

# Returns
A vector of length 3 drawn from the Dirichlet distribution
"""
function sample_π_dirichlet(r::Int64,η::Float64,λ::Array{Int64,1})
    wts = [r^η,1,1]
    if λ[r] == 1
        wts[2] = 2
    elseif λ[r] == 0
        wts[1] = r^η + 1
    else
        wts[3] = 2
    end
    rand(Dirichlet(wts))
end
#endregion

"""
    init_vars!(X, η, ζ, ι, R, aΔ, bΔ, ν, V, x_transform)

    Initialize all variables using prior distributions. Note, if x_transform is true V will be ignored and overwritten with the implied value from X.

    # Arguments
    - `X` : vector of unweighted symmetric adjacency matrices to be used as predictors. each element of the array should be 1 matrix
    - `η` : hyperparameter used to sample from the Dirichlet distribution (r^η)
    - `ζ` : hyperparameter used as the shape parameter in the gamma distribution used to sample θ
    - `ι` : hyperparameter used as the scale parameter in the gamma distribution used to sample θ
    - `R` : the dimensionality of the latent variables u, a hyperparameter
    - `aΔ`: hyperparameter used as the a parameter in the beta distribution used to sample Δ.
    - `bΔ`: hyperparameter used as the b parameter in the beta distribution used to sample Δ. aΔ and bΔ values causing the Beta distribution to have mass concentrated closer to 0 will cause more zeros in ξ
    - `ν` : hyperparameter used as the degrees of freedom parameter in the InverseWishart distribution used to sample M.
    - `V`: Value of V, the number of nodes in the original X matrix. Only input when x_transform is false. Always output.
    - `x_transform`: boolean, set to false if X has been pre-transformed into one row per sample. True by default.

    # Returns
    - `X` : matrix of re-ordered predictors. one row per sample, V*(V-1)/2 columns
    - `θ` : set to 1 draw from Gamma(ζ,ι)
    - `D` : set to a diagonal matrix of V(V-1)/2 draws from the Exponential(θ/2) distribution
    - `πᵥ`: set to a R × 3 matrix of draws from the Dirichlet distribution, where the second and third columns are draws from Dirichlet(1) and the first are draws from Dirichlet(r^η)
    - `Λ` : R × R diagonal matrix of λ values, which are sampled from [0,1,-1] with probabilities assigned from the rth row of πᵥ
    - `Δ` : set to 1 draw from Beta(aΔ, bΔ) (or 1 if aΔ or bΔ are 0).
    - `ξ` : set to V draws from Bernoulli(Δ)
    - `M` : set to R × R matrix drawn from InverseWishart(ν, I_R) (where I_R is the identity matrix with dimension R × R)
    - `u` : the latent variables u, set to a V × R matrix where each row is sampled from MultivariateNormal(0,M) if ξ[r] is 1 or set to a row of 1s otherwise.
    - `μ` : set to 1 (non-informative prior)
    - `τ²`: set to the square of 1 sample from Uniform(0,1) (non-informative prior)
    - `γ` : set to 1 draw from MultivariateNormal(uᵀΛu_upper, τ²*s), where uᵀΛu_upper is a vector of the terms in the upper triangle of uᵀΛu (does not include the diagonal)
    - `V` : dimension of original symmetric adjacency matrices
"""
function init_vars!(X::Array{Float64,2}, η::Float64, ζ::Float64, ι::Float64, R::Int64, aΔ::Float64, bΔ::Float64, ν::Int64, V::Int64=0, x_transform::Bool=true)
    # η must be greater than 1, if it's not set it to its default value of 1.01
    if (η <= 1)
        η = 1.01
        println("η value invalid, reset to default of 1.01")
    end

    if x_transform
        V = Int64(size(X[1],1))
    end
    q = floor(Int,V*(V-1)/2)

    X_new = Matrix{Float64}(undef, size(X,1), q)
    if x_transform
        for i in 1:size(X,1)
            X_new[i,:] = upper_triangle(X[i])
        end
    else
        X_new = X
    end

    θ = rand(Gamma(ζ, 1/ι))

    S = map(k -> rand(Exponential(θ/2)), 1:q)

    D = Diagonal(S)
    πᵥ = zeros(R,3)
    for r in 1:R
        πᵥ[r,:] = rand(Dirichlet([r^η,1,1]))
    end
    λ = map(r -> sample([0,1,-1], StatsBase.weights(πᵥ[r,:]),1)[1], 1:R)
    Λ = Diagonal(λ)
    Δ = sample_Beta(aΔ, bΔ)

    ξ = map(k -> rand(Binomial(1,Δ)), 1:V)
    M = rand(InverseWishart(ν,cholesky(Matrix(I,R,R))))
    u = zeros(R,V)
    for i in 1:V
        u[:,i] = sample_u(ξ[i],R,M)
    end
    μ = 1.0
    τ²= rand(Uniform(0,1))^2
    uᵀΛu = transpose(u) * Λ * u
    uᵀΛu_upper = upper_triangle(uᵀΛu)

    #TODO: this line is a bottleneck (because of τ²D)
    γ = rand(MultivariateNormal(uᵀΛu_upper, τ²*D))
    return (X_new, [θ], [D], [πᵥ], [Λ], [Δ], [ξ], [M], [u], [μ], [τ²], [γ])
end

#region update variables
"""
    update_μ(y, X, γ, τ², n)

Sample the next μ value from the normal distribution with mean 1ᵀ(y - Xγ)/n and variance τ²/n

# Arguments
- `X` : 2 dimensional array of predictor values, 1 row per sample (upper triangle of original X)
- `y` : response values
- `γ` : vector of regression parameters
- `τ²`: overall variance parameter
- `n` : number of samples (length of y)

# Returns
new value of μ
"""
function update_μ(X::Array{Float64,2}, y::Array{Float64,1}, γ::Array{Float64,1}, τ²::Float64, n::Int64)
    μₘ = (ones(1,n) * (y .- X*γ)) / n
    σₘ = sqrt(τ²/n)
    μ = rand(Normal(μₘ[1],σₘ))
end

"""
    update_γ(X, y, D, Λ, u, μ, τ²)

Sample the next γ value from the normal distribution, decomposed as described in Guha & Rodriguez 2018

# Arguments
- `X` : 2 dimensional array of predictor values, 1 row per sample (upper triangle of original X)
- `y` : response values
- `D` : diagonal matrix of s values
- `Λ` : R × R diagonal matrix of λ values
- `u` : the latent variables u
- `μ` : overall mean value for the relationship
- `τ²`: overall variance parameter
- `n` : number of samples

# Returns
new value of γ
"""
function update_γ(X::Array{Float64,2}, y::Array{Float64,1}, D::Diagonal{Float64,Array{Float64,1}}, Λ::Diagonal{Int64,Array{Int64,1}}, u::Array{Float64,2}, μ::Float64, τ²::Float64, n::Int64)
    uᵀΛu = transpose(u) * Λ * u
    W = upper_triangle(uᵀΛu)
    q = size(D,1)

    τ²D = τ²*D

    Δᵧ₁ = rand(MultivariateNormal(zeros(q), (τ²D)))
    Δᵧ₂ = rand(MultivariateNormal(zeros(n), I(n)))
    Δᵧ₃ = (X / sqrt(τ²)) * Δᵧ₁ + Δᵧ₂
    one = τ²D * (transpose(X)/sqrt(τ²)) * inv(X * D * transpose(X) + I(n))
    two = (((y - μ .* ones(n,1) - X * W) / sqrt(τ²)) - Δᵧ₃)
    γw = Δᵧ₁ + one * two
    γ = γw + W
    return γ[:,1]
end

"""
    update_τ²(X, y, μ, γ, Λ, u, D, V)

Sample the next τ² value from the InverseGaussian distribution with mean n/2 + V(V-1)/4 and variance ((y - μ1 - Xγ)ᵀ(y - μ1 - Xγ) + (γ - W)ᵀD⁻¹(γ - W)

# Arguments
- `X` : 2 dimensional array of predictor values, 1 row per sample (upper triangle of original X)
- `y` : vector of response values
- `μ` : overall mean value for the relationship
- `γ` : vector of regression parameters
- `Λ` : R × R diagonal matrix of λ values
- `u` : the latent variables u
- `D` : diagonal matrix of s values
- `V` : dimension of original symmetric adjacency matrices

# Returns
new value of τ²
"""
function update_τ²(X::Array{Float64,2}, y::Array{Float64,1}, μ::Float64, γ::Array{Float64,1}, Λ::Diagonal{Int64,Array{Int64,1}}, u::Array{Float64,2}, D::Diagonal{Float64,Array{Float64,1}}, V::Int64)
    uᵀΛu = transpose(u) * Λ * u
    W = upper_triangle(uᵀΛu)
    n  = size(y,1)

    #TODO: better variable names, not so much reassignment
    μₜ  = (n/2) + (V*(V-1)/4)
    yμ1Xγ = (y - μ.*ones(n,1) - X*γ)

    γW = (γ - W)
    yμ1Xγᵀyμ1Xγ = transpose(yμ1Xγ) * yμ1Xγ
    γWᵀγW = transpose(γW) * inv(D) * γW

    σₜ² = (yμ1Xγᵀyμ1Xγ[1] + γWᵀγW[1])/2
    τ² = rand(InverseGamma(μₜ, σₜ²))
end

"""
    update_D(γ, u, Λ, θ, τ², V)

Sample the next D value from the GeneralizedInverseGaussian distribution with p = 1/2, a=((γ - uᵀΛu)^2)/τ², b=θ

# Arguments
- `γ` : vector of regression parameters
- `u` : the latent variables u
- `Λ` : R × R diagonal matrix of λ values
- `θ` : b parameter for the GeneralizedInverseGaussian distribution
- `τ²`: overall variance parameter
- `V` : dimension of original symmetric adjacency matrices

# Returns
new value of D
"""
function update_D(γ::Array{Float64,1}, u::Array{Float64,2}, Λ::Diagonal{Int64,Array{Int64,1}}, θ::Float64, τ²::Float64, V::Int64)
    q = floor(Int,V*(V-1)/2)
    uᵀΛu = transpose(u) * Λ * u
    uᵀΛu_upper = upper_triangle(uᵀΛu)
    a_ = (γ - uᵀΛu_upper).^2 / τ²
    D = Diagonal(map(k -> sample_rgig(θ,a_[k]), 1:q))
end

"""
    update_θ(ζ, ι, V, D)

Sample the next θ value from the Gamma distribution with a = ζ + V(V-1)/2 and b = ι + ∑(s[k,l]/2)

# Arguments
- `ζ` : hyperparameter, used to construct `a` parameter
- `ι` : hyperparameter, used to construct `b` parameter
- `V` : dimension of original symmetric adjacency matrices
- `D` : diagonal matrix of s values

# Returns
new value of θ
"""
function update_θ(ζ::Float64, ι::Float64, V::Int64, D::Diagonal{Float64,Array{Float64,1}})
    a = ζ + (V*(V-1))/2
    b = ι + sum(diag(D))/2
    θ = rand(Gamma(a,1/b))
    return θ
end

"""
    update_u_ξ(u, γ, D, τ², Δ, M, Λ, V)

Sample the next u and ξ values

# Arguments
- `u` : the current latent variables u
- `γ` : vector of regression parameters
- `D` : diagonal matrix of s values
- `τ²`: overall variance parameter
- `Δ` : set to 1 draw from Beta(aΔ, bΔ)
- `M` : set to R × R matrix drawn from InverseWishart(V, I_R) (where I_R is the identity matrix with dimension R × R)
- `Λ` : R × R diagonal matrix of λ values
- `V` : dimension of original symmetric adjacency matrices

# Returns
A touple with the new values of u and ξ
"""
function update_u_ξ(u::Array{Float64,2}, γ::Array{Float64,1}, D::Diagonal{Float64,Array{Float64,1}}, τ²::Float64, Δ::Float64, M::Array{Float64,2}, Λ::Diagonal{Int64,Array{Int64,1}}, V::Int64)
    q = V*(V-1)
    w_top = zeros(V)
    u_new = zeros(size(u)...)
    ξ = zeros(Int64,V)
    mus = zeros(V,5)
    cov = zeros(V,5,5)
    for k in 1:V
        U = transpose(u[:,Not(k)])*Λ
        s = create_upper_tri(diag(D),V)
        Γ = create_upper_tri(γ, V)
        if k == 1
            γk=vcat(Γ[1,2:V])
            H = Diagonal(vcat(s[1,2:V]))
        elseif k == V
            γk=vcat(Γ[1:V-1,V])
            H = Diagonal(vcat(s[1:V-1,V]))
        else
            γk= vcat(Γ[1:k-1,k],Γ[k,k+1:V])
            H = Diagonal(vcat(s[1:k-1,k],s[k,k+1:V]))
        end
        Σ = inv(((transpose(U)*inv(H)*U)/τ²) + inv(M))
        m = Σ*(transpose(U)*inv(H)*γk)/τ²

        mvn_a = MultivariateNormal(zeros(size(H,1)),Symmetric(τ²*H))
        mvn_b_Σ = Symmetric(τ²*H + U*M*transpose(U))
        mvn_b = MultivariateNormal(zeros(size(H,1)),mvn_b_Σ)
        w_top = (1-Δ) * pdf(mvn_a,γk)
        w_bot = w_top + Δ*pdf(mvn_b,γk)
        w = w_top / w_bot

        mvn_f = MultivariateNormal(m,Symmetric(Σ))
        mus[k,:] = m
        cov[k,:,:] = Σ

        ξ[k] = update_ξ(w)

        # the paper says the first term is (1-w) but their code uses ξ. Again i think this makes more sense
        # that this term would essentially be an indicator rather than a weight
        u_new[:,k] = ξ[k].*rand(mvn_f)
    end
    return u_new,ξ
end

"""
    update_ξ(w)

Sample the next ξ value from the Bernoulli distribution with parameter 1-w

# Arguments
- `w` : parameter to use for sampling, probability that 0 is drawn

# Returns
the new value of ξ
"""
function update_ξ(w::Float64)
    if w == 0
        return 1
    elseif w == 1
        return 0
    end
    Int64(rand(Bernoulli(1 - w)))
end

"""
    update_Δ(aΔ, bΔ, ξ, V)

Sample the next Δ value from the Beta distribution with parameters a = aΔ + ∑ξ and b = bΔ + V - ∑ξ

# Arguments
- `aΔ`: hyperparameter used as part of the a parameter in the beta distribution used to sample Δ.
- `bΔ`: hyperparameter used as part of the b parameter in the beta distribution used to sample Δ.
- `ξ` : vector of ξ values, 0 or 1

# Returns
the new value of Δ
"""
function update_Δ(aΔ::Float64, bΔ::Float64, ξ::Array{Int64,1})
    a = aΔ + sum(ξ)
    b = bΔ + sum(1 .- ξ)
    return sample_Beta(a,b)
end

"""
    update_M(u,ν,V,ξ)

Sample the next M value from the InverseWishart distribution with df = V + # of nonzero columns in u and Ψ = I + ∑ uΛuᵀ

# Arguments
- `u` : R × V matrix of latent variables
- `ν` : hyperparameter, base df for IW distribution (to be added to by sum of ξs)
- `V` : dimension of original symmetric adjacency matrices
- `ξ` : vector of ξ values, 0 or 1 (sum is added to df for IW distribution)

# Returns
the new value of M
"""
function update_M(u::Array{Float64,2},ν::Int64,V::Int64,ξ::Array{Int64,1})
    R = size(u,1)
    uΛu = zeros(R,R)
    num_nonzero = 0
    for i in 1:V
        uΛu = uΛu + u[:,i]*transpose(u[:,i])
        if ξ[i] ≉ 0
            num_nonzero = num_nonzero + 1
        end
    end
    Ψ = I(R) + uΛu
    df = ν + num_nonzero
    M = rand(InverseWishart(df,Ψ))
    return M
end

"""
    update_Λ(πᵥ, R, Λ, u, D, τ², γ)

Sample the next values of λ from [1,0,-1] with probabilities determined from a normal mixture

# Arguments
- `Λ_new` : return array, populated with the new values of λ (a diagonal array of 1,0,-1s )
- `πᵥ`: 3 column vectors of dimension R used to weight normal mixture for probability values
- `R` : the dimensionality of the latent variables u
- `Λ` : R × R diagonal matrix of λ values
- `u` : R × V matrix of latent variables
- `D` : diagonal matrix of s values
- `τ²`: overall variance parameter
- `γ` : vector of regression parameters

# Returns
new value of Λ
"""
function update_Λ!(Λ_new::Diagonal{Int64,Array{Int64,1}}, πᵥ::Array{Float64,2}, R::Int64, Λ::Diagonal{Int64,Array{Int64,1}}, u::Array{Float64,2}, D::Diagonal{Float64,Array{Float64,1}}, τ²::Float64, γ::Array{Float64,1})
    λ_new = zeros(Int64,size(Λ,1))
    for r in 1:R
        Λ₋₁= deepcopy(Λ)
        Λ₋₁[r,r] = -1
        Λ₀ = deepcopy(Λ)
        Λ₀[r,r] = 0
        Λ₁ = deepcopy(Λ)
        Λ₁[r,r] = 1
        u_tr = transpose(u)
        W₋₁= upper_triangle(u_tr * Λ₋₁ * u)
        W₀ = upper_triangle(u_tr * Λ₀ * u)
        W₁ = upper_triangle(u_tr * Λ₁ * u)

        τ²D = τ²*D
        n₀ = prod(map(i -> pdf(Normal(W₀[i],sqrt(τ²D[i,i])),γ[i]),1:size(γ,1)))
        n₁ = prod(map(i -> pdf(Normal(W₁[i],sqrt(τ²D[i,i])),γ[i]),1:size(γ,1)))
        n₋₁ = prod(map(i -> pdf(Normal(W₋₁[i],sqrt(τ²D[i,i])),γ[i]),1:size(γ,1)))
        p_bot = πᵥ[r,1] * n₀ + πᵥ[r,2] * n₁ + πᵥ[r,3] * n₋₁
        p1 = πᵥ[r,1] * n₀ / p_bot
        p2 = πᵥ[r,2] * n₁ / p_bot
        p3 = πᵥ[r,3] * n₋₁ / p_bot
        λ_new[r] = sample([0,1,-1],StatsBase.weights([p1,p2,p3]))
    end
    Λ_new[:,:] = Diagonal(λ_new)
end

"""
    update_π(λ,η,R)

Sample the new values of πᵥ from the Dirichlet distribution with parameters [1 + #{r: λᵣ= 1}, #{r: λᵣ = 0} + r^η, 1 + #{r: λᵣ = -1 }]

# Arguments
- `Λ` : R × R diagonal matrix of λ values
- `η` : hyperparameter used for sampling the 0 value (r^η)
- `R` : dimension of u vectors

# Returns
new value of πᵥ
"""
function update_π(Λ::Diagonal{Int64,Array{Int64,1}},η::Float64,R::Int64)
    λ = diag(Λ)
    π_new = zeros(R,3)
    for r in 1:R
        π_new[r,:] = sample_π_dirichlet(r,η,λ)
    end
    return π_new
end
#endregion

"""
    GibbsSample!(result,X, y, θ, D, πᵥ, Λ, Δ, ξ, M, u, μ, γ, V, η, ζ, ι, R, aΔ, bΔ, ν)

Take one GibbsSample

# Arguments
- `result`: Array of outputs (see returns section for more detail)
- `X` : matrix of unweighted symmetric adjacency matrices to be used as predictors. each row should be the upper triangle of the adjacency matrix associated with one sample.
- `y` : vector of response variables
- `θ` : θ parameter, drawn from the Gamma Distribution
- `D` : Diagonal matrix of s values
- `πᵥ`: 3 column vectors of dimension R used to weight normal mixture for probability values
- `Λ` : R × R diagonal matrix of λ values
- `Δ` : Δ parameter, drawn from Beta distribution
- `M` : R × R matrix, drawn from InverseWishart
- `u` : R × V matrix of latent variables
- `γ` : (V*V-1)-vector of regression parameters, influence of each edge
- `V` : number of nodes in adjacency matrices
- `η` : hyperparameter used for sampling the 0 value of the πᵥ parameter
- `ζ` : hyperparameter used for sampling θ
- `ι` : hyperparameter used for sampling θ
- `R` : hyperparameter - depth of u vectors (and others)
- `aΔ`: hyperparameter used for sampling Δ
- `bΔ`: hyperparameter used for sampling Δ
- `ν` : hyperparameter used for sampling M

# Returns
An array of the new values, [τ²_n, u_n, ξ_n, γ_n, D_n, θ_n, Δ_n, M_n, μ_n, Λ_n, πᵥ_n]
"""
function GibbsSample!(result::Array{Any,1},X::Array{Float64,2}, y::Array{Float64,1}, θ::Float64, D::Diagonal{Float64,Array{Float64,1}}, πᵥ::Array{Float64,2}, Λ::Diagonal{Int64,Array{Int64,1}}, Δ::Float64, M::Array{Float64,2}, u::Array{Float64,2}, μ::Float64, γ::Array{Float64,1}, V::Int64, η::Float64, ζ::Float64, ι::Float64, R::Int64, aΔ::Float64, bΔ::Float64, ν::Int64)
    n = size(X,1)
    τ²_n = update_τ²(X, y, μ, γ, Λ, u, D, V)
    u_n, ξ_n = update_u_ξ(u, γ, D, τ²_n, Δ, M, Λ, V)
    γ_n = update_γ(X, y, D, Λ, u_n, μ, τ²_n, n)
    D_n = update_D(γ_n, u_n, Λ, θ, τ²_n, V)
    θ_n = update_θ(ζ, ι, V, D_n)
    Δ_n = update_Δ(aΔ, bΔ, ξ_n)
    M_n = update_M(u_n, ν, V,ξ_n)
    μ_n = update_μ(X, y, γ_n, τ²_n, n)
    #Λ_n = Array{Int64,2}(undef,(R,R))
    Λ_n = Diagonal(zeros(Int64,R))
    update_Λ!(Λ_n, πᵥ, R, Λ, u_n, D_n, τ²_n, γ_n)
    πᵥ_n = update_π(Λ_n, η,R)
    result[1:11] = [τ²_n, u_n, ξ_n, γ_n, D_n, θ_n, Δ_n, M_n, μ_n, Λ_n, πᵥ_n]
    nothing
end

function GenerateSamples!(X::Array{Float64,2}, y::Array{Float64,1}, R::Int64; η::Float64=1.01,ζ::Float64=1.0,ι::Float64=1.0,aΔ::Float64=1.0,bΔ::Float64=1.0, ν::Int64=12, nburn::Int64=30000, nsamples::Int64=20000, V::Int64=0, x_transform::Bool=true)
    if V == 0 && !x_transform
        ArgumentError("If x_transform is false a valid V value must be given")
    end

    X, θ, D, πᵥ, Λ, Δ, ξ, M, u, μ, τ², γ = init_vars!(X, η, ζ, ι, R, aΔ, bΔ, ν, V, x_transform)

    total = nburn + nsamples
    p = Progress(total,1)
    result = Array{Any,1}(undef,11)
    # burn-in
    for i in 1:nburn
        #τ²[1], u[1], ξ[1], γ[1], D[1], θ[1], Δ[1], M[1], μ[1], Λ[1], πᵥ[1] = GibbsSample(...
        GibbsSample!(result, X, y, last(θ), last(D), last(πᵥ), last(Λ), last(Δ), last(M), last(u), last(μ), last(γ), V, η, ζ, ι, R, aΔ, bΔ,ν)
        push!(τ²,result[1])
        push!(u,result[2])
        push!(ξ,result[3])
        push!(γ,result[4])
        push!(D,result[5])
        push!(θ,result[6])
        push!(Δ,result[7])
        push!(M,result[8])
        push!(μ,result[9])
        push!(Λ,result[10])
        push!(πᵥ,result[11])
        next!(p)
    end
    for i in 1:nsamples
        GibbsSample!(result, X, y, last(θ), last(D), last(πᵥ), last(Λ), last(Δ), last(M), last(u), last(μ), last(γ), V, η, ζ, ι, R, aΔ, bΔ,ν)
        push!(τ²,result[1])
        push!(u,result[2])
        push!(ξ,result[3])
        push!(γ,result[4])
        push!(D,result[5])
        push!(θ,result[6])
        push!(Δ,result[7])
        push!(M,result[8])
        push!(μ,result[9])
        push!(Λ,result[10])
        push!(πᵥ,result[11])
        next!(p)
    end
    return BNRPosteriors(γ[nburn+1:nburn+nsamples],ξ[nburn+1:nburn+nsamples],u[nburn+1:nburn+nsamples])
end