import Base: getindex, setindex!
#===============================================================================
                            SETTING VALUES
===============================================================================#
# Translate indices TS understands into indices Arrays can understand (Int Arrays)
getrows{V,T}(x::TS{V,T}, r::Int)                    = r
getrows{V,T}(x::TS{V,T}, r::T)                      = find(x.index .== r)
getrows{V,T}(x::TS{V,T}, r::AbstractArray{Int,1})   = r
getrows{V,T}(x::TS{V,T}, r::Vector{Bool})           = (@assert size(x,1)==length(r); find(r))
getrows{V,T}(x::TS{V,T}, r::BitArray{1})            = (@assert size(x,1)==length(r); find(r))
getrows{V,T}(x::TS{V,T}, r::Vector{T})              = find(overlaps(x.index, r))

getcols{V,T}(x::TS{V,T}, c::Int)                    = c
getcols{V,T}(x::TS{V,T}, c::Symbol)                 = find(x.fields .== c)
getcols{V,T}(x::TS{V,T}, c::AbstractArray{Int,1})   = c
getcols{V,T}(x::TS{V,T}, c::Vector{Bool})           = (@assert size(x,2)==length(c); find(c))
getcols{V,T}(x::TS{V,T}, c::BitArray{1})            = (@assert size(x,2)==length(c); find(c))
getcols{V,T}(x::TS{V,T}, c::Vector{Symbol})         = find(overlaps(x.fields, c))

# Ensure no bounds are violated before calling `setindex!`
function checkdims(x::TS, v; r=1:size(x,1), c=1:size(x,2))
    @assert size(v,2) == length(c)
    @assert size(v,1) == length(r)
    @assert size(x,1) >= length(r)
    @assert size(x,2) >= length(c)
    if size(x,2)==1
        @assert length(c)==1
    else
        @assert size(x,2)>=length(c)
    end
end

# All rows + all columns
setindex!{V,T}(x::TS{V,T}, v, ::Colon)                                          = (checkdims(x,v); x.values=v)
setindex!{V,T}(x::TS{V,T}, v, ::Colon, ::Colon)                                 = (checkdims(x,v); x.values=v)
# Row subset + all columns
setindex!{V,T}(x::TS{V,T}, v, r::Int, ::Colon)                                  = (checkdims(x,v,r=r); x.values[getrows(x,r),:]=v)
setindex!{V,T}(x::TS{V,T}, v, r::T, ::Colon)                                    = (checkdims(x,v,r=r); x.values[getrows(x,r),:]=v)
setindex!{V,T}(x::TS{V,T}, v, r::AbstractArray{Int,1}, ::Colon)                 = (checkdims(x,v,r=r); x.values[getrows(x,r),:]=v)
setindex!{V,T}(x::TS{V,T}, v, r::Vector{Bool}, ::Colon)                         = (checkdims(x,v,r=r); x.values[getrows(x,r),:]=v)
setindex!{V,T}(x::TS{V,T}, v, r::Vector{T}, ::Colon)                            = (checkdims(x,v,r=r); x.values[getrows(x,r),:]=v)
setindex!{V,T}(x::TS{V,T}, v, r::Int)                                           = (checkdims(x,v,r=r); x.values[getrows(x,r),:]=v)
setindex!{V,T}(x::TS{V,T}, v, r::T)                                             = (checkdims(x,v,r=r); x.values[getrows(x,r),:]=v)
setindex!{V,T}(x::TS{V,T}, v, r::AbstractArray{Int,1})                          = (checkdims(x,v,r=r); x.values[getrows(x,r),:]=v)
setindex!{V,T}(x::TS{V,T}, v, r::Vector{Bool})                                  = (checkdims(x,v,r=r); x.values[getrows(x,r),:]=v)
setindex!{V,T}(x::TS{V,T}, v, r::Vector{T})                                     = (checkdims(x,v,r=r); x.values[getrows(x,r),:]=v)
# Column subset + all rows
setindex!{V,T}(x::TS{V,T}, v, ::Colon, c::Int)                                  = (checkdims(x,v,c=c); x.values[:,getcols(x,c)]=v)
setindex!{V,T}(x::TS{V,T}, v, ::Colon, c::Symbol)                               = (checkdims(x,v,c=c); x.values[:,getcols(x,c)]=v)
setindex!{V,T}(x::TS{V,T}, v, ::Colon, c::AbstractArray{Int,1})                 = (checkdims(x,v,c=c); x.values[:,getcols(x,c)]=v)
setindex!{V,T}(x::TS{V,T}, v, ::Colon, c::Vector{Bool})                         = (checkdims(x,v,c=c); x.values[:,getcols(x,c)]=v)
setindex!{V,T}(x::TS{V,T}, v, ::Colon, c::Vector{Symbol})                       = (checkdims(x,v,c=c); x.values[:,getcols(x,c)]=v)
# Row subset + column subset
setindex!{V,T}(x::TS{V,T}, v, r::Int, c::Int)                                   = (checkdims(x,v,r=r,c=c); x.values[getrows(x,r),getcols(x,c)]=v)
setindex!{V,T}(x::TS{V,T}, v, r::Int, c::Symbol)                                = (checkdims(x,v,r=r,c=c); x.values[getrows(x,r),getcols(x,c)]=v)
setindex!{V,T}(x::TS{V,T}, v, r::Int, c::AbstractArray{Int,1})                  = (checkdims(x,v,r=r,c=c); x.values[getrows(x,r),getcols(x,c)]=v)
setindex!{V,T}(x::TS{V,T}, v, r::Int, c::Vector{Bool})                          = (checkdims(x,v,r=r,c=c); x.values[getrows(x,r),getcols(x,c)]=v)
setindex!{V,T}(x::TS{V,T}, v, r::Int, c::Vector{Symbol})                        = (checkdims(x,v,r=r,c=c); x.values[getrows(x,r),getcols(x,c)]=v)

setindex!{V,T}(x::TS{V,T}, v, r::AbstractArray{Int,1}, c::Int)                  = (checkdims(x,v,r=r,c=c); x.values[getrows(x,r),getcols(c)]=v)
setindex!{V,T}(x::TS{V,T}, v, r::AbstractArray{Int,1}, c::Symbol)               = (checkdims(x,v,r=r,c=c); x.values[getrows(x,r),getcols(c)]=v)
setindex!{V,T}(x::TS{V,T}, v, r::AbstractArray{Int,1}, c::AbstractArray{Int,1}) = (checkdims(x,v,r=r,c=c); x.values[getrows(x,r),getcols(c)]=v)
setindex!{V,T}(x::TS{V,T}, v, r::AbstractArray{Int,1}, c::Vector{Bool})         = (checkdims(x,v,r=r,c=c); x.values[getrows(x,r),getcols(c)]=v)
setindex!{V,T}(x::TS{V,T}, v, r::AbstractArray{Int,1}, c::Vector{Symbol})       = (checkdims(x,v,r=r,c=c); x.values[getrows(x,r),getcols(c)]=v)

setindex!{V,T}(x::TS{V,T}, v, r::Vector{Bool}, c::Int)                          = (checkdims(x,v,r=r,c=c); x.values[getrows(x,r),getcols(c)]=v)
setindex!{V,T}(x::TS{V,T}, v, r::Vector{Bool}, c::Symbol)                       = (checkdims(x,v,r=r,c=c); x.values[getrows(x,r),getcols(c)]=v)
setindex!{V,T}(x::TS{V,T}, v, r::Vector{Bool}, c::AbstractArray{Int,1})         = (checkdims(x,v,r=r,c=c); x.values[getrows(x,r),getcols(c)]=v)
setindex!{V,T}(x::TS{V,T}, v, r::Vector{Bool}, c::Vector{Bool})                 = (checkdims(x,v,r=r,c=c); x.values[getrows(x,r),getcols(c)]=v)
setindex!{V,T}(x::TS{V,T}, v, r::Vector{Bool}, c::Vector{Symbol})               = (checkdims(x,v,r=r,c=c); x.values[getrows(x,r),getcols(c)]=v)

setindex!{V,T}(x::TS{V,T}, v, r::Vector{T}, c::Int)                             = (checkdims(x,v,r=r,c=c); x.values[getrows(x,r),getcols(c)]=v)
setindex!{V,T}(x::TS{V,T}, v, r::Vector{T}, c::Symbol)                          = (checkdims(x,v,r=r,c=c); x.values[getrows(x,r),getcols(c)]=v)
setindex!{V,T}(x::TS{V,T}, v, r::Vector{T}, c::AbstractArray{Int,1})            = (checkdims(x,v,r=r,c=c); x.values[getrows(x,r),getcols(c)]=v)
setindex!{V,T}(x::TS{V,T}, v, r::Vector{T}, c::Vector{Bool})                    = (checkdims(x,v,r=r,c=c); x.values[getrows(x,r),getcols(c)]=v)
setindex!{V,T}(x::TS{V,T}, v, r::Vector{T}, c::Vector{Symbol})                  = (checkdims(x,v,r=r,c=c); x.values[getrows(x,r),getcols(c)]=v)

setindex!{V,T}(x::TS{V,T}, v, r::T, c::Int)                                     = (checkdims(x,v,r=r,c=c); x.values[getrows(x,r),getcols(c)]=v)
setindex!{V,T}(x::TS{V,T}, v, r::T, c::Symbol)                                  = (checkdims(x,v,r=r,c=c); x.values[getrows(x,r),getcols(c)]=v)
setindex!{V,T}(x::TS{V,T}, v, r::T, c::AbstractArray{Int,1})                    = (checkdims(x,v,r=r,c=c); x.values[getrows(x,r),getcols(c)]=v)
setindex!{V,T}(x::TS{V,T}, v, r::T, c::Vector{Bool})                            = (checkdims(x,v,r=r,c=c); x.values[getrows(x,r),getcols(c)]=v)
setindex!{V,T}(x::TS{V,T}, v, r::T, c::Vector{Symbol})                          = (checkdims(x,v,r=r,c=c); x.values[getrows(x,r),getcols(c)]=v)

#===============================================================================
							NUMERICAL INDEXING
===============================================================================#
getindex(x::TS) = x

getindex(x::TS, ::Colon)                            = x
getindex(x::TS, ::Colon, ::Colon)                   = x
getindex(x::TS, ::Colon, c::Int)                    = ts(x.values[:,c], x.index, [x.fields[c]])
getindex(x::TS, ::Colon, c::AbstractArray{Int,1})   = ts(x.values[:,c], x.index, x.fields[c])

getindex(x::TS, r::Int)                             = ts(x.values[r,:]', [x.index[r]], x.fields)
getindex(x::TS, r::Int, ::Colon)                    = ts(x.values[r,:]', [x.index[r]], x.fields)
getindex(x::TS, r::Int, c::Int)                     = ts([x.values[r,c]], [x.index[r]], [x.fields[c]])
getindex(x::TS, r::Int, c::AbstractArray{Int,1})    = ts(x.values[r,c]', [x.index[r]], x.fields[c])

getindex(x::TS, r::AbstractArray{Int,1})                            = ts(x.values[r,:], x.index[r], x.fields)
getindex(x::TS, r::AbstractArray{Int,1}, ::Colon)                   = ts(x.values[r,:], x.index[r], x.fields)
getindex(x::TS, r::AbstractArray{Int,1}, c::Int)                    = ts(x.values[r,c], x.index[r], [x.fields[c]])
getindex(x::TS, r::AbstractArray{Int,1}, c::AbstractArray{Int,1})   = ts(x.values[r, c], x.index[r], x.fields[c])

#===============================================================================
							BOOLEAN INDEXING
===============================================================================#
getindex(x::TS, r::BitArray{1})                             = TS(x.values[r,:], x.index[r], x.fields)
getindex(x::TS, r::BitArray{1}, ::Colon)                    = TS(x.values[r,:], x.index[r], x.fields)
getindex(x::TS, r::BitArray{1}, c::Int)                     = TS(x.values[r,c], x.index[r], x.fields[c])
getindex(x::TS, r::BitArray{1}, c::AbstractArray{Int,1})    = TS(x.values[r,c], x.index[r], x.fields[c])
getindex(x::TS, r::BitArray{1}, c::BitArray{1})             = TS(x.values[r,c], x.index[r], x.fields[c])
getindex(x::TS, r::BitArray{1}, c::Vector{Bool})            = TS(x.values[r,c], x.index[r], x.fields[c])

getindex(x::TS, r::Vector{Bool})                            = TS(x.values[r,:], x.index[r], x.fields)
getindex(x::TS, r::Vector{Bool}, ::Colon)                   = TS(x.values[r,:], x.index[r], x.fields)
getindex(x::TS, r::Vector{Bool}, c::Int)                    = TS(x.values[r,c], x.index[r], x.fields[c])
getindex(x::TS, r::Vector{Bool}, c::AbstractArray{Int,1})   = TS(x.values[r,c], x.index[r], x.fields[c])
getindex(x::TS, r::Vector{Bool}, c::Vector{Bool})           = TS(x.values[r,c], x.index[r], x.fields[c])
getindex(x::TS, r::Vector{Bool}, c::BitArray{1})            = TS(x.values[r,c], x.index[r], x.fields[c])

getindex(x::TS, r::AbstractArray{Int,1}, c::BitArray{1})    = TS(x.values[r,c], x.index[r], x.fields[c])
getindex(x::TS, r::AbstractArray{Int,1}, c::Vector{Bool})   = TS(x.values[r,c], x.index[r], x.fields[c])
getindex(x::TS, r::Int, c::BitArray{1})                     = TS(x.values[r,c], x.index[r], x.fields[c])
getindex(x::TS, r::Int, c::Vector{Bool})                    = TS(x.values[r,c], x.index[r], x.fields[c])
getindex(x::TS, ::Colon, c::BitArray{1})                    = TS(x.values[:,c], x.index[:], x.fields[c])
getindex(x::TS, ::Colon, c::Vector{Bool})                   = TS(x.values[:,c], x.index[:], x.fields[c])

getindex(x::TS, r::TS{Bool})                            = x[r.index[overlaps(r.index,x.index).*r.values]]
getindex(x::TS, r::TS{Bool}, ::Colon)                   = x[r.index[overlaps(r.index,x.index).*r.values]]
getindex(x::TS, r::TS{Bool}, c::Int)                    = x[r.index[overlaps(r.index,x.index).*r.values],c]
getindex(x::TS, r::TS{Bool}, c::AbstractArray{Int,1})   = x[r.index[overlaps(r.index,x.index).*r.values],c]
getindex(x::TS, r::TS{Bool}, c::Vector{Bool})           = x[r.index[overlaps(r.index,x.index).*r.values],c]
getindex(x::TS, r::TS{Bool}, c::BitArray{1})            = x[r.index[overlaps(r.index,x.index).*r.values],c]
getindex(x::TS, r::TS{Bool}, c::Symbol)                 = x[r.index[overlaps(r.index,x.index).*r.values],c]
getindex(x::TS, r::TS{Bool}, c::Vector{Symbol})         = x[r.index[overlaps(r.index,x.index).*r.values],c]

#===============================================================================
							TEMPORAL INDEXING
===============================================================================#
getindex(x::TS, r::TimeType)                            = x[x.index.==r]
getindex(x::TS, r::TimeType, ::Colon)                   = x[x.index.==r,:]
getindex(x::TS, r::TimeType, c::Int)                    = x[x.index.==r,c]
getindex(x::TS, r::TimeType, c::AbstractArray{Int,1})   = x[x.index.==r,c]
getindex(x::TS, r::TimeType, c::BitArray{1})            = x[x.index.==r,c]

getindex(x::TS, r::AbstractArray{Date,1})                           = x[overlaps(x.index, r)]
getindex(x::TS, r::AbstractArray{Date,1}, ::Colon)                  = x[overlaps(x.index, r), :]
getindex(x::TS, r::AbstractArray{Date,1}, c::Int)                   = x[overlaps(x.index, r), c]
getindex(x::TS, r::AbstractArray{Date,1}, c::AbstractArray{Int,1})  = x[overlaps(x.index, r), c]
getindex(x::TS, r::AbstractArray{Date,1}, c::BitArray{1})           = x[overlaps(x.index, r), c]

getindex(x::TS, r::AbstractArray{DateTime,1})                           = x[overlaps(x.index, r)]
getindex(x::TS, r::AbstractArray{DateTime,1}, ::Colon)                  = x[overlaps(x.index, r), :]
getindex(x::TS, r::AbstractArray{DateTime,1}, c::Int)                   = x[overlaps(x.index, r), c]
getindex(x::TS, r::AbstractArray{DateTime,1}, c::AbstractArray{Int,1})  = x[overlaps(x.index, r), c]
getindex(x::TS, r::AbstractArray{DateTime,1}, c::BitArray{1})           = x[overlaps(x.index, r), c]

#===============================================================================
							TEXTUAL INDEXING
===============================================================================#
getindex(x::TS, c::Symbol)          = x[:, x.fields.==c]
getindex(x::TS, c::Vector{Symbol})  = x[:, overlaps(x.fields, c)]

getindex(x::TS, ::Colon, c::Symbol)         = x[:, x.fields.==c]
getindex(x::TS, ::Colon, c::Vector{Symbol}) = x[r, overlaps(x.fields, c)]

getindex(x::TS, r::Int, c::Symbol)          = x[r, x.fields.==c]
getindex(x::TS, r::Int, c::Vector{Symbol})  = x[r, overlaps(x.fields, c)]

getindex(x::TS, r::TimeType, c::Symbol)         = x[r, x.fields.==c]
getindex(x::TS, r::TimeType, c::Vector{Symbol}) = x[r, overlaps(x.fields, c)]

getindex(x::TS, r::BitArray{1}, c::Symbol)          = x[r, x.fields.==c]
getindex(x::TS, r::Vector{Bool}, c::Symbol)         = x[r, x.fields.==c]
getindex(x::TS, r::BitArray{1}, c::Vector{Symbol})  = x[r, overlaps(x.fields, c)]
getindex(x::TS, r::Vector{Bool}, c::Vector{Symbol}) = x[r, overlaps(x.fields, c)]

getindex(x::TS, r::AbstractArray{Int,1}, c::Symbol)         = x[r, x.fields.==c]
getindex(x::TS, r::AbstractArray{Int,1}, c::Vector{Symbol}) = x[r, overlaps(x.fields, c)]

getindex(x::TS, r::AbstractArray{Date,1}, c::Symbol)            = x[r, x.fields.==c]
getindex(x::TS, r::AbstractArray{Date,1}, c::Vector{Symbol})    = x[r, overlaps(x.fields, c)]

getindex(x::TS, r::AbstractArray{DateTime,1}, c::Symbol)            = x[r, x.fields.==c]
getindex(x::TS, r::AbstractArray{DateTime,1}, c::Vector{Symbol})    = x[r, overlaps(x.fields, c)]

function thrudt(s::AbstractString, t::Vector{Date})
    n = length(s)
    if n == 4  # yyyy given
        y = parse(Int, s)
        ymd = Date(y, 12, 31)
    elseif n == 7  # yyyy-mm given
        a = split(s, '-')
        y = parse(Int, a[1])
        m = parse(Int, a[2])
        ymd = lastdayofmonth(Date(y, m, 1))
    elseif n == 10  # yyyy-mm-dd given
        a = split(s, '-')
        y = parse(Int, a[1])
        m = parse(Int, a[2])
        d = parse(Int, a[3])
        ymd = Date(y, m, d)
    else
        error("Unable to parse date string $s.")
    end
    return t .<= ymd
end
function fromdt(s::AbstractString, t::Vector{Date})
    n = length(s)
    if n == 4  # yyyy given
        y = parse(Int, s)
        ymd = Date(y, 1, 1)
    elseif n == 7  # yyyy-mm given
        a = split(s, '-')
        y = parse(Int, a[1])
        m = parse(Int, a[2])
        ymd = Date(y, m, 1)
    elseif n == 10  # yyyy-mm-dd given
        a = split(s, '-')
        y = parse(Int, a[1])
        m = parse(Int, a[2])
        d = parse(Int, a[3])
        ymd = Date(y, m, d)
    else
        error("Unable to parse date string $s.")
    end
    return t .>= ymd
end
function thisdt(s::AbstractString, t::Vector{Date})
    n = length(s)
    if n == 4  # yyyy given
        y = parse(Int, s)
        return year(t) .== y
    elseif n == 7  # yyyy-mm given
        a = split(s, '-')
        y = parse(Int, a[1])
        m = parse(Int, a[2])
        return (year(t) .== y) .* (month(t) .== m)
    elseif n == 10  # yyyy-mm-dd given
        a = split(s, '-')
        y = parse(Int, a[1])
        m = parse(Int, a[2])
        d = parse(Int, a[3])
        return (year(t) .== y) .* (month(t) .== m) .* day(t) .== d
    else
        error("Unable to parse date string $s.")
    end
end
function fromthru(from::AbstractString, thru::AbstractString, t::Vector{Date})
    fromidx = fromdt(from, t)
    thruidx = thrudt(thru, t)
    return fromidx .* thruidx
end
function thrudt(s::AbstractString, t::Vector{DateTime})
    n = length(s)
    if n == 4  # yyyy given
        y = parse(Int, s)
        ymdhms = DateTime(y, 12, 31)
    elseif n == 7  # yyyy-mm given
        a = split(s, '-')
        y = parse(Int, a[1])
        m = parse(Int, a[2])
        ymdhms = lastdayofmonth(DateTime(y, m, 1))
    elseif n == 10  # yyyy-mm-dd given
        a = split(s, '-')
        y = parse(Int, a[1])
        m = parse(Int, a[2])
        d = parse(Int, a[3])
        ymdhms = DateTime(y, m, d)
    elseif n == 13  # yyyy-mm-ddTHH given
        a = split(s, 'T')
        b = split(a[1], '-')
        c = split(a[2], ':')
        y = parse(Int, b[1])
        m = parse(Int, b[2])
        d = parse(Int, b[3])
        hr = parse(Int, c[1])
        min = 59
        sec = 59
        ymdhms = DateTime(y, m, d, hr, min, sec)
    elseif n == 16  # yyyy-mm-ddTHH:MM given
        a = split(s, 'T')
        b = split(a[1], '-')
        c = split(a[2], ':')
        y = parse(Int, b[1])
        m = parse(Int, b[2])
        d = parse(Int, b[3])
        hr = parse(Int, c[1])
        min = parse(Int, c[2])
        sec = 59
        ymdhms = DateTime(y, m, d, hr, min, sec)
    elseif n == 19  # yyyy-mm-ddTHH:MM:SS given
        a = split(s, 'T')
        b = split(a[1], '-')
        c = split(a[2], ':')
        y = parse(Int, b[1])
        m = parse(Int, b[2])
        d = parse(Int, b[3])
        hr = parse(Int, c[1])
        min = parse(Int, c[2])
        sec = parse(Int, c[3])
        ymdhms = DateTime(y, m, d, hr, min, sec)
    else
        error("Unable to parse date string $s.")
    end
    return t .<= ymdhms
end
function fromdt(s::AbstractString, t::Vector{DateTime})
    n = length(s)
    if n == 4  # yyyy given
        y = parse(Int, s)
        ymdhms = DateTime(y)
    elseif n == 7  # yyyy-mm given
        a = split(s, '-')
        y = parse(Int, a[1])
        m = parse(Int, a[2])
        ymdhms = DateTime(y, m)
    elseif n == 10  # yyyy-mm-dd given
        a = split(s, '-')
        y = parse(Int, a[1])
        m = parse(Int, a[2])
        d = parse(Int, a[3])
        ymdhms = DateTime(y, m, d)
    elseif n == 13  # yyyy-mm-ddTHH given
        a = split(s, 'T')
        b = split(a[1], '-')
        c = split(a[2], ':')
        y = parse(Int, b[1])
        m = parse(Int, b[2])
        d = parse(Int, b[3])
        hr = parse(Int, c[1])
        ymdhms = DateTime(y, m, d, hr, min, sec)
    elseif n == 16  # yyyy-mm-ddTHH:MM given
        a = split(s, 'T')
        b = split(a[1], '-')
        c = split(a[2], ':')
        y = parse(Int, b[1])
        m = parse(Int, b[2])
        d = parse(Int, b[3])
        hr = parse(Int, c[1])
        min = parse(Int, c[2])
        ymdhms = DateTime(y, m, d, hr, min, sec)
    elseif n == 19  # yyyy-mm-ddTHH:MM:SS given
        a = split(s, 'T')
        b = split(a[1], '-')
        c = split(a[2], ':')
        y = parse(Int, b[1])
        m = parse(Int, b[2])
        d = parse(Int, b[3])
        hr = parse(Int, c[1])
        min = parse(Int, c[2])
        sec = parse(Int, c[3])
        ymdhms = DateTime(y, m, d, hr, min, sec)
    else
        error("Unable to parse date string $s.")
    end
    return t .>= ymdhms
end
function thisdt(s::AbstractString, t::Vector{DateTime})
    n = length(s)
    if n == 4  # yyyy given
        y = parse(Int, s)
        return year(t) .== y
    elseif n == 7  # yyyy-mm given
        a = split(s, '-')
        y = parse(Int, a[1])
        m = parse(Int, a[2])
        return (year(t) .== y) .* (month(t) .== m)
    elseif n == 10  # yyyy-mm-dd given
        a = split(s, '-')
        y = parse(Int, a[1])
        m = parse(Int, a[2])
        d = parse(Int, a[3])
        return (year(t) .== y) .* (month(t) .== m) .* (day(t) .== d)
    elseif n == 13  # yyyy-mm-ddTHH given
        a = split(s, 'T')
        b = split(a[1], '-')
        c = split(a[2], ':')
        y = parse(Int, b[1])
        m = parse(Int, b[2])
        d = parse(Int, b[3])
        hr = parse(Int, c[1])
        return (year(t) .== y) .* (month(t) .== m) .* (day(t) .== d) .* (hour(t) .== hr)
    elseif n == 16  # yyyy-mm-ddTHH:MM given
        a = split(s, 'T')
        b = split(a[1], '-')
        c = split(a[2], ':')
        y = parse(Int, b[1])
        m = parse(Int, b[2])
        d = parse(Int, b[3])
        hr = parse(Int, c[1])
        min = parse(Int, c[2])
        return (year(t) .== y) .* (month(t) .== m) .* (day(t) .== d) .* (hour(t) .== hr) .* (minute(t) .== min)
    elseif n == 19  # yyyy-mm-ddTHH:MM:SS given
        a = split(s, 'T')
        b = split(a[1], '-')
        c = split(a[2], ':')
        y = parse(Int, b[1])
        m = parse(Int, b[2])
        d = parse(Int, b[3])
        hr = parse(Int, c[1])
        min = parse(Int, c[2])
        sec = parse(Int, c[3])
        return (year(t) .== y) .* (month(t) .== m) .* (day(t) .== d) .* (hour(t) .== hr) .* (minute(t) .== min) .* (second(t) .== sec)
    else
        error("Unable to parse date string $s.")
    end
end
function fromthru(from::AbstractString, thru::AbstractString, t::Vector{DateTime})
    fromidx = fromdt(from, t)
    thruidx = thrudt(thru, t)
    return fromidx .* thruidx
end

const RNG_DLM = '/'

function dtidx(s::AbstractString, t::Vector{Date})
    @assert !in('T', s) "Cannot index Date type with sub-daily frequency."
    bds = split(s, RNG_DLM)
    if length(bds) == 1  # single date
        return thisdt(s, t)
    elseif length(bds) == 2  # date range
        n1 = length(bds[1])
        n2 = length(bds[2])
        if n1 == 0 && n2 != 0  # thru date given
            return thrudt(bds[2], t)
        elseif n1 != 0 && n2 == 0  # from date given
            return fromdt(bds[1], t)
        elseif n1 != 0 && n2 != 0  # from and thru date given
            return fromthru(bds[1], bds[2], t)
        else
            error("Invalid indexing string: Unable to parse $s")
        end
    else
        error("Invalid indexing string: Unable to parse $s")
    end
end
function dtidx(s::AbstractString, t::Vector{DateTime})
    @assert !in('T', s) "Cannot index Date type with sub-daily frequency."
    bds = split(s, RNG_DLM)
    if length(bds) == 1  # single date
        return thisdt(s, t)
    elseif length(bds) == 2  # date range
        n1 = length(bds[1])
        n2 = length(bds[2])
        if n1 == 0 && n2 != 0  # thru date given
            return thrudt(bds[2], t)
        elseif n1 != 0 && n2 == 0  # from date given
            return fromdt(bds[1], t)
        elseif n1 != 0 && n2 != 0  # from and thru date given
            return fromthru(bds[1], bds[2], t)
        else
            error("Invalid indexing string: Unable to parse $s")
        end
    else
        error("Invalid indexing string: Unable to parse $s")
    end
end

getindex(x::TS, r::AbstractString)                          = x[dtidx(r, x.index)]
getindex(x::TS, r::AbstractString, ::Colon)                 = x[dtidx(r, x.index)]
getindex(x::TS, r::AbstractString, c::Int)                  = x[dtidx(r, x.index), c]
getindex(x::TS, r::AbstractString, c::AbstractArray{Int,1}) = x[dtidx(r, x.index), c]
getindex(x::TS, r::AbstractString, c::BitArray{1})          = x[dtidx(r, x.index), c]
getindex(x::TS, r::AbstractString, c::Symbol)               = x[dtidx(r, x.index), c]
getindex(x::TS, r::AbstractString, c::Vector{Symbol})       = x[dtidx(r, x.index), c]


# functions for io

"""
    map = read_maps(snpmapfile,qtlmapfile,qtleffectfile)

Read three map and effect files, and create a single map object, `QMSimMap`.
"""
function read_maps(snpmapfile,qtlmapfile,qtleffectfile)
   # number of chromosomes
   maxChrM = get_number_of_chromosomes(snpmapfile)
   maxChrQ = get_number_of_chromosomes(qtlmapfile)
   maxChr = max(maxChrM,maxChrQ)
   chrMap = Vector{QMSimChromosomeMap}()

   # number of markers/QTLs
   nSNP = get_number_of_loci(snpmapfile,maxChr)
   nQTL = get_number_of_loci(qtlmapfile,maxChr)
   nLoci = nSNP .+ nQTL

   # number of QTL allele
   maxna,maxAllele = get_number_of_QTL_allele(qtleffectfile,maxChr)

   # allocate mutable chromosome maps for temporary use
   maps = Vector{QMSimChromosomeMap}(undef,maxChr)
   @inbounds for i in 1:maxChr
      na = maxAllele[i]
      maps[i] = QMSimChromosomeMap(nLoci[i],nSNP[i],nQTL[i],zeros(Int,nLoci[i]),zeros(Float64,nLoci[i]),na,zeros(Int,nQTL[i]),zeros(Float64,na,nQTL[i]))
   end

   # read positions
   load_SNP_QTL_maps!(snpmapfile,qtlmapfile,maps)

   # sort positions to merge SNP and QTL loci
   for m in maps
      sort_by_position!(m.pos, m.seqQTL)
   end

   # QTL effect
   load_QTL_effect!(qtleffectfile,maps)

   # a set of maps
   return QMSimMap(maxChr,sum(nSNP),sum(nQTL),maps)
end

function get_number_of_chromosomes(mapfile)
   maxChr = 0
   open(mapfile,"r") do io
      line = readline(io)
      while !eof(io)
         line = readline(io)
         items = split(line)
         c = tryparse(Int,items[2])
         maxChr = max(maxChr,c)
      end
   end
   return maxChr
end

function get_number_of_loci(mapfile,maxChr::Int)
   nLoci = zeros(Int,maxChr)
   open(mapfile,"r") do io
      line = readline(io)
      while !eof(io)
         line = readline(io)
         items = split(line)
         c = tryparse(Int,items[2])
         nLoci[c] = nLoci[c] + 1
      end
   end
   return nLoci
end

function load_SNP_QTL_maps!(snpmapfile,qtlmapfile,maps)
   nChr = length(maps)
   nLoci = zeros(Int,nChr)
   open(snpmapfile,"r") do io
      line = readline(io)
      while !eof(io)
         line = readline(io)
         items = split(line)
         c = tryparse(Int,items[2])
         pos = tryparse(Float64,items[3])
         nLoci[c] = nLoci[c] + 1
         maps[c].pos[nLoci[c]] = pos
      end
   end
   SeqNo = 0
   open(qtlmapfile,"r") do io
      line = readline(io)
      while !eof(io)
         line = readline(io)
         items = split(line)
         c = tryparse(Int,items[2])
         pos = tryparse(Float64,items[3])
         nLoci[c] = nLoci[c] + 1
         maps[c].pos[nLoci[c]] = pos
         SeqNo = SeqNo + 1
         maps[c].seqQTL[nLoci[c]] = SeqNo
      end
   end
   return nothing
end

function sort_by_position!(pos,seqQTL)
   perm = sortperm(pos)
   pos .= pos[perm]
   seqQTL .= seqQTL[perm]
   return nothing
end

function get_number_of_QTL_allele(qtleffectfile,maxChr)
   maxna = 0
   maxAllele = zeros(Int,maxChr)
   open(qtleffectfile,"r") do io
      line = readline(io)
      while !eof(io)
         line = readline(io)
         na = Int(round((length(line)-13)/14))
         maxna = max(maxna,na)

         items = split(line)
         c = tryparse(Int,items[2])
         maxAllele[c] = max(maxAllele[c],na)
      end
   end
   return maxna,maxAllele
end

function load_QTL_effect!(qtleffectfile,maps)
   maxChr = length(maps)
   nQTL = zeros(Int,maxChr)
   open(qtleffectfile,"r") do io
      line = readline(io)
      while !eof(io)
         line = readline(io)
         items = split(line)
         c = tryparse(Int,items[2])
         na = Int(round((length(line)-13)/14))
         nQTL[c] = nQTL[c] + 1
         # <--13--><--5--><--9--><--5--><--9--><--5--><--9-->
         for i=1:na
            fst = 13 + (i-1)*14 + 5 + 1
            lst = fst + 8
            maps[c].effQTL[i,nQTL[c]] = parse(Float64,strip(line[fst:lst]))
         end
         maps[c].naQTL[nQTL[c]] = na
      end
   end
end

"""
    g = read_genotypes(snpfile,qtlfile,map)

Read a set of genotypes from files given a map structure.
"""
function read_genotypes(snpfile,qtlfile,gmap)
   if !isfile(snpfile); throw(ArgumentError("file $(snpfile) not found")); end
   if !isfile(qtlfile); throw(ArgumentError("file $(qtlfile) not found")); end

   # temporary storage
   snp = zeros(Int8, gmap.totalSNP*2)
   qtl = zeros(Int8, gmap.totalQTL*2)

   # number of animals
   nanimsnp = countlines(snpfile)-1
   nanimqtl = countlines(qtlfile)-1
   if nanimsnp != nanimqtl
      throw(ArgumentError("different number of individuals in snp and qtl files"))
   end
   if nanimsnp==0
      throw(ArgumentError("no individuals in the file"))
   end

   # initialization
   g = Vector{Vector{QMSimChromosomeGenome}}(undef,nanimsnp)

   # qtl data for individual k
   k = 0
   use_snpcode = is_snpcode_file(snpfile,gmap.totalSNP)
   if use_snpcode
      offset = get_leading_width_snpcode(snpfile)
      open(snpfile,"r") do io
         line = readline(io)
         while !eof(io)
            k = k + 1
            line = readline(io)
            text_to_snpcode!(line,gmap.totalSNP,snp,offset)
            # snp code to haplotypes
            chromosome_set = generate_chromosome_set(gmap)
            convert_snpcode_to_haplotype!(snp,gmap,chromosome_set)
            g[k] = chromosome_set
         end
      end
   else
      offset = get_leading_width_standard(snpfile)
      open(snpfile,"r") do io
         line = readline(io)
         while !eof(io)
            k = k + 1
            line = readline(io)
            text_to_code!(line,gmap.totalSNP,snp,offset)
            # snp code to haplotypes
            chromosome_set = generate_chromosome_set(gmap)
            convert_snpdata_to_haplotype!(snp,gmap,chromosome_set)
            g[k] = chromosome_set
         end
      end
   end

   # qtl data for individual k
   k = 0
   offset = get_leading_width_standard(qtlfile)
   open(qtlfile,"r") do io
      line = readline(io)
      while !eof(io)
         line = readline(io)
         text_to_code!(line,gmap.totalQTL,qtl,offset)
         # qtl code to haplotypes
         k = k + 1
         convert_qtldata_to_haplotype!(qtl,gmap,g[k])
      end
   end

   # true breeding value added to genome
   genotypes = Vector{QMSimIndividualGenome}(undef,nanimsnp)
   for i in 1:nanimsnp
      tbv = get_true_breeding_value(gmap,g[i])
      genotypes[i] = QMSimIndividualGenome(tbv,g[i])
   end

   return genotypes
end

function generate_chromosome_set(gmap)
   chromosome_set = Vector{QMSimChromosomeGenome}(undef, gmap.nchr)
   for i in 1:gmap.nchr
      chromosome_set[i] = QMSimChromosomeGenome(zeros(Int8,gmap.chr[i].nLoci),zeros(Int8,gmap.chr[i].nLoci))
   end
   return chromosome_set
end

function text_to_code!(line,totalSNPQTL,snpqtl,offset)
   # <---7/8---><-1-><-[1]-><-1-><-[1]->
   for i in 1:totalSNPQTL*2
      k = offset + (i-1)*2 + 1 + 1
      snpqtl[i] = parse(Int8,line[k:k])
   end
   return nothing
end

# offset = 7 for v1, or 8 for v2
function text_to_snpcode!(line,totalSNPQTL,snp,offset)
   # <---7/8---><-[1]-><-[1]->
   for i in 1:totalSNPQTL
      k = offset + i
      snp[i] = parse(Int8,line[k:k])
   end
   return nothing
end

function is_snpcode_file(snpfile,totalSNP)
   n = 0
   open(snpfile,"r") do io
      line = readline(io)
      while !eof(io)
         line = readline(io)
         n = length(line)
         break
      end
   end
   if n<1
      error("file '$(snpfile)' not valid")
   elseif n > 8 + totalSNP
      return false
   else
      return true
   end
end

function get_leading_width_snpcode(snpfile)
   open(snpfile,"r") do io
      line = readline(io)
      while !eof(io)
         line = readline(io)
         if line[8]==' '
            return 8   # v2
         else
            return 7   # v1
         end
      end
   end
end

function get_leading_width_standard(file)
   open(file,"r") do io
      line = readline(io)
      while !eof(io)
         line = readline(io)
         if line[9]==' '
            return 8   # v2
         else
            return 7   # v1
         end
      end
   end
end

# paternal and maternal haplotype
function convert_snpcode_to_haplotype!(snp,gmap,chromosome_set)
   # decode
   loc = 0
   @inbounds for i in 1:gmap.nchr
      nLoci = gmap.chr[i].nLoci
      for j in 1:nLoci
         # non QTL
         if gmap.chr[i].seqQTL[j]==0
            loc = loc + 1
            if snp[loc] == 0
               chromosome_set[i].gp[j] = 0
               chromosome_set[i].gm[j] = 0
            elseif snp[loc]==2
               chromosome_set[i].gp[j] = 1
               chromosome_set[i].gm[j] = 1
            elseif snp[loc]==3
               chromosome_set[i].gp[j] = 0
               chromosome_set[i].gm[j] = 1
            elseif snp[loc]==4
               chromosome_set[i].gp[j] = 1
               chromosome_set[i].gm[j] = 0
            else
               error("illegal SNP code: $(snp[loc])")
            end
         end
      end
   end
   return nothing
end

function convert_snpdata_to_haplotype!(snp,gmap,chromosome_set)
   # decode
   loc = 0
   @inbounds for i in 1:gmap.nchr
      nLoci = gmap.chr[i].nLoci
      for j in 1:nLoci
         # non QTL
         if gmap.chr[i].seqQTL[j]==0
            loc = loc + 1
            chromosome_set[i].gp[j] = snp[loc]-1
            loc = loc + 1
            chromosome_set[i].gm[j] = snp[loc]-1
         end
      end
   end
   return nothing
end

function convert_qtldata_to_haplotype!(qtl,gmap,chromosome_set)
   # decode
   loc = 0
   @inbounds for i in 1:gmap.nchr
      nLoci = gmap.chr[i].nLoci
      for j in 1:nLoci
         # QTL
         if gmap.chr[i].seqQTL[j]>0
            loc = loc + 1
            chromosome_set[i].gp[j] = qtl[loc]
            loc = loc + 1
            chromosome_set[i].gm[j] = qtl[loc]
         end
      end
   end
   return nothing
end

function get_true_breeding_value(gmap::QMSimMap,chromosome_set::Vector{QMSimChromosomeGenome})
   tbv = 0.0
   for i in 1:gmap.nchr
      qlocus = 0
      nLoci = gmap.chr[i].nLoci
      for j in 1:nLoci
         # QTL
         if gmap.chr[i].seqQTL[j]>0
            qlocus = qlocus + 1
            tbv = tbv + gmap.chr[i].effQTL[chromosome_set[i].gp[j],qlocus]
            tbv = tbv + gmap.chr[i].effQTL[chromosome_set[i].gm[j],qlocus]
         end
      end
   end
   return tbv
end

function get_true_breeding_value(gmap::QMSimMap,individual_genome::QMSimIndividualGenome)
   return get_true_breeding_value(gmap,individual_genome.chr)
end

"""
    g = read_qmsim_data(snpmapfile,qtlmapfile,qtleffectfile,snpfile,qtlfile)

Read map, QTL effect, and genotype files to give a unified structure, `g::QMSimPopulationGenome`.
"""
function read_qmsim_data(snpmapfile,qtlmapfile,qtleffectfile,snpfile,qtlfile)
   gmap = read_maps(snpmapfile,qtlmapfile,qtleffectfile)
   genotypes = read_genotypes(snpfile,qtlfile,gmap)
   return QMSimPopulationGenome(gmap, genotypes)
end

"""
    export_qmsim_individual_blupf90(map,id,individual,snpfile; append=true)

Export individual genotypes to a text file, `snpfile` with an integer code `id`, using a format supported by BLUPF90.
If the file does not exist, the function creates a new file.
If there is the same file name as `snpfile`, the funtion appends the data to existing file by default.
If you want to recreate the file, please remove the file before calling this function, or use `append=false`.
"""
function export_qmsim_individual_blupf90(gmap,id,individual,snpfile; append=true)
   # existing file
   if !append && isfile(snpfile)
      rm(snpfile,force=true)
   end
   # append
   nSNP = gmap.totalSNP
   open(snpfile,append=true) do io
      packed = pack_markrs(gmap,individual) .+ UInt8(48)
      write(io,@sprintf("%-8d",id)*String(packed)*"\n")
   end
end

# for BLUPF90 output
function pack_markrs(gmap::QMSimMap,individual::QMSimIndividualGenome)
   # storage
   packed = Vector{UInt8}(undef,gmap.totalSNP)
   fst = 1
   lst = 0
   for i in 1:gmap.nchr
      fst = lst + 1
      lst = fst + gmap.chr[i].nSNP - 1
      packed[fst:lst] = individual.chr[i].gp[ gmap.chr[i].seqQTL .==0 ]
      packed[fst:lst] .+= individual.chr[i].gm[ gmap.chr[i].seqQTL .==0 ]
   end
   return packed
end


using JANAF, Test

# Test import and functions
@testset "Import and Thermo Functions" begin
    path = "./"
    specie = ["H2O"]
    testT = 400

    @test isa(JanafDict(path,specie),Dict)
    @test isa(Cp(JanafDict(path,specie),specie[1],testT),Number)
    @test isa(S(JanafDict(path,specie),specie[1],testT),Number)
    @test isa(Hs(JanafDict(path,specie),specie[1],testT),Number)
    @test isa(Hf(JanafDict(path,specie),specie[1],testT),Number)
    @test isa(Gf(JanafDict(path,specie),specie[1],testT),Number)
    @test isa(Kf(JanafDict(path,specie),specie[1],testT),Number)

end


using Xpress, Compat.Test
using Compat.SparseArrays

@testset "IIS computation" begin
    @testset "Feasible problem, C API" begin
        # simple model:
        #    min x
        #    s.t. x >= 0
        model = Xpress.Model("lp_01", :minimize)
        add_cvar!(model, 1.0, 0.0, Inf)
        optimize(model)

        iisdata = Xpress.getfirstiis(model)

        @test iisdata.stat == 1
        @test iisdata.rownumber == 0
        @test iisdata.colnumber == 0
        @test length(iisdata.miisrow) == 0
        @test length(iisdata.miiscol) == 0
        @test length(iisdata.constrainttype) == 0
        @test length(iisdata.colbndtype) == 0
    end

    @testset "Infeasible problem, C API" begin
        # simple model:
        #    min x
        #    s.t. x >= 2 && x <= 1
        model = Xpress.Model("lp_01", :minimize)
        add_cvar!(model, 1.0, 0.0, Inf)
        add_constr!(model, [1.], '>', 2.)
        add_constr!(model, [1.], '<', 1.)
        optimize(model)

        iisdata = Xpress.getfirstiis(model)

        @test iisdata.stat == 0
        @test iisdata.rownumber == 2
        @test iisdata.colnumber == 0
        @test length(iisdata.miisrow) == 2
        @test iisdata.miisrow[1] == 0
        @test iisdata.miisrow[2] == 1
        @test length(iisdata.miiscol) == 0
        @test length(iisdata.constrainttype) == 0
        @test length(iisdata.colbndtype) == 0
    end

    @testset "Infeasible problem, more complex, C API" begin
        # simple model:
        #    min x
        #    s.t. x <= -1
        model = Xpress.Model("lp_01", :minimize)
        add_cvar!(model, 1.0, 0.0, Inf)
        add_constr!(model, [1.], '<', -1.)
        optimize(model)

        iisdata = Xpress.getfirstiis(model)

        @test iisdata.stat == 0
        @test iisdata.rownumber == 1
        @test iisdata.colnumber == 1
        @test length(iisdata.miisrow) == 1
        @test iisdata.miisrow[1] == 0
        @test length(iisdata.miiscol) == 1
        @test iisdata.miiscol[1] == 0
        @test length(iisdata.constrainttype) == 1
        @test iisdata.constrainttype[1] == UInt8('L')
        @test length(iisdata.colbndtype) == 1
        @test iisdata.colbndtype[1] == UInt8('L')
    end
end


"""
    synthesize_spectra(spec, disk; seed_rng=false, verbose=true, top=NaN)

Synthesize spectra given parameters in `spec` and `disk` instances.

# Arguments
- `spec::SpecParams`: SpecParams instance
- `disk::DiskParams`: DiskParams instance
"""
function synthesize_spectra(spec::SpecParams, disk::DiskParams;
                            seed_rng::Bool=false, verbose::Bool=true,
                            top::Float64=NaN)
    # allocate memory for synthesis
    Nλ = length(spec.lambdas)
    prof = ones(Nλ)
    outspec = zeros(Nλ, disk.Nt)

    # run the simulation (outspec modified in place)
    disk_sim(spec, disk, prof, outspec, seed_rng=seed_rng, verbose=verbose, top=top)
    return spec.lambdas, outspec
end

# line loop function, update prof in place
function line_loop(prof::AA{T,1}, mid::T, depth::T,
                   rot_shift::T, conv_blueshift::T,
                   lambdas::AA{T,1}, wsp::SynthWorkspace{T}; top::T=NaN) where T<:AF
    # first trim the bisectors to the correct depth
    trim_bisector_chop!(depth, wsp.wavt, wsp.bist, wsp.dept, wsp.widt, top=top)

    # update the line profile
    line_profile!(mid, rot_shift, conv_blueshift, lambdas, prof, wsp)
    return nothing
end

function time_loop(t_loop::Int, prof::AA{T,1}, rot_shift::T,
                   key::Tuple{Symbol, Symbol}, liter::UnitRange{Int},
                   spec::SpecParams{T}, wsp::SynthWorkspace{T}; top::T=NaN) where T<:AF
    # some assertions
    @assert all(prof .== one(T))

    # get views needed for line synthesis
    wsp.wavt .= view(spec.soldata.wav[key], :, t_loop)
    wsp.bist .= view(spec.soldata.bis[key], :, t_loop)
    wsp.dept .= view(spec.soldata.dep[key], :, t_loop)
    wsp.widt .= view(spec.soldata.wid[key], :, t_loop)

    # loop over specified synthetic lines
    for l in liter
        wsp.wavt .*= spec.variability[l]
        line_loop(prof, spec.lines[l], spec.depths[l], rot_shift,
                  spec.conv_blueshifts[l], spec.lambdas, wsp, top=top)
    end
    return nothing
end

function generate_indices(Nt::Integer, len::Integer)
    # initialize variable to get total number of indices
    sum_lens = 0

    # start at random index less than len and go to len
    start = Iterators.take(rand(1:len):len, Nt)
    sum_lens += length(start)

    # return if that's all that's needed
    if length(start) == Nt
         @assert sum_lens == Nt
        return Iterators.flatten(start)
    end

    # find out how many more cycles are needed
    niter = ceil(Int, (Nt - length(start))/len)

    # make vector of iterators and
    inds = Vector{Base.Iterators.Take{UnitRange{Int64}}}(undef, niter + 1)
    inds[1] = start
    for i in 2:niter
        inds[i] = Iterators.take(1:len, len)
        sum_lens += len
    end

    # ensure the last one only takes the the remainder
    inds[end] = Iterators.take(1:len, Nt - sum_lens)
    sum_lens += length(inds[end])
    @assert sum_lens == Nt

    # return flattened iterator
    return Iterators.flatten(inds)
end

function disk_sim(spec::SpecParams{T}, disk::DiskParams{T,Int64}, prof::AA{T,1},
                  outspec::AA{T,2}; top::T=NaN, seed_rng::Bool=false,
                  skip_times::BitVector=BitVector(zeros(disk.Nt)),
                  verbose::Bool=true) where T<:AF
    # make grid
    grid = make_grid(N=disk.N)

    # set pre-allocations and make generator that will be re-used
    outspec .= zero(T)
    wsp = SynthWorkspace(ndepths=100)
    liter = 1:length(spec.lines)

    # seeding rng
    if seed_rng
        if verbose println("Seeding RNG") end
        Random.seed!(42)
    end

    # loop over grid positions
    for i in grid
        for j in grid
            # move to next iteration if off grid
            (i^2 + j^2) > one(T) && continue

            # get input data for place on disk
            key = get_key_for_pos(i, j)
            len = spec.soldata.len[key]

            # get redshift z and norm term for location on disk
            rot_shift = patch_velocity_los(i, j, pole=disk.pole)
            norm_term = calc_norm_term(i, j, disk)

            # loop over time, starting at random epoch
            inds = generate_indices(disk.Nt, len)
            for (t, t_loop) in enumerate(inds)
                # if skip times is true, continue to next iter
                skip_times[t] && continue

                # update profile in place
                prof .= one(T)
                time_loop(t_loop, prof, rot_shift, key, liter, spec, wsp, top=top)

                # apply normalization term and add to outspec
                outspec[:,t] .+= (prof .* norm_term)
            end
        end
    end

    # set instances of outspec where skip is true to 0 and return
    outspec ./= maximum(outspec, dims=1)
    outspec[:, skip_times] .= zero(T)
    return nothing
end


mutable struct RecurMTK{T,V,S}
  cell::T
  p::V
  paramlength::Int
  state::S
end
function RecurMTK(cell; seq_len=1)
  p = initial_params(cell)
  RecurMTK(cell, p, length(p), cell.state0)
end
function (m::RecurMTK)(x, p=m.p)
  m.state, y = m.cell(m.state, x, p)
  return y
end
Base.show(io::IO, m::RecurMTK) = print(io, "RecurMTK(", m.cell, ")")
initial_params(m::RecurMTK) = m.p
paramlength(m::RecurMTK) = m.paramlength

Flux.@functor RecurMTK (p,)
Flux.trainable(m::RecurMTK) = (m.p,)

struct MTKCell{NET,SYS,PROB,SOLVER,SENSEALG,V,OP,S}
  in::Int
  out::Int
  net::NET
  sys::SYS
  prob::PROB
  solver::SOLVER
  sensealg::SENSEALG
  p::V
  paramlength::Int
  param_names::Vector{String}
  outpins::OP
	infs::Vector
  state0::S
end
function MTKCell(in::Int, out::Int, net, sys, solver, sensealg; seq_len=1)

  defs = ModelingToolkit.get_defaults(sys)
	prob = ODEProblem(sys, defs, Float32.((0,1))) # TODO: jac, sparse ???

  param_names = collect(parameters(sys))
  input_idxs = Int8[findfirst(x->contains(string(x), string(Symbol("x$(i)_InPin"))), param_names) for i in 1:in]
	param_names = param_names[in+1:end]

  outpins = [getproperty(net, Symbol("x", i, "_OutPin"), namespace=false) for i in 1:out]

  state0 = reshape(prob.u0, :, 1)
	infs = fill(Inf32, size(state0,1))

  p_ode = prob.p[in+1:end]
  @show prob.u0
  p = Float32.(vcat(p_ode, prob.u0))
  # p = p_ode

  @show param_names
  @show prob.u0
  @show size(state0)
  @show prob.f.syms
  @show length(prob.p)
  @show length(prob.p[in+1:end])
  @show input_idxs
  @show outpins
	# @show sys.states

  MTKCell(in, out, net, sys, prob, solver, sensealg, p, length(p), string.(param_names), infs, outpins, state0)
end
function (m::MTKCell)(h, xs::AbstractVecOrMat{T}, p) where {PROB,SOLVER,SENSEALG,V,T}
  # size(h) == (N,1) at the first MTKCell invocation. Need to duplicate batchsize times
  num_reps = size(xs,2)-size(h,2)+1
  hr = repeat(h, 1, num_reps)
  p_ode_l = size(p)[1] - size(hr)[1]
  p_ode = p[1:p_ode_l]
  solve_ensemble(m,hr,xs,p_ode)
end

function solve_ensemble(m, u0s, xs, p_ode, tspan=(0f0,1f0))

  batchsize = size(xs,2)
  # @show batchsize
	infs = m.infs

  function prob_func(prob,i,repeat)
    x = xs[:,i]
    u0 = u0s[:,i]
    p = vcat(x, p_ode)
    remake(prob; tspan, p, u0)
  end

  function output_func(sol,i)
    sol.retcode != :Success && return infs, false
		sol[:,end], false
  end

  ensemble_prob = EnsembleProblem(m.prob; prob_func, output_func, safetycopy=false) # TODO: safetycopy ???
  sol = solve(ensemble_prob, m.solver, EnsembleThreads(), trajectories=batchsize,
              sensealg=m.sensealg, save_everystep=false, save_start=false,
              dense = false,
	  # save_on = false,
	  # save_end = true,
	  #alias_u0 = true,
	  # calck = false
		) # TODO: saveat ?

	get_quantities_of_interest(sol, m)
end

function get_quantities_of_interest(sol::EnsembleSolution, m::MTKCell)
	s = Array(sol)
  return s, s[end-m.out+1:end, :]

  # outpins = m.outpins
	# outb = Flux.Zygote.Buffer(u0s, m.out, batchsize)
	# for j in 1:batchsize
	# 	solj = sol[j]
	# 	for i in 1:m.out
	# 		# @show outpins[j].x               # x1_OutPin₊x(t)
	# 		# @show sol[j][outpins[j].x, end]  # e.g., -0.06400018f0
	# 		# obs = observed_var_sol(m.sys, sol[j], outpins[j].x)
	# 		obs = solj[outpins[i].x,1]
	# 		# @show obs
	# 		# obs = sol[j][outpins[j].x]
	# 		outb[i,j] = obs
	# 		# @show sol[j][outpins[j].x]
	# 		# outb[i,j] = sol[j][outpins[j].x][end]
	# 	end
	# end
	# out = copy(outb)
	# return Array(sol)[:,end,:], out
end

Base.show(io::IO, m::MTKCell) = print(io, "MTKCell(", m.in, ",", m.out, ")")
initial_params(m::MTKCell) = m.p
paramlength(m::MTKCell) = m.paramlength

Flux.@functor MTKCell (p,)
Flux.trainable(m::MTKCell) = (m.p,)

reset!(m::RecurMTK, p=m.p) = (m.state = reshape(p[end-length(m.cell.state0)+1:end],:,1))

reset_state!(m::RecurMTK, p=m.p) = (m.state = reshape(p[end-size(m.cell.state0,1)+1:end], :, 1))

function get_bounds(m::RecurMTK)
  cell_lb = Float32[]
  cell_ub = Float32[]

  params = collect(parameters(m.cell.sys))[m.cell.in+1:end]
  states = collect(ModelingToolkit.states(m.cell.sys))
  for v in vcat(params,states)
		contains(string(v), "OutPin") && continue
		hasmetadata(v, VariableOutput) && continue
    lower = hasmetadata(v, VariableLowerBound) ? getmetadata(v, VariableLowerBound) : -Inf
    upper = hasmetadata(v, VariableUpperBound) ? getmetadata(v, VariableUpperBound) : Inf
    push!(cell_lb, lower)
    push!(cell_ub, upper)
  end
  return cell_lb, cell_ub
end


seed!(42)
L = 3
D = 10
N = 20
nSamples = 10
b = 5
x = rand(N, D)
y = rand(N)
@testset "Inference" begin
    i = AnalyticVI()
    @test length(i) == 1
end


module acopf
include("opfdata.jl")
# include("jumpmodel.jl")
# include("IpoptTest.jl")
using CuArrays, CUDAnative
using ForwardDiff
using TimerOutputs

export solve, model, initialPt_IPOPT, outputAll, computeAdmitances
export create_arrays, objective, constraints
export myseed!

# GPU and CPU kernels
include("kernels.jl")

mutable struct spmat{T}
  colptr::CuVector{Int64}
  rowval::CuVector{Int64}
  nzval::CuVector{T}

  function spmat{T}(colptr::Vector{Int64}, rowval::Vector{Int64}, nzval::Vector{T}) where T
    return new(CuVector{Int64}(colptr), CuVector{Int64}(rowval), CuVector{T}(nzval))
  end
end

mutable struct CompArrays
  cuPg ; cuQg ; cuVa ; cuVm
  baseMVA ;nbus ; nline ; ngen
  coeff0 ; coeff1 ; coeff2
  viewToR ; viewFromR ; viewToI ; viewFromI
  viewVmTo ; viewVmFrom ; viewVaTo ; viewVaFrom
  viewYffR ; viewYttR ; viewYffI ; viewYttI
  cuYffR ; cuYffI ; cuYttR ; cuYttI ; cuYftR ; cuYftI ; cuYtfR ; cuYtfI ; cuYshR ; cuYshI
  viewPg ; viewQg ; sumPg; sumQg; sizeBusGeners
  cuPd ; cuQd
  cuflowmax
  Yff_abs2 ; Yft_abs2 ; Ytf_abs2 ; Ytt_abs2
  Yrefrom ; Yimfrom ; Yreto ; Yimto
  viewVmToFromLines ; viewcuYftRFromLines ; viewVaToFromLines ; viewcuYftIFromLines ; viewVmFromToLines ; viewcuYtfRToLines ; viewVaFromToLines ; viewcuYtfIToLines
  sizeFromLines ; sizeToLines
  cuBusGeners; mapfromlines; maptolines
  stype
end

function myseed!(duals::AbstractArray{ForwardDiff.Dual{T,V,N}}, x,
               seeds::AbstractArray{ForwardDiff.Partials{N,V}}, timeroutput) where {T,V,N}

  @timeit timeroutput "myseed 1" begin
    for i in 1:size(duals,1)
        duals[i] = ForwardDiff.Dual{T,V,N}(x[i], seeds[i])
    end
  end
  # @timeit timeroutput "myseed 2" begin
  #   zeroseed::ForwardDiff.Partials{N,V} = zero(ForwardDiff.Partials{N,V})
  #   for i in N+1:size(duals,1)
  #       duals[i] = ForwardDiff.Dual{T,V,N}(x[i], zeroseed)
  #   end
  # end
    return duals
end

function create_arrays(cuPg::T, cuQg::T, cuVa::T, cuVm::T, opf_data, timeroutput, stype = CuArray) where T
  @timeit timeroutput "create arrays" begin
  #shortcuts for compactness
  lines = opf_data.lines; buses = opf_data.buses; generators = opf_data.generators; baseMVA = opf_data.baseMVA
  busIdx = opf_data.BusIdx; FromLines = opf_data.FromLines; ToLines = opf_data.ToLines; BusGeners = opf_data.BusGenerators;
  nbus  = length(buses); nline = length(lines); ngen  = length(generators)

  arraytype = stype
  if arraytype == CuArray
    TVector = CuVector
  elseif arraytype == Array
    TVector = Vector
  else
    error("Unkown array type $arraytype.")
  end

  ## Arrays for objective

  # Penalty coefficients
  coeff0 = TVector{Float64}(undef, ngen)
  for (i,v) in enumerate(generators)
    coeff0[i] = v.coeff[v.n]
  end
  coeff1 = TVector{Float64}(undef, ngen)
  for (i,v) in enumerate(generators)
    coeff1[i] = v.coeff[v.n - 1]
  end
  coeff2 = TVector{Float64}(undef, ngen)
  for (i,v) in enumerate(generators)
    coeff2[i] = v.coeff[v.n - 2]
  end

  ## Arrays for constraints

  # Demand arrays
  cuPd = TVector{Float64}(undef, nbus)
  for (b,v) in enumerate(cuPd)
    cuPd[b] = buses[b].Pd
  end
  cuQd = TVector{Float64}(undef, nbus)
  for (b,v) in enumerate(cuQd)
    cuQd[b] = buses[b].Qd
  end

  # Indices
  cuBusGeners = Array{TVector{Int64}}(undef, nbus)
  for (b,v) in enumerate(BusGeners) cuBusGeners[b] = cu(BusGeners[b]) end
  cuFromLines = Array{TVector{Int64}}(undef, nbus)
  for (b,v) in enumerate(FromLines) cuFromLines[b] = cu(FromLines[b]) end
  cuToLines = Array{TVector{Int64}}(undef, nbus)
  for (b,v) in enumerate(ToLines) cuToLines[b] = cu(ToLines[b]) end

  sizeFromLines = CuArray{Int64,1,Nothing}(size.(cuFromLines,1))
  sizeToLines = CuArray{Int64,1,Nothing}(size.(cuToLines,1))

  # Views
  connect = maximum(size.(cuBusGeners,1))
  sumQg  = T(undef, nbus)
  sumPg  = T(undef, nbus)

  function spfill(from::T, ranges, nbus) where T
    k = 1
    colptr = Vector{Int64}()
    rowval = Vector{Int64}()
    nzval  = Vector{T.parameters[1]}()
    for b in 1:nbus
      push!(colptr,k)
      for (j,i) in enumerate(ranges[b])
        # to[j,b] = from[i]
        push!(nzval, from[i])
        push!(rowval, i)
        k += 1
      end
    end
    push!(colptr,k)
    return spmat{T.parameters[1]}(colptr, rowval, nzval)
  end
  viewPg = spfill(cuPg, cuBusGeners, nbus)
  viewQg = spfill(cuQg, cuBusGeners, nbus)
  sizeBusGeners = TVector{Int64}(size.(cuBusGeners,1))

  # Bus voltage

  mapbus2lineto = TVector{Int64}([busIdx[lines[l].to] for l in 1:nline])
  mapbus2linefrom = TVector{Int64}([busIdx[lines[l].from] for l in 1:nline])
  viewVmTo = view(cuVm, mapbus2lineto)
  viewVmFrom = view(cuVm, mapbus2linefrom)
  viewVaTo = view(cuVa, mapbus2lineto)
  viewVaFrom = view(cuVa, mapbus2linefrom)

  # Branch admitances
  YffR,YffI,YttR,YttI,YftR,YftI,YtfR,YtfI,YshR,YshI = computeAdmitances(lines, buses, baseMVA)
  cuYffR = TVector{Float64}(YffR) ; cuYffI = TVector{Float64}(YffI) ; cuYttR = TVector{Float64}(YttR) ; cuYttI = TVector{Float64}(YttI) ; cuYftR = TVector{Float64}(YftR)
  cuYftI = TVector{Float64}(YftI) ; cuYtfR = TVector{Float64}(YtfR) ; cuYtfI = TVector{Float64}(YtfI) ; cuYshR = TVector{Float64}(YshR) ; cuYshI = TVector{Float64}(YshI)

  viewYffR = TVector{Float64}(undef, nbus) ;
  for b in 1:nbus viewYffR[b] = sum(cuYffR[cuFromLines[b]]) end
  viewYttR = TVector{Float64}(undef, nbus) ;
  for b in 1:nbus viewYttR[b] = sum(cuYttR[cuToLines[b]]) end
  viewYffI = TVector{Float64}(undef, nbus) ;
  for b in 1:nbus viewYffI[b] = sum(.- cuYffI[cuFromLines[b]]) end
  viewYttI = TVector{Float64}(undef, nbus) ;
  for b in 1:nbus viewYttI[b] = sum(.- cuYttI[cuToLines[b]]) end

  viewToR = T(undef, nbus)
  viewFromR = T(undef, nbus)
  viewToI = T(undef, nbus)
  viewFromI = T(undef, nbus)

  # Line limits
  culinelimit = TVector{Float64}(undef, nline)
  nlinelimit = 0
  for l in 1:nline
    if lines[l].rateA!=0 && lines[l].rateA<1.0e10
      nlinelimit += 1
      culinelimit[l] = 1.0
    else
      culinelimit[l] = 0.0
    end
  end
  curate = TVector{Float64}(undef, nline)
  for l in 1:nline curate[l] = lines[l].rateA end
  cuflowmax= TVector{Float64}(undef, nline)
  cuflowmax .= (curate ./ baseMVA).^2

  Yff_abs2=TVector{Float64}(undef, nline); Yft_abs2=TVector{Float64}(undef, nline);
  Ytf_abs2=TVector{Float64}(undef, nline); Ytt_abs2=TVector{Float64}(undef, nline);
  Yrefrom=TVector{Float64}(undef, nline); Yimfrom=TVector{Float64}(undef, nline);
  Yreto=TVector{Float64}(undef, nline); Yimto=TVector{Float64}(undef, nline);
  Yff_abs2 .= cuYffR.^2 .+ cuYffI.^2; Yft_abs2 .= cuYftR.^2 .+ cuYftI.^2
  Ytf_abs2 .= cuYtfR.^2 .+ cuYtfI.^2; Ytt_abs2 .= cuYttR.^2 .+ cuYttI.^2
  Yrefrom .= cuYffR .* cuYftR .+ cuYffI .* cuYftI; Yimfrom .= .- cuYffR .* cuYftI .+ cuYffI .* cuYftR
  Yreto   .= cuYtfR .* cuYttR .+ cuYtfI .* cuYttI; Yimto   .= .- cuYtfR .* cuYttI .+ cuYtfI .* cuYttR

  # Voltage and bus connectivity

  mapfromlines = Array{TVector{Int64},1}(undef, nbus)

  for b in 1:nbus mapfromlines[b] = mapbus2lineto[cuFromLines[b]] end
  viewVmToFromLines   = spfill(cuVm, mapfromlines, nbus)
  viewcuYftRFromLines = spfill(cuYftR, cuFromLines, nbus)
  viewVaToFromLines   = spfill(cuVa, mapfromlines, nbus)
  viewcuYftIFromLines = spfill(cuYftI, cuFromLines, nbus)

  maptolines = Array{TVector{Int64},1}(undef, nbus)

  for b in 1:nbus maptolines[b] = mapbus2linefrom[cuToLines[b]] end
  viewVmFromToLines = spfill(cuVm, maptolines, nbus)
  viewcuYtfRToLines = spfill(cuYtfR, cuToLines, nbus)
  viewVaFromToLines = spfill(cuVa, maptolines, nbus)
  viewcuYtfIToLines = spfill(cuYtfI, cuToLines, nbus)

  ret = CompArrays(cuPg, cuQg, cuVa, cuVm, baseMVA, nbus, nline, ngen,
                    coeff0, coeff1, coeff2,
                    viewToR, viewFromR, viewToI, viewFromI,
                    viewVmTo, viewVmFrom, viewVaTo, viewVaFrom,
                    viewYffR, viewYttR, viewYffI, viewYttI,
                    cuYffR, cuYffI, cuYttR, cuYttI, cuYftR, cuYftI, cuYtfR, cuYtfI, cuYshR, cuYshI,
                    viewPg, viewQg, sumPg, sumQg, sizeBusGeners,
                    cuPd, cuQd,
                    cuflowmax,
                    Yff_abs2,
                    Yft_abs2,
                    Ytf_abs2,
                    Ytt_abs2,
                    Yrefrom,
                    Yimfrom,
                    Yreto,
                    Yimto,
                    viewVmToFromLines, viewcuYftRFromLines, viewVaToFromLines, viewcuYftIFromLines,
                    viewVmFromToLines, viewcuYtfRToLines, viewVaFromToLines, viewcuYtfIToLines,
                    sizeFromLines, sizeToLines,
                    cuBusGeners, mapfromlines, maptolines,
                    stype)
  end
  return ret
end

function update_arrays!(arrays::CompArrays, cuPg::T, cuQg::T, cuVa::T, cuVm::T, timeroutput) where T
  @timeit timeroutput "update arrays" begin
  arrays.cuPg .= cuPg
  arrays.cuQg .= cuQg
  arrays.cuVa .= cuVa
  arrays.cuVm .= cuVm

  function spfill!(to, from::T, ranges, nbus) where T
    k = 1
    for b in 1:nbus
      for (j,i) in enumerate(ranges[b])
        to.nzval[k] = from[i]
        k += 1
      end
    end
    return nothing
  end
  spfill!(arrays.viewPg, cuPg, arrays.cuBusGeners, arrays.nbus)
  spfill!(arrays.viewQg, cuQg, arrays.cuBusGeners, arrays.nbus)

  spfill!(arrays.viewVmToFromLines, cuVm, arrays.mapfromlines, arrays.nbus)
  spfill!(arrays.viewVaToFromLines, cuVa, arrays.mapfromlines, arrays.nbus)

  spfill!(arrays.viewVmFromToLines, cuVm, arrays.maptolines, arrays.nbus)
  spfill!(arrays.viewVaFromToLines, cuVa, arrays.maptolines, arrays.nbus)
  end
end

function objective(arrays::CompArrays, timeroutput) where T
  @timeit timeroutput "objective" begin
  # minimize active power
  return sum(arrays.coeff2 .* (arrays.baseMVA .* arrays.cuPg).^2
          .+ arrays.coeff1 .* (arrays.baseMVA .* arrays.cuPg)
          .+ arrays.coeff0)
  end
end

function constraints(rbalconst::T, ibalconst::T, limitsto::T, limitsfrom::T, arrays::CompArrays, timeroutput::TimerOutput) where T
  @timeit timeroutput "constraints" begin

  @timeit timeroutput "cuda kernels" begin
  nbus = arrays.nbus

  arrays.sumPg .= 0.0
  arrays.sumQg .= 0.0
  arrays.viewToR   .= 0.0
  arrays.viewFromR .= 0.0
  arrays.viewToI   .= 0.0
  arrays.viewFromI .= 0.0
  arraytype = arrays.stype

  # nthreads=nbus
  # nblocks=1
  nthreads=256
  # println("Threads: ", nthreads)
  nblocks=ceil(Int64, nbus/nthreads)
  # println("Blocks: ", nblocks)

  kernels.@sync arraytype begin
  kernels.@dispatch arraytype threads=nthreads blocks=nblocks sumPg(arrays.sumPg, arrays.viewPg.colptr, arrays.viewPg.nzval)
  kernels.@dispatch arraytype threads=nthreads blocks=nblocks sumQg(arrays.sumQg, arrays.viewQg.colptr, arrays.viewQg.nzval)

  kernels.@dispatch arraytype threads=nthreads blocks=nblocks term1(arrays.viewToR, arrays.cuVm,
                                        arrays.viewVmToFromLines.colptr, arrays.viewVmToFromLines.nzval,
                                        arrays.viewcuYftRFromLines.colptr, arrays.viewcuYftRFromLines.nzval,
                                        arrays.cuVa,
                                        arrays.viewVaToFromLines.colptr, arrays.viewVaToFromLines.nzval,
                                        arrays.viewcuYftIFromLines.colptr, arrays.viewcuYftIFromLines.nzval,
                                        arrays.sizeFromLines)

  kernels.@dispatch arraytype threads=nthreads blocks=nblocks term2(arrays.viewFromR, arrays.cuVm,
                                        arrays.viewVmFromToLines.colptr, arrays.viewVmFromToLines.nzval,
                                        arrays.viewcuYtfRToLines.colptr, arrays.viewcuYtfRToLines.nzval,
                                        arrays.cuVa,
                                        arrays.viewVaFromToLines.colptr, arrays.viewVaFromToLines.nzval,
                                        arrays.viewcuYtfIToLines.colptr, arrays.viewcuYtfIToLines.nzval,
                                        arrays.sizeToLines)

  kernels.@dispatch arraytype threads=nthreads blocks=nblocks term3(arrays.viewToI, arrays.cuVm,
                                        arrays.viewVmToFromLines.colptr, arrays.viewVmToFromLines.nzval,
                                        arrays.viewcuYftIFromLines.colptr, arrays.viewcuYftIFromLines.nzval,
                                        arrays.cuVa,
                                        arrays.viewVaToFromLines.colptr, arrays.viewVaToFromLines.nzval,
                                        arrays.viewcuYftRFromLines.colptr, arrays.viewcuYftRFromLines.nzval,
                                        arrays.sizeFromLines)

  kernels.@dispatch arraytype threads=nthreads blocks=nblocks term4(arrays.viewFromI, arrays.cuVm,
                                        arrays.viewVmFromToLines.colptr, arrays.viewVmFromToLines.nzval,
                                        arrays.viewcuYtfIToLines.colptr, arrays.viewcuYtfIToLines.nzval,
                                        arrays.cuVa,
                                        arrays.viewVaFromToLines.colptr, arrays.viewVaFromToLines.nzval,
                                        arrays.viewcuYtfRToLines.colptr, arrays.viewcuYtfRToLines.nzval,
                                        arrays.sizeToLines)
  end
  end
  @timeit timeroutput "balance constraints" begin
  rbalconst .= (
               (arrays.viewYffR .+ arrays.viewYttR .+ arrays.cuYshR) .* arrays.cuVm.^2
               .+ arrays.viewToR    # kernel term 1
               .+ arrays.viewFromR  # kernel term 2
               .- (((arrays.sumPg .* arrays.baseMVA) .- arrays.cuPd) ./ arrays.baseMVA)
               )

  ibalconst .= (
               (arrays.viewYffI .+ arrays.viewYttI .- arrays.cuYshI) .* arrays.cuVm.^2
               .+ arrays.viewToI    # kernel term 3
               .+ arrays.viewFromI  # kernel term 4
               .- (((arrays.sumQg .* arrays.baseMVA) .- arrays.cuQd) ./ arrays.baseMVA)
               )
  end

  if arraytype == CuArray
    mycos = CUDAnative.cos
    mysin = CUDAnative.sin
  elseif arraytype == Array
    mycos = cos
    mysin = sin
  else
    error("Unkown array type $arraytype.")
  end

  # branch apparent power limits (from bus)
  @timeit timeroutput "line constraints" begin
  limitsto .= (
              (arrays.viewVmFrom.^2
              .* (arrays.Yff_abs2 .* arrays.viewVmFrom.^2 .+ arrays.Yft_abs2 .* arrays.viewVmTo.^2
              .+ 2.0 .* arrays.viewVmFrom .* arrays.viewVmTo
              .* (arrays.Yrefrom .* mycos.(arrays.viewVaFrom .- arrays.viewVaTo)
                  .- arrays.Yimfrom .* mysin.(arrays.viewVaFrom .- arrays.viewVaTo)))
              .- arrays.cuflowmax)
              )
  # branch apparent power limits (to bus)
  limitsfrom .= (
                (arrays.viewVmTo.^2
                .* (arrays.Ytf_abs2 .* arrays.viewVmFrom.^2 .+ arrays.Ytt_abs2 .* arrays.viewVmTo.^2
                .+ 2.0 .* arrays.viewVmFrom .* arrays.viewVmTo
                .* (arrays.Yreto .* mycos.(arrays.viewVaFrom - arrays.viewVaTo)
                    .- arrays.Yimto .* mysin.(arrays.viewVaFrom .- arrays.viewVaTo)))
                .- arrays.cuflowmax)
                )
  end
  end
  return nothing
end

function benchmark(opfdata, Pg, Qg, Vm, Va, npartials, mpartials, loops, timeroutput, stype)

  t1s{N} =  ForwardDiff.Dual{Nothing,Float64, N} where N
  t2s{M,N} =  ForwardDiff.Dual{Nothing,t1s{N}, M} where {N, M}

  arraytype = stype
  if arraytype == CuArray
    T = arraytype{Float64, 1, Nothing}
    t1sT = arraytype{t1s{npartials}, 1, Nothing}
    t2sT = arraytype{t2s{mpartials, npartials}, 1, Nothing}
  end
  if arraytype == Array
    T = arraytype{Float64, 1}
    t1sT = arraytype{t1s{npartials}, 1}
    t2sT = arraytype{t2s{mpartials, npartials}, 1}
  end

  t1sseedvec = zeros(Float64, npartials)
  t1sseeds = Array{ForwardDiff.Partials{npartials,Float64},1}(undef, size(Pg,1))
  for i in 1:npartials
    t1sseedvec[i] = 1.0
    t1sseeds[i] = ForwardDiff.Partials{npartials, Float64}(NTuple{npartials, Float64}(t1sseedvec))
    t1sseedvec[i] = 0.0
  end
  for i in npartials+1:size(Pg,1)
    t1sseeds[i] = ForwardDiff.Partials{npartials, Float64}(NTuple{npartials, Float64}(t1sseedvec))
  end

  @timeit timeroutput "t1s seeding" begin
  t1scuPg = acopf.myseed!(t1sT(undef, size(Pg,1)), Pg, t1sseeds, timeroutput)
  t1scuQg = ForwardDiff.seed!(t1sT(undef, size(Qg,1)), Qg)
  t1scuVa = ForwardDiff.seed!(t1sT(undef, size(Va,1)), Va)
  t1scuVm = ForwardDiff.seed!(t1sT(undef, size(Vm,1)), Vm)
  end

  # # t2sseedvec = Array{t1s{npartials},1}(undef, mpartials)
  # t2sseedvec = Array{t1s{npartials},1}(undef, npartials)
  # # t2sseeds = Array{ForwardDiff.Partials{mpartials,t1s{npartials}},1}(undef, size(Pg,1))
  # t2sseeds = Array{ForwardDiff.Partials{mpartials,t1s{npartials}},1}(undef, mpartials)
  # t2sseedvec .= 0.0
  # for i in 1:mpartials
  #   t2sseedvec[i] = 1.0
  #   # t2sseeds[i] = ForwardDiff.Partials{mpartials, t1s{npartials}}(NTuple{mpartials, t1s{npartials}}(t2sseedvec))
  #   t2sseeds[i] = ForwardDiff.Partials{mpartials, t1s{npartials}}(NTuple{npartials, t1s{npartials}}(t2sseedvec))
  #   t2sseedvec[i] = 0.0
  # end
  # for i in mpartials+1:size(Pg,1)
  #   t2sseeds[i] = ForwardDiff.Partials{mpartials, t1s{npartials}}(NTuple{mpartials, t1s{npartials}}(t2sseedvec))
  # end
  # t2sseedtup = NTuple{size(Pg,1), ForwardDiff.Partials{size(Pg,1), Float64}}(t1sseeds)
  @timeit timeroutput "t2s seeding" begin
  # t2scuPg = acopf.myseed!(t2sT(undef, size(Pg,1)), t1scuPg, t2sseeds, timeroutput)
  # t2scuPg = acopf.myseed!(t2sT(undef, size(Pg,1)), t1scuPg, t2sseeds, timeroutput)
  # t2scuPg = ForwardDiff.seed!(t2sT(undef, size(Qg,1)), t1scuPg)
  # t2scuPg = ForwardDiff.seed!(t2sT(undef, size(Pg,1)), t1scuPg)
  # t2scuQg = ForwardDiff.seed!(t2sT(undef, size(Qg,1)), t1scuQg)
  # t2scuVa = ForwardDiff.seed!(t2sT(undef, size(Va,1)), t1scuVa)
  # t2scuVm = ForwardDiff.seed!(t2sT(undef, size(Vm,1)), t1scuVm)
  t2scuPg = t2sT(undef, size(Pg,1))
  t2scuQg = t2sT(undef, size(Qg,1))
  t2scuVa = t2sT(undef, size(Va,1))
  t2scuVm = t2sT(undef, size(Vm,1))
  end
  # @show t2scuPg

  # Pg0,Qg0,Vm0,Va0 = acopf.acopf_initialPt_IPOPT(opfdata)
  # cuPg = ForwardDiff.seed!(arraytype{ForwardDiff.Dual{Nothing,Float64,size(Pg,1)}, 1, Nothing}(undef, size(Pg,1)), Pg0, seedtup)
  # cuQg = ForwardDiff.seed!(arraytype{ForwardDiff.Dual{Nothing,Float64,size(Pg,1)}, 1, Nothing}(undef, size(Qg,1)), Qg0)
  # cuVa = ForwardDiff.seed!(arraytype{ForwardDiff.Dual{Nothing,Float64,size(Pg,1)}, 1, Nothing}(undef, size(Va,1)), Va0)
  # cuVm = ForwardDiff.seed!(arraytype{ForwardDiff.Dual{Nothing,Float64,size(Pg,1)}, 1, Nothing}(undef, size(Vm,1)), Vm0)
  T = typeof(t1scuQg)
  t1srbalconst = T(undef, length(opfdata.buses))
  t1sibalconst = T(undef, length(opfdata.buses))
  t1slimitsto = T(undef, length(opfdata.lines))
  t1slimitsfrom = T(undef, length(opfdata.lines))
  println("Create t1s arrays")
  @timeit timeroutput "Create t1s arrays" begin
  t1sarrays = acopf.create_arrays(t1scuPg, t1scuQg, t1scuVa, t1scuVm, opfdata, timeroutput, stype)
  end
  println("Initial t1s objective")
  @timeit timeroutput "Initial t1s objective" begin
  t1sPg = acopf.objective(t1sarrays, timeroutput)
  end
  println("t1s objective")
  @timeit timeroutput "t1s objective" begin
    for i in 1:loops
      t1sPg = acopf.objective(t1sarrays, timeroutput)
    end
  end
  println("Initial t1s constraints")
  @timeit timeroutput "Initial t1s constraints" begin
  acopf.constraints(t1srbalconst, t1sibalconst, t1slimitsto, t1slimitsfrom, t1sarrays, timeroutput)
  end
  println("t1s constraints $npartials")
  @timeit timeroutput "t1s constraints $npartials" begin
    for i in 1:loops
      acopf.constraints(t1srbalconst, t1sibalconst, t1slimitsto, t1slimitsfrom, t1sarrays, timeroutput)
    end
  end
  T = typeof(t2scuQg)
  t2srbalconst = T(undef, length(opfdata.buses))
  t2sibalconst = T(undef, length(opfdata.buses))
  t2slimitsto = T(undef, length(opfdata.lines))
  t2slimitsfrom = T(undef, length(opfdata.lines))
  println("Create t2s arrays")
  @timeit timeroutput "Create t2s arrays" begin
  t2sarrays = acopf.create_arrays(t2scuPg, t2scuQg, t2scuVa, t2scuVm, opfdata, timeroutput, arraytype)
  end
  println("Initial t2s objective")
  @timeit timeroutput "Initial t2s objective" begin
  t2sPg = acopf.objective(t2sarrays, timeroutput);
  end
  println("t2s objective")
  @timeit timeroutput "t2s objective" begin
    for i in 1:loops
      t2sPg = acopf.objective(t2sarrays, timeroutput)
    end
  end
  println("Initial t2s constraints")
  @timeit timeroutput "Initial t2s constraints" begin
  acopf.constraints(t2srbalconst, t2sibalconst, t2slimitsto, t2slimitsfrom, t2sarrays, timeroutput)
  end
  println("t2s constraints")
  @timeit timeroutput "t2s constraints $npartials" begin
    for i in 1:loops
      acopf.constraints(t2srbalconst, t2sibalconst, t2slimitsto, t2slimitsfrom, t2sarrays, timeroutput)
    end
  end
  return nothing
end
end


# License for this file: MIT (expat)
# Copyright 2017-2018, DLR Institute of System Dynamics and Control
#
# This file is part of module
#   Modia3D.Composition (Modia3D/Composition/_module.jl)
#
# Functions that have to be provided from an object used for contact pair response

#contactStart(material::Modia3D.AbstractContactMaterial, obj1, obj2, rContact, contactNormal, elasticContactReductionFactor) =
#             error("Function contactStart not defined for material ", String(typeof(material)))

#contactEnd(material::Modia3D.AbstractContactMaterial, obj1, obj2) =
#             error("Function contactEnd not defined for material ", String(typeof(material)))

#responseCalculation(material::Modia3D.AbstractContactMaterial, obj1, obj2, rContact, contactNormal, distanceWithHysteresis, time) = error("Function responseCalculation not defined for material ", String(typeof(material)))

# Utility functions

"""
    responseMaterial = contactStart(obj1, obj2, rContact, contactNormal)

Return a response material object at contact start.
"""
function contactStart(obj1::Object3D, obj2::Object3D, rContact::SVector{3,Float64},
                      contactNormal::SVector{3,Float64}, elasticContactReductionFactor::Float64)
    name1 = obj1.feature.contactMaterial
    name2 = obj2.feature.contactMaterial
    material = Shapes.getContactPairMaterial(name1, name2)
    return contactStart(material, obj1, obj2, rContact, contactNormal, elasticContactReductionFactor)
end

"""
    delta_dot = normalRelativeVelocityAtContact(obj1, obj2, rContact, contactNormal)

Return the relative velocity in normal direction `contactNormal` at
contact point `rContact` of the two penetrating objects `obj1, obj2`.
"""
function normalRelativeVelocityAtContact(obj1::Object3D, obj2::Object3D,
                                         rContact::Frames.Vector3D, e_n::Frames.Vector3D)
  r_rel1 = rContact - obj1.r_abs
  r_rel2 = rContact - obj2.r_abs
  w1     = obj1.R_abs'*obj1.w
  w2     = obj2.R_abs'*obj2.w
  v1     = obj1.v0 + cross(w1,r_rel1)
  v2     = obj2.v0 + cross(w2,r_rel2)
  v_rel  = v2 - v1
  delta_dot = dot(v_rel,e_n)
  return delta_dot
end


module DISSEQT

# temporary submodule
include("Levenshtein/Levenshtein.jl")

# submodules
include("AnnotatedArrays/AnnotatedArrays.jl")
include("BamReader/BamReader.jl")
include("Kruskal/Kruskal.jl")
include("SynapseTools/SynapseTools.jl")
include("AlignUtils/AlignUtils.jl")
include("Plots/Plots.jl")

using JLD
using StatsBase # for sample
using Statistics
using LinearAlgebra
using DataStructures
using DataFrames
using FASTX

using .AnnotatedArrays
using .BamReader

using .Kruskal

# TODO: get rid of these dependencies - only used by estimatesamplemix
using Convex
using SCS

include("fasta.jl")
include("consensus.jl")
include("bamcounts.jl")
include("codoncounts.jl")
include("codonfrequency.jl")
include("nuccounts.jl")
include("nucfrequency.jl")
include("swarm.jl")
include("timeseries.jl")
include("readcoverage.jl")
include("features.jl")
include("limitofdetection.jl")
include("estimatesamplemix.jl")
include("sparsepca.jl")
include("floydwarshall.jl")
include("mds.jl")
include("hierarchicalclustering.jl")
include("medianfilter.jl")
include("fitnessmodels.jl")
include("isomap.jl")

export
	Swarm,
	SwarmSegment,
	consensus,
	loadfasta,
	savefasta,
	seq2codons,
	codon2aa,
	codonqualitycount,
	nucqualitycount,
	qualityfilter!,
	removeambiguous!,
	mlcodonfreqs,
	mlnucfreqs,
	positions,
	coverage,
	positionfilter!,
	segments,
	segment,
	segmentinfo,
	segmentnames,
	segmentlengths,
	consensus,
	meta,
	processingmeta,
	loadswarm,
	timeseriesindices,
	timeseries,
	timeseriesat,
	timeseriesmean,
	distancecurve,
	distancecurveequals,
	bifurcationtime,
	evaluatedistancecurve,
	distancecurvemean,
	readcoverage,
	featurepositions,
	featuresegment,
	featureat,
	codonposition,
	annotatevariants!,
	variantdescriptions,
	limitofdetection,
	estimatemix,
	sparsepcal1,
	floydwarshall!,
	floydwarshall,
	mds,
	hierarchicalclustering,
	samplesinclusters,
	medianfilter,
	squareddistances,
	LandscapeModel,
	landscapekernelwidth,
	leaveoneoutlandscapekernelwidth,
	NearestNeighborModel,
	GroupModel,
	predictfitness,
	leaveoneoutpredict,
	varianceunexplained,
	kruskalmds,
	makeconnected!,
	isomapmatrix,
	isomap,
	kruskalisomap

end


## --- Query TC1 properties

    tc1_550 = readdlm(joinpath(moduleresourcepath,"tc1","tc1_550.csv"), ',')
    tc1_1300 = readdlm(joinpath(moduleresourcepath,"tc1","tc1_1300.csv"), ',')
    tc1_age = readdlm(joinpath(moduleresourcepath,"tc1","tc1_age.csv"), ',', Int)

    """
    `find_tc1_crust(lat::Number,lon::Number)`

    Find the depth to the 550C isotherm for the 1x1 arc degree grid cell
    containing `lat` and `lon`

    `find_tc1_crust(lat::AbstractArray,lon::AbstractArray)`

    For each pair of latitudes and longitudes given by lat and lon, find the
    depth to the 550C isotherm for the 1x1 arc degree grid cell containing
    `lat[i]` and `lon[i]`
    """
    function find_tc1_crust(lat::Number,lon::Number)
        if !isnan(lat) && !isnan(lon)
            i = round(Int, 91-lat)
            j = round(Int, lon+181)
            crust=tc1_550[i,j]
        else
            crust = NaN
        end
        return crust
    end
    function find_tc1_crust(lat::AbstractArray,lon::AbstractArray)
        # Check input dimensions match
        if length(lat) != length(lon)
            error("lat and lon must be equal length\n")
        end

        # Query the tc1_550 array for our lat and lon
        crust = fill(NaN, size(lat))
        for n=1:length(lat)
            if !isnan(lat[n]) && !isnan(lon[n])
                i = round(Int, 91-lat[n])
                j = round(Int, lon[n]+181)
                crust[n]=tc1_550[i,j]
            end
        end
        return crust
    end
    export find_tc1_crust

    """
    `find_tc1_lith(lat::Number,lon::Number)`

    Find the depth to the 1300C isotherm for the 1x1 arc degree grid cell
    containing `lat` and `lon`

    `find_tc1_lith(lat::AbstractArray,lon::AbstractArray)`

    For each pair of latitudes and longitudes given by lat and lon, find the
    depth to the 1300C isotherm for the 1x1 arc degree grid cell containing
    `lat[i]` and `lon[i]`
    """
    function find_tc1_lith(lat::Number,lon::Number)
        if !isnan(lat) && !isnan(lon)
            i = round(Int, 91-lat)
            j = round(Int, lon+181)
            lith=tc1_1300[i,j]
        else
            lith = NaN
        end
        return lith
    end
    function find_tc1_lith(lat::AbstractArray,lon::AbstractArray)
        # Check input dimensions match
        if length(lat) != length(lon)
            error("lat and lon must be equal length\n")
        end

        # Query the tc1_1300 array for our lat and lon
        lith = fill(NaN, size(lat))
        for n=1:length(lat)
            if !isnan(lat[n]) && !isnan(lon[n])
                i = round(Int, 91-lat[n])
                j = round(Int, lon[n]+181)
                lith[n]=tc1_1300[i,j]
            end
        end
        return lith
    end
    export find_tc1_lith

    """
    `find_tc1_age(lat::Number,lon::Number)`

    Return a tuple `(age, age_min, age_max)` containing the nominal, upper, and
    lower tc1 age bounds for the 1x1 arc degree grid cell containing `lat`
    and `lon`

    `find_tc1_age(lat::AbstractArray,lon::AbstractArray)`

    Return a tuple `(age, age_min, age_max)` where `age`, `age_min`, and
    `age_max` are arrays containing the nominal, upper and lower tc1 age bounds
    for each location pair `lat[i]`, `lon[i]`
    """
    function find_tc1_age(lat::Number,lon::Number)
        ages=[ NaN  NaN  NaN
                25    0   50
               150   50  250
               395  250  540
               695  540  850
               975  850 1100
              1400 1100 1700
              2100 1700 2500
              2750 2500 3000
              3250 3000 3500]

        if !isnan(lat) && !isnan(lon)
            i = round(Int, 91-lat)
            j = round(Int, lon+181)
            result = (ages[tc1_age[i,j],:]...,)
        else
            result = (NaN, NaN, NaN,)
        end
        return result
    end
    function find_tc1_age(lat::AbstractArray,lon::AbstractArray)
        # Check input dimensions match
        if length(lat) != length(lon)
            error("lat and lon must be equal length\n")
        end

        ages=[ NaN  NaN  NaN
                25    0   50
               150   50  250
               395  250  540
               695  540  850
               975  850 1100
              1400 1100 1700
              2100 1700 2500
              2750 2500 3000
              3250 3000 3500]

        # Query the tc1_1300 array for our lat and lon
        age = fill(NaN, size(lat))
        minage = fill(NaN, size(lat))
        maxage = fill(NaN, size(lat))
        for n=1:length(lat)
            if !isnan(lat[n]) && !isnan(lon[n])
                i = round(Int, 91-lat[n])
                j = round(Int, lon[n]+181)
                ageindex = tc1_age[i,j]
                age[n] = ages[ageindex,1]
                minage[n] = ages[ageindex,2]
                maxage[n] = ages[ageindex,3]
            end
        end
        return (age, minage, maxage)
    end
    export find_tc1_age

## --- End of File


# Different ways to simulate molecules

export
    simulate!,
    VelocityVerlet,
    VelocityFreeVerlet

"""
    VelocityVerlet()

The velocity Verlet integrator.
"""
struct VelocityVerlet <: Simulator end

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

Run a simulation according to the rules of the given simulator.
Custom simulators should implement this function.
"""
function simulate!(s::Simulation{false},
                    ::VelocityVerlet,
                    n_steps::Integer;
                    parallel::Bool=true)
    # See https://www.saylor.org/site/wp-content/uploads/2011/06/MA221-6.1.pdf for
    #   integration algorithm - used shorter second version
    n_atoms = length(s.coords)
    find_neighbours!(s, s.neighbour_finder, 0, parallel=parallel)
    accels_t = accelerations(s, parallel=parallel)
    accels_t_dt = zero(s.coords)

    @showprogress for step_n in 1:n_steps
        for logger in values(s.loggers)
            log_property!(logger, s, step_n)
        end

        # Update coordinates
        for i in 1:length(s.coords)
            s.coords[i] += s.velocities[i] * s.timestep + accels_t[i] * (s.timestep ^ 2) / 2
            s.coords[i] = adjust_bounds.(s.coords[i], s.box_size)
        end

        accels_t_dt = accelerations(s, parallel=parallel)

        # Update velocities
        for i in 1:length(s.velocities)
            s.velocities[i] += (accels_t[i] + accels_t_dt[i]) * s.timestep / 2
        end

        apply_thermostat!(s.velocities, s, s.thermostat)
        find_neighbours!(s, s.neighbour_finder, step_n, parallel=parallel)

        accels_t = accels_t_dt
        s.n_steps_made[1] += 1
    end
    return s.coords
end

function simulate!(s::Simulation{true},
                    ::VelocityVerlet,
                    n_steps::Integer;
                    parallel::Bool=true)
    n_atoms = length(s.coords)
    is = hcat([collect(1:n_atoms) for i in 1:n_atoms]...)
    js = permutedims(is, (2, 1))
    coords_is = view(s.coords, is)
    coords_js = view(s.coords, js)
    atoms_is = view(s.atoms, is)
    atoms_js = view(s.atoms, js)
    find_neighbours!(s, s.neighbour_finder, 0)
    if isa(s.coords, CuArray)
        self_interactions = CuArray(Diagonal(ones(typeof(s.timestep), n_atoms)))
    else
        self_interactions = Array(Diagonal(ones(typeof(s.timestep), n_atoms)))
    end
    accels_t = accelerations(s, s.coords, coords_is, coords_js, atoms_is, atoms_js, self_interactions)
    accels_t_dt = zero(s.coords)

    for step_n in 1:n_steps
        for logger in values(s.loggers)
            log_property!(logger, s, step_n)
        end

        # In-place updates here required to work with views but are not Zygote-compatible
        s.coords .+= s.velocities .* s.timestep .+ (accels_t .* s.timestep ^ 2) ./ 2
        s.coords .= adjust_bounds_vec.(s.coords, s.box_size)
        accels_t_dt = accelerations(s, s.coords, coords_is, coords_js, atoms_is, atoms_js, self_interactions)
        s.velocities .+= (accels_t .+ accels_t_dt) .* s.timestep / 2

        s.velocities .= apply_thermostat!(s.velocities, s, s.thermostat)
        find_neighbours!(s, s.neighbour_finder, 0)

        accels_t = accels_t_dt
        s.n_steps_made[1] += 1
    end
    return s.coords
end

"""
    VelocityFreeVerlet()

The velocity-free Verlet integrator, also known as the Störmer method.
In this case the `velocities` given to the `Simulator` act as the previous step
coordinates for the first step.
"""
struct VelocityFreeVerlet <: Simulator end

function simulate!(s::Simulation,
                    ::VelocityFreeVerlet,
                    n_steps::Integer;
                    parallel::Bool=true)
    n_atoms = length(s.coords)
    find_neighbours!(s, s.neighbour_finder, 0, parallel=parallel)
    coords_last = s.velocities

    @showprogress for step_n in 1:n_steps
        for logger in values(s.loggers)
            log_property!(logger, s, step_n)
        end

        accels_t = accelerations(s, parallel=parallel)

        # Update coordinates
        coords_copy = s.coords
        for i in 1:length(s.coords)
            s.coords[i] = s.coords[i] + vector(coords_last[i], s.coords[i], s.box_size) + accels_t[i] * s.timestep ^ 2
            s.coords[i] = adjust_bounds.(s.coords[i], s.box_size)
        end
        coords_last = coords_copy

        apply_thermostat!(coords_last, s, s.thermostat)
        find_neighbours!(s, s.neighbour_finder, step_n, parallel=parallel)

        s.n_steps_made[1] += 1
    end
    return s.coords
end

function simulate!(s::Simulation, n_steps::Integer; parallel::Bool=true)
    simulate!(s, s.simulator, n_steps, parallel=parallel)
end

function simulate!(s::Simulation; parallel::Bool=true)
    simulate!(s, s.n_steps - first(s.n_steps_made), parallel=parallel)
end