const SUPPORTED_TYPES = Union{Array{<:Number}, Number}

"""
A Markov Chain Monte Carlo observable.
"""
mutable struct Observable{MeasurementType<:SUPPORTED_TYPES, MeanType<:SUPPORTED_TYPES, InMemory} <: AbstractObservable

    # parameters (external)
    name::String
    alloc::Int
    outfile::String
    group::String # where to put data in JLD file

    # internal
    n_meas::Int # total number of measurements
    elsize::Tuple{Vararg{Int}}
    n_dims::Int
    timeseries::Vector{MeasurementType}
    tsidx::Int # points to next free slot in timeseries (!= n_meas+1 for inmemory == false)
    colons::Vector{Colon} # substitute for .. for JLD datasets

    mean::MeanType # estimate for mean

    Observable{T, MT, IM}() where {T, MT, IM} = new()
end

# -------------------------------------------------------------------------
#   Constructor / Initialization
# -------------------------------------------------------------------------
"""
    Observable(t, name; keyargs...)

Create an observable of type `t`.

The following keywords are allowed:

* `alloc`: preallocated size of time series container
* `outfile`: default HDF5/JLD output file for io operations
* `group`: target path within `outfile`
* `inmemory`: wether to keep the time series in memory or on disk
* `meantype`: type of the mean (should be compatible with measurement type `t`)

See also [`Observable`](@ref).
"""
function Observable(::Type{T};
                    name::String="unnamed",
                    alloc::Int=1000,
                    inmemory::Bool=true,
                    outfile::String="Observables.jld",
                    group::String=name,
                    meantype::DataType=Type{Union{}}) where T

    @assert T <: SUPPORTED_TYPES "Only numbers or arrays of numbers supported as measurement types."
    @assert isconcretetype(T) "Type must be concrete."

    # load old memory dump if !inmemory
    oldfound = false
    if !inmemory && isfile(outfile)
        jldopen(outfile) do f
            HDF5.has(f.plain, group) && (oldfound = true)
        end
    end
    oldfound && (return loadobs_frommemory(outfile, group))

    # trying to find sensible DataType for mean if not given
    mt = meantype
    if mt == Type{Union{}} # not set
        if eltype(T)<:Real
            mt = ndims(T)>0 ? Array{Float64, ndims(T)} : Float64
        else
            mt = ndims(T)>0 ? Array{ComplexF64, ndims(T)} : ComplexF64
        end
    end

    @assert ndims(T) == ndims(mt)

    obs = Observable{T, mt, inmemory}()
    obs.name = name
    obs.alloc = alloc
    obs.outfile = outfile
    obs.group = group

    _init!(obs)
    return obs
end

Observable(::Type{T}, name::String; kw...) where T = Observable(T; name=name, kw...)

DiskObservable(args...; kw...) = Observable(args...; kw..., inmemory=false)

"""
    _init!(obs)

Initialize non-external fields of observable `obs`.
"""
function _init!(obs::Observable{T}) where T
    # internal
    obs.n_meas = 0
    obs.elsize = (-1,) # will be determined on first push! call
    obs.colons = [Colon() for _ in 1:ndims(T)]
    obs.n_dims = ndims(T)

    obs.tsidx = 1
    obs.timeseries = Vector{T}(undef, obs.alloc) # init with Missing values in Julia 1.0?

    if ndims(T) == 0
        obs.mean = convert(T, zero(eltype(T)))
    else
        obs.mean = convert(T, fill(zero(eltype(T)), fill(0, ndims(T))...))
    end
    nothing
end

"""
    reset!(obs::Observable{T})

Resets all measurement information in `obs`.
"""
reset!(obs::Observable{T}) where T = _init!(obs)

# -------------------------------------------------------------------------
#   Constructor macros
# -------------------------------------------------------------------------
"""
Convenience macro for generating an Observable from a vector of measurements.
"""
macro obs(arg)
    return quote
        # local o = Observable($(esc(eltype))($(esc(arg))), $(esc(string(arg))))
        local o = Observable($(esc(eltype))($(esc(arg))), "unnamed")
        push!(o, $(esc(arg)))
        o
    end
end

"""
Convenience macro for generating a "disk observable" (`inmemory=false`) from a vector of measurements.
"""
macro diskobs(arg)
    return quote
        # local o = Observable($(esc(eltype))($(esc(arg))), $(esc(string(arg))))
        local o = Observable($(esc(eltype))($(esc(arg))), "unnamed"; inmemory=false)
        push!(o, $(esc(arg)))
        o
    end
end

# -------------------------------------------------------------------------
#   Basic properties (mostly adding methods to Base functions)
# -------------------------------------------------------------------------
"""
    eltype(obs::Observable{T})

Returns the type `T` of a measurment of the observable.
"""
@inline Base.eltype(obs::Observable{T}) where T = T

"""
Length of observable's time series.
"""
@inline Base.length(obs::Observable{T}) where T = obs.n_meas

"""
Last index of the observable's time series.
"""
@inline Base.lastindex(obs::Observable{T}) where T = length(obs)

"""
Size of the observable (of one measurement).
"""
@inline Base.size(obs::Observable{T}) where T = obs.elsize

"""
Number of dimensions of the observable (of one measurement).

Equivalent to `ndims(T)`.
"""
@inline Base.ndims(obs::Observable{T}) where T = ndims(T)

"""
Returns `true` if the observable hasn't been measured yet.
"""
Base.isempty(obs::Observable{T}) where T = obs.n_meas == 0

"""
    iterate(iter [, state]) -> Tuple{Array{Complex{Float64},1},Int64}

Implementation of Julia's iterator interface
"""
Base.iterate(obs::Observable, state::Int=0) = state+1 <= length(obs) ? (obs[state+1], state+1) : nothing
# TODO: Maybe optimize for disk observables, i.e. load full timeseries in start

"""
Name of the Observable.
"""
name(obs::Observable{T}) where T = obs.name

"""
    rename(obs::Observable, name)

Renames the observable.
"""
rename!(obs::Observable{T}, name::AbstractString) where T = begin obs.name = name; nothing end

"""
Checks wether the observable is kept in memory (vs. on disk).
"""
@inline inmemory(obs::Observable{T, MT, IM}) where {T, MT, IM} = IM

"""
Checks wether the observable is kept in memory (vs. on disk).
"""
@inline isinmemory(obs::Observable) = inmemory(obs)

"""
Check if two observables have equal timeseries.
"""
function Base.:(==)(a::Observable, b::Observable)
    timeseries(a) == timeseries(b)
end

# -------------------------------------------------------------------------
#   Cosmetics: Base.show, Base.summary
# -------------------------------------------------------------------------
function _println_header(io::IO, obs::Observable{T}) where T
    sizestr = ""
    if length(obs) > 0
        if ndims(T) == 0
            nothing
        elseif ndims(T) == 1
            @inbounds sizestr = "$(size(obs)[1])-element "
        else
            sizestr = string(join(size(obs), "x"), " ")
        end
    end
    # disk = inmemory(obs) ? "" : "Disk-"
    println(io, "$(sizestr)$(T) Observable")
    nothing
end

function _println_body(io::IO, obs::Observable{T}) where T
    println("| Name: ", name(obs))
    !inmemory(obs) && print("| In Memory: ", false,"\n")
    print("| Measurements: ", length(obs))
    if length(obs) > 0
        if ndims(obs) == 0
            print("\n| Mean: ", round(mean(obs), digits=5))
            print("\n| Std: ", round(std(obs), digits=5))
        end
    end
end

Base.show(io::IO, obs::Observable{T}) where T = begin
    _println_header(io, obs)
    _println_body(io, obs)
    nothing
end
Base.show(io::IO, m::MIME"text/plain", obs::Observable{T}) where T = print(io, obs)

Base.summary(io::IO, obs::Observable{T}) where T = _println_header(io, obs)
Base.summary(obs::Observable{T}) where T = summary(stdout, obs)

# -------------------------------------------------------------------------
#   push! and push!
# -------------------------------------------------------------------------
"""
Add measurements to an observable.

    push!(obs::Observable{T}, measurement::T; verbose=false)
    push!(obs::Observable{T}, measurements::AbstractArray{T}; verbose=false)

Note that because of internal preallocation this isn't really a push.
"""
function Base.push!(obs::Observable) end

# adding single: numbers
Base.push!(obs::Observable{T}, measurement::S; kw...) where {T<:Number, S<:Number} = _push!(obs, measurement; kw...);

# adding single: arrays
Base.push!(obs::Observable{Array{T,N}}, measurement::AbstractArray{S,N}; kw...) where {T, S<:Number, N} = _push!(obs, measurement; kw...);

# adding multiple: vector of measurements
function Base.push!(obs::Observable{T}, measurements::AbstractVector{T}; kw...) where T
    @inbounds for i in eachindex(measurements)
        _push!(obs, measurements[i]; kw...)
    end
    nothing
end

# adding multiple: arrays one dimension higher (last dim == ts dim)
function Base.push!(obs::Observable{T}, measurements::AbstractArray{S, N}; kw...) where {T,S<:Number,N}
    N == obs.n_dims + 1 || throw(DimensionMismatch("Dimensions of given measurements ($(N-1)) don't match observable's dimensions ($(obs.n_dims))."))
    length(obs) == 0 || size(measurements)[1:N-1] == obs.elsize || error("Sizes of measurements don't match observable size.")

    @inbounds for i in Base.axes(measurements, ndims(measurements))
        _push!(obs, measurements[.., i]; kw...)
    end
    nothing
end

Base.append!(obs::Observable, measurement; kwargs...) = push!(obs, measurement; kwargs...)

# implementation
@inline function _push!(obs::Observable{T}, measurement; verbose=false) where T
    if obs.elsize == (-1,) # first add
        obs.elsize = size(measurement)
        obs.mean = zero(measurement)
    end

    size(measurement) == obs.elsize || error("Measurement size != observable size")

    # add to time series
    verbose && println("Adding measurment to time series [chunk].")
    obs.timeseries[obs.tsidx] = copy(measurement)
    obs.tsidx += 1

    # update mean estimate
    verbose && println("Updating mean estimate.")
    obs.mean = (obs.n_meas * obs.mean + measurement) / (obs.n_meas + 1)
    obs.n_meas += 1

    if obs.tsidx == length(obs.timeseries)+1 # next push! would overflow
        verbose && println("Handling time series [chunk] overflow.")
        if inmemory(obs)
            verbose && println("Increasing time series size.")
            tslength = length(obs.timeseries)
            new_timeseries = Vector{T}(undef, tslength + obs.alloc)
            new_timeseries[1:tslength] = obs.timeseries
            obs.timeseries = new_timeseries
        else
            verbose && println("Dumping time series chunk to disk.")
            flush(obs)
            verbose && println("Setting time series index to 1.")
            obs.tsidx = 1
        end
    end
    verbose && println("Done.")
    nothing
end

"""
    flush(obs::Observable)

This is the crucial function if `inmemory(obs) == false`. It updates the time series on disk.
It is called from `push!` everytime the alloc limit is reached (overflow).

You can call the function manually to save an intermediate state.
"""
function Base.flush(obs::Observable)
    @assert !isinmemory(obs) "Can only flush disk observables (`!inmemory(obs)`)."

    fname = obs.outfile
    grp = endswith(obs.group, "/") ? obs.group : obs.group*"/"
    tsgrp = joinpath(grp, "timeseries")
    alloc = obs.alloc

    try
        jldopen(fname, isfile(fname) ? "r+" : "w", compress=true) do f
            if !HDF5.has(f.plain, grp) # first flush?
                write(f, joinpath(grp,"count"), length(obs))
                write(f, joinpath(grp, "mean"), mean(obs))

                write(f, joinpath(grp, "name"), name(obs))
                write(f, joinpath(grp, "alloc"), obs.alloc)
                write(f, joinpath(grp, "elsize"), [obs.elsize...])
                write(f, joinpath(grp, "eltype"), string(eltype(obs)))
                write(f, joinpath(tsgrp,"chunk_count"), 1)
                if obs.tsidx == length(obs.timeseries) + 1 # regular flush
                    # write full chunk
                    write(f, joinpath(tsgrp,"ts_chunk1"), TimeSeriesSerializer(obs.timeseries))
                else # (early) manual flush
                    # write partial chunk
                    hdf5ver = HDF5.libversion
                    hdf5ver >= v"1.10" || @warn "HDF5 version $(hdf5ver) < 1.10.x Manual flushing might lead to larger output file because space won't be freed on dataset delete."
                    write(f, joinpath(tsgrp,"ts_chunk1"), TimeSeriesSerializer(obs.timeseries[1:obs.tsidx-1]))
                end

            else # not first flush
                c = read(f[joinpath(grp, "count")])
                cc = read(f[joinpath(tsgrp, "chunk_count")])

                if !(cc * alloc == c) # was last flushed manually
                    # delete last incomplete chunk
                    delete!(f, joinpath(tsgrp, "ts_chunk$(cc)"))
                    cc -= 1
                end

                if obs.tsidx == length(obs.timeseries) + 1 # regular flush
                    # write full chunk
                    write(f, joinpath(tsgrp,"ts_chunk$(cc+1)"), TimeSeriesSerializer(obs.timeseries))
                else # (early) manual flush
                    obs.tsidx == 1 && (return nothing) # there is nothing to flush

                    # write partial chunk
                    hdf5ver = HDF5.libversion
                    hdf5ver >= v"1.10" || @warn "HDF5 version $(hdf5ver) < 1.10.x Manual flushing might lead to larger output file because space won't be freed on dataset delete."
                    write(f, joinpath(tsgrp,"ts_chunk$(cc+1)"), TimeSeriesSerializer(obs.timeseries[1:obs.tsidx-1]))
                end

                delete!(f, joinpath(tsgrp, "chunk_count"))
                write(f, joinpath(tsgrp,"chunk_count"), cc+1)

                delete!(f, joinpath(grp, "count"))
                write(f, joinpath(grp,"count"), length(obs))

                delete!(f, joinpath(grp, "mean"))
                write(f, joinpath(grp, "mean"), mean(obs))
            end
        end
    catch er
        error("Couldn't update observable on disk! Error: ", er)
    end

    nothing
end

# -------------------------------------------------------------------------
#   getindex, view, and timeseries access
# -------------------------------------------------------------------------
"""
Returns the time series of the observable.

If `isinmemory(obs) == false` it will read the time series from disk.

See also [`getindex`](@ref) and [`view`](@ref).
"""
timeseries(obs::Observable{T}) where T = obs[1:end]
ts(obs::Observable) = timeseries(obs)

# interface
"""
    view(obs::Observable{T}, args...)

Get, if possible, a view into the time series of the observable.
"""
function Base.view(obs::Observable) end

"""
    getindex(obs::Observable{T}, args...)

Get an element of the time series of the observable.
"""
function Base.getindex(obs::Observable) end

# implementation
function Base.view(obs::Observable{T}, idx::Int) where T
    1 <= idx <= length(obs) || throw(BoundsError(typeof(obs), idx))
    if inmemory(obs)
        view(obs.timeseries, idx)
    else
        error("Only supported for `inmemory(obs) == true`. Alternatively, load the timeseries as an array (e.g. with timeseries_frommemory_flat) and use views into this array.");
    end
end
function Base.view(obs::Observable{T}, rng::UnitRange{Int}) where T
    rng.start >= 1 && rng.stop <= length(obs) || throw(BoundsError(typeof(obs), rng))
    if inmemory(obs)
        view(obs.timeseries, rng)
    else
        error("Only supported for `inmemory(obs) == true`. Alternatively, load the timeseries as an array (e.g. with timeseries_frommemory_flat) and use views into this array.");
    end
end
Base.view(obs::Observable, c::Colon) = view(obs, 1:length(obs))

function Base.getindex(obs::Observable{T}, idx::Int) where T
    1 <= idx <= length(obs) || throw(BoundsError(typeof(obs), idx))
    if inmemory(obs)
        return getindex(obs.timeseries, idx)
    else
        if length(obs) < obs.alloc # no chunk dumped to disk yet
            return obs.timeseries[idx]
        else
            return getindex_fromfile(obs, idx)
        end
    end
end
function Base.getindex(obs::Observable{T}, rng::UnitRange{Int}) where T
    rng.start >= 1 && rng.stop <= length(obs) || throw(BoundsError(typeof(obs), rng))
    if inmemory(obs)
        return getindex(obs.timeseries, rng)
    else
        if length(obs) < obs.alloc # no chunk dumped to disk yet
            return obs.timeseries[rng]
        else
            return getindexrange_fromfile(obs, rng)
        end
    end
end
Base.getindex(obs::Observable, c::Colon) = getindex(obs, 1:length(obs))

# disk observables: get from file
function getindex_fromfile(obs::Observable{T}, idx::Int)::T where T
    tsgrp = joinpath(obs.group, "timeseries/")

    currmemchunk = ceil(Int, obs.n_meas / obs.alloc)
    chunknr = ceil(Int,idx / obs.alloc)
    idx_in_chunk = mod1(idx, obs.alloc)

    if chunknr != currmemchunk
        return _getindex_ts_chunk(obs, chunknr, idx_in_chunk)
    else
        if idx_in_chunk < obs.tsidx
            return obs.timeseries[idx_in_chunk]
        else
            return _getindex_ts_chunk(obs, chunknr, idx_in_chunk)
        end
    end
end

function getindexrange_fromfile(obs::Observable{T}, rng::UnitRange{Int})::Vector{T} where T

    getchunknr = i -> fld1(i, obs.alloc)
    chunknr_start = getchunknr(rng.start)
    chunknr_stop = getchunknr(rng.stop)

    chunkidx_first_start = mod1(rng.start, obs.alloc)
    chunkidx_first_stop = chunknr_start * obs.alloc
    chunkidx_last_start = 1
    chunkidx_last_stop = mod1(rng.stop, obs.alloc)

    if chunknr_start == chunknr_stop # all in one chunk
        startidx = mod1(rng.start, obs.alloc)
        stopidx = mod1(rng.stop, obs.alloc)
        return _getindex_ts_chunk(obs, chunknr_start, startidx:stopidx)
    else
        # fallback: load full time series and extract range
        return vcat(timeseries_frommemory(obs), obs.timeseries[1:obs.tsidx-1])[rng]

        # While the following is cheaper on memory, it is much(!) slower. TODO: bring it up to speed
        # v = Vector{T}(undef, length(rng))
        # i = 1 # pointer to first free slot in v
        # @indbounds for c in chunknr_start:chunknr_stop
        #     if c == chunknr_start
        #         r = chunkidx_first_start:chunkidx_first_stop

        #         _getindex_ts_chunk!(v[1:length(r)], obs, c, r)
        #         i += length(r)

        #     elseif c == chunknr_stop
        #         r = chunkidx_last_start:chunkidx_last_stop
        #         _getindex_ts_chunk!(v[i:lastindex(v)], obs, c, r)
        #     else
        #         _getindex_ts_chunk!(v[i:(i+obs.alloc-1)], obs, c, Colon())
        #         i += obs.alloc
        #     end
        # end

        # return v

    end
end

function _getindex_ts_chunk(obs::Observable{T}, chunknr::Int, idx_in_chunk::Int)::T where T
    tsgrp = joinpath(obs.group, "timeseries/")

    # Use hyperslab to only read the requested element from disk
    return jldopen(obs.outfile, "r") do f
        val = f[joinpath(tsgrp, "ts_chunk$(chunknr)")][obs.colons..., idx_in_chunk]
        res = dropdims(val, dims=obs.n_dims+1)
        return ndims(T) == 0 ? res[1] : res
    end
end

function _getindex_ts_chunk(obs::Observable{T}, chunknr::Int, rng::UnitRange{Int})::Vector{T} where T
    tsgrp = joinpath(obs.group, "timeseries/")

    # Use hyperslab to only read the requested elements from disk
    return jldopen(obs.outfile, "r") do f
        val = f[joinpath(tsgrp, "ts_chunk$(chunknr)")][obs.colons..., rng]
        return [val[.., i] for i in 1:size(val, ndims(val))]
    end
end

function _getindex_ts_chunk(obs::Observable{T}, chunknr::Int, c::Colon)::Vector{T} where T
    _getindex_ts_chunk(obs, chunknr, 1:obs.alloc)
end

# function _getindex_ts_chunk!(out::AbstractVector{T}, obs::Observable{T}, chunknr::Int, rng::UnitRange{Int})::Nothing where T
#     @assert length(out) == length(rng)
#     tsgrp = joinpath(obs.group, "timeseries/")

#     # Use hyperslab to only read the requested elements from disk
#     jldopen(obs.outfile, "r") do f
#         val = f[joinpath(tsgrp, "ts_chunk$(chunknr)")][obs.colons..., rng]
#         # return [val[.., i] for i in 1:size(val, ndims(val))]

#         for i in 1:size(val, ndims(val))
#             out[i] = val[.., i]
#         end
#     end
#     nothing
# end

# function _getindex_ts_chunk!(out::AbstractVector{T}, obs::Observable{T}, chunknr::Int, c::Colon)::Nothing where T
#     _getindex_ts_chunk!(out, obs, chunknr, 1:obs.alloc)
#     nothing
# end

# -------------------------------------------------------------------------
#   Exporting results
# -------------------------------------------------------------------------
"""
    export_results(obs::Observable{T}[, filename::AbstractString, group::AbstractString; timeseries::Bool=false])

Export result for given observable nicely to JLD.

Will export name, number of measurements, estimates for mean and one-sigma error.
Optionally (`timeseries==true`) exports the full time series as well.
"""
function export_result(obs::Observable{T}, filename::AbstractString=obs.outfile, group::AbstractString=obs.group*"_export"; timeseries=false, error=true) where T
    grp = endswith(group, "/") ? group : group*"/"

    jldopen(filename, isfile(filename) ? "r+" : "w") do f
        !HDF5.has(f.plain, grp) || delete!(f, grp)
        write(f, joinpath(grp, "name"), name(obs))
        write(f, joinpath(grp, "count"), length(obs))
        timeseries && write(f, joinpath(grp, "timeseries"), TimeSeriesSerializer(MonteCarloObservable.timeseries(obs)))
        write(f, joinpath(grp, "mean"), mean(obs))
        if error
            err = std_error(obs)
            write(f, joinpath(grp, "error"), err)
            write(f, joinpath(grp, "error_rel"), abs.(err./mean(obs)))
        end
    end
    nothing
end

"""
    export_error(obs::Observable{T}[, filename::AbstractString, group::AbstractString;])

Export one-sigma error estimate and convergence flag.
"""
function export_error(obs::Observable{T}, filename::AbstractString=obs.outfile, group::AbstractString=obs.group) where T
    grp = endswith(group, "/") ? group : group*"/"

    jldopen(filename, isfile(filename) ? "r+" : "w") do f
        !HDF5.has(f.plain, grp*"error") || delete!(f, grp*"error")
        !HDF5.has(f.plain, grp*"error_rel") || delete!(f, grp*"error_rel")
        err = std_error(obs)
        write(f, joinpath(grp, "error"), err)
        write(f, joinpath(grp, "error_rel"), abs.(err./mean(obs)))
    end
    nothing
end

# -------------------------------------------------------------------------
#   load things from memory dump
# -------------------------------------------------------------------------
"""
    loadobs_frommemory(filename::AbstractString, group::AbstractString)

Create an observable based on a memory dump (`inmemory==false`).
"""
function loadobs_frommemory(filename::AbstractString, group::AbstractString)
    grp = endswith(group, "/") ? group : group*"/"
    tsgrp = joinpath(grp, "timeseries")

    isfile(filename) || error("File not found.")

    jldopen(filename) do f
        HDF5.has(f.plain, grp) || error("Group not found in file.")
        name = read(f, joinpath(grp, "name"))
        alloc = read(f, joinpath(grp, "alloc"))
        outfile = filename
        group = grp[1:end-1]
        c = read(f, joinpath(grp, "count"))
        elsize = Tuple(read(f,joinpath(grp, "elsize")))
        element_type = read(f, joinpath(grp, "eltype"))
        themean = read(f,joinpath(grp, "mean"))
        cc = read(f,joinpath(tsgrp, "chunk_count"))

        T = jltype(element_type)
        MT = typeof(themean)
        obs = Observable{T, MT, false}()

        obs.name = name
        obs.alloc = alloc
        obs.outfile = outfile
        obs.group = group
        _init!(obs)

        obs.n_meas = c
        obs.elsize = elsize
        obs.mean = themean

        if !(cc * alloc == c) # was last flushed manually
            last_ts_chunk = read(f, joinpath(tsgrp, "ts_chunk$(cc)"))
            for i in axes(last_ts_chunk, ndims(last_ts_chunk))
                obs.timeseries[i] = last_ts_chunk[..,i]
            end
            obs.tsidx = size(last_ts_chunk, ndims(last_ts_chunk)) + 1
        end

        return obs
    end
end

mean_frommemory(filename::AbstractString, group::AbstractString) = _frommemory(filename, group, "mean")
error_frommemory(filename::AbstractString, group::AbstractString) = _frommemory(filename, group, "error")
function _frommemory(filename::AbstractString, group::AbstractString, field::AbstractString)
    grp = endswith(group, "/") ? group : group*"/"
    d = joinpath(grp, field)
    return jldopen(filename) do f
        return read(f[d])
    end
end

# time series
timeseries(filename::AbstractString, group::AbstractString; kw...) = timeseries_frommemory(filename, group; kw...)
ts(filename::AbstractString, group::AbstractString; kw...) = timeseries_frommemory(filename, group; kw...)
"""
    timeseries_frommemory(filename::AbstractString, group::AbstractString)

Load time series from memory dump (`inmemory==false`) in HDF5/JLD file.

Will load and concatenate time series chunks. Output will be a vector of measurements.
"""
function timeseries_frommemory(filename::AbstractString, group::AbstractString; kw...)
    ts = timeseries_frommemory_flat(filename, group; kw...)
    r = [ts[.., i] for i in 1:size(ts, ndims(ts))]
    return r
end
timeseries_frommemory(obs::Observable{T}; kw...) where T = timeseries_frommemory(obs.outfile, obs.group; kw...)

timeseries_flat(filename::AbstractString, group::AbstractString; kw...) = timeseries_frommemory_flat(filename, group; kw...)
ts_flat(filename::AbstractString, group::AbstractString; kw...) = timeseries_frommemory_flat(filename, group; kw...)
"""
    timeseries_frommemory_flat(filename::AbstractString, group::AbstractString)

Load time series from memory dump (`inmemory==false`) in HDF5/JLD file.

Will load and concatenate time series chunks. Output will be higher-dimensional
array whose last dimension corresponds to Monte Carlo time.
"""
function timeseries_frommemory_flat(filename::AbstractString, group::AbstractString; verbose=false)
    grp = endswith(group, "/") ? group : group*"/"
    tsgrp = joinpath(grp, "timeseries")

    isfile(filename) || error("File not found.")
    jldopen(filename) do f
        HDF5.has(f.plain, grp) || error("Group not found in file.")
        if typeof(f[grp]) == JLD.JldGroup && HDF5.has(f.plain, tsgrp) && typeof(f[tsgrp]) == JLD.JldGroup
            # n_meas = read(f, joinpath(grp, "count"))
            element_type = read(f, joinpath(grp, "eltype"))
            chunk_count = read(f,joinpath(tsgrp, "chunk_count"))
            T = jltype(element_type)

            firstchunk = read(f, joinpath(tsgrp,"ts_chunk1"))
            # ignore potential empty last chunk
            lastchunk = read(f, joinpath(tsgrp,"ts_chunk$(chunk_count)"))
            if length(lastchunk) == 0
                chunk_count -= 1
            end

            chunks = Vector{typeof(firstchunk)}(undef, chunk_count)
            chunks[1] = firstchunk

            for c in 2:chunk_count
                chunks[c] = read(f, joinpath(tsgrp,"ts_chunk$(c)"))
            end

            flat_timeseries = cat(chunks..., dims=ndims(T)+1)

            return flat_timeseries

        else
            if typeof(f[grp]) == JLD.JldDataset
                return read(f, grp)
            elseif HDF5.has(f.plain, joinpath(grp, "timeseries"))
                verbose && println("Loading time series (export_result or old format).")
                flat_timeseries = read(f, joinpath(grp, "timeseries"))
                return flat_timeseries

            elseif HDF5.has(f.plain, joinpath(grp, "timeseries_real"))
                verbose && println("Loading complex time series (old format).")
                flat_timeseries = read(f, joinpath(grp, "timeseries_real")) + im*read(f, joinpath(grp, "timeseries_imag"))
                return flat_timeseries

            else
                error("No timeseries/observable found.")
            end
        end
    end
end

# -------------------------------------------------------------------------
#   Basic Statistics
# -------------------------------------------------------------------------
"""
Mean of the observable's time series.
"""
Statistics.mean(obs::Observable{T}) where T = length(obs) > 0 ? obs.mean : error("Can't calculate mean of empty observable.")

"""
Standard deviation of the observable's time series (assuming uncorrelated data).

See also [`mean(obs)`](@ref), [`var(obs)`](@ref), and [`std_error(obs)`](@ref).
"""
Statistics.std(obs::Observable{T}) where T = length(obs) > 0 ? std(timeseries(obs)) : error("Can't calculate std of empty observable.")

"""
Variance of the observable's time series (assuming uncorrelated data).

See also [`mean(obs)`](@ref), [`std(obs)`](@ref), and [`std_error(obs)`](@ref).
"""
Statistics.var(obs::Observable{T}) where T = length(obs) > 0 ? var(timeseries(obs)) : error("Can't calculate variance of empty observable.")

# -------------------------------------------------------------------------
#   Statistics: error estimation
# -------------------------------------------------------------------------
"""
    std_error(obs::Observable[; method=:full])

Estimates the standard error of the mean.
Respects correlations between measurements through binning analysis.

Optional `method` keyword can be `:log`, `:full`, or `:jackknife`.

See also [`mean(obs)`](@ref).
"""
BinningAnalysis.std_error(obs::Observable; method=:full) = BinningAnalysis.std_error(ts(obs); method=method)

"""
Integrated autocorrelation time (obtained by binning analysis).

See also [`error(obs)`](@ref).
"""
BinningAnalysis.tau(obs::Observable) = BinningAnalysis.tau(ts(obs))

"""
    jackknife(g::Function, obs1, ob2, ...)

Computes the jackknife one sigma error of `g(obs1, obs2, ...)` by performing
a "leave-one-out" analysis.

See BinningAnalysis.jl for more details.
"""
BinningAnalysis.jackknife(g::Function, obs::Observable{T}) where T = BinningAnalysis.jackknife(g, timeseries(obs))
BinningAnalysis.jackknife(g::Function, obss::Observable{T}...) where T = BinningAnalysis.jackknife(g, hcat(timeseries.(obss)...))

"""
    iswithinerrorbars(a, b, δ[, print=false])

Checks whether numbers `a` and `b` are equal up to given error `δ`.
Will print `x ≈ y + k·δ` for `print=true`.

Is equivalent to `isapprox(a,b,atol=δ,rtol=zero(b))`.
"""
function iswithinerrorbars(a::T, b::S, δ::Real, print::Bool=false) where T<:Number where S<:Number
  equal = isapprox(a,b,atol=δ,rtol=zero(δ))
  if print && !equal
    out = a>b ? abs(a-(b+δ))/δ : -abs(a-(b-δ))/δ
    println("x ≈ y + ",round(out, digits=4),"·δ")
  end
  return equal
end
"""
    iswithinerrorbars(A::AbstractArray{T<:Number}, B::AbstractArray{T<:Number}, Δ::AbstractArray{<:Real}[, print=false])

Elementwise check whether `A` and `B` are equal up to given real error matrix `Δ`.
Will print `A ≈ B + K.*Δ` for `print=true`.
"""
function iswithinerrorbars(A::AbstractArray{T}, B::AbstractArray{S},
                           Δ::AbstractArray{<:Real}, print::Bool=false) where T<:Number where S<:Number
  size(A) == size(B) == size(Δ) || error("A, B and Δ must have same size.")

  R = iswithinerrorbars.(A,B,Δ,false)
  allequal = all(R)

  if print && !all(R)
    if T<:Real && S<:Real
      O = similar(A, promote_type(T,S))
      for i in eachindex(O)
        a = A[i]; b = B[i]; δ = Δ[i]
        O[i] = R[i] ? 0.0 : round(a>b ? abs(a-(b+δ))/δ : -abs(a-(b-δ))/δ, digits=4)
      end
      println("A ≈ B + K.*Δ, where K is:")
      display(O)
    else
      @warn "Unfortunately print=true is only supported for real input."
    end
  end

  return allequal
end
iswithinerrorbars(A::Observable, B::Observable, Δ, print=false) = iswithinerrorbars(timeseries(A), timeseries(B), Δ, print)


function gencodeelem2(filename::String, f, xdg, udg, odg, wdg, uinf, param, time)

foldername = "app";
opufile = "opu" * filename;
cpufile = "cpu" * filename;
gpufile = "gpu" * filename;

stropu = "template <typename T> void " * opufile;
strgpu = "template <typename T>  __global__  void kernel" * gpufile;

tmp = "(T *f, T *xdg, T *udg, T *odg, T *wdg, T *uinf, T *param, T time, int modelnumber, int ng, int nc, int ncu, int nd, int ncx, int nco, int ncw, int nce, int npe, int ne)\n";

stropu = stropu * tmp * "{\n";
stropu = stropu * "\tfor (int i = 0; i <ng; i++) {\n";

strgpu = strgpu * tmp * "{\n";
strgpu = strgpu * "\tint i = threadIdx.x + blockIdx.x * blockDim.x;\n";
strgpu = strgpu * "\twhile (i<ng) {\n";

str = "\t\tint j = i%npe;\n";
str = str * "\t\tint k = (i-j)/npe;\n";

fstr = string(f[:]);
str = varsassign(str, "param", length(param), 0, fstr);
str = varsassign(str, "uinf", length(uinf), 0, fstr);

varname = "xdg";
for i = 1:length(xdg)
    str1 = varname * string(i);
    str2 = varname * "[j+npe*" * string(i-1) * "+npe*ncx*k" * "]";
    str = str * "\t\tT " * str1 * " = " * str2 * ";\n";
end
varname = "udg";
for i = 1:length(udg)
    str1 = varname * string(i);
    str2 = varname * "[j+npe*" * string(i-1) * "+npe*nc*k" * "]";
    str = str * "\t\tT " * str1 * " = " * str2 * ";\n";
end
varname = "odg";
for i = 1:length(odg)
    str1 = varname * string(i);
    str2 = varname * "[j+npe*" * string(i-1) * "+npe*nco*k" * "]";
    str = str * "\t\tT " * str1 * " = " * str2 * ";\n";
end
varname = "wdg";
for i = 1:length(wdg)
    str1 = varname * string(i);
    str2 = varname * "[j+npe*" * string(i-1) * "+npe*ncw*k" * "]";
    str = str * "\t\tT " * str1 * " = " * str2 * ";\n";
end

n = length(f);
#for i=1:n
#    f[i] = sympy.simplify(f[i]);
#end
ts, fs = sympy.cse(f);
m = length(ts);
for i = 1:m
    str1 = sympy.ccode(ts[i][1]);
    str2 = sympy.ccode(ts[i][2]);
    str = str * "\t\tT " * str1 * " = " * str2 * ";\n";
end
for i = 1:n
    str1 = "\t\tf[j+npe*" * string(i-1) * "+npe*nce*k" * "]";
    str2 = sympy.ccode(fs[1][i]);
    str = str * str1 * " = " * str2 * ";\n";
end

stropu = stropu * str * "\t}\n" * "}\n\n";
tmp = "template void " * opufile;
tmp = tmp * "(double *, double *, double *, double *, double *, double *, double *, double, int, int, int, int, int, int, int, int, int, int, int);\n";
tmp = tmp * "template void " * opufile;
tmp = tmp * "(float *, float *, float *, float *, float *, float *, float *, float, int, int, int, int, int, int, int, int, int, int, int);\n";
stropu = stropu * tmp;

ioopu = open(foldername * "/" * opufile * ".cpp", "w");
write(ioopu, stropu);
close(ioopu);

strgpu = strgpu * str * "\t\ti += blockDim.x * gridDim.x;\n";
strgpu = strgpu * "\t}\n" * "}\n\n";
tmp = "template <typename T> void " * gpufile;
tmp = tmp * "(T *f, T *xdg, T *udg, T *odg, T *wdg, T *uinf, T *param, T time, int modelnumber, int ng, int nc, int ncu, int nd, int ncx, int nco, int ncw, int nce, int npe, int ne)\n";
tmp = tmp * "{\n";
tmp = tmp * "\tint blockDim = 256;\n";
tmp = tmp * "\tint gridDim = (ng + blockDim - 1) / blockDim;\n";
tmp = tmp * "\tgridDim = (gridDim>1024)? 1024 : gridDim;\n";
tmp = tmp * "\tkernel" * gpufile * "<<<gridDim, blockDim>>>(f, xdg, udg, odg, wdg, uinf, param, time, modelnumber, ng, nc, ncu, nd, ncx, nco, ncw, nce, npe, ne);\n";
tmp = tmp * "}\n\n";
tmp = tmp * "template void " * gpufile;
tmp = tmp * "(double *, double *, double *, double *, double *, double *, double *, double, int, int, int, int, int, int, int, int, int, int, int);\n";
tmp = tmp * "template void " * gpufile;
tmp = tmp * "(float *, float *, float *, float *, float *, float *, float *, float, int, int, int, int, int, int, int, int, int, int, int);\n";
strgpu = strgpu * tmp;

iogpu = open(foldername * "/" * gpufile * ".cu", "w");
write(iogpu, strgpu);
close(iogpu);

strcpu = replace(stropu, "opu" => "cpu");
strcpu = replace(strcpu, "for (int i = 0; i <ng; i++) {" => "#pragma omp parallel for\n\tfor (int i = 0; i <ng; i++) {");

iocpu = open(foldername * "/" * cpufile * ".cpp", "w");
write(iocpu, strcpu);
close(iocpu);

end


using Arrow
using DataFrames
using DrWatson
using MAT

"""

`fdt` files are a type of binary file format that stores "float data".

"""
fdt_parser(path, mat) =
    open(path) do io
        read!(
            io,
            Array{Float32}(undef, (Int(mat["EEG"]["nbchan"]), Int(mat["EEG"]["pnts"]))),
        )
    end

# Reference: https://stackoverflow.com/questions/20484581/search-for-files-in-a-folder
searchdir(path, key) = filter(x -> occursin(key, x), readdir(path))

function fdt_to_arrow(file_path)

    subjects = searchdir(file_path, "sub")

    for sub_num in subjects
        sessions = readdir(file_path * "/" * sub_num)
        for ses_num in sessions
            eeg_sessions = readdir(file_path * "/" * sub_num * "/" * ses_num)
            for eeg_num in eeg_sessions
                files = searchdir(
                    file_path * "/" * sub_num * "/" * ses_num * "/" * eeg_num,
                    ".fdt",
                )
                headers = searchdir(
                    file_path * "/" * sub_num * "/" * ses_num * "/" * eeg_num,
                    ".set",
                )
                for (file, header) in zip(files, headers)
                    fdt_file =
                        file_path *
                        "/" *
                        sub_num *
                        "/" *
                        ses_num *
                        "/" *
                        eeg_num *
                        "/" *
                        file
                    header_file =
                        file_path *
                        "/" *
                        sub_num *
                        "/" *
                        ses_num *
                        "/" *
                        eeg_num *
                        "/" *
                        header
                    header_data = matread(header_file)
                    fdt_data = transpose(fdt_parser(fdt_file, header_data))
                    Arrow.write(
                        datadir(
                            "exp_pro",
                            "$sub_num",
                            "$ses_num",
                            "$eeg_num",
                            "$(file[1:end - 3])arrow",
                        ),
                        DataFrame(fdt_data),
                    )
                end
            end
        end
    end
end

function move_files(file_path, file_type)
    subjects = searchdir(file_path, "sub")
    for sub_num in subjects
        sessions = readdir(file_path * "/" * sub_num)
        for ses_num in sessions
            eeg_sessions = readdir(file_path * "/" * sub_num * "/" * ses_num)
            for eeg_num in eeg_sessions
                files = searchdir(
                    file_path * "/" * sub_num * "/" * ses_num * "/" * eeg_num,
                    "$file_type",
                )
                for file in files
                    mv_file =
                        file_path *
                        "/" *
                        sub_num *
                        "/" *
                        ses_num *
                        "/" *
                        eeg_num *
                        "/" *
                        file
                    dest_path = "data/exp_pro/" * sub_num * "/" * ses_num * "/" *  eeg_num * "/" * file
                    cp(mv_file, dest_path)
                end
            end
        end
    end
end

@quickactivate "NeuriViz"

# fdt_to_arrow(datadir("exp_raw"))


using Plots, Test

@testset "Shapes" begin
    @testset "Type" begin
        square = Shape([(0, 0.0), (1, 0.0), (1, 1.0), (0, 1.0)])
        @test isa(square, Shape{Int64,Float64})
        @test coords(square) isa Tuple{Vector{S},Vector{T}} where {T,S}
    end

    @testset "Copy" begin
        square = Shape([(0, 0), (1, 0), (1, 1), (0, 1)])
        square2 = Shape(square)
        @test square2.x == square.x
        @test square2.y == square.y
    end

    @testset "Center" begin
        square = Shape([(0, 0), (1, 0), (1, 1), (0, 1)])
        @test Plots.center(square) == (0.5, 0.5)
    end

    @testset "Translate" begin
        square = Shape([(0, 0), (1, 0), (1, 1), (0, 1)])
        squareUp = Shape([(0, 1), (1, 1), (1, 2), (0, 2)])
        squareUpRight = Shape([(1, 1), (2, 1), (2, 2), (1, 2)])

        @test Plots.translate(square, 0, 1).x == squareUp.x
        @test Plots.translate(square, 0, 1).y == squareUp.y

        @test Plots.center(translate!(square, 1)) == (1.5, 1.5)
    end

    @testset "Rotate" begin
        # 2 radians rotation matrix
        R2 = [cos(2) sin(2); -sin(2) cos(2)]
        coords = [0 0; 1 0; 1 1; 0 1]'
        coordsRotated2 = R2 * (coords .- 0.5) .+ 0.5

        square = Shape([(0, 0), (1, 0), (1, 1), (0, 1)])

        # make a new, rotated square
        square2 = Plots.rotate(square, -2)
        @test square2.x ≈ coordsRotated2[1, :]
        @test square2.y ≈ coordsRotated2[2, :]

        # unrotate the new square in place
        rotate!(square2, 2)
        @test square2.x ≈ coords[1, :]
        @test square2.y ≈ coords[2, :]
    end

    @testset "Plot" begin
        ang = range(0, 2π, length = 60)
        ellipse(x, y, w, h) = Shape(w * sin.(ang) .+ x, h * cos.(ang) .+ y)
        myshapes = [ellipse(x, rand(), rand(), rand()) for x = 1:4]
        @test coords(myshapes) isa Tuple{Vector{Vector{S}},Vector{Vector{T}}} where {T,S}
        local p
        @test_nowarn p = plot(myshapes)
        @test p[1][1][:seriestype] == :shape
    end
end

@testset "Brush" begin
    @testset "Colors" begin
        baseline = brush(1, RGB(0, 0, 0))
        @test brush(:black) == baseline
        @test brush("black") == baseline
    end
    @testset "Weight" begin
        @test brush(10).size == 10
        @test brush(0.1).size == 1
    end
    @testset "Alpha" begin
        @test brush(0.4).alpha == 0.4
        @test brush(20).alpha == nothing
    end
    @testset "Bad Argument" begin
        # using test_logs because test_warn seems to not work anymore
        @test_logs (:warn, "Unused brush arg: nothing (Nothing)") begin
            brush(nothing)
        end
    end
end

@testset "Fonts" begin
    @testset "Scaling" begin
        sizesToCheck = [
            :titlefontsize,
            :legendfontsize,
            :legendtitlefontsize,
            :xtickfontsize,
            :ytickfontsize,
            :ztickfontsize,
            :xguidefontsize,
            :yguidefontsize,
            :zguidefontsize,
        ]
        # get inital font sizes
        initialSizes = [Plots.default(s) for s in sizesToCheck]

        #scale up font sizes
        scalefontsizes(2)

        # get inital font sizes
        doubledSizes = [Plots.default(s) for s in sizesToCheck]

        @test doubledSizes == initialSizes * 2

        # reset font sizes
        resetfontsizes()

        finalSizes = [Plots.default(s) for s in sizesToCheck]

        @test finalSizes == initialSizes
    end
end

@testset "Series Annotations" begin
    square = Shape([(0, 0), (1, 0), (1, 1), (0, 1)])
    @test_logs (:warn, "Unused SeriesAnnotations arg: triangle (Symbol)") begin
        p = plot(
            [1, 2, 3],
            series_annotations = (
                ["a"],
                2,    # pass a scale factor
                (1, 4), # pass two scale factors (overwrites first one)
                square, # pass a shape
                font(:courier), # pass an annotation font
                :triangle,  # pass an incorrect argument
            ),
        )
        sa = p.series_list[1].plotattributes[:series_annotations]
        @test sa.strs == ["a"]
        @test sa.font.family == "courier"
        @test sa.baseshape == square
        @test sa.scalefactor == (1, 4)
    end
    spl = scatter(
        4.53 .* [1/1 1/2 1/3 1/4 1/5],
        [0 0 0 0 0],
        layout = (5, 1),
        ylims = (-1.1, 1.1),
        xlims = (0, 5),
        series_annotations = permutedims([["1/1"],["1/2"],["1/3"],["1/4"],["1/5"]]),
    )
    @test spl.series_list[1].plotattributes[:series_annotations].strs == ["1/1"]
    @test spl.series_list[2].plotattributes[:series_annotations].strs == ["1/2"]
    @test spl.series_list[3].plotattributes[:series_annotations].strs == ["1/3"]
    @test spl.series_list[4].plotattributes[:series_annotations].strs == ["1/4"]
    @test spl.series_list[5].plotattributes[:series_annotations].strs == ["1/5"]
end


abstract type AbstractFunJac{J2} end
mutable struct FunJac{F, F2, J, P, M, J2, uType, uType2, Prec, PS} <: AbstractFunJac{J2}
    fun::F
    fun2::F2
    jac::J
    p::P
    mass_matrix::M
    jac_prototype::J2
    prec::Prec
    psetup::PS
    u::uType
    du::uType
    resid::uType2
end
FunJac(fun,jac,p,m,jac_prototype,prec,psetup,u,du) = FunJac(fun,nothing,jac,p,m,jac_prototype,prec,psetup,u,du,nothing)
FunJac(fun,jac,p,m,jac_prototype,prec,psetup,u,du,resid) = FunJac(fun,nothing,jac,p,m,jac_prototype,prec,psetup,u,du,resid)

function cvodefunjac(t::Float64,
                     u::N_Vector,
                     du::N_Vector,
                     funjac::FunJac)
    funjac.u = unsafe_wrap(Vector{Float64}, __N_VGetArrayPointer_Serial(u),length(funjac.u))
    funjac.du = unsafe_wrap(Vector{Float64}, __N_VGetArrayPointer_Serial(du),length(funjac.du))
    _du = funjac.du
    _u = funjac.u
    funjac.fun(_du, _u, funjac.p, t)
    return CV_SUCCESS
end

function cvodefunjac2(t::Float64,
                     u::N_Vector,
                     du::N_Vector,
                     funjac::FunJac)
    funjac.u = unsafe_wrap(Vector{Float64}, __N_VGetArrayPointer_Serial(u),length(funjac.u))
    funjac.du = unsafe_wrap(Vector{Float64}, __N_VGetArrayPointer_Serial(du),length(funjac.du))
    _du = funjac.du
    _u = funjac.u
    funjac.fun2(_du, _u, funjac.p, t)
    return CV_SUCCESS
end

function cvodejac(t::realtype,
                  u::N_Vector,
                  du::N_Vector,
                  J::SUNMatrix,
                  funjac::AbstractFunJac{Nothing},
                  tmp1::N_Vector,
                  tmp2::N_Vector,
                  tmp3::N_Vector)

    funjac.u = unsafe_wrap(Vector{Float64}, __N_VGetArrayPointer_Serial(u),length(funjac.u))
    _u = funjac.u
    funjac.jac(convert(Matrix, J), _u, funjac.p, t)
    return CV_SUCCESS
end

function cvodejac(t::realtype,
                  u::N_Vector,
                  du::N_Vector,
                  _J::SUNMatrix,
                  funjac::AbstractFunJac{<:SparseMatrixCSC},
                  tmp1::N_Vector,
                  tmp2::N_Vector,
                  tmp3::N_Vector)
    jac_prototype = funjac.jac_prototype
    J = convert(SparseMatrixCSC,_J)

    funjac.u = unsafe_wrap(Vector{Float64}, __N_VGetArrayPointer_Serial(u),length(funjac.u))
    _u = funjac.u

    funjac.jac(jac_prototype, _u, funjac.p, t)
    J.nzval .= jac_prototype.nzval
    # Sundials resets the value pointers each time, so reset it too
    @. J.rowval = jac_prototype.rowval - 1
    @. J.colptr = jac_prototype.colptr - 1
    return CV_SUCCESS
end

function idasolfun(t::Float64, u::N_Vector, du::N_Vector, resid::N_Vector, funjac::FunJac)
    funjac.u = unsafe_wrap(Vector{Float64}, __N_VGetArrayPointer_Serial(u),length(funjac.u))
    _u = funjac.u
    funjac.du = unsafe_wrap(Vector{Float64}, __N_VGetArrayPointer_Serial(du),length(funjac.du))
    _du = funjac.du
    funjac.resid = unsafe_wrap(Vector{Float64}, __N_VGetArrayPointer_Serial(resid),length(funjac.resid))
    _resid = funjac.resid
    funjac.fun(_resid, _du, _u, funjac.p, t)
    return IDA_SUCCESS
end

function idajac(t::realtype,
                cj::realtype,
                u::N_Vector,
                du::N_Vector,
                res::N_Vector,
                J::SUNMatrix,
                funjac::AbstractFunJac{Nothing},
                tmp1::N_Vector,
                tmp2::N_Vector,
                tmp3::N_Vector)

    funjac.u = unsafe_wrap(Vector{Float64}, __N_VGetArrayPointer_Serial(u),length(funjac.u))
    _u = funjac.u
    funjac.du = unsafe_wrap(Vector{Float64}, __N_VGetArrayPointer_Serial(du),length(funjac.du))
    _du = funjac.du

    funjac.jac(convert(Matrix, J), _du,
               _u, funjac.p, cj, t)
    return IDA_SUCCESS
end

function idajac(t::realtype,
                cj::realtype,
                u::N_Vector,
                du::N_Vector,
                res::N_Vector,
                _J::SUNMatrix,
                funjac::AbstractFunJac{<:SparseMatrixCSC},
                tmp1::N_Vector,
                tmp2::N_Vector,
                tmp3::N_Vector)

  jac_prototype = funjac.jac_prototype
  J = convert(SparseMatrixCSC,_J)

  funjac.u = unsafe_wrap(Vector{Float64}, __N_VGetArrayPointer_Serial(u),length(funjac.u))
  _u = funjac.u
  funjac.du = unsafe_wrap(Vector{Float64}, __N_VGetArrayPointer_Serial(du),length(funjac.du))
  _du = funjac.du

  funjac.jac(jac_prototype, _du, convert(Vector, _u), funjac.p, cj, t)
  J.nzval .= jac_prototype.nzval
  # Sundials resets the value pointers each time, so reset it too
  @. J.rowval = jac_prototype.rowval - 1
  @. J.colptr = jac_prototype.colptr - 1

  return IDA_SUCCESS
end

function massmat(t::Float64,
                 _M::SUNMatrix,
                 mmf::AbstractFunJac,
                 tmp1::N_Vector,
                 tmp2::N_Vector,
                 tmp3::N_Vector)
  if typeof(mmf.mass_matrix) <: Array
    M = convert(Matrix, _M)
  else
    M = convert(SparseMatrixCSC, _M)
  end
  M .= mmf.mass_matrix

  return IDA_SUCCESS
end

function jactimes(v::N_Vector,
                 Jv::N_Vector,
                 t::Float64,
                 y::N_Vector,
                 fy::N_Vector,
                 fj::AbstractFunJac,
                 tmp::N_Vector)
    DiffEqBase.update_coefficients!(fj.jac_prototype,y,fj.p,t)
    mul!(convert(Vector,Jv),fj.jac_prototype,convert(Vector,v))
    return CV_SUCCESS
end

function idajactimes(
                 t::Float64,
                 y::N_Vector,
                 fy::N_Vector,
                 r::N_Vector,
                 v::N_Vector,
                 Jv::N_Vector,
                 cj::Float64,
                 fj::AbstractFunJac,
                 tmp1::N_Vector,
                 tmp2::N_Vector)
    DiffEqBase.update_coefficients!(fj.jac_prototype,y,fj.p,t)
    mul!(convert(Vector,Jv),fj.jac_prototype,convert(Vector,v))
    return IDA_SUCCESS
end

function precsolve(t::Float64,
                   y::N_Vector,
                   fy::N_Vector,
                   r::N_Vector,
                   z::N_Vector,
                   gamma::Float64,
                   delta::Float64,
                   lr::Int,
                   fj::AbstractFunJac)
    fj.prec(convert(Vector,z),convert(Vector,r),fj.p,t,convert(Vector,y),convert(Vector,fy),gamma,delta,lr)
    return CV_SUCCESS
end

function precsetup(t::Float64,
                   y::N_Vector,
                   fy::N_Vector,
                   jok::Int,
                   jcurPtr::Ref{Int},
                   gamma::Float64,
                   fj::AbstractFunJac)
    fj.psetup(fj.p,t,convert(Vector,y),convert(Vector,fy),jok==1,Base.unsafe_wrap(Vector{Int}, jcurPtr, 1),gamma)
    return CV_SUCCESS
end

function idaprecsolve(t::Float64,
                   y::N_Vector,
                   fy::N_Vector,
                   resid::N_Vector,
                   r::N_Vector,
                   z::N_Vector,
                   gamma::Float64,
                   delta::Float64,
                   lr::Int,
                   fj::AbstractFunJac)
    fj.prec(convert(Vector,z),convert(Vector,r),fj.p,t,convert(Vector,y),convert(Vector,fy),convert(Vector,resid),gamma,delta,lr)
    return IDA_SUCCESS
end

function idaprecsetup(t::Float64,
                      y::N_Vector,
                      fy::N_Vector,
                      rr::N_Vector,
                      gamma::Float64,
                      fj::AbstractFunJac)
    fj.psetup(fj.p,t,convert(Vector,rr),convert(Vector,y),convert(Vector,fy),gamma)
    return IDA_SUCCESS
end


# About PML setup
#
# - Let's define the computational domain as the region where we perform computation, and
# the physical domain where actual physical phenomena are simulated.  The physical domain is
# the computational domain outside PML, because PML is an artificial region that is
# introduced to absorb physical waves.
#
# Currently, we specify the size of the computational domain first, and then specify PML
# such that it "eats in" the computational domain to leave the physical domain.  This means
# you always need to define the computational domain larger than the physical domain by the
# PML thickness.
#
# This seems inconvenient.  It may seem more convenient to specify the physical domain first,
# specify the PML thickness, and then construct the computational domain by attaching PML
# to the physical domain.  That way, we would be able to turn on and off the absorbing
# boundary without changing the size of the physical domain of interest.  Also, increasing
# the PML thickness would be done simply by increasing the PML thickness, wherease with the
# current method it requires increasing the computational domain as well to keep the size of
# the physical domain the same.
#
# However, there is a reason for the current method.  One of the most popular use cases of
# PML is the simulation of an infinite waveguide, where we need to define the waveguide
# inside PML.  Therefore, we cannot "hide" PML from the users: we have to let the users put
# objects inside PML.  In that case, it is better to let the users specify the size of the
# region including PML, because that way the users would know how deep they should place the
# waveguide.
#
# You way think that you hide PML from the users by automatically constructing the material
# parameters inside PML, by taking the material parameters on the boundary of the physical
# domain and extending it homogeneously into PML.  However, such a homogeneous extension is
# not always what we want.  For example, to construct PML for a phothonic crystal waveguide,
# we have to specify the photonic crystal structure inside PML.  In that case the material
# composition inside PML is not simply the homogeneous extension of their values at the PML
# boundary
#
# - Another question that could be raised about the current method is the way to set the
# PML thickness.  Currently we specify the number of grid cells inside PML and multiply the
# cell size ∆l to figure out the PML thickness, rather than specifying the PML thickness and
# divide it by ∆l to figure out the number of grid cells inside PML.
#
# The latter look like a better method, because the reflectance of PML is a function of the
# PML loss parameter and thickness.  Therefore, for a given PML loss parameter, the
# reflectance does not change with the spatial resolution of the grid if the PML thickness
# is maintained.  Conversely, if we fix the number of grid cells inside PML, the PML
# thickness changes with the spacial resolution, so the PML loss parameter needs to change
# as well to achieve the same PML reflectance.
#
# However, in my experience, when using iterative methods to solve Maxwell's equations, the
# number of grid cells inside PML affects the convergence of iterative methods significantly,
# regardless of the spatial resolution.  For example, when I doubled the spatial resolution
# while keeping the PML thickness and therefore doubled the number of grid cells inside PML,
# the iterative methods no longer converged.  On the other hand, even if I doubled the
# spatial resolution, if I keep the number of grid cells inside PML to halve the PML
# thickness, I still got convergence.
#
# Of course, I have to use a stronger PML loss parameter to achieve the same reflectance
# with a thinner PML, and if we use a too strong loss parameter, the discretized PML model
# deviates from the continuous model and we get stronger reflectance than the target value.
# Still, I found that the reflectance remains pretty low, and the convergence of iterative
# methods was more important.  So, I decided to specify the number of grid cells inside PML
# rather than the PML thickness.  This decision may change in the future.

export PMLParam
export create_stretched_∆l

mutable struct PMLParam
    m::Float  # degree of polynomial grading for σ and κ
    R::Float  # R: target reflection coefficient of PML
    κmax::Float  # maximum of real stretch factor
    amax::Float  # maximum of complex frequency shift
    ma::Float  # degree of polynomial grading for a
    PMLParam() = new(4.0, exp(-16), 1.0, 0.0, 4.0)
end

function create_stretched_∆l(ω::Number, ∆l::Tuple2{AbsVecReal}, l::Tuple2{AbsVecReal},
                             bounds::Tuple2{Real}, Npml::Tuple2{Integer}, pml::PMLParam=PMLParam())
    s∆l = create_stretched_∆l(ω, tuple.(∆l), tuple.(l), SVec{1}.(bounds), SVec{1}.(Npml), pml)

    return (s∆l[nPR][1], s∆l[nDL][1])
end

# Wrapper for converting AbstractVector's to SVec's
create_stretched_∆l(ω::Number, grid::Grid{K}, Npml::Tuple2{AbsVecInteger}, pml::PMLParam=PMLParam()) where {K} =
    create_stretched_∆l(ω, grid, SVec{K}.(Npml), pml)

create_stretched_∆l(ω::Number, ∆l::Tuple2{NTuple{K,AbsVecReal}}, l::Tuple2{NTuple{K,AbsVecReal}},
                    bounds::Tuple2{AbsVecReal}, Npml::Tuple2{AbsVecInteger}, pml::PMLParam=PMLParam()) where {K} =
    create_stretched_∆l(ω, ∆l, l, SVec{K}.(bounds), SVec{K}.(Npml), pml)

# Create vectors containig a stretched version of a given ∆l.
create_stretched_∆l(ω::Number, grid::Grid{K}, Npml::Tuple2{SInteger{K}}, pml::PMLParam=PMLParam()) where {K} =
    create_stretched_∆l(ω, grid.∆l, grid.l, grid.bounds, Npml, pml)

function create_stretched_∆l(ω::Number,  # angular frequency
                             ∆l::Tuple2{NTuple{K,AbsVecReal}},  # grid point spacings at primal and dual grid point locations
                             l::Tuple2{NTuple{K,AbsVecReal}},  # locations of primal and dual grid points w/o ghost points
                             bounds::Tuple2{SReal{K}},  # bounds[NEG][k] = boundary of domain at (-) end in k-direction
                             Npml::Tuple2{SInteger{K}},  # Npml[NEG][k] = number of cells inside PML at (-) end in k-direction
                             pml::PMLParam=PMLParam()  # PML parameters
                             ) where {K}
    lpml, Lpml = pml_loc(l[nPR], bounds, Npml)
    sfactor = create_sfactor(ω, l, lpml, Lpml, pml)

    s∆lprim = map((x,y)->x.*y, sfactor[nPR], ∆l[nPR])
    s∆ldual = map((x,y)->x.*y, sfactor[nDL], ∆l[nDL])

    s∆l = (s∆lprim, s∆ldual)

    return s∆l
end

# Wrapper for converting AbstractVector's to SVec's
create_sfactor(ω::Number, l::Tuple2{NTuple{K,AbsVecReal}}, lpml::Tuple2{AbsVecReal},
               Lpml::Tuple2{AbsVecReal}, pml::PMLParam=PMLParam()) where {K} =
    create_sfactor(ω, l, SVec{K}.(lpml), SVec{K}.(Lpml), pml)

# Create vectors of stretching factors.
function create_sfactor(ω::Number,  # angular frequency
                        l::Tuple2{NTuple{K,AbsVecReal}},  # locations of primal and dual grid points
                        lpml::Tuple2{SReal{K}},  # locations of PML interfaces
                        Lpml::Tuple2{SReal{K}},  # thicknesses of PML
                        pml::PMLParam=PMLParam()  # PML parameters
                        ) where {K}
    N = length.(l[nPR])  # (Nx, Ny, Nz)
    sfactor = (ones.(ComplexF,N), ones.(ComplexF,N))
    for k = 1:K
        lpmlₙ, lpmlₚ = lpml[nN][k], lpml[nP][k]
        Lpmlₙ, Lpmlₚ = Lpml[nN][k], Lpml[nP][k]
        for ngt = nPD
            r = l[ngt][k]
            s = sfactor[ngt][k]
            for i = 1:N[k]
                r[i] < lpmlₙ && (s[i] = calc_sfactor(ω, lpmlₙ-r[i], Lpmlₙ, pml))
                r[i] > lpmlₚ && (s[i] = calc_sfactor(ω, r[i]-lpmlₚ, Lpmlₚ, pml))
            end
        end
    end

    return sfactor
end

# Calculate the stretch factor at a given depth into PML.
function calc_sfactor(ω::Number,  # angular frequency
                      d::Real,  # depth into PML
                      Lpml::Real,  # thickness of PML
                      pml::PMLParam)  # PML parameters
    σmax = -(pml.m+1) * log(pml.R) /2Lpml  # -(m+1) ln(R) / (2 η Lpml), where η = 1 in units of η₀
    σ = σmax * (d/Lpml)^pml.m
    κ = 1 + (pml.κmax-1) * (d/Lpml)^pml.m
    a = pml.amax * (1 - d/Lpml)^pml.ma

    s_factor = κ + σ / (a + im*ω)  # s = κ + σ/(a + i ω ε), where ε = 1 in units of ε₀
end

# Wrapper for converting AbstractVector's to SVec's
pml_loc(lprim::NTuple{K,AbsVecReal}, bounds::Tuple2{AbsVecReal}, Npml::Tuple2{AbsVecInteger}) where {K} =
    pml_loc(lprim, SVec{K}.(bounds), SVec{K}.(Npml))

# Get the PML interface locations.
# The primal grid plane locations *without* the ghost plane locations are passed, so the
# positive-end boundaries of the computational domain cannot be retrieved from them.
# Therefore, the boundary information is passed separately.
function pml_loc(lprim::NTuple{K,AbsVecReal},  # lprim[k] = primal vertex locations in k-direction (excluding ghost location at (+) end)
                 bounds::Tuple2{SReal{K}},  # bounds[NEG][k] = boundary of domain at (-) end in k-direction
                 Npml::Tuple2{SInteger{K}}  # Npml[NEG][k] = number of cells inside PML at (-) end in k-direction
                 ) where {K}
    # Below, if Npml[nP] = 0, lprim[end+1-Npml[nP]] = lprim[end+1] is out of bound.  Because
    # lprim[end+1] points to the ghost point, we return the positive-end boundary in this
    # case.
    lpml = (map((v,n)->v[1+n], lprim, Npml[nN]), map((v,b,n)->(n==0 ? b : v[end+1-n]), lprim, bounds[nP], Npml[nP]))  # Tuple2{SFloat{K}}
    Lpml = (lpml[nN]-bounds[nN], bounds[nP]-lpml[nP])  # Tuple2{SFloat{K}}

    return lpml, Lpml
end


@testset "dict to dataset roundtrip" begin
    rng = MersenneTwister(42)
    J = 8
    K = 6
    nchains = 4
    ndraws = 500
    vars = Dict(
        "a" => randn(rng, nchains, ndraws),
        "b" => randn(rng, nchains, ndraws, J, K)
    )
    coords = Dict("bi" => 1:J, "bj" => 1:K)
    dims = Dict("b" => ["bi", "bj"])
    attrs = Dict("mykey" => 5)

    ds = ArviZ.dict_to_dataset(vars; coords = coords, dims = dims, attrs = attrs)
    vars2, kwargs = ArviZ.dataset_to_dict(ds)
    for (k, v) in vars
        @test k ∈ keys(vars2)
        @test vars2[k] ≈ v
    end
    @test kwargs.coords == coords
    @test kwargs.dims == dims
    for (k, v) in attrs
        @test k ∈ keys(kwargs.attrs)
        @test kwargs.attrs[k] == v
    end
end


# Copyright 2017 Eric S. Tellez
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

using Random

export montecarlo, kfolds

function montecarlo(evalfun::Function, X::Vector{ItemType}, y::Vector{LabelType}; runs::Int=3, trainratio=0.7, testratio=0.3) where {ItemType, LabelType}
    Z = zip(X, y) |> collect
    R = Float64[]
    for i in 1:runs
        shuffle!(Z)
        sep = floor(Int, length(Z) * trainratio)
        last = min(length(Z), sep + 1 + floor(Int, length(Z) * testratio))
        train = @view Z[1:sep]
        test = @view Z[1+sep:last]
        s = evalfun([z[1] for z in train], [z[2] for z in train], [z[1] for z in test], [z[2] for z in test])
        push!(R, s)
    end

    return R
end

function kfolds(evalfun::Function, X::Vector{ItemType}, y::Vector{LabelType}; folds::Int=3, shuffle=true) where {ItemType, LabelType}
    Z = zip(X, y) |> collect
    if shuffle
        shuffle!(Z)
    end
    R = Float64[]
    bsize = floor(Int, length(Z) / folds)
    begin
        test = @view Z[1:bsize]
        train = @view Z[bsize+1:end]
        s = evalfun([z[1] for z in train], [z[2] for z in train], [z[1] for z in test], [z[2] for z in test])
        push!(R, s)
    end

    for i in 1:folds-1
        train = vcat(Z[1:bsize*i], Z[bsize*(i+1)+1:end])
        test = @view Z[bsize*i+1:bsize*(i+1)]
        s = evalfun([z[1] for z in train], [z[2] for z in train], [z[1] for z in test], [z[2] for z in test])
        push!(R, s)
    end

    return R
end


"""
Overview: structures and functions associated to mesh
import and management.
"""

"""
> mutable struct Domain
- Sen : number of supported elements
- Set : type of supported elements
- Senn : number of nodes of the supported elements
- ne : number of elements of each type in the domain
- e2n : volume element connectivity
- mat : material associated to the domain
"""
mutable struct Domain
  #
  Sen::Int64
  Set::Vector{Symbol}
  Senn::Vector{Int64}
  eskip::Vector{Int64}
  ne::Vector{Int64}
  e2n::Vector{Int64}
  mat::material
  #
  Domain() = new()
end

"""
> mutable struct Boundary
- Sen : number of supported elements
- Set : type of supported elements
- Senn : number of nodes of the supported elements
- ne : number of elements of each type in the domain
- e2n : surface element connectivity
- bcdofs : which dofs are constrained
- bcvals : value associated to the boundary condition
"""
mutable struct Boundary
  #
  Sen::Int64
  Set::Vector{Symbol}
  Senn::Vector{Int64}
  eskip::Vector{Int64}
  ne::Vector{Int64}
  e2n::Vector{Int64}
  bcdofs::Vector{Int64}
  bcvals::Vector{Float64}
  #
  Boundary() = new()
  #
end

"""
> mutable struct grid
- netype : number of supperted types of elements
- etype : array that stores the number of nodes for each element
- nn : number of nodes
- nΩ : number of domains
- nΓ : number of boundaries
- n2c : nodes coordinates
- Ω : domains
- Γ : boundaries
- tcn : total cells number
- grid() : empty initialization method
"""
mutable struct Grid
  #
  nn::Int64
  nΩ::Int64
  nΓ::Int64
  #
  n2c::Vector{Float64}
  Ω::Vector{Domain}
  Γ::Vector{Boundary}
  #
  tcn::Int64
  tnn::Int64
  #
  Grid() = new()
  #
end

"""
> read_mesh( mesh_file::String, Ω_list::Vector{Vector{Int64}}, Γ_list::Vector{Vector{Int64}}, mat::Vector{material}, bc_dof::Vector{Vector{Int64}}, bc_vals::Vector{Vector{Float64}})
It reads the mesh file and it inializes the grid data structure
- mesh_file : name of the mesh file to read
- Ω_list : list of domains in which the structures is organised
- Γ_list : list of boundaries
- mat : list of materials associated to each domain
- bc_dof : list of constrained degrees of freedom of each boundary
- bc_vals : boundary condition values
"""
function read_mesh(mesh_file::String, Ω_list,
                   Γ_list, mat::Vector{String},
                   bc_dof::Vector{Vector{Int64}}, bc_vals::Vector{Vector{Float64}})
  mesh = Grid()
  if contains(mesh_file,".med")
    import_mesh!(mesh,mesh_file,Ω_list,Γ_list,mat,bc_dof,bc_vals,Val{:MED})
  elseif contains(mesh_file,".mphtxt")
    import_mesh!(mesh,mesh_file,Ω_list,Γ_list,mat,bc_dof,bc_vals,Val{:MPHTXT})
  else
    println("Mesh format not supported.")
  end
  return mesh
end

"""
> get_coor!(mesh::Grid, conn::Vector{Int64}, X::Vector{Float64}, nn::Int64)
it retrieves element nodes coordinates.
- mesh : Grid
- conn : connectivity of the element
- X : coordinates of the nodes in {x₁, y₁, z₁, ..., xₙ, yₙ, zₙ} format
- nn : number of nodes
"""
function get_coor!(mesh::Grid, conn::Vector{Int64}, X::Vector{Float64}, nn::Int64)
  #
  @inbounds for i = 1:nn
    X[1+(i-1)*dim:i*dim] = mesh.n2c[1+(conn[i]-1)*dim:conn[i]*dim]
  end
  #
  return nothing
end

"""
> get_coor!(mesh::Grid, conn::SubArray{Int64, 1, Vector{Int64}, Tuple{UnitRange{Int64}}, true}, X::Vector{Float64}, nn::Int64)
it retrieves element nodes coordinates.
- mesh : Grid
- conn : connectivity of the element
- X : coordinates of the nodes in {x₁, y₁, z₁, ..., xₙ, yₙ, zₙ} format
- nn : number of nodes
"""
function get_coor!(mesh::Grid, conn::SubArray{Int64, 1, Vector{Int64}, Tuple{UnitRange{Int64}}, true},
                   X::Vector{Float64}, nn::Int64)
  #
  @inbounds for i = 1:nn
    X[1+(i-1)*dim:i*dim] = mesh.n2c[1+(conn[i]-1)*dim:conn[i]*dim]
  end
  #
  return nothing
end

function import_mesh!(mesh::Grid, mesh_file::String,
                      Ω_list,
                      Γ_list,
                      mat::Vector{String}, bc_dof::Vector{Vector{Int64}},
                      bc_vals::Vector{Vector{Float64}}, ::Type{Val{:MPHTXT}})
  #
  # it instantiates a mesh_type structure from a COMSOL .mphtxt file
  #
  local neT6,e2nT6,e2gT6
  local neQ9,e2nQ9,e2gQ9
  local neT10,e2nT10,e2gT10
  local neP18,e2nP18,e2gP18
  local neH27,e2nH27,e2gH27
  #
  fname = "./mesh/"*mesh_file
  # extract mesh infos
  open(fname,"r") do fhand
    while !eof(fhand)
      line = readline(fhand)
      if (contains(line,"# number of mesh vertices"))
        line = split(line)
        mesh.nn = Meta.parse(line[1])
        mesh.n2c = Vector{Float64}(undef,mesh.nn*dim)
      elseif (contains(line,"# Mesh vertex coordinates"))
        for i = 1:mesh.nn
          line = split(readline(fhand))
          for j = 1:dim
            mesh.n2c[j+(i-1)*dim] = Meta.parse(line[j])
          end
        end
      elseif (contains(line,"tri2 # type name"))
        line = readline(fhand)
        line = readline(fhand)
        line = readline(fhand)
        line = split(readline(fhand))
        neT6 = Meta.parse(line[1])
        line = readline(fhand)
        e2nT6 = Vector{Int64}(undef,neT6*TR6n)
        e2gT6 = Vector{Int64}(undef,neT6)
        for i = 1:neT6
          line = split(readline(fhand))
          for j = 1:TR6n
            e2nT6[j+(i-1)*TR6n] = Meta.parse(line[j])+1
          end
        end
        while (true)
          line = readline(fhand)
          if (contains(line,"# Geometric entity indices"))
            for i = 1:neT6
              line = readline(fhand)
              e2gT6[i] = Meta.parse(line)+1
            end
            break
          end
        end
      elseif (contains(line,"quad2 # type name"))
        line = readline(fhand)
        line = readline(fhand)
        line = readline(fhand)
        line = split(readline(fhand))
        neQ9 = Meta.parse(line[1])
        line = readline(fhand)
        e2nQ9 = Vector{Int64}(undef,neQ9*QU9n)
        e2gQ9 = Vector{Int64}(undef,neQ9)
        for i = 1:neQ9
          line = split(readline(fhand))
          for j = 1:QU9n
            e2nQ9[j+(i-1)*QU9n] = Meta.parse(line[j])+1
          end
        end
        while (true)
          line = readline(fhand)
          if (contains(line,"# Geometric entity indices"))
            for i = 1:neQ9
              line = readline(fhand)
              e2gQ9[i] = Meta.parse(line)+1
            end
            break
          end
        end
      elseif (contains(line,"tet2 # type name"))
        line = readline(fhand)
        line = readline(fhand)
        line = readline(fhand)
        line = split(readline(fhand))
        neT10 = Meta.parse(line[1])
        line = readline(fhand)
        e2nT10 = Vector{Int64}(undef,neT10*T10n)
        e2gT10 = Vector{Int64}(undef,neT10)
        for i = 1:neT10
          line = split(readline(fhand))
          for j = 1:T10n
            e2nT10[j+(i-1)*T10n] = Meta.parse(line[j])+1
          end
        end
        while (true)
          line = readline(fhand)
          if (contains(line,"# Geometric entity indices"))
            for i = 1:neT10
              line = readline(fhand)
              e2gT10[i] = Meta.parse(line)
            end
            break
          end
        end
      elseif (contains(line,"prism2 # type name"))
        line = readline(fhand)
        line = readline(fhand)
        line = readline(fhand)
        line = split(readline(fhand))
        neP18 = Meta.parse(line[1])
        line = readline(fhand)
        e2nP18 = Vector{Int64}(undef,neP18*P18n)
        e2gP18 = Vector{Int64}(undef,neP18)
        for i = 1:neP18
          line = split(readline(fhand))
          for j = 1:P18n
            e2nP18[j+(i-1)*P18n] = Meta.parse(line[j])+1
          end
        end
        while (true)
          line = readline(fhand)
          if (contains(line,"# Geometric entity indices"))
            for i = 1:neP18
              line = readline(fhand)
              e2gP18[i] = Meta.parse(line)
            end
            break
          end
        end
      elseif (contains(line,"hex2 # type name"))
        line = readline(fhand)
        line = readline(fhand)
        line = readline(fhand)
        line = split(readline(fhand))
        neH27 = Meta.parse(line[1])
        line = readline(fhand)
        e2nH27 = Vector{Int64}(undef,neH27*H27n)
        e2gH27 = Vector{Int64}(undef,neH27)
        for i = 1:neH27
          line = split(readline(fhand))
          for j = 1:H27n
            e2nH27[j+(i-1)*H27n] = Meta.parse(line[j])+1
          end
        end
        while (true)
          line = readline(fhand)
          if (contains(line,"# Geometric entity indices"))
            for i = 1:neH27
              line = readline(fhand)
              e2gH27[i] = Meta.parse(line)
            end
            break
          end
        end
      end
    end
  end
  # if var is not defined and assign default value
  if (!@isdefined e2gT6)
    neT6 = 0
    e2gT6 = [0]
  end
  if (!@isdefined e2gQ9)
    neQ9 = 0
    e2gQ9 = [0]
  end
  if (!@isdefined e2gT10)
    neT10 = 0
    e2gT10 = [0]
  end
  if (!@isdefined e2gP18)
    neP18 = 0
    e2gP18 = [0]
  end
  if (!@isdefined e2gH27)
    neH27 = 0
    e2gH27 = [0]
  end

  nVol = maximum([maximum(e2gT10), maximum(e2gP18), maximum(e2gH27)])
  nSurf = maximum([maximum(e2gT6), maximum(e2gQ9)])

  surf = Array{Int64}(undef,nSurf)
  vol  = Array{Int64}(undef,nVol)
  fill!(surf,-1)
  fill!(vol,-1)

  mesh.nΩ = size(Ω_list)[1]
  mesh.nΓ = size(Γ_list)[1]

  for i = 1:mesh.nΩ
    for j = 1:size(Ω_list[i])[1]
      ith_b = Ω_list[i][j]
      vol[ith_b] = i
    end
  end

  for i = 1:mesh.nΓ
    for j = 1:size(Γ_list[i])[1]
      ith_b = Γ_list[i][j]
      surf[ith_b] = i
    end
  end

  neΓT6  = zeros(Int64,mesh.nΓ)
  neΓQ9  = zeros(Int64,mesh.nΓ)
  neΩT10 = zeros(Int64,mesh.nΩ)
  neΩW18 = zeros(Int64,mesh.nΩ)
  neΩH27 = zeros(Int64,mesh.nΩ)

  # number of nodes for supported surface elements
  Se_nn = [TR3n, TR6n, QU4n, QU8n, QU9n]
  #Set_Γ = [:TR3, :TR6, :QU4, :QU8, :QU9]
  # number of nodes for supported volume elements
  Ve_nn = [TE4n, T10n, HE8n, H20n, H27n, PE6n, P15n, P18n]
  #Set_Ω = [:TE4, :T10, :HE8, :H20, :H27, :PE6, :P15, :P18]
  #
  neΩ = zeros(Int64,(size(Ve_nn)[1], mesh.nΩ ))
  neΓ = zeros(Int64,(size(Se_nn)[1], mesh.nΓ ))

  for i = 1:neT6
    ith_s = e2gT6[i]
    if (surf[ith_s]>0)
      neΓ[2,surf[ith_s]] += 1
    end
  end

  for i = 1:neQ9
    ith_s = e2gQ9[i]
    if (surf[ith_s]>0)
      neΓ[5,surf[ith_s]] += 1
    end
  end

  for i = 1:neT10
    ith_s = e2gT10[i]
    if (vol[ith_s]>0)
      neΩ[2,vol[ith_s]] += 1
    end
  end

  for i = 1:neP18
    ith_s = e2gP18[i]
    if (vol[ith_s]>0)
      neΩ[7,vol[ith_s]] += 1
    end
  end

  for i = 1:neH27
    ith_s = e2gH27[i]
    if (vol[ith_s]>0)
      neΩ[5,vol[ith_s]] += 1
    end
  end
  #
  mesh.Ω = [Domain() for i in 1:mesh.nΩ]
  mesh.Γ = [Boundary() for i = 1:mesh.nΓ]
  # fill mesh data structure
  for b = 1:mesh.nΓ
    iΓ = mesh.Γ[b]
    #
    iΓ.Sen = size(Se_nn)[1]
    iΓ.Set = [:TR3, :TR6, :QU4, :QU8, :QU9]
    iΓ.Senn = Se_nn
    iΓ.ne = neΓ[:,b]
    iΓ.bcdofs = bc_dof[b]
    iΓ.bcvals = bc_vals[b]
    iΓ.eskip = zeros(Int64,iΓ.Sen)
    nn = 0
    for j = 1:iΓ.Sen-1
      nn += neΓ[j,b]*Se_nn[j]
      iΓ.eskip[j+1] = nn
    end
    nn += neΓ[end,b]*Se_nn[end]
    iΓ.e2n = Vector{Int64}(undef,nn)
  end
  #
  for d = 1:mesh.nΩ
    iΩ = mesh.Ω[d]
    #
    iΩ.Sen = size(Ve_nn)[1]
    iΩ.Set = [:TE4, :T10, :HE8, :H20, :H27, :PE6, :P15, :P18]
    iΩ.Senn = Ve_nn
    iΩ.ne = neΩ[:,d]
    iΩ.mat = MORFE_load_material(mat[d])
    iΩ.eskip = zeros(Int64,iΩ.Sen)
    #
    nn = 0
    for j = 1:iΩ.Sen-1
      nn += neΩ[j,d]*Ve_nn[j]
      iΩ.eskip[j+1] = nn
    end
    nn += neΩ[end,d]*Ve_nn[end]
    iΩ.e2n = Vector{Int64}(undef,nn)
  end
  # fill boundaries connectivity
  for b = 1:mesh.nΓ
    iΓ = mesh.Γ[b]
    #
    skip = iΓ.eskip[2]+1
    for e = 1:neT6
      ith_s = e2gT6[e]
      ith_s = surf[ith_s]
      if (ith_s==b)
        iΓ.e2n[skip:skip+TR6n-1] = e2nT6[1+(e-1)*TR6n:e*TR6n]
        skip += TR6n
      end
    end
    #
    skip = iΓ.eskip[5]+1
    for e = 1:neQ9
      ith_s = e2gQ9[e]
      ith_s = surf[ith_s]
      if (ith_s==b)
        iΓ.e2n[skip:skip+QU9n-1] = e2nQ9[1+(e-1)*QU9n:e*QU9n]
        skip += QU9n
      end
    end
    #
  end
  #
  for d = 1:mesh.nΩ
    iΩ = mesh.Ω[d]
    #
    skip = iΩ.eskip[2]
    for e = 1:neT10
      ith_s = e2gT10[e]
      ith_s = vol[ith_s]
      #
      if (ith_s==d)
        iΩ.e2n[skip+1]  =  e2nT10[1+(e-1)*T10n]
        iΩ.e2n[skip+2]  =  e2nT10[2+(e-1)*T10n]
        iΩ.e2n[skip+3]  =  e2nT10[4+(e-1)*T10n]
        iΩ.e2n[skip+4]  =  e2nT10[3+(e-1)*T10n]
        iΩ.e2n[skip+5]  =  e2nT10[5+(e-1)*T10n]
        iΩ.e2n[skip+6]  =  e2nT10[9+(e-1)*T10n]
        iΩ.e2n[skip+7]  =  e2nT10[8+(e-1)*T10n]
        iΩ.e2n[skip+8]  =  e2nT10[7+(e-1)*T10n]
        iΩ.e2n[skip+9]  =  e2nT10[6+(e-1)*T10n]
        iΩ.e2n[skip+10] = e2nT10[10+(e-1)*T10n]
        skip += T10n
      end
      #
    end
    #
    skip = iΩ.eskip[5]+1
    for e = 1:neH27
      ith_s = e2gH27[e]
      ith_s = vol[ith_s]
      if (ith_s==d)
        iΩ.e2n[skip:skip+H27n-1] = e2nH27[1+(e-1)*H27n:e*H27n]
        skip += H27n
      end
    end
    #
    skip = iΩ.eskip[7]
    for e = 1:neP18
      ith_s = e2gP18[e]
      ith_s = vol[ith_s]
      if (ith_s==d)
        iΩ.e2n[skip+1]  =  e2nP18[1+(e-1)*P18n]
        iΩ.e2n[skip+2]  =  e2nP18[2+(e-1)*P18n]
        iΩ.e2n[skip+3]  =  e2nP18[3+(e-1)*P18n]
        iΩ.e2n[skip+4]  =  e2nP18[4+(e-1)*P18n]
        iΩ.e2n[skip+5]  =  e2nP18[5+(e-1)*P18n]
        iΩ.e2n[skip+6]  =  e2nP18[6+(e-1)*P18n]
        iΩ.e2n[skip+7]  =  e2nP18[7+(e-1)*P18n]
        iΩ.e2n[skip+8]  =  e2nP18[9+(e-1)*P18n]
        iΩ.e2n[skip+9]  =  e2nP18[8+(e-1)*P18n]
        iΩ.e2n[skip+10] = e2nP18[16+(e-1)*P18n]
        #
        iΩ.e2n[skip+11] = e2nP18[18+(e-1)*P18n]
        iΩ.e2n[skip+12] = e2nP18[17+(e-1)*P18n]
        iΩ.e2n[skip+13] = e2nP18[10+(e-1)*P18n]
        iΩ.e2n[skip+14] = e2nP18[12+(e-1)*P18n]
        iΩ.e2n[skip+15] = e2nP18[15+(e-1)*P18n]
        #iΩ.e2n[skip+16] = e2nP18[11+(e-1)*P18n]
        #iΩ.e2n[skip+17] = e2nP18[13+(e-1)*P18n]
        #iΩ.e2n[skip+18] = e2nP18[14+(e-1)*P18n]
        #
        skip += P15n
      end
    end
    #
  end
  #
  return nothing
  #
end

"""
> import_mesh!( mesh::Grid, mesh_file::String, Ω_list::Vector{Vector{Int64}}, Γ_list::Vector{Vector{Int64}}, mat::Vector{material}, bc_dof::Vector{Vector{Int64}}, bc_vals::Vector{Vector{Float64}}, ::Type{Val{:MED}})
It reads a MED mesh format
- mesh_file : name of the mesh file
- Ω_list : list of domains in which the structures is organised
- Γ_list : list of boundaries
- mat : list of materials associated to each domain
- bc_dof : list of constrained degrees of freedom of each boundary
- bc_vals : boundary condition values
- :MED : specializes function to read MED mesh format
"""
function import_mesh!(mesh::Grid, mesh_file::String,
                      Ω_list,
                      Γ_list,
                      mat::Vector{String}, bc_dof::Vector{Vector{Int64}},
                      bc_vals::Vector{Vector{Float64}}, ::Type{Val{:MED}})
  #
  pth = joinpath(".","mesh",mesh_file)
  #
  MeshFile = aster_read_mesh(pth)
  # mesh nodes
  nodes = MeshFile["nodes"]
  mesh.nn = length(nodes)
  mesh.n2c = Vector{Float64}(undef,mesh.nn*dim)
  for i = 1:mesh.nn
    for j = 1:dim
      mesh.n2c[j+(i-1)*dim] = nodes[i][j]
    end
  end
  # allocate domains and boundaries
  mesh.nΩ = size(Ω_list)[1]
  mesh.nΓ = size(Γ_list)[1]
  mesh.Ω = [Domain() for i ∈ 1:mesh.nΩ]
  mesh.Γ = [Boundary() for i ∈ 1:mesh.nΓ]
  #
  Elements = MeshFile["elements"]
  ElementTypes = MeshFile["element_types"]
  ElementGroups = MeshFile["element_sets"]
  #
  neΓ = zeros(Int64,(Snse,mesh.nΓ))
  neΩ = zeros(Int64,(Vnse,mesh.nΩ))
  #
  for i = 1:mesh.nΓ
    sname = Γ_list[i]
    selem = ElementGroups[sname]
    for j = 1:size(selem)[1]
      etype = ElementTypes[selem[j]]
      if (etype == :TR3)
        neΓ[1,i] += 1
      elseif (etype == :TR6)
        neΓ[2,i] += 1
      elseif (etype == :QU4)
        neΓ[3,i] += 1
      elseif (etype == :QU8)
        neΓ[4,i] += 1
      elseif (etype == :QU9)
        neΓ[5,i] += 1
      else
        println("Warning, element not supported")
      end
    end
  end
  #
  for i = 1:mesh.nΩ
    vname = Ω_list[i]
    velem = ElementGroups[vname]
    for j = 1:size(velem)[1]
      etype = ElementTypes[velem[j]]
      if (etype == :TE4)
        neΩ[1,i] += 1
      elseif (etype == :T10)
        neΩ[2,i] += 1
      elseif (etype == :HE8)
        neΩ[3,i] += 1
      elseif (etype == :H20)
        neΩ[4,i] += 1
      elseif (etype == :H27)
        neΩ[5,i] += 1
      elseif (etype == :PE6)
        neΩ[6,i] += 1
      elseif (etype == :P15)
        neΩ[7,i] += 1
      elseif (etype == :P18)
        neΩ[8,i] += 1
      else
        println("Warning, element not supported")
      end
    end
  end
  #
  # number of nodes for supported surface elements
  Se_nn = [TR3n, TR6n, QU4n, QU8n, QU9n]
  # number of nodes for supported volume elements
  Ve_nn = [TE4n, T10n, HE8n, H20n, H27n, PE6n, P15n, P18n]
  # allocate connectivity
  for i = 1:mesh.nΩ
    iΩ = mesh.Ω[i]
    #
    iΩ.Sen = size(Ve_nn)[1]
    iΩ.Set = [:TE4, :T10, :HE8, :H20, :H27, :PE6, :P15, :P18]
    iΩ.Senn = Ve_nn
    iΩ.ne = neΩ[:,i]
    iΩ.mat = MORFE_load_material(mat[i])
    iΩ.eskip = zeros(Int64,iΩ.Sen)
    #
    nn = 0
    for j = 1:iΩ.Sen-1
      nn += neΩ[j,i]*Ve_nn[j]
      iΩ.eskip[j+1] = nn
    end
    nn += neΩ[end,i]*Ve_nn[end]
    iΩ.e2n = Vector{Int64}(undef,nn)
  end
  #
  for i = 1:mesh.nΓ
    iΓ = mesh.Γ[i]
    #
    iΓ.Sen = size(Se_nn)[1]
    iΓ.Set = [:TR3, :TR6, :QU4, :QU8, :QU9]
    iΓ.Senn = Se_nn
    iΓ.ne = neΓ[:,i]
    iΓ.bcdofs = bc_dof[i]
    iΓ.bcvals = bc_vals[i]
    iΓ.eskip = zeros(Int64,iΓ.Sen)
    #
    nn = 0
    for j = 1:iΓ.Sen-1
      nn += neΓ[j,i]*Se_nn[j]
      iΓ.eskip[j+1] = nn
    end
    nn += neΓ[end,i]*Se_nn[end]
    iΓ.e2n = Vector{Int64}(undef,nn)
  end
  # fill boundaries with mesh connectivity
  for i = 1:mesh.nΓ
    iΓ = mesh.Γ[i]
    sname = Γ_list[i]
    selem = ElementGroups[sname]
    ecount = zeros(Int64,size(Se_nn)[1] )
    for j = 1:size(selem)[1]
      etype = ElementTypes[selem[j]]
      if (etype == :TR3)
        skip = iΓ.eskip[1]+ecount[1]*Se_nn[1]
        iΓ.e2n[1+skip:skip+Se_nn[1]] = Elements[selem[j]]
        ecount[1] += 1
      elseif (etype == :TR6)
        skip = iΓ.eskip[2]+ecount[2]*Se_nn[2]
        iΓ.e2n[1+skip:skip+Se_nn[2]] = Elements[selem[j]]
        ecount[2] += 1
      elseif (etype == :QU4)
        skip = iΓ.eskip[3]+ecount[3]*Se_nn[3]
        iΓ.e2n[1+skip:skip+Se_nn[3]] = Elements[selem[j]]
        ecount[3] += 1
      elseif (etype == :QU8)
        skip = iΓ.eskip[4]+ecount[4]*Se_nn[4]
        iΓ.e2n[1+skip:skip+Se_nn[4]] = Elements[selem[j]]
        ecount[4] += 1
      elseif (etype == :QU9)
        skip = iΓ.eskip[5]+ecount[5]*Se_nn[5]
        iΓ.e2n[1+skip:skip+Se_nn[5]] = Elements[selem[j]]
        ecount[5] += 1
      else
        println("Warning, element not supported")
      end
    end
    # fill domains with mesh connectivity
    for i = 1:mesh.nΩ
      iΩ = mesh.Ω[i]
      sname = Ω_list[i]
      velem = ElementGroups[sname]
      ecount = zeros(Int64,size(Ve_nn)[1] )
      for j = 1:size(velem)[1]
        etype = ElementTypes[velem[j]]
        if (etype == :TE4)
          skip = iΩ.eskip[1]+ecount[1]*Ve_nn[1]
          iΩ.e2n[1+skip:skip+Ve_nn[1]] = Elements[velem[j]]
          ecount[1] += 1
        elseif (etype == :T10)
          skip = iΩ.eskip[2]+ecount[2]*Ve_nn[2]
          iΩ.e2n[1+skip:skip+Ve_nn[2]] = Elements[velem[j]]
          ecount[2] += 1
        elseif (etype == :HE8)
          skip = iΩ.eskip[3]+ecount[3]*Ve_nn[3]
          iΩ.e2n[1+skip:skip+Ve_nn[3]] = Elements[velem[j]]
          ecount[3] += 1
        elseif (etype == :H20)
          skip = iΩ.eskip[4]+ecount[4]*Ve_nn[4]
          iΩ.e2n[1+skip:skip+Ve_nn[4]] = Elements[velem[j]]
          ecount[4] += 1
        elseif (etype == :H27)
          skip = iΩ.eskip[5]+ecount[5]*Ve_nn[5]
          iΩ.e2n[1+skip:skip+Ve_nn[5]] = Elements[velem[j]]
          ecount[5] += 1
        elseif (etype == :PE6)
          skip = iΩ.eskip[6]+ecount[6]*Ve_nn[6]
          iΩ.e2n[1+skip:skip+Ve_nn[6]] = Elements[velem[j]]
          ecount[6] += 1
        elseif (etype == :P15)
          skip = iΩ.eskip[7]+ecount[7]*Ve_nn[7]
          iΩ.e2n[1+skip:skip+Ve_nn[7]] = Elements[velem[j]]
          ecount[7] += 1
        elseif (etype == :P18)
          skip = iΩ.eskip[8]+ecount[8]*Ve_nn[8]
          iΩ.e2n[1+skip:skip+Ve_nn[8]] = Elements[velem[j]]
          ecount[8] += 1
        else
          println("Warning, element not supported")
        end
      end
    end
  end
  #
  mesh.tcn = 0
  mesh.tnn = 0
  for i = 1:mesh.nΩ
    iΩ = mesh.Ω[i]
    for se = 1:iΩ.Sen
      mesh.tcn += iΩ.ne[se]
      mesh.tnn += iΩ.ne[se]*iΩ.Senn[se]
    end
  end
  #
  return nothing
end


# Test the inf_alleles() function from src/Evolve.jl
# to run from the test folder:  julia -p 4 -L CGP.jl test_inf_alleles.jl

#include("../src/CGP.jl")
include("../src/Inf_alleles.jl")
include("../src/Propsel.jl")
using Main.CGP
MyInt = Main.CGP.MyInt

nreps = 4
numinputs = 3
numoutputs = 2
numints = 10
levsback = 10
popsize = 5:5:20
max_pop_gens = 50000
ngoals = 8   # number goals in goallist
tourn_size = 0
#println("pwd: ",pwd())
# The following lines allow this file to included from CGP.jl, CGP.jl/test, and CGP.jl/src.
cwd = pwd()
if cwd[end-5:end] == "CGP.jl"
  csvfile = "$cwd/test/data/test_inf_alleles.csv"
else
  csvfile = "../test/data/test_inf_alleles.csv"
end
#println("csvfile: ",csvfile)
#inf_alleles( p, popsize, max_pop_gens, gl, tourn_size=tourn_size )
df = run_inf_alleles(nreps,numinputs,numoutputs,numints,ngoals,levsback,popsize,max_pop_gens,tourn_size,csvfile)


# Simulate a 2D actomyosin network
# Alex Tam, 12/10/2020

# Simulate network
"Control function for actomyosin network simulations"
function actomyosin_network(parN, parA, parM)
    # Specify domain width
    Lxx::Float64 = 2.0; Lxy::Float64 = 0; Lyx::Float64 = 0; Lyy::Float64 = 2.0; # Actual domain widths
    # Pre-allocate
    Force = [[0.0, 0.0, 0.0, 0.0] for idx in 1:parN.nT]; # [pN] Network force
    Bulk_Stress = [0.0 for idx in 1:parN.nT]; # Bulk stress
    Motor_Pos = [0.0 for idx in 1:parN.nT]; # Bulk stress
    Curvature = [0.0 for idx in 1:parN.nT]; # Mean filament curvature
    Index = [0.0 for idx in 1:parN.nT]; # Two-filament index
    Theta = [0.0 for idx in 1:parN.nT]; # Angle between filaments
    # Generate initial conditions
    state = State{Float64}(Vector{Vector{Vector}}(), Vector{Vector}()); # Initialise empty State struct
    mm = Vector{Myosin_Motor}(); # Pre-allocate empty myosin motors
    af, state = actin_ic(state, parN, parA, Lxx, Lyy); # Initialise actin filaments
    xl = intersection_search(state, af, mm); # Initialise cross-links
    mm, xl, state = myosin_ic(state, mm, parN, xl, Lxx, Lxy, Lyx, Lyy); # Initialise myosin motors
    state_old = state; # Store initial state to compute force
    # Time-stepping
    animation = @animate for i = 1:parN.nT
        # Compute force and draw network
        Force[i] = network_force(state, state_old, af, xl, mm, parN, parA, parM, Lxx, Lxy, Lyx, Lyy);
        draw_network(state, af, mm, parN, parA, Force[i], Lxx, Lxy, Lyx, Lyy);
        savefig("2f-$i.svg"); # Save image
        # Spatial statistics
        if parA.nSeg > 1
            Curvature[i] = curvature(af, state, Lxx, Lxy, Lyx, Lyy); # Compute curvature at current time step
        else
            Curvature[i] = 0;
        end
        Index[i], Theta[i] = two_filament_index(mm, state, Lxx, Lxy, Lyx, Lyy); # Compute two-filament index at current time step
        Bulk_Stress[i] = 0.5*(Force[i][1]/Lyy + Force[i][4]/Lxx);
        Motor_Pos[i] = 0.5*(state.mp[1][1]+state.mp[1][2]);
        # Compute solution
        if i != parN.nT # Ensure correct looping sequence
            # Turnover and polymerisation
            af = segment_translations(state, af); # Update filament translations
            state_old = state; # Store current state for energy functional
            # Compute network solution
            new_dof = optimise_network(state_old, af, mm, parN, parA, parM, Lxx, Lxy, Lyx, Lyy);
            state = build_state(new_dof, af, mm); # Construct State from vector
        end
    end
    # Output
    gif(animation, "2f.gif", fps = 50) # Save .gif of the network
    # Plot quantities versus time
    draw_stress(parN, Force, parN.nT, Lxx, Lyy); savefig("2f_stress.svg"); # Stress components
    draw_bulk_stress(parN, Force, parN.nT, Lxx, Lyy); savefig("2f_bulk_stress.svg"); # Bulk stress
    draw_angle(parN, Theta, parN.nT); savefig("2f_angle.svg"); # Angle
    # Write data to files
    writedlm("2f_angle.csv", Theta);
    writedlm("2f_index.csv", Index);
    writedlm("2f_bulk_stress.csv", Bulk_Stress);
    writedlm("2f_motor_pos.csv", Motor_Pos);
    # Compute time-integrated statistics
    Curvature_Int, Index_Int = integrated_statistics(parN, Curvature, Index);
    return state, af, mm, Force, Curvature_Int, Index_Int
end


using NeuralVerification
using Test

macro no_error(ex)
    quote
        try $(esc(ex))
            true
        catch
            false
        end
    end
end

at = @__DIR__

# Small MNIST Network

mnist_small = read_nnet("$at/../examples/networks/mnist_small.nnet")

# entry 23 in MNIST datset
input_center = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,121,254,136,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,13,230,253,248,99,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,118,253,253,225,42,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,61,253,253,253,74,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,32,206,253,253,186,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,211,253,253,239,69,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,254,253,253,133,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,142,255,253,186,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,149,229,254,207,21,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,54,229,253,254,105,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,152,254,254,213,26,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,112,251,253,253,26,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,29,212,253,250,149,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,36,214,253,253,137,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,75,253,253,253,59,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,93,253,253,189,17,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,224,253,253,84,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,43,235,253,126,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,99,248,253,119,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,225,235,49,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
output_center = [-1311.1257826380004,4633.767704436501,-654.0718535670002,-1325.349417307,1175.2361184373997,-1897.8607293569007,-470.3405972940001,830.8337987382,-377.7467076115001,572.3674015264198]

in_epsilon = 1 #0-255
out_epsilon = 10 #logit domain

input_low = input_center .- in_epsilon
input_high = input_center .+ in_epsilon

output_low = output_center .- out_epsilon
output_high = output_center .+ out_epsilon

inputSet = Hyperrectangle(low=input_low, high=input_high)
outputSet = Hyperrectangle(low=output_low, high=output_high)

problem_hyperrect_small = Problem(mnist_small, inputSet, outputSet)
solver_reluVal = ReluVal(max_iter = 2)

@test @no_error solve(solver_reluVal, problem_hyperrect_small)

solver_reluplex=Reluplex()
@test @no_error solve(solver_reluplex, problem_hyperrect_small)

# Deep MNIST network
mnist_large = read_nnet("$at/../examples/networks/mnist_large.nnet")

#mnist_large = read_nnet("/Users/christopherlazarus/Desktop/jsand/NNet/readNNet/python/mnist_large.nnet")

# entry 23 in MNIST datset
input_center = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,121,254,136,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,13,230,253,248,99,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,118,253,253,225,42,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,61,253,253,253,74,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,32,206,253,253,186,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,211,253,253,239,69,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,254,253,253,133,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,142,255,253,186,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,149,229,254,207,21,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,54,229,253,254,105,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,152,254,254,213,26,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,112,251,253,253,26,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,29,212,253,250,149,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,36,214,253,253,137,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,75,253,253,253,59,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,93,253,253,189,17,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,224,253,253,84,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,43,235,253,126,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,99,248,253,119,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,225,235,49,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
output_center = [131.8781755,134.8987015,141.6166255,158.34307,129.8803525,104.8286425,98.64196,133.6358395,131.1716215,105.10621]

in_epsilon = 1 #0-255
out_epsilon = 10 #logit domain

input_low = input_center .- in_epsilon
input_high = input_center .+ in_epsilon

output_low = output_center .- out_epsilon
output_high = output_center .+ out_epsilon

inputSet = Hyperrectangle(low=input_low, high=input_high)
outputSet = Hyperrectangle(low=output_low, high=output_high)

problem_hyperrect_deep = Problem(mnist_large, inputSet, outputSet)
solver_reluVal = ReluVal(max_iter = 2)

@test @no_error solve(solver_reluVal, problem_hyperrect_deep)

solver_reluplex=Reluplex()
@test @no_error solve(solver_reluplex, problem_hyperrect_deep)

# Wide MNIST network

mnist_wide = read_nnet("$at/../examples/networks/mnist-1-100.nnet")
input_center = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,121,254,136,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,13,230,253,248,99,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,118,253,253,225,42,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,61,253,253,253,74,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,32,206,253,253,186,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,211,253,253,239,69,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,254,253,253,133,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,142,255,253,186,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,149,229,254,207,21,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,54,229,253,254,105,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,152,254,254,213,26,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,112,251,253,253,26,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,29,212,253,250,149,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,36,214,253,253,137,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,75,253,253,253,59,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,93,253,253,189,17,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,224,253,253,84,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,43,235,253,126,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,99,248,253,119,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,225,235,49,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
output_center = [-1219.6781446362043,2713.019467032775,-3826.9579099907123,-3306.084501236369,2641.9250628243003,-1829.0475478735848,-2477.0765438475523,84.03007306734946,-2072.196780418711,145.0876643197917]

in_epsilon = 1 #0-255
out_epsilon = 10 #logit domain

input_low = input_center .- in_epsilon
input_high = input_center .+ in_epsilon

output_low = output_center .- out_epsilon
output_high = output_center .+ out_epsilon

inputSet = Hyperrectangle(low=input_low, high=input_high)
outputSet = Hyperrectangle(low=output_low, high=output_high)

problem_hyperrect_wide = Problem(mnist_wide, inputSet, outputSet)
solver_reluVal = ReluVal(max_iter = 2)

@test @no_error solve(solver_reluVal, problem_hyperrect_wide)

solver_reluplex=Reluplex()
@test @no_error solve(solver_reluplex, problem_hyperrect_wide)


using JuMP, Gurobi, OneClassSampling, SVDD, OneClassActiveLearning
include(joinpath(@__DIR__, "config.jl"))

experiment_name="warmup"

data_dirs = ["Parkinson"]

solver = with_optimizer(Gurobi.Optimizer; OutputFlag=0, Threads=1)
quality_metrics = Dict(:mcc => matthews_corr, :kappa => cohens_kappa, :f1 => f1_score)

threshold_strategies = [
    GroundTruthThresholdStrategy(),
    OutlierPercentageThresholdStrategy(),
    OutlierPercentageThresholdStrategy(0.05),
]

model_init_strategy = Dict{Symbol, Any}(
                            :svdd => Dict{Symbol, Any}(
                                :ROT_CTax => SVDD.SimpleCombinedStrategy(SVDD.RuleOfThumbScott(), SVDD.BoundedTaxErrorEstimate(0.05, 0.02, 0.98)),
                                :ROT_C1 => SVDD.SimpleCombinedStrategy(SVDD.RuleOfThumbScott(), SVDD.FixedCStrategy(1)),
                            ),
                            :sample_qe => Dict{Symbol, Any}(
                                :threshold_strategies => threshold_strategies
                        ))
NUM_RERUNS = 1
KDE_GAMMA = :scott

baselines = [
    RandomRatioSampler(0.1),
    HDS(0.1, KDE_GAMMA, OutlierPercentageThresholdStrategy())
]

competitors = [
    BPS(nothing, 0.05),
    DAEDS(30, 0.1, 0.3),
    DBSRSVDD(7, 0.3),
    HSR(5, 0.01),
    IESRSVDD(KDE_GAMMA, 0.5),
    FBPE(360),
    KFNCBD(100, 0.2),
    NDPSR(20, 10)
]

density_strategies = [
    RAPID(OutlierPercentageThresholdStrategy(), KDE_GAMMA),
]

sampling_strategies = Vector{Dict{Symbol, Any}}()

for b in baselines
    push!(sampling_strategies, Dict{Symbol, Any}(
        :method => b,
        :seed => 0
    ))
end

for c in competitors
    push!(sampling_strategies, Dict{Symbol, Any}(
        :method => c,
        :seed => 0
    ))
end

for t in threshold_strategies
    push!(sampling_strategies, Dict{Symbol, Any}(
        :method => PreFilteringWrapper(HSR(5, 0.1), KDE_GAMMA, t),
        :seed => 0
    ))
end

for d in density_strategies
    push!(sampling_strategies, Dict{Symbol, Any}(
        :method => d,
        :seed => 0
    ))
end