module MadNLPMumps

import StaticArrays: SVector, setindex
import MUMPS_seq_jll
import MadNLP:
    parsefile, dlopen,
    @kwdef, Logger, @debug, @warn, @error,
    SparseMatrixCSC, SubVector, StrideOneVector,
    SymbolicException,FactorizationException,SolveException,InertiaException,
    AbstractOptions, AbstractLinearSolver, set_options!,
    introduce, factorize!, solve!, improve!, is_inertia, inertia, findIJ, nnz

const INPUT_MATRIX_TYPE = :csc
const version = parsefile(joinpath(dirname(pathof(MUMPS_seq_jll)),"..","Project.toml"))["version"]

@kwdef mutable struct Options <: AbstractOptions
    mumps_dep_tol::Float64 = 0.
    mumps_mem_percent::Int = 1000
    mumps_permuting_scaling::Int = 7
    mumps_pivot_order::Int = 7
    mumps_pivtol::Float64 = 1e-6
    mumps_pivtolmax::Float64 = .1
    mumps_scaling::Int = 77
end

if version == "5.3.5+0"
    @kwdef mutable struct Struc
        sym::Cint = 0
        par::Cint = 0
        job::Cint = 0

        comm_fortran::Cint = 0

        icntl::SVector{60,Cint} = zeros(60)
        keep::SVector{500,Cint} = zeros(500)
        cntl::SVector{15,Cdouble} = zeros(15)
        dkeep::SVector{230,Cdouble} = zeros(230)
        keep8::SVector{150,Int64} = zeros(150)
        n::Cint = 0
        nblk::Cint = 0

        nz_alloc::Cint = 0

        nz::Cint = 0
        nnz::Int64 = 0
        irn::Ptr{Cint} = C_NULL
        jcn::Ptr{Cint} = C_NULL
        a::Ptr{Cdouble} = C_NULL

        nz_loc::Cint = 0
        nnz_loc::Int64 = 0
        irn_loc::Ptr{Cint} = C_NULL
        jcn_loc::Ptr{Cint} = C_NULL
        a_loc::Ptr{Cdouble} = C_NULL ###

        nelt::Cint = 0
        eltptr::Ptr{Cint} = C_NULL
        eltvar::Ptr{Cint} = C_NULL
        a_elt::Ptr{Cdouble} = C_NULL

        blkptr::Ptr{Cint} = C_NULL
        blkvar::Ptr{Cint} = C_NULL

        perm_in::Ptr{Cint} = C_NULL

        sym_perm::Ptr{Cint} = C_NULL
        uns_perm::Ptr{Cint} = C_NULL

        colsca::Ptr{Cdouble} = C_NULL
        rowsca::Ptr{Cdouble} = C_NULL
        colsca_from_mumps::Cint = 0
        rowsca_from_mumps::Cint = 0

        rhs::Ptr{Cdouble} = C_NULL
        redrhs::Ptr{Cdouble} = C_NULL
        rhs_sparse::Ptr{Cdouble} = C_NULL
        sol_loc::Ptr{Cdouble} = C_NULL
        rhs_loc::Ptr{Cdouble} = C_NULL

        irhs_sparse::Ptr{Cint} = C_NULL
        irhs_ptr::Ptr{Cint} = C_NULL
        isol_loc::Ptr{Cint} = C_NULL
        irhs_loc::Ptr{Cint} = C_NULL

        nrhs::Cint = 0
        lrhs::Cint = 0
        lredrhs::Cint = 0
        nz_rhs::Cint = 0
        lsol_loc::Cint = 0
        nloc_rhs::Cint = 0
        lrhs_loc::Cint = 0

        schur_mloc::Cint = 0
        schur_nloc::Cint = 0
        schur_lld::Cint = 0

        mblock::Cint = 0
        nblock::Cint = 0
        nprow::Cint = 0
        npcol::Cint = 0

        info::SVector{80,Cint} = zeros(80)
        infog::SVector{80,Cint} = zeros(80)
        rinfo::SVector{40,Cdouble} = zeros(40)
        rinfog::SVector{40,Cdouble} = zeros(40)

        deficiency::Cint = 0
        pivnul_list::Ptr{Cint} = C_NULL
        mapping::Ptr{Cint} = C_NULL

        size_schur::Cint = 0
        listvar_schur::Ptr{Cint} = C_NULL
        schur::Ptr{Cdouble} = C_NULL ##

        instance_number::Cint = 0
        wk_user::Ptr{Cdouble} = C_NULL

        version_number::SVector{32,Cchar} = zeros(32)

        ooc_tmpdir::SVector{256,Cchar} = zeros(256)
        ooc_prefix::SVector{64,Cchar} = zeros(64)

        write_problem::SVector{256,Cchar} = zeros(256)
        lwk_user::Cint = 0

        save_dir::SVector{256,Cchar} = zeros(256)
        save_prefix::SVector{256,Cchar} = zeros(256)

        metis_options::SVector{40,Cint} = zeros(40)
    end
elseif version == "5.2.1+4"
    @kwdef mutable struct Struc
        sym::Cint = 0
        par::Cint = 0
        job::Cint = 0

        comm_fortran::Cint = 0

        icntl::SVector{60,Cint} = zeros(60)
        keep::SVector{500,Cint} = zeros(500)
        cntl::SVector{15,Cdouble} = zeros(15)
        dkeep::SVector{230,Cdouble} = zeros(230)
        keep8::SVector{150,Int64} = zeros(150)
        n::Cint = 0

        nz_alloc::Cint = 0

        nz::Cint = 0
        nnz::Int64 = 0
        irn::Ptr{Cint} = C_NULL
        jcn::Ptr{Cint} = C_NULL
        a::Ptr{Cdouble} = C_NULL

        nz_loc::Cint = 0
        nnz_loc::Int64 = 0
        irn_loc::Ptr{Cint} = C_NULL
        jcn_loc::Ptr{Cint} = C_NULL
        a_loc::Ptr{Cdouble} = C_NULL ###

        nelt::Cint = 0
        eltptr::Ptr{Cint} = C_NULL
        eltvar::Ptr{Cint} = C_NULL
        a_elt::Ptr{Cdouble} = C_NULL

        perm_in::Ptr{Cint} = C_NULL

        sym_perm::Ptr{Cint} = C_NULL
        uns_perm::Ptr{Cint} = C_NULL

        colsca::Ptr{Cdouble} = C_NULL
        rowsca::Ptr{Cdouble} = C_NULL
        colsca_from_mumps::Cint = 0
        rowsca_from_mumps::Cint = 0

        rhs::Ptr{Cdouble} = C_NULL
        redrhs::Ptr{Cdouble} = C_NULL
        rhs_sparse::Ptr{Cdouble} = C_NULL
        sol_loc::Ptr{Cdouble} = C_NULL
        rhs_loc::Ptr{Cdouble} = C_NULL

        irhs_sparse::Ptr{Cint} = C_NULL
        irhs_ptr::Ptr{Cint} = C_NULL
        isol_loc::Ptr{Cint} = C_NULL
        irhs_loc::Ptr{Cint} = C_NULL

        nrhs::Cint = 0
        lrhs::Cint = 0
        lredrhs::Cint = 0
        nz_rhs::Cint = 0
        lsol_loc::Cint = 0
        nloc_rhs::Cint = 0
        lrhs_loc::Cint = 0

        schur_mloc::Cint = 0
        schur_nloc::Cint = 0
        schur_lld::Cint = 0

        mblock::Cint = 0
        nblock::Cint = 0
        nprow::Cint = 0
        npcol::Cint = 0

        info::SVector{80,Cint} = zeros(80)
        infog::SVector{80,Cint} = zeros(80)
        rinfo::SVector{40,Cdouble} = zeros(40)
        rinfog::SVector{40,Cdouble} = zeros(40)

        deficiency::Cint = 0
        pivnul_list::Ptr{Cint} = C_NULL
        mapping::Ptr{Cint} = C_NULL

        size_schur::Cint = 0
        listvar_schur::Ptr{Cint} = C_NULL
        schur::Ptr{Cdouble} = C_NULL ##

        instance_number::Cint = 0
        wk_user::Ptr{Cdouble} = C_NULL

        version_number::SVector{32,Cchar} = zeros(32)

        ooc_tmpdir::SVector{256,Cchar} = zeros(256)
        ooc_prefix::SVector{64,Cchar} = zeros(64)

        write_problem::SVector{256,Cchar} = zeros(256)
        lwk_user::Cint = 0

        save_dir::SVector{256,Cchar} = zeros(256)
        save_prefix::SVector{256,Cchar} = zeros(256)

        metis_options::SVector{40,Cint} = zeros(40)
    end
# elseif version == "4.10.0+0"
#     @kwdef mutable struct Struc
#         sym::Cint = 0
#         par::Cint = 0
#         job::Cint = 0

#         comm_fortran::Cint = 0

#         icntl::SVector{40,Cint} = zeros(40)
#         cntl::SVector{15,Cdouble} = zeros(15)

#         n::Cint = 0
#         nz_alloc::Cint = 0

#         nz::Cint = 0
#         irn::Ptr{Cint} = C_NULL
#         jcn::Ptr{Cint} = C_NULL
#         a::Ptr{Cdouble} = C_NULL

#         nz_loc::Cint = 0
#         irn_loc::Ptr{Cint} = C_NULL
#         jcn_loc::Ptr{Cint} = C_NULL
#         a_loc::Ptr{Cdouble} = C_NULL

#         nelt::Cint = 0
#         eltptr::Ptr{Cint} = C_NULL
#         eltvar::Ptr{Cint} = C_NULL
#         a_elt::Ptr{Cdouble} = C_NULL

#         perm_in::Ptr{Cint} = C_NULL

#         sym_perm::Ptr{Cint} = C_NULL
#         uns_perm::Ptr{Cint} = C_NULL

#         colsca::Ptr{Cdouble} = C_NULL
#         rowsca::Ptr{Cdouble} = C_NULL

#         rhs::Ptr{Cdouble} = C_NULL
#         redrhs::Ptr{Cdouble} = C_NULL
#         rhs_sparse::Ptr{Cdouble} = C_NULL
#         sol_loc::Ptr{Cdouble} = C_NULL

#         irhs_sparse::Ptr{Cint} = C_NULL
#         irhs_ptr::Ptr{Cint} = C_NULL
#         isol_loc::Ptr{Cint} = C_NULL

#         nrhs::Cint = 0
#         lrhs::Cint = 0
#         lredrhs::Cint = 0
#         nz_rhs::Cint = 0
#         lsol_loc::Cint = 0

#         schur_mloc::Cint = 0
#         schur_nloc::Cint = 0
#         schur_lld::Cint = 0

#         mblock::Cint = 0
#         nblock::Cint = 0
#         nprow::Cint = 0
#         npcol::Cint = 0

#         info::SVector{40,Cint} = zeros(40)
#         infog::SVector{40,Cint} = zeros(40)
#         rinfo::SVector{40,Cdouble} = zeros(40)
#         rinfog::SVector{40,Cdouble} = zeros(40)

#         deficiency::Cint = 0
#         pivnul_list::Ptr{Cint} = C_NULL
#         mapping::Ptr{Cint} = C_NULL

#         size_schur::Cint = 0
#         listvar_schur::Ptr{Cint} = C_NULL
#         schur::Ptr{Cdouble} = C_NULL

#         instance_number::Cint = 0
#         wk_user::Ptr{Cdouble} = C_NULL

#         version_number::SVector{16,Cchar} = zeros(16)

#         ooc_tmpdir::SVector{256,Cchar} = zeros(256)
#         ooc_prefix::SVector{64,Cchar} = zeros(64)

#         write_problem::SVector{256,Cchar} = zeros(256)
#         lwk_user::Cint = 0
#     end
else
    error("MUMPS_seq_jll version not supported")
end

mutable struct Solver <: AbstractLinearSolver
    csc::SparseMatrixCSC{Float64,Int32}
    I::Vector{Int32}
    J::Vector{Int32}
    sym_perm::Vector{Int32}
    pivnul_list::Vector{Int32}
    mumps_struc::Struc
    is_singular::Bool
    opt::Options
    logger::Logger
end

dmumps_c(mumps_struc::Struc)=ccall(
    (:dmumps_c,MUMPS_seq_jll.libdmumps),
    Cvoid,
    (Ref{Struc},),
    mumps_struc)

# this is necessary, when multi-threaded calls are made with Mumps, not to clash with MPI
mumps_lock = Threads.SpinLock()
function locked_dmumps_c(mumps_struc::Struc)
    lock(mumps_lock)
    try
        dmumps_c(mumps_struc)
    finally
        unlock(mumps_lock)
    end
end
# ---------------------------------------------------------------------------------------

function Solver(csc::SparseMatrixCSC{Float64,Int32};
                option_dict::Dict{Symbol,Any}=Dict{Symbol,Any}(),
                opt=Options(),logger=Logger(),
                kwargs...)

    set_options!(opt,option_dict,kwargs)

    I,J = findIJ(csc)
    sym_perm = zeros(Int32,csc.n)
    pivnul_list = zeros(Int32,csc.n)

    mumps_struc = Struc()

    mumps_struc.sym =  2
    mumps_struc.par =  1
    mumps_struc.job = -1
    mumps_struc.comm_fortran = -987654 # MPI.COMM_WORLD.val

    locked_dmumps_c(mumps_struc)
    mumps_struc.n = csc.n;
    mumps_struc.nz= nnz(csc);
    mumps_struc.a = pointer(csc.nzval)
    mumps_struc.irn = pointer(I)
    mumps_struc.jcn = pointer(J)
    mumps_struc.sym_perm = pointer(sym_perm)
    mumps_struc.pivnul_list = pointer(pivnul_list)

    # symbolic factorization
    mumps_struc.job = 1;

    mumps_struc.icntl = setindex(mumps_struc.icntl,0,2)
    mumps_struc.icntl = setindex(mumps_struc.icntl,0,3)
    mumps_struc.icntl = setindex(mumps_struc.icntl,0,4)
    mumps_struc.icntl = setindex(mumps_struc.icntl,opt.mumps_permuting_scaling,6)
    mumps_struc.icntl = setindex(mumps_struc.icntl,opt.mumps_pivot_order,7)
    mumps_struc.icntl = setindex(mumps_struc.icntl,opt.mumps_scaling,8)
    mumps_struc.icntl = setindex(mumps_struc.icntl,0,10)
    mumps_struc.icntl = setindex(mumps_struc.icntl,1,13)
    mumps_struc.icntl = setindex(mumps_struc.icntl,opt.mumps_mem_percent,14)

    mumps_struc.cntl = setindex(mumps_struc.cntl,opt.mumps_pivtol,1)

    a = copy(csc.nzval) # would there be a better way?
    csc.nzval.=1

    locked_dmumps_c(mumps_struc);
    mumps_struc.info[1] < 0 && throw(SymbolicException())

    csc.nzval.=a

    M = Solver(csc,I,J,sym_perm,pivnul_list,mumps_struc,false,opt,logger)
    finalizer(finalize,M)

    return M
end

function factorize!(M::Solver)
    M.is_singular = false
    M.mumps_struc.job = 2;
    cnt = 0
    while true
        locked_dmumps_c(M.mumps_struc)
        if M.mumps_struc.info[1] in [-8,-9]
            cnt >= 10 && throw(FactorizationException())
            M.mumps_struc.icntl = setindex(M.mumps_struc.icntl,M.mumps_struc.icntl[14]*2.,14)
            cnt += 1
        elseif M.mumps_struc.info[1] == -10
            M.is_singular = true
            break
        elseif M.mumps_struc.info[1] < 0
            throw(FactorizationException())
        else
            break
        end
    end
    return M
end

function solve!(M::Solver,rhs::StrideOneVector{Float64})
    M.is_singular && return rhs
    M.mumps_struc.rhs = pointer(rhs)
    M.mumps_struc.job = 3
    locked_dmumps_c(M.mumps_struc)
    M.mumps_struc.info[1] < 0 && throw(SolveException())
    return rhs
end

is_inertia(::Solver) = true
function inertia(M::Solver)
    return (M.csc.n-M.is_singular-M.mumps_struc.infog[12],
            M.is_singular,
            M.mumps_struc.infog[12])
end

function improve!(M::Solver)
    if M.mumps_struc.cntl[1] == M.opt.mumps_pivtolmax
        @debug(M.logger,"improve quality failed.")
        return false
    end
    M.mumps_struc.cntl = setindex(M.mumps_struc.cntl,min(M.opt.mumps_pivtolmax,M.mumps_struc.cntl[1]^.5),1)
    @debug(M.logger,"improved quality: pivtol = $(M.mumps_struc.cntl[1])")
    return true
end

function finalize(M::Solver)
    M.mumps_struc.job = -2
    locked_dmumps_c(M.mumps_struc);
end

introduce(::Solver)="mumps"

end # module


using jInv.Mesh;
using MAT;
using jInv
using jInv.InverseSolve
using ShapeReconstructionPaLS
using ParamLevelSet;
using ShapeReconstructionPaLS.Utils;
using ShapeReconstructionPaLS.PointCloud;
using DelimitedFiles
using LinearAlgebra
using Statistics
using Distributed
using SparseArrays
using Random

ENV["MPLBACKEND"] = "Qt4Agg"

function ind2subv(shape, indices)
    CI = CartesianIndices(shape)
    return getindex.(Ref(CI), indices)
end

plotting = false;

if plotting
	using PyPlot;
	using jInvVisPyPlot
	close("all");
end

invertDip = true;
invertVis = false;

locateRBFwithGrads = true;
n = [128,128,128];
mesh = getRegularMesh([0.0 1.5 0.0 1.5 0.0 1.5],n);
n_tup = collect(n);
n_tup = tuple(n_tup...)

################################################
### Reading the model and pad ##################
################################################

model = "fandisksmall";
file = open(string(pwd(),"/../models/",model,".xyz"));

#file = open("pc_full.xyz")
A = readlines(file);
A = split.(A);
B =  Array{Array{Float64,1},1}(undef,length(A));
N = Array{Array{Float64,1},1}(undef,length(A));
for i = 1:1:size(A,1)
	 global B[i] = parse.(Float64,A[i])[1:3] .+ 0.75;
	 global N[i] = parse.(Float64,A[i])[4:6];
end
close(file);
A = Array{Float64,2}(transpose(hcat(B...)));
Normals = Array{Float64,2}(transpose(hcat(N...)));

##Sort the PC:
PC  = [ A Normals];
PC = PC[sortperm(PC[:, 1]), :];
writedlm("SortedPC.txt",PC);
println("PC:",size(PC)," ", PC[end,:])

npc = size(A,1); #Number of points in point cloud
B = [];

P = A;
println("Size of P:",size(P));

writedlm(string("originalPC.dat"),convert(Array{Float16},A));

#Clear vars:
indices = [];
A = []; tmp = [];
linIdx = [];

noiseAnglesDeg = 1;
#original was mean(2*Mesh.h);
noiseTrans = 0.015;
noiseAngles = deg2rad(noiseAnglesDeg);
#########################################################################################################
### Dipping Data ########################################################################################
#########################################################################################################

method = RBF10Based; methodName="RBF10Based";

nWorkers = nworkers();
println("nworkers=",nWorkers)
misfun = PCFun; ## least squares

	dipDataFilename = string("pointCloudData",model,"_dataRes",n,1);
	trans = [0.0 0 0 ; 0.0 0 0 ]
	theta_phi_dip = [0.0 0.0 ; 0.0 0.0];
	noiseTrans = 0.0;
	noiseAngles = deg2rad(0.0);
	npc = 2;
	prepareSyntheticPointCloudData(PC,mesh,npc,theta_phi_dip,trans,noiseTrans,noiseAngles,dipDataFilename);

	#########################################################################################################
	### Read the dipping data ###############################################################################
	#########################################################################################################
	Data,P,Normals,trans,theta_phi_dip,npc = readPointCloudDataFile(dipDataFilename);
	### Set up the dip inversion
	P =  Array{Array{Float64,2}}(P);
	Normals = Array{Array{Float64,2}}(Normals);
	pForDip = getPointCloudParam(mesh,P,Normals,theta_phi_dip,trans,nWorkers,method);
	ndips = npc;
	if(locateRBFwithGrads)
		pForDip_MATFree = getPointCloudParam(mesh,P,Normals,theta_phi_dip,trans,nWorkers,MATFree);
	end

	### Create Dip param if we use the GRAD of MATFREE to locate new RBFs
	dobsDirect = divideDataToWorkersData(nWorkers,Data);
	println("size of dobs",size(dobsDirect))
	#nnz = filter(x -> x != 0,dobsDirect[1]);
	#println("length of dobsdirect: ",length(dobsDirect[1]),"_",length(nnz));
	Wd_Dip = Array{Array{Float32}}(undef,nWorkers);
	for k=1:nWorkers
		Wd_Dip[k] = ones(Float32,size(dobsDirect[k]));
	end
	println("Wd dip:",size(Wd_Dip))

n_Moves_all = npc;
println("npc = ",npc);

isSimple = 0;
isRBF10 = 0;

###############################################################################################
############## USE RBF dictionary #############################################################
###############################################################################################

	nRBF = 2;
	isRBF10 = (method == RBF10BasedSimple1 || method==RBF10BasedSimple2 || method==RBF10Based);
	isSimple = (method==RBFBasedSimple1 || method==RBF10BasedSimple1 || method==RBFBasedSimple2 || method==RBF10BasedSimple2);

	if isRBF10
		numParamOfRBF = 10;
	else
		numParamOfRBF = 5;
	end
	n_m_simple = numParamOfRBF*nRBF;
	nAll = isSimple ? numParamOfRBF*nRBF : numParamOfRBF*nRBF + n_Moves_all*5;

	modfun = identityMod;

	if isSimple
		modfunForPlotting = (m)->MeshFreeParamLevelSetModelFunc(mesh,m ,computeJacobian = 0,
			sigma = getDefaultHeaviside() ,bf = 1, numParamOfRBF = numParamOfRBF);
	else
		modfunForPlotting = (m)->MeshFreeParamLevelSetModelFunc(mesh,m[1:(numParamOfRBF*nRBF)],computeJacobian = 0,
			sigma = getDefaultHeaviside() ,bf = 1, numParamOfRBF = numParamOfRBF);
	end

	Iact = 1.0;
	IactPlot = 1.0;
	sback = zero(Float32);
	a = -100000000.0;
	b = 10000000.0;
	boundsHigh = zeros(Float32,nAll) .+ b;
	# boundsHigh[end-5*npc+1:end] .= 0.01;

	boundsLow = zeros(Float32,nAll) .+ a;
	# boundsLow[end-5*npc+1:end] .= -0.01;
	# m0 - initial guess. mref - reference for regularization.
	mref = zeros(Float32,nAll);
	m0   = zeros(Float64,n_m_simple);
	mref[1:numParamOfRBF:n_m_simple] .= ParamLevelSet.centerHeavySide*2.0;
	if isRBF10
		# here we initialize B as an identity matrix
		mref[2:10:n_m_simple] .= 15.0;
		mref[5:10:n_m_simple] .= 15.0;
		mref[7:10:n_m_simple] .= 15.0;
		boundsLow[2:10:n_m_simple] .= 0.05;
		boundsLow[5:10:n_m_simple] .= 0.05;
		boundsLow[7:10:n_m_simple] .= 0.05;
		#boundsLow[end-9:end] .= -0.01
		#boundsHigh[end-9:end] .= 0.01;
	else
		mref[2:5:n_m_simple] .= 4.0;
		boundsLow[2:5:n_m_simple] .= 0.05;
	end
	mref[(numParamOfRBF-2):numParamOfRBF:n_m_simple] .= 0.75;
	mref[(numParamOfRBF-1):numParamOfRBF:n_m_simple] .= 0.75;
	mref[numParamOfRBF:numParamOfRBF:n_m_simple] .= 0.75;
	m0[:] .= mref[1:n_m_simple];

	if !isSimple
		m0 = wrapRBFparamAndRotationsTranslations(m0,theta_phi_dip,trans);
	end
	mref[:] = m0;
	mref[end-5*npc+1:end].=0.0;
	m0[(numParamOfRBF-2):numParamOfRBF:n_m_simple] .+= 0.1*randn(nRBF);
	m0[(numParamOfRBF-1):numParamOfRBF:n_m_simple] .+= 0.1*randn(nRBF);
	m0[numParamOfRBF:numParamOfRBF:n_m_simple]     .+= 0.1*randn(nRBF);

	alpha = 5e-4;

	II = (sparse(1.0I, nAll,nAll));
	regfun = (m, mref, M)->TikhonovReg(m,mref,M,II);
	HesPrec = getEmptyRegularizationPreconditioner();

	if isRBF10
		spd_reg = (m,mref,M) -> RBF_SPD_regularization(m,mref,nRBF);
		regfun 	 		= [regfun;spd_reg];
		alpha 		 	= [alpha;1e-1*alpha]; #was 1e-4 for cube\prism !!!!
		mref 			= [mref zeros(size(mref))];
	end

pMisRFs = getMisfitParam(pForDip, Wd_Dip, dobsDirect, misfun, Iact,sback);

cgit  = 100; # not used.
maxit = 10;
pcgTol = 1e-2; #not used

function myDump(mc::Vector,Dc,iter,pInv,pMis,method="",ndips=0,noiseAngle=0,noiseTranslation=0,useVisual=false,plotFun=modfunForPlotting)
	# if resultsFilename!=""
		# Temp = splitext(resultsFilename);
		# Temp = string(Temp[1],"_GN",iter,Temp[2]);

			#ntup = tuple((pInv.MInv.n)...);
		ntup = tuple(n...)
		fullMc = plotFun(mc)[1];
		#println("size of fullmc:",size(fullMc))
		writedlm(string("FandiskDirectParams_iter_",iter,".dat"),convert(Array{Float64},mc));
		writedlm(string("FandiskDirectVolume_iter_",iter,"_",ntup,".dat"),convert(Array{Float16},fullMc));
		if plotting
			fullMc = reshape(convert(Array{Float32},fullMc[:]),ntup);
			close(888);
			figure(888);
			plotModel(fullMc,includeMeshInfo = true,M_regular = pInv.MInv);
			savefig(string("FandiskDirectIt",iter,"_",ntup,".png"));
			sleep(0.5);
			# writedlm(string("FandiskDirectParams_iter_",iter,".dat"),convert(Array{Float64},mc));
			# writedlm(string("FandiskDirectVolume_iter_",iter,"_",ntup,".dat"),convert(Array{Float16},fullMc));

		end
	# end
end
pInv = getInverseParam(mesh,modfun,regfun,alpha,mref,copy(boundsLow),copy(boundsHigh),
                     maxStep = 2e-1,pcgMaxIter=cgit,pcgTol=pcgTol,
					 minUpdate = 0.01, maxIter = maxit,HesPrec=HesPrec);

# end

#### Projected Gauss Newton
mc = m0;
pInv.maxIter = 100;
Dc = nothing;
mc,Dc,flag,his = projGN(mc,pInv,pMisRFs,solveGN=projGNexplicit);
#mc[end-5*npc+1:end] .= 0.0;
print("DC shape:",size(Dc))

pInv.maxIter = 50;
myDump(mc,0,1,pInv,0,"",0,noiseAnglesDeg,noiseTrans,invertVis);

nRBF=nRBF;
times=0;

if !(method == MATBased || method == MATFree)
	outerIter = 80;
	grad_u = nothing;
	new_RBF_location = [];
	@sync for iterNum=2:outerIter
		println("Inversion using ", nRBF," basis functions")
		new_nRBF = 5;
		if (times > 100)
		  #times=0;
		  new_nRBF = 0;
		end
		global times = times + 1;

		# Dc - the data using our method.
		global Dc = Dc;

		#Lets compute the gradient of the misfit function:
		locateRBFwithGrads = true;
		print(mc[end-5*npc+1:end])
		if(locateRBFwithGrads)
			#(m11,theta_phi1,b1) 	= splitRBFparamAndRotationsTranslations(mc,nRBF,ndips,numParamOfRBF)
			#updateAngles(pForDip_MATFree,theta_phi1);
			pMis_Free = getMisfitParam(pForDip_MATFree, Wd_Dip, dobsDirect, misfun, Iact,sback);
			sigmaH = getDefaultHeaviside();
			allPoints = [P[1] ; P[2]];
			Dd, = computeMisfit(mc,pMisRFs);
			gradMis = (fetch(Dd[1])-Data).^2;
			sp = sortperm(gradMis,rev = true);
			sp = sp[1:round(Int64,length(sp)/4.0)];
			sp = sp[randperm(length(sp))];
			idxs = sp[1:new_nRBF];
			bwdSubs = [P[1] ; P[2]];
			println("all P size:",size(allPoints));
			Ii = allPoints[idxs,:];
		else
			u = modfunForPlotting(mc)[1];
			#Ii = findall(x -> x >0.2 && x<0.5,u);
			Ii = findall(x -> x >0.4 && x<0.8,u);
		end

		if(locateRBFwithGrads == false)
		if length(Ii) > new_nRBF
		Ii = Ii[randperm(length(Ii))[1:new_nRBF]];
		end
		end

    #while(Ii in new_RBF_location || Ii.-1 in new_RBF_location || Ii.+1 in new_RBF_location)
    #  Ii = findall(x -> x .>0.2 && x.<0.5,u);
    #  Ii = Ii[randperm(length(Ii))[1:new_nRBF]];
    #end
    	# println("length of I",size(Ii));
		global new_RBF_location = append!(new_RBF_location,Ii);

		if(locateRBFwithGrads)
			# println("Added rbfs at locations:",Ii);
			X1 = Ii[:,1]; X2 = Ii[:,2]; X3 = Ii[:,3];
		else
			println("ii:",Ii)
			println(ind2subv(n_tup,Ii))
			indices = getindex.(ind2subv(n_tup,Ii),[1 2 3 ]);
			println(indices)

			I1 = indices[:,1];
			I2 = indices[:,2];
			I3 = indices[:,3];

			X1 = I1*mesh.h[1] .- 0.5*mesh.h[1] .+ mesh.domain[1];
			X2 = I2*mesh.h[2] .- 0.5*mesh.h[2] .+ mesh.domain[3];
			X3 = I3*mesh.h[3] .- 0.5*mesh.h[3] .+ mesh.domain[5];
		end
		# println("X1:",X1);println("X2:",X2);println("X3:",X3);

		n_mc_old = numParamOfRBF*nRBF;
		n_mc_new = numParamOfRBF*nRBF + size(Ii,1)*numParamOfRBF;
		mc_new = zeros(length(mc) + size(Ii,1)*numParamOfRBF);
		if isRBF10
			mref = pInv.mref[:,1];
		else
			mref = pInv.mref;
		end
		mref_new = zeros(Float32,length(mc) + size(Ii,1)*numParamOfRBF);

		mc_new[1:n_mc_old] = mc[1:n_mc_old];

		mc_new[(n_mc_new+1):end] = mc[(n_mc_old+1):end];
		mref_new[(n_mc_new+1):end] = mref[(n_mc_old+1):end];

		mc_new[(n_mc_old+1):numParamOfRBF:n_mc_new] .= 0.0; #ParamLevelSet.centerHeavySide - ParamLevelSet.deltaHeavySide;
		mc_new[(n_mc_old+numParamOfRBF-2):numParamOfRBF:n_mc_new] = X1;
		mc_new[(n_mc_old+numParamOfRBF-1):numParamOfRBF:n_mc_new] = X2;
		mc_new[(n_mc_old+numParamOfRBF):numParamOfRBF:n_mc_new] = X3;

		boundsLow = zeros(Float32,length(mc_new)).+a;
		boundsHigh = zeros(Float32,length(mc_new)).+b;
		boundsLow[1:n_mc_old] = pInv.boundsLow[1:n_mc_old];
		boundsHigh[1:n_mc_old] = pInv.boundsHigh[1:n_mc_old];

		eeps =  1/(0.01171875*2.5)
		if isRBF10
			mc_new[(n_mc_old+2):10:n_mc_new] .= eeps #10.0;
			mc_new[(n_mc_old+5):10:n_mc_new] .= eeps#10.0;
			mc_new[(n_mc_old+7):10:n_mc_new] .= eeps#10.0;
			boundsLow[(n_mc_old+2):10:n_mc_new] .= 0.05;
			boundsLow[(n_mc_old+5):10:n_mc_new] .= 0.05;
			boundsLow[(n_mc_old+7):10:n_mc_new] .= 0.05;
		else
			mc_new[(n_mc_old+2):5:n_mc_new] .= 6.0;
			boundsLow[(n_mc_old+2):5:n_mc_new] .= 0.05;
		end
		# boundsLow[end-9:end] .= -0.01;
		# boundsHigh[end-9:end] .= 0.01;
		pInv.boundsLow = boundsLow;
		pInv.boundsHigh = boundsHigh;
		mref_new[1:n_mc_new] = mc_new[1:n_mc_new];
		mref_new[end-5*npc+1:end].=0.0;
		global mc = mc_new;
		global nRBF += new_nRBF;

		#II = speye(Float32,length(mc_new));
		len = Int64(length(mc_new));
		IIs = sparse(1.0I,len,len);
		pInv.regularizer = (m, mref, M)->TikhonovReg(m,mref,M,IIs);

		if isRBF10
			spd_reg = (m,mref,M) -> RBF_SPD_regularization(m,mref,nRBF);
			pInv.regularizer  = [pInv.regularizer;spd_reg];
			pInv.mref 			  = [mref_new zeros(length(mref_new))];
			pInv.alpha .*= 0.8;
		else
			pInv.mref = mref_new;
			pInv.alpha *= 0.8;
		end

		mc,Dc,flag,his = projGN(mc,pInv,pMisRFs,solveGN=projGNexplicit);
		#mc[end-5*npc+1:end] .= 0.0;
		myDump(mc,0,iterNum,pInv,0,methodName,0,noiseAnglesDeg,noiseTrans,invertVis);

		if iterNum==outerIter
			pInv.alpha *= 0.1;
			pInv.maxIter = 200;
			mc, = projGN(mc,pInv,pMisRFs,solveGN=projGNexplicit);
			myDump(mc,0,iterNum+1,pInv,0,methodName,0,noiseAnglesDeg,noiseTrans,invertVis);
		end

	end

		meshForPlotting = getRegularMesh(Mesh.domain,Mesh.n*1);
		ntup = tuple((meshForPlotting.n)...);
		if isSimple
			modfunForPlotting = (m)->MeshFreeParamLevelSetModelFunc(meshForPlotting,m,Xc = P,computeJacobian = 0,
				sigma = getDefaultHeavySide() ,bf = 1, numParamOfRBF = numParamOfRBF);
		else
			modfunForPlotting = (m)->MeshFreeParamLevelSetModelFunc(meshForPlotting,m[1:(numParamOfRBF*nRBF)],Xc = P,computeJacobian = 0,
				sigma = getDefaultHeavySide() ,bf = 1, numParamOfRBF = numParamOfRBF);
		end
		fullMc = modfunForPlotting(mc)[1];
		writedlm(string("Fandisk200Direct_Volume_final_",ntup,"_from_",tuple((Mesh.n)...),".dat"),convert(Array{Float16},fullMc));
		#m = convert(Array{Float16},m);
		#writedlm(string("Fandisk200Direct_Volume_orig.dat"),m);
		#writedlm(string("Fandisk200Direct_RBF_centers.dat"),mCenters);

end


include("convective_bl_model.jl")
function main()

    # TODO: this will move to the future namelist functionality
    cbl_args = ArgParseSettings(autofix_names = true)
    add_arg_group!(cbl_args, "ConvectiveBoundaryLayer")
    @add_arg_table! cbl_args begin
        "--surface-flux"
        help = "specify surface flux for energy and moisture"
        metavar = "prescribed|bulk"
        arg_type = String
        default = "bulk"
    end

    cl_args = ClimateMachine.init(parse_clargs = true, custom_clargs = cbl_args)

    surface_flux = cl_args["surface_flux"]

    FT = Float64
    config_type = AtmosLESConfigType

    # DG polynomial order
    N = 4
    # Domain resolution and size
    Δh = FT(80)
    Δv = FT(80)

    resolution = (Δh, Δh, Δv)

    # Prescribe domain parameters
    xmax = FT(4800)
    ymax = FT(4800)
    zmax = FT(3200)

    t0 = FT(0)

    # Full simulation requires 16+ hours of simulated time
    timeend = FT(3600 * 0.1)
    CFLmax = FT(0.4)

    # Choose default Explicit solver
    ode_solver_type = ClimateMachine.ExplicitSolverType()

    model = convective_bl_model(FT, config_type, zmax, surface_flux)
    ics = model.problem.init_state_prognostic

    # Assemble configuration
    driver_config = ClimateMachine.AtmosLESConfiguration(
        "ConvectiveBoundaryLayer",
        N,
        resolution,
        xmax,
        ymax,
        zmax,
        param_set,
        ics,
        solver_type = ode_solver_type,
        model = model,
    )
    solver_config = ClimateMachine.SolverConfiguration(
        t0,
        timeend,
        driver_config,
        init_on_cpu = true,
        Courant_number = CFLmax,
        CFL_direction = HorizontalDirection(),
    )
    dgn_config = config_diagnostics(driver_config)

    cbtmarfilter = GenericCallbacks.EveryXSimulationSteps(1) do
        Filters.apply!(
            solver_config.Q,
            ("moisture.ρq_tot",),
            solver_config.dg.grid,
            TMARFilter(),
        )
        nothing
    end

    check_cons = (
        ClimateMachine.ConservationCheck("ρ", "1mins", FT(0.0001)),
        ClimateMachine.ConservationCheck("ρe", "1mins", FT(0.0025)),
    )

    result = ClimateMachine.invoke!(
        solver_config;
        user_callbacks = (cbtmarfilter,),
        diagnostics_config = dgn_config,
        check_cons = check_cons,
        check_euclidean_distance = true,
    )
end

main()


import ..build_algorithm_dict
import Flux: Descent

"""
    Agent for learning online in the four rooms environment.
"""
mutable struct FourRoomsAgent{P<:Resampling.AbstractPolicy} <: JuliaRL.AbstractAgent
    μ::P
    ER::ExperienceReplay
    WER::WeightedExperienceReplay
    gvf::GVF
    training_gap::Int64
    buffer_size::Int64
    batch_size::Int64
    warm_up::Int64
    algo_dict::Dict{String, Resampling.LearningUpdate}
    sample_dict::Dict{String, String}
    value_dict::Dict{String, Array{Resampling.TabularLayer, 1}}
    counter::Int64
    state_t::Array{Int64, 1}
    action_t::Int64
    α_arr::Array{Float64, 1}
    function FourRoomsAgent(μ::P, gvf::GVF, α_arr::Array{Float64, 1},
                            training_gap, buffer_size, batch_size,
                            warm_up, parsed, env_size; max_is_ratio=1.0) where {P<:Resampling.AbstractPolicy}
        algo_dict, sample_dict, value_type_dict = build_algorithm_dict(parsed; max_is=max_is_ratio)
        value_dict = Dict{String, Array{Resampling.TabularLayer, 1}}()
        for key in keys(algo_dict)
            if value_type_dict[key] == "State"
                value_dict[key] = [Tabular(env_size...) for α in α_arr]
            else
                value_dict[key] = [Tabular(4, env_size...) for α in α_arr]
            end
        end
        er_names = [:s_t, :s_tp1, :a_t, :r, :γ_tp1, :terminal, :mu_t, :pi_t, :ρ]
        er_types = [Array{Int64, 1}, Array{Int64, 1}, Int64, Float64, Float64, Bool, Float64, Float64, Float64]
        ER = ExperienceReplay(buffer_size, er_types; column_names=er_names)
        WER = WeightedExperienceReplay(buffer_size, er_types; column_names=er_names)
        new{P}(μ, ER, WER, gvf, training_gap, buffer_size, batch_size, warm_up,
               algo_dict, sample_dict, value_dict, warm_up, [0, 0], 0, α_arr)
    end
end

JuliaRL.get_action(agent::FourRoomsAgent, s; rng=Random.GLOBAL_RNG) =
    StatsBase.sample(rng, agent.μ, s)

function JuliaRL.start!(agent::FourRoomsAgent, env_s_tp1; rng=Random.GLOBAL_RNG, kwargs...)
    agent.counter = agent.warm_up
    agent.state_t = env_s_tp1
    agent.action_t = JuliaRL.get_action(agent, env_s_tp1; rng=rng)
    return agent.action_t
end

function JuliaRL.step!(agent::FourRoomsAgent, env_s_tp1, r, terminal; rng=Random.GLOBAL_RNG, kwargs...)

    # After taking action at timestep t
    μ_prob = get(agent.μ, agent.state_t, agent.action_t, nothing, nothing, nothing)
    c, γ, π_prob = get(agent.gvf, agent.state_t, agent.action_t, env_s_tp1, nothing, nothing)

    experience = (agent.state_t, env_s_tp1, agent.action_t, c, γ, terminal, μ_prob, π_prob, π_prob/μ_prob)

    add!(agent.ER, experience)
    add!(agent.WER, experience, π_prob/μ_prob)

    if agent.counter == 0
        agent.counter = agent.training_gap
        train_value_functions(agent; rng=rng)
    end
    agent.counter -= 1

    agent.state_t .= env_s_tp1
    agent.action_t = JuliaRL.get_action(agent, agent.state_t; rng=rng)
    return agent.action_t
end

function train_value_functions(agent::FourRoomsAgent; rng=Random.GLOBAL_RNG)

    α_arr = agent.α_arr

    samp_er = sample(agent.ER, agent.batch_size; rng=rng)
    samp_wer = sample(agent.WER, agent.batch_size; rng=rng)

    action_tp1_er = [StatsBase.sample(rng, agent.μ, s) for s in samp_er[:s_tp1]]
    action_tp1_wer = [StatsBase.sample(rng, agent.μ, s) for s in samp_wer[:s_tp1]]
    # action_tp1 = StatsBase.sample(rng, [1, 2], StatsBase.weights(π), batch_size)

    arg_er = (samp_er[:ρ], samp_er[:s_t], samp_er[:s_tp1],
              samp_er[:r], samp_er[:γ_tp1], samp_er[:terminal],
              samp_er[:a_t], action_tp1_er, agent.gvf.policy)

    # println("ER:", samp_er[:s_t])

    arg_wer = (ones(length(samp_wer[:ρ])), samp_wer[:s_t], samp_wer[:s_tp1],
               samp_wer[:r], samp_wer[:γ_tp1], samp_wer[:terminal],
               samp_er[:a_t], action_tp1_wer, agent.gvf.policy)
    # println("WER:", samp_wer[:s_t])

    for (α_idx, α) in enumerate(α_arr)
        opt = Descent(α)
        for key in keys(agent.algo_dict)
            if agent.sample_dict[key] == "IR"
                corr_term = 1.0
                if key == "BCIR"
                    corr_term = Resampling.total(agent.WER)/size(agent.WER)[1]
                end
                update!(agent.value_dict[key][α_idx], opt, agent.algo_dict[key], arg_wer...; corr_term=corr_term)
            elseif agent.sample_dict[key] == "ER"
                update!(agent.value_dict[key][α_idx], opt, agent.algo_dict[key], arg_er...;)
            elseif agent.sample_dict[key] == "Optimal"
                samp_opt = Resampling.getrow(agent.ER.buffer, 1:size(agent.ER.buffer)[1])
                arg_opt = (samp_opt[:ρ], samp_opt[:s_t], samp_opt[:s_tp1],
                           samp_opt[:r], samp_opt[:γ_tp1], samp_opt[:terminal])
                update!(agent.value_dict[key][α_idx], opt, agent.algo_dict[key], arg_opt...;)
            end
        end
    end
end


export arrowClass, generalClass, getTConsHead
export InstRec, GlobalTC, LocalTC, show_hints
const TCState = Base._return_type(mk_tcstate, (Vector{HMT}, ))
Gensym = Symbol

"""
datatype for type class instances
"""
mutable struct InstRec
    t :: HMT             # instance type
    ln :: LineNumberNode # where the instance defined
    gensym :: ExprImpl   # the actual gensym(IR level) to access this instance
    isPruned :: Bool     # is the instance type pruned?
end
@typedIO InstRec(HMT, LineNumberNode, ExprImpl, Bool)

const InstResolCtx = LinkedList{InstRec}

function toVec(t::Type{LinkedList{E}}, x::Cons{E}) where E
    [toVec(E, x.head), toVec(t, x.tail)]
end

function toVec(::Type{LinkedList{E}}, ::Nil{E}) where E
    nothing
end

function fromVec(::Type{LinkedList{E}}, x::Nothing) where E
    Some(nil(E))
end

function fromVec(t::Type{LinkedList{E}}, x::Vector) where E
    length(x) !== 2 && return nothing
    head = fromVec(E, x[1])
    head === nothing && return nothing
    tail = fromVec(t, x[2])
    tail === nothing && return nothing
    Some(cons(head.value, tail.value))
end

InstRec(t::HMT, ln::LineNumberNode, gensym::Symbol, isPruned::Bool) = InstRec(t, ln, IR.EVar(gensym), isPruned)

const arrowClass = Nom(:arrow_class)
const generalClass = Nom(:general_class)

getTConsHead(h::HMT) =
    @match h begin
        Nom(_) => h
        App(t, _) ||
        Forall(_, t) => getTConsHead(t)
        Arrow(Implicit(_), t) => getTConsHead(t)
        Arrow(_, _) => arrowClass
        _ => generalClass
    end

struct GlobalTC
    tcstate :: TCState
    count :: Ref{UInt}
    globalImplicits :: Dict{Nom, Vector{InstRec}}
    globalImplicitDeltas :: Dict{Nom, Int32}
    queries :: Vector{Pair{String, HMT}}
    moduleName :: Symbol
end

Base.show(io::IO, ::GlobalTC) = Base.show(io, "<GlobalTC>")

Base.empty(::Type{GlobalTC}, moduleName::Symbol = :main) =
    GlobalTC(
        mk_tcstate(HMT[]),
        Ref(UInt(0)),
        Dict{Nom, Vector{InstRec}}(),
        Dict{Nom, UInt32}(),
        Pair{String, HMT}[],
        moduleName
    )

function show_hints(globalTC::GlobalTC)
    prune = globalTC.tcstate.prune
    for (k, v) in globalTC.queries
        println(k, ": ",  prune(v))
    end
end
struct LocalTC
    typeEnv :: Store{Symbol, HMT}
    symmap :: Store{Symbol, Gensym}
    localImplicits :: InstResolCtx
    ln :: LineNumberNode
end

Base.empty(::Type{LocalTC}) =
    LocalTC(
        Store{Symbol, HMT}(),
        Store{Symbol, Symbol}(),
        nil(InstRec),
        LineNumberNode(1, :unknown))

const αβ = UInt8[('a':'z')...]

symgen(g::GlobalTC) =
    let i = g.count[] + 1
        g.count[] = i
        chars = UInt8[]
        while i != 0
            j = i%26
            push!(chars, αβ[j + 1])
            i = div(i, 26)
        end
        Symbol(:|, g.moduleName, :|, Symbol(chars))
    end


function device_mass_matrix_entries!(::AbstractArray, ::DynamicWrapper{T}) where {T}
    error("Mass Matrix not implemented for models $T")
end

function device_mass_matrix_entries!(
    mass_matrix::AbstractArray,
    dynamic_device::DynamicWrapper{DynG},
) where {DynG <: PSY.DynamicGenerator}
    global_index = get_global_index(dynamic_device)
    mass_matrix_tg_entries!(mass_matrix, PSY.get_prime_mover(dynamic_device), global_index)
    mass_matrix_pss_entries!(mass_matrix, PSY.get_pss(dynamic_device), global_index)
    mass_matrix_avr_entries!(mass_matrix, PSY.get_avr(dynamic_device), global_index)
    mass_matrix_machine_entries!(mass_matrix, PSY.get_machine(dynamic_device), global_index)
    mass_matrix_shaft_entries!(mass_matrix, PSY.get_shaft(dynamic_device), global_index)
    return
end

function device!(
    device_states::AbstractArray{T},
    output_ode::AbstractArray{T},
    voltage_r::T,
    voltage_i::T,
    current_r::AbstractArray{T},
    current_i::AbstractArray{T},
    global_vars::AbstractArray{T},
    inner_vars::AbstractArray{T},
    dynamic_device::DynamicWrapper{DynG},
    t,
) where {DynG <: PSY.DynamicGenerator, T <: Real}
    if get_connection_status(dynamic_device) < 1.0
        output_ode .= zero(T)
        return
    end

    inner_vars[VR_gen_var] = voltage_r
    inner_vars[VI_gen_var] = voltage_i

    sys_ω = global_vars[GLOBAL_VAR_SYS_FREQ_INDEX]

    #Update Inner Vars
    _update_inner_vars!(device_states, output_ode, sys_ω, inner_vars, dynamic_device)

    #Obtain ODEs and Mechanical Power for Turbine Governor
    mdl_tg_ode!(device_states, output_ode, inner_vars, sys_ω, dynamic_device)

    #Obtain ODEs for PSS
    mdl_pss_ode!(device_states, output_ode, inner_vars, sys_ω, dynamic_device)

    #Obtain ODEs for AVR
    mdl_avr_ode!(device_states, output_ode, inner_vars, dynamic_device)

    #Obtain ODEs for Machine
    mdl_machine_ode!(
        device_states,
        output_ode,
        inner_vars,
        current_r,
        current_i,
        dynamic_device,
    )

    #Obtain ODEs for PSY.Shaft
    mdl_shaft_ode!(device_states, output_ode, inner_vars, sys_ω, dynamic_device)
    return
end

function device!(
    voltage_r::T,
    voltage_i::T,
    current_r::AbstractArray{T},
    current_i::AbstractArray{T},
    ::AbstractArray{T},
    ::AbstractArray{T},
    device::StaticWrapper{PSY.Source, U},
    t,
) where {T <: Real, U <: BusCategory}
    mdl_source!(voltage_r, voltage_i, current_r, current_i, device)
    return
end

function device!(
    voltage_r::T,
    voltage_i::T,
    current_r::AbstractArray{T},
    current_i::AbstractArray{T},
    ::AbstractArray{T},
    ::AbstractArray{T},
    device::StaticWrapper{PSY.PowerLoad, U},
    t,
) where {T <: Real, U <: LoadCategory}
    # For now model all loads as constant impedance
    if get_connection_status(device) < 1.0
        current_r .= zero(T)
        current_i .= zero(T)
    else
        mdl_Zload!(voltage_r, voltage_i, current_r, current_i, device.device)
    end
    return
end

function device_mass_matrix_entries!(
    mass_matrix::AbstractArray{Float64},
    dynamic_device::DynamicWrapper{DynI},
) where {DynI <: PSY.DynamicInverter}
    global_index = get_global_index(dynamic_device)
    mass_matrix_DCside_entries!(
        mass_matrix,
        PSY.get_dc_source(dynamic_device),
        global_index,
    )
    mass_matrix_freq_estimator_entries!(
        mass_matrix,
        PSY.get_freq_estimator(dynamic_device),
        global_index,
    )
    mass_matrix_outer_entries!(
        mass_matrix,
        PSY.get_outer_control(dynamic_device),
        global_index,
    )
    mass_matrix_inner_entries!(
        mass_matrix,
        PSY.get_inner_control(dynamic_device),
        global_index,
    )
    mass_matrix_converter_entries!(
        mass_matrix,
        PSY.get_converter(dynamic_device),
        global_index,
    )
    mass_matrix_filter_entries!(mass_matrix, PSY.get_filter(dynamic_device), global_index)
    return
end

function device!(
    device_states::AbstractArray{T},
    output_ode::AbstractArray{T},
    voltage_r::T,
    voltage_i::T,
    current_r::AbstractArray{T},
    current_i::AbstractArray{T},
    global_vars::AbstractArray{T},
    inner_vars::AbstractArray{T},
    dynamic_device::DynamicWrapper{DynI},
    t,
) where {DynI <: PSY.DynamicInverter, T <: Real}
    if get_connection_status(dynamic_device) < 1.0
        output_ode .= zero(T)
        return
    end

    #Obtain global vars
    sys_ω = global_vars[GLOBAL_VAR_SYS_FREQ_INDEX]

    #Update Voltage data
    inner_vars[Vr_inv_var] = voltage_r
    inner_vars[Vi_inv_var] = voltage_i

    #Update V_ref
    V_ref = get_V_ref(dynamic_device)
    inner_vars[V_oc_var] = V_ref

    #Update current inner_vars
    _update_inner_vars!(device_states, output_ode, sys_ω, inner_vars, dynamic_device)

    #Obtain ODES for DC side
    mdl_DCside_ode!(device_states, output_ode, sys_ω, inner_vars, dynamic_device)

    #Obtain ODEs for PLL
    mdl_freq_estimator_ode!(device_states, output_ode, inner_vars, sys_ω, dynamic_device)

    #Obtain ODEs for OuterLoop
    mdl_outer_ode!(device_states, output_ode, inner_vars, sys_ω, dynamic_device)

    #Obtain inner controller ODEs and modulation commands
    mdl_inner_ode!(device_states, output_ode, inner_vars, dynamic_device)

    #Obtain converter relations
    mdl_converter_ode!(device_states, output_ode, inner_vars, dynamic_device)

    #Obtain ODEs for output filter
    mdl_filter_ode!(
        device_states,
        output_ode,
        current_r,
        current_i,
        inner_vars,
        sys_ω,
        dynamic_device,
    )

    return
end

function device_mass_matrix_entries!(
    mass_matrix::AbstractArray,
    dynamic_device::DynamicWrapper{PSY.PeriodicVariableSource},
)
    global_index = get_global_index(dynamic_device)
    mass_matrix_pvs_entries!(mass_matrix, dynamic_device, global_index)
    return
end

function mass_matrix_pvs_entries!(
    mass_matrix,
    pvs::DynamicWrapper{PSY.PeriodicVariableSource},
    global_index::ImmutableDict{Symbol, Int64},
)
    @debug "Using default mass matrix entries $pvs"
end

function device!(
    device_states::AbstractArray{T},
    output_ode::AbstractArray{T},
    voltage_r::T,
    voltage_i::T,
    current_r::AbstractArray{T},
    current_i::AbstractArray{T},
    ::AbstractArray{T},
    ::AbstractArray{T},
    dynamic_device::DynamicWrapper{PSY.PeriodicVariableSource},
    t,
) where {T <: Real}
    ω_θ = PSY.get_internal_angle_frequencies(get_device(dynamic_device))
    ω_V = PSY.get_internal_angle_frequencies(get_device(dynamic_device))

    dV = 0
    for (ix, A) in
        enumerate(PSY.get_internal_voltage_coefficients(get_device(dynamic_device)))
        t <= 0 && continue
        dV += ω_V[ix] * (A[1] * cos(ω_V[ix] * t) - A[2] * sin(ω_V[ix] * t))
    end

    dθ = 0
    for (ix, A) in
        enumerate(PSY.get_internal_angle_coefficients(get_device(dynamic_device)))
        t <= 0 && continue
        dθ += ω_θ[ix] * (A[1] * cos(ω_θ[ix] * t) - A[2] * sin(ω_θ[ix] * t))
    end

    # Internal Voltage states
    V_R = device_states[1] * cos(device_states[2])
    V_I = device_states[1] * sin(device_states[2])
    output_ode[1] = dV
    output_ode[2] = dθ

    #update current
    R_th = PSY.get_R_th(get_device(dynamic_device))
    X_th = PSY.get_X_th(get_device(dynamic_device))
    Zmag = R_th^2 + X_th^2
    current_r[1] += R_th * (V_R - voltage_r[1]) / Zmag + X_th * (V_I - voltage_i[1]) / Zmag #in system pu flowing out
    current_i[1] += R_th * (V_I - voltage_i[1]) / Zmag - X_th * (V_R - voltage_r[1]) / Zmag #in system pu flowing out

    return
end

function _update_inner_vars!(
    device_states,
    output_ode,
    ω_sys,
    inner_vars,
    dynamic_device::DynamicWrapper{PSY.DynamicGenerator{M, S, A, TG, P}},
) where {M <: PSY.Machine, S <: PSY.Shaft, A <: PSY.AVR, TG <: PSY.TurbineGov, P <: PSY.PSS}
    #do nothing
end

function _update_inner_vars!(
    device_states,
    output_ode,
    ω_sys,
    inner_vars,
    dynamic_device::DynamicWrapper{PSY.DynamicGenerator{M, S, A, TG, P}},
) where {
    M <: Union{PSY.RoundRotorQuadratic, PSY.RoundRotorExponential},
    S <: PSY.Shaft,
    A <: PSY.AVR,
    TG <: PSY.TurbineGov,
    P <: PSY.PSS,
}
    #Obtain indices for component w/r to device
    machine = PSY.get_machine(dynamic_device)
    local_ix = get_local_state_ix(dynamic_device, M)

    #Define internal states for component
    internal_states = @view device_states[local_ix]
    eq_p = internal_states[1]
    ed_p = internal_states[2]
    ψ_kd = internal_states[3]
    ψ_kq = internal_states[4]

    #Obtain external states inputs for component
    external_ix = get_input_port_ix(dynamic_device, M)
    δ = device_states[external_ix[1]]

    #Obtain inner variables for component
    V_tR = inner_vars[VR_gen_var]
    V_tI = inner_vars[VI_gen_var]

    #Get parameters
    R = PSY.get_R(machine)
    Xd = PSY.get_Xd(machine)
    Xd_p = PSY.get_Xd_p(machine)
    Xd_pp = PSY.get_Xd_pp(machine)
    Xq_pp = Xd_pp
    Xl = PSY.get_Xl(machine)
    γ_d1 = PSY.get_γ_d1(machine)
    γ_q1 = PSY.get_γ_q1(machine)
    γ_d2 = PSY.get_γ_d2(machine)

    #RI to dq transformation
    V_dq = ri_dq(δ) * [V_tR; V_tI]

    #Additional Fluxes
    ψq_pp = γ_q1 * ed_p + ψ_kq * (1 - γ_q1)
    ψd_pp = γ_d1 * eq_p + γ_d2 * (Xd_p - Xl) * ψ_kd
    ψ_pp = sqrt(ψd_pp^2 + ψq_pp^2)
    #Currents
    I_d =
        (1.0 / (R^2 + Xq_pp * Xd_pp)) *
        (-R * (V_dq[1] - ψq_pp) + Xq_pp * (-V_dq[2] + ψd_pp))
    Se = saturation_function(machine, ψ_pp)
    Xad_Ifd = eq_p + (Xd - Xd_p) * (γ_d1 * I_d - γ_d2 * ψ_kd + γ_d2 * eq_p) + Se * ψd_pp

    #Update Xad_Ifd
    inner_vars[Xad_Ifd_var] = Xad_Ifd
end

function _update_inner_vars!(
    device_states,
    output_ode,
    ω_sys,
    inner_vars,
    dynamic_device::DynamicWrapper{
        PSY.DynamicGenerator{PSY.SalientPoleQuadratic, S, A, TG, P},
    },
) where {S <: PSY.Shaft, A <: PSY.AVR, TG <: PSY.TurbineGov, P <: PSY.PSS}
    #Obtain indices for component w/r to device
    machine = PSY.get_machine(dynamic_device)
    local_ix = get_local_state_ix(dynamic_device, typeof(machine))

    #Define internal states for component
    internal_states = @view device_states[local_ix]
    eq_p = internal_states[1]
    ψ_kd = internal_states[2]
    ψq_pp = internal_states[3]

    #Obtain external states inputs for component
    external_ix = get_input_port_ix(dynamic_device, typeof(machine))
    δ = device_states[external_ix[1]]

    #Obtain inner variables for component
    V_tR = inner_vars[VR_gen_var]
    V_tI = inner_vars[VI_gen_var]

    #Get parameters
    R = PSY.get_R(machine)
    Xd = PSY.get_Xd(machine)
    Xd_p = PSY.get_Xd_p(machine)
    Xd_pp = PSY.get_Xd_pp(machine)
    Xl = PSY.get_Xl(machine)
    γ_d1 = PSY.get_γ_d1(machine)
    γ_q1 = PSY.get_γ_q1(machine)
    γ_d2 = PSY.get_γ_d2(machine)

    #RI to dq transformation
    V_d, V_q = ri_dq(δ) * [V_tR; V_tI]

    #Additional Fluxes
    ψd_pp = γ_d1 * eq_p + γ_q1 * ψ_kd

    #Currents
    I_d = (1.0 / (R^2 + Xd_pp^2)) * (-R * (V_d + ψq_pp) + Xd_pp * (ψd_pp - V_q))
    Se = saturation_function(machine, eq_p)
    Xad_Ifd =
        eq_p + Se * eq_p + (Xd - Xd_p) * (I_d + γ_d2 * (eq_p - ψ_kd - (Xd_p - Xl) * I_d))

    #Update Xad_Ifd
    inner_vars[Xad_Ifd_var] = Xad_Ifd
end

function _update_inner_vars!(
    device_states,
    output_ode,
    ω_sys,
    inner_vars,
    dynamic_device::DynamicWrapper{
        PSY.DynamicGenerator{PSY.SalientPoleExponential, S, A, TG, P},
    },
) where {S <: PSY.Shaft, A <: PSY.AVR, TG <: PSY.TurbineGov, P <: PSY.PSS}
    #Obtain indices for component w/r to device
    machine = PSY.get_machine(dynamic_device)
    local_ix = get_local_state_ix(dynamic_device, typeof(machine))

    #Define internal states for component
    internal_states = @view device_states[local_ix]
    eq_p = internal_states[1]
    ψ_kd = internal_states[2]
    ψq_pp = internal_states[3]

    #Obtain external states inputs for component
    external_ix = get_input_port_ix(dynamic_device, typeof(machine))
    δ = device_states[external_ix[1]]

    #Obtain inner variables for component
    V_tR = inner_vars[VR_gen_var]
    V_tI = inner_vars[VI_gen_var]

    #Get parameters
    R = PSY.get_R(machine)
    Xd = PSY.get_Xd(machine)
    Xd_p = PSY.get_Xd_p(machine)
    Xd_pp = PSY.get_Xd_pp(machine)
    Xq_pp = Xd_pp
    Xl = PSY.get_Xl(machine)
    γ_d1 = PSY.get_γ_d1(machine)
    γ_q1 = PSY.get_γ_q1(machine)
    γ_d2 = PSY.get_γ_d2(machine)

    #RI to dq transformation
    V_d, V_q = ri_dq(δ) * [V_tR; V_tI]

    #Additional Fluxes
    ψd_pp = γ_d1 * eq_p + γ_q1 * ψ_kd
    ψ_pp = sqrt(ψd_pp^2 + ψq_pp^2)

    #Currents
    I_d = (1.0 / (R^2 + Xd_pp^2)) * (-R * (V_d - ψq_pp) + Xq_pp * (-V_q + ψd_pp))
    Se = saturation_function(machine, ψ_pp)
    Xad_Ifd =
        eq_p + Se * ψd_pp + (Xd - Xd_p) * (I_d + γ_d2 * (eq_p - ψ_kd - (Xd_p - Xl) * I_d))

    #Update Xad_Ifd
    inner_vars[Xad_Ifd_var] = Xad_Ifd
end

function _update_inner_vars!(
    device_states,
    output_ode,
    ω_sys,
    inner_vars,
    dynamic_device::DynamicWrapper{PSY.DynamicInverter{C, O, IC, DC, P, F}},
) where {
    C <: PSY.Converter,
    O <: PSY.OuterControl,
    IC <: PSY.InnerControl,
    DC <: PSY.DCSource,
    P <: PSY.FrequencyEstimator,
    F <: PSY.Filter,
}
    #do nothing
end

function _update_inner_vars!(
    device_states,
    output_ode,
    ω_sys,
    inner_vars,
    dynamic_device::DynamicWrapper{
        PSY.DynamicInverter{PSY.RenewableEnergyConverterTypeA, O, IC, DC, P, PSY.RLFilter},
    },
) where {
    O <: PSY.OuterControl,
    IC <: PSY.InnerControl,
    DC <: PSY.DCSource,
    P <: PSY.FrequencyEstimator,
}
    V_R = inner_vars[Vr_inv_var]
    V_I = inner_vars[Vi_inv_var]
    V_t = sqrt(V_R^2 + V_I^2)
    θ = atan(V_I / V_R)

    #Get Converter parameters
    converter = PSY.get_converter(dynamic_device)
    Brkpt = PSY.get_Brkpt(converter)
    Zerox = PSY.get_Zerox(converter)
    Lvpl1 = PSY.get_Lvpl1(converter)
    Vo_lim = PSY.get_Vo_lim(converter)
    Lv_pnt0, Lv_pnt1 = PSY.get_Lv_pnts(converter)
    K_hv = PSY.get_K_hv(converter)
    Lvpl_sw = PSY.get_Lvpl_sw(converter)
    R_source = PSY.get_R_source(converter)
    X_source = PSY.get_X_source(converter)

    #Define internal states for Converter
    converter_ix = get_local_state_ix(dynamic_device, PSY.RenewableEnergyConverterTypeA)
    converter_states = @view device_states[converter_ix]
    Ip = converter_states[1]
    Iq = converter_states[2]
    Vmeas = converter_states[3]

    #Saturate Ip if LVPL is active
    Ip_sat = Ip
    if Lvpl_sw == 1
        LVPL = get_LVPL_gain(Vmeas, Zerox, Brkpt, Lvpl1)
        Ip_sat = Ip <= LVPL ? Ip : LVPL
    end
    G_lv = get_LV_current_gain(V_t, Lv_pnt0, Lv_pnt1)
    Iq_extra = max(K_hv * (V_t - Vo_lim), 0.0)
    Id_cnv = G_lv * Ip_sat
    Iq_cnv = -Iq - Iq_extra
    #Reference Transformation
    Ir_cnv = Id_cnv * cos(θ) - Iq_cnv * sin(θ)
    Ii_cnv = Id_cnv * sin(θ) + Iq_cnv * cos(θ)

    #Obtain parameters
    filt = PSY.get_filter(dynamic_device)
    rf = PSY.get_rf(filt)
    lf = PSY.get_lf(filt)

    function V_cnv_calc(Ir_cnv, Ii_cnv, Vr_inv, Vi_inv)
        if lf != 0.0 || rf != 0.0
            Z_source_mag_sq = R_source^2 + X_source^2
            Zf_mag_sq = rf^2 + lf^2
            r_total_ratio = rf / Zf_mag_sq + R_source / Z_source_mag_sq
            l_total_ratio = lf / Zf_mag_sq + X_source / Z_source_mag_sq
            denom = 1.0 / (r_total_ratio^2 + l_total_ratio^2)
            rf_ratio = rf / Zf_mag_sq
            denom * (
                (Ir_cnv + Vi_inv * lf_ratio + Vr_inv * rf_ratio) * r_total_ratio -
                (Ii_cnv + Vi_inv * rf_ratio - Vr_inv * lf_ratio) * l_total_ratio
            )
            Vi_cnv =
                denom * (
                    (Ir_cnv + Vi_inv * lf_ratio + Vr_inv * rf_ratio) * l_total_ratio +
                    (Ii_cnv + Vi_inv * rf_ratio - Vr_inv * lf_ratio) * r_total_ratio
                )
        else
            Vr_cnv = Vr_inv
            Vi_cnv = Vi_inv
        end
        return Vr_cnv, Vi_cnv
    end

    Vr_cnv, Vi_cnv = V_cnv_calc(Ir_cnv, Ii_cnv, V_R, V_I)

    #Compute output currents
    if lf != 0.0 || rf != 0.0
        Vr_inv = V_R
        Vi_inv = V_I
        Zmag_squared = rf^2 + lf^2
        Ir_filt = (1.0 / Zmag_squared) * ((Vr_cnv - Vr_inv) * rf + (Vi_cnv - Vi_inv) * lf)
        Ii_filt = (1.0 / Zmag_squared) * ((Vi_cnv - Vi_inv) * rf - (Vr_cnv - Vr_inv) * lf)
    else
        Ir_filt = Ir_cnv
        Ii_filt = Ii_cnv
    end

    #Update inner_vars
    inner_vars[Ir_cnv_var] = Ir_cnv
    inner_vars[Ii_cnv_var] = Ii_cnv
    inner_vars[Vr_cnv_var] = Vr_cnv
    inner_vars[Vi_cnv_var] = Vi_cnv
    inner_vars[Ir_inv_var] = Ir_filt
    inner_vars[Ii_inv_var] = Ii_filt
end


"""
    like(state, agent_idx, config)

Generate a like for a post.

# Arguments
- `state`: A tuple of the current graph and agent_list
- `agent_idx`: Agent index
- `config`: Config object as provided by Config

See also: [`Config`](@ref), [`Agent`](@ref)
"""
function like!(
    state::Tuple{AbstractGraph, AbstractArray},
    agent_idx::Integer,
    config::Config
)

    agent_list = state[2]
    this_agent = agent_list[agent_idx]

    for post in this_agent.feed
        if (
            (
                abs(post.opinion - this_agent.opinion)
                < config.opinion_threshs.like
            )
            && !(post in this_agent.liked_posts)
            && !(post in this_agent.disliked_posts)
        )
            post.like_count += 1
            post.weight *= 1.01
            push!(this_agent.liked_posts, post)
        end
    end

    return state

end

# suppress output of include()
;


# Types

export
    Interaction,
    GeneralInteraction,
    SpecificInteraction,
    AbstractCutoff,
    AbstractCoupler,
    NeighborFinder,
    Logger,
    InteractionList2Atoms,
    InteractionList3Atoms,
    InteractionList4Atoms,
    Atom,
    mass,
    AtomData,
    NeighborList,
    NeighborListVec,
    System

const DefaultFloat = Float64

"An interaction between atoms that contributes to forces on the atoms."
abstract type Interaction end

"""
A general interaction that will apply to all or most atom pairs.
Custom general interactions should sub-type this type.
"""
abstract type GeneralInteraction <: Interaction end

"""
A specific interaction between sets of specific atoms, e.g. a bond angle.
Custom specific interactions should sub-type this type.
"""
abstract type SpecificInteraction <: Interaction end

"""
A general type of cutoff encoding the approximation used for a potential.
Interactions can be parameterized by the cutoff behavior.
"""
abstract type AbstractCutoff end

"""
A way to keep properties of a system constant.
Custom temperature and pressure couplers should sub-type this type.
"""
abstract type AbstractCoupler end

"""
A way to find near atoms to save on simulation time.
Custom neighbor finders should sub-type this type.
"""
abstract type NeighborFinder end

"""
A way to record a property, e.g. the temperature, throughout a simulation.
Custom loggers should sub-type this type.
"""
abstract type Logger end

"""
    InteractionList2Atoms(is, js, inters)
    InteractionList2Atoms(inter_type)

A list of specific interactions between two atoms.
"""
struct InteractionList2Atoms{T}
    is::Vector{Int}
    js::Vector{Int}
    inters::T
end

"""
    InteractionList3Atoms(is, js, ks, inters)
    InteractionList3Atoms(inter_type)

A list of specific interactions between three atoms.
"""
struct InteractionList3Atoms{T}
    is::Vector{Int}
    js::Vector{Int}
    ks::Vector{Int}
    inters::T
end

"""
    InteractionList4Atoms(is, js, ks, ls, inters)
    InteractionList4Atoms(inter_type)

A list of specific interactions between four atoms.
"""
struct InteractionList4Atoms{T}
    is::Vector{Int}
    js::Vector{Int}
    ks::Vector{Int}
    ls::Vector{Int}
    inters::T
end

InteractionList2Atoms(T) = InteractionList2Atoms{Vector{T}}([], [], T[])
InteractionList3Atoms(T) = InteractionList3Atoms{Vector{T}}([], [], [], T[])
InteractionList4Atoms(T) = InteractionList4Atoms{Vector{T}}([], [], [], [], T[])

"""
    Atom(; <keyword arguments>)

An atom and its associated information.
Properties unused in the simulation or in analysis can be left with their
default values.
The types used should be bits types if the GPU is going to be used.

# Arguments
- `index::Int`: the index of the atom in the system.
- `charge::C=0.0`: the charge of the atom, used for electrostatic interactions.
- `mass::M=0.0u"u"`: the mass of the atom.
- `σ::S=0.0u"nm"`: the Lennard-Jones finite distance at which the inter-particle
    potential is zero.
- `ϵ::E=0.0u"kJ * mol^-1"`: the Lennard-Jones depth of the potential well.
"""
struct Atom{C, M, S, E}
    index::Int
    charge::C
    mass::M
    σ::S
    ϵ::E
end

function Atom(;
                index=1,
                charge=0.0,
                mass=0.0u"u",
                σ=0.0u"nm",
                ϵ=0.0u"kJ * mol^-1")
    return Atom(index, charge, mass, σ, ϵ)
end

"""
    mass(atom)

The mass of an atom.
"""
mass(atom::Atom) = atom.mass

AtomsBase.atomic_mass(atom::Atom) = mass(atom)

function Base.show(io::IO, a::Atom)
    print(io, "Atom with index ", a.index, ", charge=", a.charge,
            ", mass=", a.mass, ", σ=", a.σ, ", ϵ=", a.ϵ)
end

"""
    AtomData(atom_type, atom_name, res_number, res_name)

Data associated with an atom.
Storing this separately allows the atom types to be bits types and hence
work on the GPU.
"""
struct AtomData
    atom_type::String
    atom_name::String
    res_number::Int
    res_name::String
    element::String
end

function AtomData(;
                    atom_type="?",
                    atom_name="?",
                    res_number=1,
                    res_name="???",
                    element="?")
    return AtomData(atom_type, atom_name, res_number, res_name, element)
end

"""
    NeighborList()
    NeighborList(n, list)

Structure to contain pre-allocated neighbor lists.
"""
mutable struct NeighborList
    n::Int # Number of neighbors in list (n <= length(list))
    list::Vector{Tuple{Int, Int, Bool}}
end

NeighborList() = NeighborList(0, [])

function Base.empty!(nl::NeighborList)
    nl.n = 0
    return nl
end

function Base.push!(nl::NeighborList, element::Tuple{Int, Int, Bool})
    nl.n += 1
    if nl.n > length(nl.list)
        push!(nl.list, element)
    else
        nl.list[nl.n] = element
    end
    return nl
end

function Base.append!(nl::NeighborList, list::AbstractVector{Tuple{Int, Int, Bool}})
    for element in list
        push!(nl, element)
    end
    return nl
end

Base.append!(nl::NeighborList, nl_app::NeighborList) = append!(nl, @view(nl_app.list[1:nl_app.n]))

"""
    NeighborListVec(n, list)

Structure to contain neighbor lists for broadcasting.
"""
struct NeighborListVec{T}
    nbsi::Vector{Int} # Sorted ascending
    nbsj::Vector{Int}
    atom_bounds_i::Vector{Int}
    atom_bounds_j::Vector{Int}
    sortperm_j::Vector{Int}
    weights_14::T
end

"""
    System(; <keyword arguments>)

A physical system to be simulated.
Properties unused in the simulation or in analysis can be left with their
default values.
`atoms`, `atoms_data`, `coords` and `velocities` should have the same length.
This is a sub-type of `AbstractSystem` from AtomsBase.jl and implements the
interface described there.

# Arguments
- `atoms::A`: the atoms, or atom equivalents, in the system. Can be
    of any type but should be a bits type if the GPU is used.
- `atoms_data::AD`: other data associated with the atoms, allowing the atoms to
    be bits types and hence work on the GPU.
- `general_inters::GI=()`: the general interactions in the system, i.e.
    interactions between all or most atoms such as electrostatics. Typically a
    `Tuple`.
- `specific_inter_lists::SI=()`: the specific interactions in the system,
    i.e. interactions between specific atoms such as bonds or angles. Typically
    a `Tuple`.
- `coords::C`: the coordinates of the atoms in the system. Typically a
    vector of `SVector`s of 2 or 3 dimensions.
- `velocities::V=zero(coords)`: the velocities of the atoms in the system.
- `box_size::B`: the size of the box in which the simulation takes place.
    Typically a `SVector` of 2 or 3 dimensions.
- `neighbor_finder::NF=NoNeighborFinder()`: the neighbor finder used to find
    close atoms and save on computation.
- `loggers::L=Dict{String, Logger}()`: the loggers that record properties of
    interest during a simulation.
- `force_unit::F=u"kJ * mol^-1 * nm^-1"`: the unit of force of the system.
- `energy_unit::E=u"kJ * mol^-1"`: the unit of energy of the system.
- `gpu_diff_safe::Bool`: whether to use the GPU implementation. Defaults to
    `isa(coords, CuArray)`.
"""
mutable struct System{D, S, G, A, AD, GI, SI, C, V, B, NF, L, F, E} <: AbstractSystem{D, S}
    atoms::A
    atoms_data::AD
    general_inters::GI
    specific_inter_lists::SI
    coords::C
    velocities::V
    box_size::B
    neighbor_finder::NF
    loggers::L
    force_unit::F
    energy_unit::E
end

function System(;
                atoms,
                atoms_data=[],
                general_inters=(),
                specific_inter_lists=(),
                coords,
                velocities=zero(coords),
                box_size,
                neighbor_finder=NoNeighborFinder(),
                loggers=Dict{String, Logger}(),
                force_unit=u"kJ * mol^-1 * nm^-1",
                energy_unit=u"kJ * mol^-1",
                gpu_diff_safe=isa(coords, CuArray))
    D = length(box_size)
    S = eltype(atoms)
    A = typeof(atoms)
    AD = typeof(atoms_data)
    GI = typeof(general_inters)
    SI = typeof(specific_inter_lists)
    C = typeof(coords)
    V = typeof(velocities)
    B = typeof(box_size)
    NF = typeof(neighbor_finder)
    L = typeof(loggers)
    F = typeof(force_unit)
    E = typeof(energy_unit)
    return System{D, S, gpu_diff_safe, A, AD, GI, SI, C, V, B, NF, L, F, E}(
                    atoms, atoms_data, general_inters, specific_inter_lists,
                    coords, velocities, box_size, neighbor_finder, loggers,
                    force_unit, energy_unit)
end

Base.getindex(s::System, i::Integer) = s.atoms[i]
Base.length(s::System) = length(s.atoms)

AtomsBase.species(s::System) = s.atoms
AtomsBase.species(s::System, i::Integer) = s.atoms[i]

AtomsBase.position(s::System) = s.coords
AtomsBase.position(s::System, i::Integer) = s.coords[i]

AtomsBase.velocity(s::System) = s.velocities
AtomsBase.velocity(s::System, i::Integer) = s.velocities[i]

AtomsBase.boundary_conditions(::System{3}) = SVector(Periodic(), Periodic(), Periodic())
AtomsBase.boundary_conditions(::System{2}) = SVector(Periodic(), Periodic())

edges_to_box(bs::SVector{3}, z) = SVector{3}([SVector(bs[1], z, z), SVector(z, bs[2], z),
                                                SVector(z, z, bs[3])])
edges_to_box(bs::SVector{2}, z) = SVector{2}([SVector(bs[1], z), SVector(z, bs[2])])

function AtomsBase.bounding_box(s::System)
    bs = s.box_size
    z = zero(bs[1])
    bb = edges_to_box(bs, z)
    return unit(z) == NoUnits ? (bb)u"nm" : bb # Assume nm without other information
end

function Base.show(io::IO, ::MIME"text/plain", s::System)
    print(io, "System with ", length(s), " atoms, box size ", s.box_size)
end


# Different ways to simulate molecules

export
    SteepestDescentMinimizer,
    simulate!,
    VelocityVerlet,
    Verlet,
    StormerVerlet,
    Langevin

"""
    SteepestDescentMinimizer(; <keyword arguments>)

Steepest descent energy minimization.

# Arguments
- `step_size::D=0.01u"nm"`: the initial maximum displacement.
- `max_steps::Int=1000`: the maximum number of steps.
- `tol::F=1000.0u"kJ * mol^-1 * nm^-1"`: the maximum force below which to
    finish minimization.
- `run_loggers::Bool=false`: whether to run the loggers during minimization.
- `log_stream::L=devnull`: stream to print minimization progress to.
"""
struct SteepestDescentMinimizer{D, F, L}
    step_size::D
    max_steps::Int
    tol::F
    run_loggers::Bool
    log_stream::L
end

function SteepestDescentMinimizer(;
                                    step_size=0.01u"nm",
                                    max_steps=1_000,
                                    tol=1000.0u"kJ * mol^-1 * nm^-1",
                                    run_loggers=false,
                                    log_stream=devnull)
    return SteepestDescentMinimizer(step_size, max_steps, tol,
                                    run_loggers, log_stream)
end

"""
    simulate!(system, simulator, n_steps; parallel=true)
    simulate!(system, simulator; parallel=true)

Run a simulation on a system according to the rules of the given simulator.
Custom simulators should implement this function.
"""
function simulate!(sys,
                    sim::SteepestDescentMinimizer;
                    parallel::Bool=true)
    neighbors = find_neighbors(sys, sys.neighbor_finder; parallel=parallel)
    sim.run_loggers && run_loggers!(sys, neighbors, 0; parallel=parallel)
    E = potential_energy(sys, neighbors)
    println(sim.log_stream, "Step 0 - potential energy ",
            E, " - max force N/A - N/A")
    hn = sim.step_size

    for step_n in 1:sim.max_steps
        F = forces(sys, neighbors; parallel=parallel)
        max_force = maximum(norm.(F))

        coords_copy = sys.coords
        sys.coords += hn * F ./ max_force
        sys.coords = wrap_coords_vec.(sys.coords, (sys.box_size,))

        neighbors_copy = neighbors
        neighbors = find_neighbors(sys, sys.neighbor_finder, neighbors, step_n;
                                    parallel=parallel)
        E_trial = potential_energy(sys, neighbors)
        if E_trial < E
            hn = 6 * hn / 5
            E = E_trial
            println(sim.log_stream, "Step ", step_n, " - potential energy ",
                    E_trial, " - max force ", max_force, " - accepted")
        else
            sys.coords = coords_copy
            neighbors = neighbors_copy
            hn = hn / 5
            println(sim.log_stream, "Step ", step_n, " - potential energy ",
                    E_trial, " - max force ", max_force, " - rejected")
        end

        sim.run_loggers && run_loggers!(sys, neighbors, step_n;
                                        parallel=parallel)

        if max_force < sim.tol
            break
        end
    end
    return sys
end

# Forces are often expressed per mol but this dimension needs removing for use in the integrator
function remove_molar(x)
    fx = first(x)
    if dimension(fx) == u"𝐋 * 𝐍^-1 * 𝐓^-2"
        T = typeof(ustrip(fx))
        return x / T(Unitful.Na)
    else
        return x
    end
end

"""
    VelocityVerlet(; <keyword arguments>)

The velocity Verlet integrator.

# Arguments
- `dt::T`: the time step of the simulation.
- `coupling::C=NoCoupling()`: the coupling which applies during the simulation.
- `remove_CM_motion::Bool=true`: whether to remove the centre of mass motion
    every time step.
"""
struct VelocityVerlet{T, C}
    dt::T
    coupling::C
    remove_CM_motion::Bool
end

function VelocityVerlet(; dt, coupling=NoCoupling(), remove_CM_motion=true)
    return VelocityVerlet(dt, coupling, remove_CM_motion)
end

function simulate!(sys,
                    sim::VelocityVerlet,
                    n_steps::Integer;
                    parallel::Bool=true)
    neighbors = find_neighbors(sys, sys.neighbor_finder; parallel=parallel)
    run_loggers!(sys, neighbors, 0; parallel=parallel)
    accels_t = accelerations(sys, neighbors; parallel=parallel)
    accels_t_dt = zero(accels_t)
    sim.remove_CM_motion && remove_CM_motion!(sys)

    for step_n in 1:n_steps
        sys.coords += sys.velocities .* sim.dt .+ (remove_molar.(accels_t) .* sim.dt ^ 2) ./ 2
        sys.coords = wrap_coords_vec.(sys.coords, (sys.box_size,))

        accels_t_dt = accelerations(sys, neighbors; parallel=parallel)

        sys.velocities += remove_molar.(accels_t .+ accels_t_dt) .* sim.dt / 2

        sim.remove_CM_motion && remove_CM_motion!(sys)
        apply_coupling!(sys, sim, sim.coupling)

        run_loggers!(sys, neighbors, step_n; parallel=parallel)

        if step_n != n_steps
            neighbors = find_neighbors(sys, sys.neighbor_finder, neighbors, step_n;
                                        parallel=parallel)
            accels_t = accels_t_dt
        end
    end
    return sys
end

"""
    Verlet(; <keyword arguments>)

The leapfrog Verlet integrator.
This is a leapfrog integrator, so the velocities are offset by half a time step
behind the positions.

# Arguments
- `dt::T`: the time step of the simulation.
- `coupling::C=NoCoupling()`: the coupling which applies during the simulation.
- `remove_CM_motion::Bool=true`: whether to remove the centre of mass motion
    every time step.
"""
struct Verlet{T, C}
    dt::T
    coupling::C
    remove_CM_motion::Bool
end

function Verlet(; dt, coupling=NoCoupling(), remove_CM_motion=true)
    return Verlet(dt, coupling, remove_CM_motion)
end

function simulate!(sys,
                    sim::Verlet,
                    n_steps::Integer;
                    parallel::Bool=true)
    neighbors = find_neighbors(sys, sys.neighbor_finder; parallel=parallel)
    run_loggers!(sys, neighbors, 0; parallel=parallel)
    sim.remove_CM_motion && remove_CM_motion!(sys)

    for step_n in 1:n_steps
        accels_t = accelerations(sys, neighbors; parallel=parallel)

        sys.velocities += remove_molar.(accels_t) .* sim.dt

        sys.coords += sys.velocities .* sim.dt
        sys.coords = wrap_coords_vec.(sys.coords, (sys.box_size,))

        sim.remove_CM_motion && remove_CM_motion!(sys)
        apply_coupling!(sys, sim, sim.coupling)

        run_loggers!(sys, neighbors, step_n; parallel=parallel)

        if step_n != n_steps
            neighbors = find_neighbors(sys, sys.neighbor_finder, neighbors, step_n;
                                        parallel=parallel)
        end
    end
    return sys
end

"""
    StormerVerlet(; <keyword arguments>)

The Störmer-Verlet integrator.
Does not currently work with coupling methods that alter the velocity.

# Arguments
- `dt::T`: the time step of the simulation.
- `coupling::C=NoCoupling()`: the coupling which applies during the simulation.
"""
struct StormerVerlet{T, C}
    dt::T
    coupling::C
end

StormerVerlet(; dt, coupling=NoCoupling()) = StormerVerlet(dt, coupling)

function simulate!(sys,
                    sim::StormerVerlet,
                    n_steps::Integer;
                    parallel::Bool=true)
    neighbors = find_neighbors(sys, sys.neighbor_finder; parallel=parallel)
    run_loggers!(sys, neighbors, 0; parallel=parallel)
    coords_last = sys.coords

    for step_n in 1:n_steps
        accels_t = accelerations(sys, neighbors; parallel=parallel)

        coords_copy = sys.coords
        if step_n == 1
            # Use the velocities at the first step since there is only one set of coordinates
            sys.coords += sys.velocities .* sim.dt .+ (remove_molar.(accels_t) .* sim.dt ^ 2) ./ 2
        else
            sys.coords += vector.(coords_last, sys.coords, (sys.box_size,)) .+ remove_molar.(accels_t) .* sim.dt ^ 2
        end
        sys.coords = wrap_coords_vec.(sys.coords, (sys.box_size,))

        # This is accurate to O(dt)
        sys.velocities = vector.(coords_copy, sys.coords, (sys.box_size,)) ./ sim.dt

        apply_coupling!(sys, sim, sim.coupling)

        run_loggers!(sys, neighbors, step_n; parallel=parallel)

        if step_n != n_steps
            neighbors = find_neighbors(sys, sys.neighbor_finder, neighbors, step_n;
                                        parallel=parallel)
            coords_last = coords_copy
        end
    end
    return sys
end

"""
    Langevin(; <keyword arguments>)

The Langevin integrator, based on the Langevin Middle Integrator in OpenMM.
This is a leapfrog integrator, so the velocities are offset by half a time step
behind the positions.

# Arguments
- `dt::T`: the time step of the simulation.
- `temperature::K`: the temperature of the simulation.
- `friction::F`: the friction coefficient of the simulation.
- `remove_CM_motion::Bool=true`: whether to remove the centre of mass motion
    every time step.
"""
struct Langevin{S, K, F, T}
    dt::S
    temperature::K
    friction::F
    remove_CM_motion::Bool
    vel_scale::T
    noise_scale::T
end

function Langevin(; dt, temperature, friction, remove_CM_motion=true)
    vel_scale = exp(-dt * friction)
    noise_scale = sqrt(1 - vel_scale^2)
    return Langevin(dt, temperature, friction, remove_CM_motion, vel_scale, noise_scale)
end

function simulate!(sys,
                    sim::Langevin,
                    n_steps::Integer;
                    parallel::Bool=true,
                    rng=Random.GLOBAL_RNG)
    neighbors = find_neighbors(sys, sys.neighbor_finder; parallel=parallel)
    run_loggers!(sys, neighbors, 0; parallel=parallel)
    sim.remove_CM_motion && remove_CM_motion!(sys)

    for step_n in 1:n_steps
        accels_t = accelerations(sys, neighbors; parallel=parallel)

        sys.velocities += remove_molar.(accels_t) .* sim.dt

        sys.coords += sys.velocities .* sim.dt / 2
        noise = random_velocities(sys, sim.temperature; rng=rng)
        sys.velocities = sys.velocities .* sim.vel_scale .+ noise .* sim.noise_scale

        sys.coords += sys.velocities .* sim.dt / 2
        sys.coords = wrap_coords_vec.(sys.coords, (sys.box_size,))
        sim.remove_CM_motion && remove_CM_motion!(sys)

        run_loggers!(sys, neighbors, step_n; parallel=parallel)

        if step_n != n_steps
            neighbors = find_neighbors(sys, sys.neighbor_finder, neighbors, step_n;
                                        parallel=parallel)
        end
    end
    return sys
end


@generated function Base.:(-)(v::Vec{W,T}) where {W, T <: Union{Float32,Float64}}
    vtyp = vtype(W, T)
    instrs = "%res = fneg nsz arcp contract afn reassoc $vtyp %0\nret $vtyp %res"
    quote
        $(Expr(:meta, :inline))
        Vec(llvmcall($instrs, _Vec{$W,$T}, Tuple{_Vec{$W,$T}}, data(v)))
    end
end
# for T ∈ [Float32,Float64]
#     W = 2
#     typ = LLVM_TYPES[T]
#     while W ≤ pick_vector_width(T)
#         vtyp = "<$W x $typ>"
#         instrs = "%res = fneg fast $vtyp %0\nret $vtyp %res"
#         @eval @inline Base.:(-)(v::Vec{$W,$T}) = Vec(llvmcall($instrs, _Vec{$W,$T}, Tuple{_Vec{$W,$T}}, data(v)))
#         W += W
#     end
# end

# @inline Base.:(-)(v::VecUnroll) = fmap(-, v)
# @inline Base.:(-)(v::VecTile) = fmap(-, v)
# @inline Base.:(-)(v::Vec{<:Any,<:FloatingTypes}) = -v
@inline Base.:(-)(v::Vec{1,<:FloatingTypes}) = -first(v)
@inline Base.:(-)(v::Vec{<:Any,<:NativeTypes}) = zero(v) - v

@inline Base.inv(v::Vec) = vdiv(one(v), v)

@inline Base.:(~)(v::AbstractSIMD{W,T}) where {W,T<:IntegerTypesHW} = v ⊻ vbroadcast(Val(W), -1 % T)


include("OpticsUtils.jl")
include("GasOptics.jl")
include("CloudOptics.jl")
include("GrayOpticsKernels.jl")

function compute_optical_props_kernel!(
    op::AbstractOpticalProps{FT},
    as::AtmosphericState{FT},
    glaycol,
    sf::AbstractSourceLW{FT},
    igpt,
    lkp::LookUpLW{I, FT},
    lkp_cld::LookUpCld,
) where {I <: Int, FT <: AbstractFloat}
    ibnd = lkp.major_gpt2bnd[igpt]

    compute_optical_props_kernel!(op, as, glaycol, sf, igpt, lkp) # longwave gas optics
    add_cloud_optics_2stream(op, as, lkp, lkp_cld, glaycol, ibnd, igpt) # add cloud optics
    return nothing
end

function compute_optical_props_kernel!(
    op::AbstractOpticalProps{FT},
    as::AtmosphericState{FT},
    glaycol,
    sf::AbstractSourceLW{FT},
    igpt,
    lkp::LookUpLW{I, FT},
) where {I <: Int, FT <: AbstractFloat}
    t_sfc = as.t_sfc[glaycol[2]]
    t_lev = as.t_lev
    compute_optical_props_kernel!(op, as, glaycol, igpt, lkp, sf, t_lev, t_sfc)
    return nothing
end

function compute_optical_props_kernel!(
    op::AbstractOpticalProps{FT},
    as::AtmosphericState{FT},
    glaycol,
    igpt,
    lkp::LookUpSW{I, FT},
    lkp_cld::LookUpCld,
) where {I <: Int, FT <: AbstractFloat}
    ibnd = lkp.major_gpt2bnd[igpt]

    compute_optical_props_kernel!(op, as, glaycol, igpt, lkp) # shortwave gas optics
    add_cloud_optics_2stream(op, as, lkp, lkp_cld, glaycol, ibnd, igpt) # add cloud optics
    return nothing
end

function compute_optical_props_kernel!(
    op::AbstractOpticalProps{FT},
    as::AtmosphericState{FT},
    glaycol::Tuple{I, I},
    igpt::I,
    lkp::LookUpLW{I, FT},
    src_args...,
) where {I <: Int, FT <: AbstractFloat}

    vmr = as.vmr
    col_dry = as.col_dry[glaycol...]
    p_lay = as.p_lay[glaycol...]
    t_lay = as.t_lay[glaycol...]
    ibnd = lkp.major_gpt2bnd[igpt]
    τ, ssa = compute_τ_ssa_lw_src!(lkp, vmr, col_dry, igpt, ibnd, p_lay, t_lay, glaycol, src_args...)

    @inbounds op.τ[glaycol...] = τ

    if op isa TwoStream
        @inbounds op.ssa[glaycol...] = ssa
        @inbounds op.g[glaycol...] = FT(0) # initializing asymmetry parameter
    end
    return nothing
end

function compute_optical_props_kernel!(
    op::AbstractOpticalProps{FT},
    as::AtmosphericState{FT},
    glaycol::Tuple{I, I},
    igpt::I,
    lkp::LookUpSW{I, FT},
) where {I <: Int, FT <: AbstractFloat}

    vmr = as.vmr
    col_dry = as.col_dry[glaycol...]
    p_lay = as.p_lay[glaycol...]
    t_lay = as.t_lay[glaycol...]
    ibnd = lkp.major_gpt2bnd[igpt]
    τ, ssa = compute_τ_ssa_lw_src!(lkp, vmr, col_dry, igpt, ibnd, p_lay, t_lay, glaycol)

    @inbounds op.τ[glaycol...] = τ

    if op isa TwoStream
        @inbounds op.ssa[glaycol...] = ssa
        @inbounds op.g[glaycol...] = FT(0) # initializing asymmetry parameter
    end
    return nothing
end


module TestMI
using Test, PlmDCA
MI = PlmDCA.mutualinfo("test.fasta",pseudocount=0)
@test MI == zero(MI)
@test true
printstyled("All TestMI passed!\n",color=:light_green,bold=true)
end


using JLD
# using Pandas
using JSON
using Base.Profile
@everywhere using Base.Test
@everywhere import Base.convert
@everywhere using MAT
@everywhere using ForwardDiff
@everywhere using Polynomials
@everywhere import ForwardDiff.DiffBase

# local
# @everywhere include("/home/genis/cluster_home/model_fitting_julia/gradient_utils.jl")
# @everywhere include("/home/genis/cluster_home/model_fitting_julia/functions_julia.jl")
# path="/home/genis/cluster_home/model_fitting_julia/"

#portatil

path="/home/emma/fit_behaviour_julia/"
@everywhere include(path*"functions/functions_julia.jl")
@everywhere include(path*"functions/utils.jl")
@everywhere include(path*"functions/analysis.jl")
@everywhere include(path*"functions/parameter_functions.jl")

#SUBJECTS=[25,35,37]
SUBJECTS = [25]
# filename="synthetic_data_DW_Nstim10000.jld"
#model_fit="Stconst_DW"
#model_fit="StLogisticDw_2"
#model_fit="StLinTimeDw"
#model_fit="PI_Stconst"
#for model_fit in ["StLinTimePi"]
#for model_fit in ["StLinTimeDw","StLinDw","StLogDw_1"]
for model_fit in ["StLinTimeDw"]
	Nframes=20
	Tframe=0.5

	#subject=6
	for subject in SUBJECTS
		println("subject: ",subject)
		# Nconditions=1
		j=1
		path="/home/emma/fit_behaviour_julia/"
		path_files=path*"results/model_fit"*model_fit*"/subject"*string(subject)*"_1sec/"

		save_fit=analysis_minimization_subjects(path_files)
		path_save=path*"results/summary/model_fit"*model_fit*"/"
		mkpath(path_save)
		filename_save=path_save*"rat"*string(subject)*"_1sec.jld"
		JLD.save(filename_save,save_fit)
		# println(type(save_fit))
		# df=Pandas.DataFrame(save_fit)
		filename_save=path_save*"rat"*string(subject)*"_1sec.json"

		stringdata = JSON.json(save_fit)

		# write the file with the stringdata variable information
		open(filename_save, "w") do f
		        write(f, stringdata)
		     end
		# Pandas.to_json(save_fit,filename_save)
	end
end


module ParetoSmooth
    using Requires
    using DocStringExtensions

    function __init__()
        @require Turing =
            "fce5fe82-541a-59a6-adf8-730c64b5f9a0" include("TuringHelpers.jl")
        @require MCMCChains =
            "c7f686f2-ff18-58e9-bc7b-31028e88f75d" include("MCMCChainsHelpers.jl")
    end

    include("ESS.jl")
    include("GPD.jl")
    include("InternalHelpers.jl")
    include("ImportanceSampling.jl")
    include("LooStructs.jl")
    include("LeaveOneOut.jl")
    include("PublicHelpers.jl")
    include("LooCompare.jl")

end


using ImageCore, Colors, FixedPointNumbers, OffsetArrays, Test

if VERSION >= v"1.2.0-DEV.229"
    sumsz(img) = Base.dims2string(size(img)) * ' '
else
    sumsz(img) = ""
end

# N0f8 is shown as either N0f8 or Normed{UInt8, 8}
# RGB is shown as ColorTypes.RGB or RGB
function typestring(::Type{T}) where T
    buf = IOBuffer()
    show(buf, T)
    String(take!(buf))
end
N0f8_str = typestring(N0f8)
N0f16_str = typestring(N0f16)
RGB_str = typestring(RGB)

@testset "show" begin
    rgb32 = rand(RGB{Float32}, 3, 5)
    v = view(rgb32, 2:3, :)
    @test summary(v) == "2×5 view(::Array{RGB{Float32},2}, 2:3, :) with eltype $(RGB_str){Float32}"
    a = channelview(rgb32)
    @test summary(a) == "3×3×5 reinterpret(Float32, ::Array{RGB{Float32},3})"
    num64 = rand(3,5)
    b = colorview(RGB, num64)
    @test summary(b) == "5-element reshape(reinterpret(RGB{Float64}, ::$(typeof(num64))), 5) with eltype $(RGB_str){Float64}"
    rgb8 = rand(RGB{N0f8}, 3, 5)
    c = rawview(channelview(rgb8))
    @test summary(c) == "3×3×5 rawview(reinterpret(N0f8, ::Array{RGB{N0f8},3})) with eltype UInt8"
    @test summary(rgb8) == "3×5 Array{RGB{N0f8},2} with eltype $(RGB_str){$(N0f8_str)}"
    rand8 = rand(UInt8, 3, 5)
    d = normedview(permuteddimsview(rand8, (2,1)))
    @test summary(d) == "5×3 normedview(N0f8, PermutedDimsArray(::$(typeof(rand8)), (2, 1))) with eltype $(N0f8_str)"
    e = permuteddimsview(normedview(rand8), (2,1))
    @test summary(e) == "5×3 PermutedDimsArray(reinterpret(N0f8, ::$(typeof(rand8))), (2, 1)) with eltype $(N0f8_str)"
    rand16 = rand(UInt16, 3, 5)
    f = permuteddimsview(normedview(N0f16, rand16), (2,1))
    @test summary(f) == "5×3 PermutedDimsArray(reinterpret(N0f16, ::$(typeof(rand16))), (2, 1)) with eltype $(N0f16_str)"
    g = channelview(rgb8)
    @test summary(g) == "3×3×5 reinterpret(N0f8, ::Array{RGB{N0f8},3})"
    h = OffsetArray(rgb8, -1:1, -2:2)
    @test summary(h) == "$(sumsz(h))OffsetArray(::Array{RGB{N0f8},2}, -1:1, -2:2) with eltype $(RGB_str){$(N0f8_str)} with indices -1:1×-2:2"
    i = channelview(h)
    @test summary(i) == "$(sumsz(i))reinterpret(N0f8, OffsetArray(::Array{RGB{N0f8},3}, 1:1, -1:1, -2:2)) with indices 1:3×-1:1×-2:2"
    c = channelview(rand(RGB{N0f8}, 2))
    o = OffsetArray(c, -1:1, 0:1)
    @test summary(o) == "$(sumsz(o))OffsetArray(reinterpret(N0f8, ::Array{RGB{N0f8},2}), -1:1, 0:1) with eltype $(N0f8_str) with indices -1:1×0:1"
    # Issue #45
    a = collect(tuple())
    @test summary(a) == "0-element $(typeof(a))"
    b = view(a, :)
    @test summary(b) == "0-element view(::$(typeof(a)), :) with eltype Union{}"
end

nothing