################################################################################
#
#	INTERFACE FOR DEFAULT POINTS IN RECIPROCAL SPACE
#	(implementations see below)
#
################################################################################

# general interface
function getReciprocalPoint(
            :: Type{R},
            unitcell   :: U,
            identifier :: Symbol,
            instance   :: Int64 = 1
        ) :: R where {D,N,LS,LB, R<:AbstractReciprocalPoint{D}, S<:AbstractSite{LS,D}, B<:AbstractBond{LB,N}, U<:AbstractUnitcell{S,B}}

    # maybe instance is passed from another source in some notation as e.g. X_2
    if instance == 1 && occursin('_', string(identifier))
        return getReciprocalPoint(R, unitcell, Val(Symbol(split(string(identifier),'_')[1])), Val(Meta.eval(Meta.parse(split(string(identifier),'_')[2]))))
    end
    # return the specific val typed function
    return getReciprocalPoint(R, unitcell, Val(identifier), Val(instance))
end
function getReciprocalPoint(
            :: Type{R},
            unitcell   :: U,
            identifier :: Tuple{Symbol, Int64}
        ) :: R where {D,N,LS,LB, R<:AbstractReciprocalPoint{D}, S<:AbstractSite{LS,D}, B<:AbstractBond{LB,N}, U<:AbstractUnitcell{S,B}}

    # return the specific val typed function
    return getReciprocalPoint(R, unitcell, Val(identifier[1]), Val(identifier[2]))
end

# interface for own concrete reciprocal point type
function getReciprocalPoint(
            unitcell   :: U,
            identifier :: Symbol,
            instance   :: Int64 = 1
        ) :: ReciprocalPoint{D} where {D,N,LS,LB, S<:AbstractSite{LS,D}, B<:AbstractBond{LB,N}, U<:AbstractUnitcell{S,B}}

    # maybe instance is passed from another source in some notation as e.g. X_2
    if instance == 1 && occursin('_', string(identifier))
        return getReciprocalPoint(ReciprocalPoint{D}, unitcell, Val(Symbol(split(string(identifier),'_')[1])), Val(Meta.eval(Meta.parse(split(string(identifier),'_')[2]))))
    end
    # return the specific val typed function
    return getReciprocalPoint(ReciprocalPoint{D}, unitcell, Val(identifier), Val(instance))
end

# Fallback for all functions
function getReciprocalPoint(
            :: Type{R},
            unitcell   :: U,
            identifier :: Val{I1},
            instance   :: Val{I2}
        ) :: R where {I1,I2,D,N,LS,LB, R<:AbstractReciprocalPoint{D}, S<:AbstractSite{LS,D}, B<:AbstractBond{LB,N},U<:AbstractUnitcell{S,B}}

    # fallback / fail due to missing implementation
    error("Implementation of reciprocal point " * string(I1) * " (instance " * string(I2) * ") not implemented yet " *
    "for concrete reciprocal point type " * string(R) * " and unitcells of dimension D=" * string(D) * " and N="*string(N))
end

# export the general interface function
export getReciprocalPoint

################################################################################
#
#	IMPLEMENTATIONS OF DEFAULT POINTS IN RECIPROCAL SPACE
#
################################################################################

#########################
#
#	GAMMA POINT
#
#########################

# Gamma point in arbitrary dimension (Fallback)
function getReciprocalPoint(
            :: Type{R},
            unitcell   :: U,
            identifier :: Val{:Gamma},
            instance   :: Val{I}
        ) :: R where {I,D,N,LS,LB, R<:AbstractReciprocalPoint{D}, S<:AbstractSite{LS,D}, B<:AbstractBond{LB,N},U<:AbstractUnitcell{S,B}}

    # fallback / fail due to missing implementation
    error("Implementation of Gamma point in " * string(D) * " spatial dimensions missing " *
    "for concrete reciprocal point type " * string(R))
end

# Gamma point in 2D
function getReciprocalPoint(
            :: Type{R},
            unitcell   :: U,
            identifier :: Val{:Gamma},
            instance   :: Val{I}
        ) :: R where {I,N,LS,LB, R<:AbstractReciprocalPoint{2}, S<:AbstractSite{LS,2}, B<:AbstractBond{LB,N},U<:AbstractUnitcell{S,B}}

    # return the specific type
    return newReciprocalPoint(
        R,
        [0.0, 0.0],
        "Gamma",
        "\\Gamma"
    )
end

# Gamma point in 3D
function getReciprocalPoint(
            :: Type{R},
            unitcell   :: U,
            identifier :: Val{:Gamma},
            instance   :: Val{I}
        ) :: R where {I,N,LS,LB, R<:AbstractReciprocalPoint{3}, S<:AbstractSite{LS,3}, B<:AbstractBond{LB,N},U<:AbstractUnitcell{S,B}}

    # return the specific type
    return newReciprocalPoint(
        R,
        [0.0, 0.0, 0.0],
        "Gamma",
        "\\Gamma"
    )
end

#########################
#
#	K POINT (and K')
#
#########################

# K point (Based on BZ object) in 2D
function getReciprocalPoint(
            :: Type{R},
            unitcell   :: U,
            identifier :: Val{:K},
            instance   :: Val{I}
        ) :: R where {D,I,N,LS,LB, R<:AbstractReciprocalPoint{2}, S<:AbstractSite{LS,2}, B<:AbstractBond{LB,N},U<:AbstractUnitcell{S,B}}

    # get the bz
    bz = getBrillouinZone(getReciprocalUnitcell(unitcell))

    # return the specific type
    return newReciprocalPoint(
        R,
        corners(bz)[faces(bz)[1][mod(I,length(corners(bz))) + 1]],
        "K",
        "K"
    )
end

# K' point (Based on BZ object) in 2D
function getReciprocalPoint(
            :: Type{R},
            unitcell   :: U,
            identifier :: Val{:Kp},
            instance   :: Val{I}
        ) :: R where {D,I,N,LS,LB, R<:AbstractReciprocalPoint{2}, S<:AbstractSite{LS,2}, B<:AbstractBond{LB,N},U<:AbstractUnitcell{S,B}}

    # get the bz
    bz = getBrillouinZone(getReciprocalUnitcell(unitcell))

    # return the specific type
    return newReciprocalPoint(
        R,
        corners(bz)[faces(bz)[1][mod(I+1,length(corners(bz))) + 1]],
        "K\'",
        "K\'"
    )
end

# K point (Based on BZ object) in 3D
function getReciprocalPoint(
            :: Type{R},
            unitcell   :: U,
            identifier :: Val{:K},
            instance   :: Val{I}
        ) :: R where {D,I,N,LS,LB, R<:AbstractReciprocalPoint{3}, S<:AbstractSite{LS,3}, B<:AbstractBond{LB,N},U<:AbstractUnitcell{S,B}}

    # get the bz
    bz = getBrillouinZone(getReciprocalUnitcell(unitcell))

    # get the faceindex
    faceindex = mod(div(I, 10), length(faces(bz))) + 1
    face = faces(bz)[faceindex]
    # first and second index
    cornerindex = mod(I, 10)
    i = face[mod(cornerindex, length(face))+1]

    # return the specific type
    return newReciprocalPoint(
        R,
        corners(bz)[i],
        "K",
        "K"
    )
end

# K' point (Based on BZ object) in 3D
function getReciprocalPoint(
            :: Type{R},
            unitcell   :: U,
            identifier :: Val{:Kp},
            instance   :: Val{I}
        ) :: R where {D,I,N,LS,LB, R<:AbstractReciprocalPoint{3}, S<:AbstractSite{LS,3}, B<:AbstractBond{LB,N},U<:AbstractUnitcell{S,B}}

    # get the bz
    bz = getBrillouinZone(getReciprocalUnitcell(unitcell))

    # get the faceindex
    faceindex = mod(div(I, 10), length(faces(bz))) + 1
    face = faces(bz)[faceindex]
    # first and second index
    cornerindex = mod(I, 10)
    i = face[mod(cornerindex+1, length(face))+1]

    # return the specific type
    return newReciprocalPoint(
        R,
        corners(bz)[i],
        "K\'",
        "K\'"
    )
end

#########################
#
#	M POINT (and M')
#
#########################

# M point (Based on BZ object) in 2D
function getReciprocalPoint(
            :: Type{R},
            unitcell   :: U,
            identifier :: Val{:M},
            instance   :: Val{I}
        ) :: R where {D,I,N,LS,LB, R<:AbstractReciprocalPoint{2}, S<:AbstractSite{LS,2}, B<:AbstractBond{LB,N},U<:AbstractUnitcell{S,B}}

    # get the bz
    bz = getBrillouinZone(getReciprocalUnitcell(unitcell))

    # first and second index
    i1 = faces(bz)[1][mod(I-1,length(faces(bz)[1])) + 1]
    i2 = faces(bz)[1][mod(I,length(faces(bz)[1])) + 1]
    # find the first and next corner
    c1 = corners(bz)[i1]
    c2 = corners(bz)[i2]

    # return the specific type
    return newReciprocalPoint(
        R,
        (c1.+c2)./2,
        "M",
        "M"
    )
end

# M' point (Based on BZ object) in 2D
function getReciprocalPoint(
            :: Type{R},
            unitcell   :: U,
            identifier :: Val{:Mp},
            instance   :: Val{I}
        ) :: R where {D,I,N,LS,LB, R<:AbstractReciprocalPoint{2}, S<:AbstractSite{LS,2}, B<:AbstractBond{LB,N},U<:AbstractUnitcell{S,B}}

    # get the bz
    bz = getBrillouinZone(getReciprocalUnitcell(unitcell))

    # first and second index
    i1 = faces(bz)[1][mod(I,length(faces(bz)[1])) + 1]
    i2 = faces(bz)[1][mod(I+1,length(faces(bz)[1])) + 1]
    # find the first and next corner
    c1 = corners(bz)[i1]
    c2 = corners(bz)[i2]

    # return the specific type
    return newReciprocalPoint(
        R,
        (c1.+c2)./2,
        "M\'",
        "M\'"
    )
end

# M point (Based on BZ object) in 3D
function getReciprocalPoint(
            :: Type{R},
            unitcell   :: U,
            identifier :: Val{:M},
            instance   :: Val{I}
        ) :: R where {D,I,N,LS,LB, R<:AbstractReciprocalPoint{3}, S<:AbstractSite{LS,3}, B<:AbstractBond{LB,N},U<:AbstractUnitcell{S,B}}

    # get the bz
    bz = getBrillouinZone(getReciprocalUnitcell(unitcell))

    # get the faceindex
    faceindex = mod(div(I, 10), length(faces(bz))) + 1
    face = faces(bz)[faceindex]
    # first and second index
    cornerindex = mod(I, 10)
    i1 = face[mod(cornerindex-1, length(face)) + 1]
    i2 = face[mod(cornerindex,   length(face)) + 1]
    # find the first and next corner
    c1 = corners(bz)[i1]
    c2 = corners(bz)[i2]

    # return the specific type
    return newReciprocalPoint(
        R,
        (c1.+c2)./2,
        "M",
        "M"
    )
end

# M' point (Based on BZ object) in 3D
function getReciprocalPoint(
            :: Type{R},
            unitcell   :: U,
            identifier :: Val{:Mp},
            instance   :: Val{I}
        ) :: R where {D,I,N,LS,LB, R<:AbstractReciprocalPoint{3}, S<:AbstractSite{LS,3}, B<:AbstractBond{LB,N},U<:AbstractUnitcell{S,B}}

    # get the bz
    bz = getBrillouinZone(getReciprocalUnitcell(unitcell))

    # get the faceindex
    faceindex = mod(div(I, 10), length(faces(bz))) + 1
    face = faces(bz)[faceindex]
    # first and second index
    cornerindex = mod(I, 10)
    i1 = face[mod(cornerindex, length(face)) + 1]
    i2 = face[mod(cornerindex+1,   length(face)) + 1]
    # find the first and next corner
    c1 = corners(bz)[i1]
    c2 = corners(bz)[i2]

    # return the specific type
    return newReciprocalPoint(
        R,
        (c1.+c2)./2,
        "M\'",
        "M\'"
    )
end

#########################
#
#	X POINT (center of face)
#
#########################

# X point (Based on BZ object) in 3D
function getReciprocalPoint(
            :: Type{R},
            unitcell   :: U,
            identifier :: Val{:X},
            instance   :: Val{I}
        ) :: R where {D,I,N,LS,LB, R<:AbstractReciprocalPoint{3}, S<:AbstractSite{LS,3}, B<:AbstractBond{LB,N},U<:AbstractUnitcell{S,B}}

    # get the bz
    bz = getBrillouinZone(getReciprocalUnitcell(unitcell))

    # get the faceindex
    faceindex = mod(I-1, length(faces(bz))) + 1
    face = faces(bz)[faceindex]
    # find the center
    center = [0.0, 0.0, 0.0]
    for c in face
        center .+= corners(bz)[c]
    end
    center ./= length(face)

    # return the specific type
    return newReciprocalPoint(
        R,
        center,
        "X",
        "X"
    )
end


struct Events{T <: DiseaseStateSequence}
  exposure::Union{Nothing, Vector{Float64}}
  infection::Vector{Float64}
  removal::Union{Nothing, Vector{Float64}}

  function Events{T}(e::V, i::V, r::V) where {T <: SEIR, V <: Vector{Float64}}
    if length(unique((length.([e; i; r])))) != 1
      @error "Length of event time vectors must be equal"
    end
    return new{T}(e, i, r)
  end

  function Events{T}(n::Integer) where T <: SEIR
    return new{T}(fill(NaN, n), fill(NaN, n), fill(NaN, n))
  end

  function Events{T}(e::V, i::V) where {T <: SEI, V <: Vector{Float64}}
    if length(unique((length.([e; i])))) != 1
      @error "Length of event time vectors must be equal"
    end
    return new{T}(e, i, nothing)
  end

  function Events{T}(n::Integer) where T <: SEI
    return new{T}(fill(NaN, n), fill(NaN, n), nothing)
  end

  function Events{T}(i::V, r::V) where {T <: SIR, V <: Vector{Float64}}
    if length(unique((length.([i; r])))) != 1
      @error "Length of event time vectors must be equal"
    end
    return new{T}(nothing, i, r)
  end

  function Events{T}(n::Integer) where T <: SIR
    return new{T}(nothing, fill(NaN, n), fill(NaN, n))
  end

  function Events{T}(i::V) where {T <: SI, V <: Vector{Float64}}
    return new{T}(nothing, i, nothing)
  end

  function Events{T}(n::Integer) where T <: SI
    return new{T}(nothing, fill(NaN, n), nothing)
  end
end

function Events{T}(a::Array{Float64,2}) where T <: DiseaseStateSequence
  if size(a, 2) == 3
    return Events{T}(a[:,1], a[:,2], a[:,3])
  elseif size(a, 2) == 2
    return Events{T}(a[:,1], a[:,2])
  elseif size(a, 2) == 1
    return Events{T}(a[:,1])
  else
    @error "Invalid array size for construction of an $(Events{T}) object"
  end
end

function Events{T}(x::DiseaseStates) where T <: DiseaseStateSequence
  events = Events{T}(length(x))
  for i = 1:length(x)
    if x[i] in convert(DiseaseStates, T)
      for j = convert(DiseaseStates, T)[2:findfirst(Ref(x[i]) .== convert(DiseaseStates, T))]
        events[j][i] = -Inf
      end
    else
      @error "Invalid initial state for individual $i"
    end
  end
  return events
end

function individuals(x::Events{M}) where{
                     M <: DiseaseStateSequence}
  return length(x.infection)
end

function Base.show(io::IO, x::Events{T}) where T <: DiseaseStateSequence
  return print(io, "$T model event times (n=$(individuals(x)))")
end

function Base.getindex(x::Events{T}, new_state::DiseaseState) where T <: DiseaseStateSequence
  if new_state == State_E
    return x.exposure
  elseif new_state == State_I
    return x.infection
  elseif new_state == State_R
    return x.removal
  else
    error("Unsupported indexing disease state")
  end
end

function Base.getindex(x::Events{T}, states::DiseaseStates) where T <: DiseaseStateSequence
  y = x[states[1]]
  for i = 2:length(states)
    y = hcat(y, x[states[i]])
  end
  return y
end

function Base.convert(::Type{Array{Float64, 2}}, x::Events{T}) where T <: DiseaseStateSequence
  return x[convert(DiseaseStates, T)[2:end]]
end

function Base.convert(::Type{Vector{Float64}}, x::Events{T}) where T <: DiseaseStateSequence
  return x[convert(DiseaseStates, T)[2:end]][:]
end

function Base.convert(::Type{Array{Float64, 2}}, x::Array{Events{T}, 1}) where T <: DiseaseStateSequence
  y = convert(Vector{Float64}, x[1])'
  for i = 2:length(x)
    y = vcat(y, convert(Vector{Float64}, x[i])')
  end
  return y
end

function Base.minimum(x::Events{T}) where T <: DiseaseStateSequence
  y = convert(Array{Float64, 2}, x)
  return minimum(y[(y .!== NaN) .& (y .> -Inf)])
end

function Base.maximum(x::Events{T}) where T <: DiseaseStateSequence
  y = convert(Array{Float64, 2}, x)
  return maximum(y[(y .!== NaN) .& (y .> -Inf)])
end

function mean(x::Vector{Events{T}}) where T <: DiseaseStateSequence
  return Events{T}(mean([convert(Array{Float64, 2}, i) for i in x]))
end


###
### Janzen-Connell Model
### (c) Daniel Vedder, MIT license
###

"""
A cons cell to implement a double linked list for the forest
(Boy, do I miss Lisp :-/ )
"""
mutable struct Cons
    car::Union{Tree,Nothing}   # The Tree object held by this cell
    cdr::Union{Cons,Nothing}   # A pointer to the next cell down the line
    prev::Union{Cons,Nothing}  # A pointer to the previous cell
end

# The first cons cells in the forest list, and the number of trees
const forest = Cons(nothing,nothing,nothing)
forestlen = 0 #XXX do I actually need this?

"""
Insert a new tree object at the correct position in the forest list.
(Sorted by ascending x values.)
"""
function planttree!(tree::Tree,cons::Cons=forest)
    #FIXME sometimes doesn't insert trees into the forest list
    # (at least when called from initworld()
    while cons != nothing
        if !isa(cons.car, Tree)
            # If we have an empty cons cell, plant the tree here
            cons.car = tree
            global forestlen += 1
            @debug "Planted tree $(tree.uid) @$(tree.position.x)/$(tree.position.y)"
            return
        elseif tree.position.x < cons.car.position.x
            # If the next tree in the list is further east, insert a new cons cell
            if cons.prev != nothing #inserting in the middle
                newcons = Cons(tree,cons,cons.prev)
                cons.prev.cdr = newcons
                cons.prev = newcons
            else #if we're inserting before the first cell
                newcons = Cons(cons.car, cons.cdr, cons)
                cons.car = tree
                cons.cdr = newcons
            end
            global forestlen += 1
            @debug "Planted tree $(tree.uid) @$(tree.position.x)/$(tree.position.y)"
            return
        elseif cons.cdr == nothing
            # If we're at the end of the list, append a new cons cell
            cons.cdr = Cons(nothing,nothing,cons)
        end
        cons = cons.cdr
    end
end

"""
Remove a tree from the forest list.
(Don't let Idefix see this function :D )
"""
function killtree!(tree::Tree,cons::Cons=forest,reason::String="malice aforethought")
    while cons != nothing
        if cons.car == tree
            if cons.prev != nothing #excise a cons cell from the list
                cons.prev.cdr = cons.cdr
                cons.cdr != nothing && (cons.cdr.prev = cons.prev)
                cons = nothing
            else
                #removing the first tree is a bit more tricky, because the
                #first cons cell (global variable `forest`) mustn't be deleted
                if cons.cdr != nothing
                    cons.car = cons.cdr.car
                    cons.cdr.cdr != nothing && (cons.cdr.cdr.prev = cons)
                    cons.cdr = cons.cdr.cdr
                else
                    cons.car = nothing
                end
            end
            #cleanup and decrease the tree count
            global forestlen -= 1
            @debug "Killed tree $(tree.uid) @$(tree.position.x)/$(tree.position.y) because of $(reason)"
            tree = nothing
            return
        end
        cons = cons.cdr
    end
    @warn "Attempted to remove nonexistent tree."
end

"""
Produce seeds and disperse them in the landscape, planting them where possible
"""
function disperse!(cons::Cons=forest)
    i::Int16 = 1
    while cons != nothing
        tree = cons.car
        if !tree.mature
            i += 1
            cons = cons.cdr
            continue
        end
        @debug "Reproducing tree $(tree.uid)"
        dx = tree.species.dispersal_distance
        # Each tree produces multiple seeds
        for s in 1:tree.species.seed_production
            #TODO implement a proper dispersal kernel
            # Find a random location in a circle around the tree
            sx = tree.position.x + rand(-dx:dx)
            #FIXME sqrt() was called with an argument of -32768.0?
            # (only happens in scenarios with variance?)
            dy = convert(Int16, round(sqrt(abs(dx^2-(sx-tree.position.x)^2))))
            sy = tree.position.y + rand(-dy:dy)
            if sx >= -settings["worldsize"] && sx <= settings["worldsize"] &&
                sy >= -settings["worldsize"] && sy <= settings["worldsize"]
                seed = Tree(tree.species, sx, sy)
                planttree!(seed)
            end
        end
        i += 1
        cons = cons.cdr
    end
end

"""
Test whether the trees in two cons cells intersect, and if so, kill the
smaller tree. The integer return value indicates the outcome:
0 - no intersection (no tree killed)
1 - the first tree is larger (second tree killed)
2 - the second tree is larger (first tree killed)
Needed by `compete_individual!()`
"""
function compete_pair!(cons1::Cons, cons2::Cons)::UInt8
    tree1 = cons1.car
    tree2 = cons2.car
    (tree1 == nothing || tree2 == nothing) && return
    mindist = (tree1.size + tree2.size)/2
    dx = abs(tree1.position.x - tree2.position.x)
    dy = abs(tree1.position.y - tree2.position.y)
    if  dx >= mindist || dy >= mindist || hypot(dx,dy) >= mindist
        return 0
    elseif tree1.size > tree2.size
        killtree!(tree2, cons2, "competition")
        return 1
    else
        killtree!(tree1, cons1, "competition")
        return 2
    end
end

"""
Check whether the tree in this cons cell conflicts with trees in the
vicinity and, if so, kill the smaller one.
Needed by `compete!()`
"""
function compete_individual!(cons::Cons)
    tree = cons.car
    # go right until we're sure we won't find any more conflicts
    next = cons.cdr
    while next != nothing && abs(next.car.position.x - tree.position.x) < (tree.size/2)+2^7
        next2 = next.cdr # We have to save the coming step already, in case `next` is killed
        conflict = compete_pair!(cons, next)
        if conflict == 2
            return # This tree was killed, so we can break off
        else # otherwise, keep going
            next = next2
        end
    end
    # then go left and repeat
    next = cons.prev
    while next != nothing && abs(next.car.position.x - tree.position.x) < (tree.size/2)+2^7
        next2 = next.prev
        conflict = compete_pair!(cons, next)
        if conflict == 2
            return
        else
            next = next2
        end
    end
end

"""
Go through the landscape and check each tree for space conflicts
"""
function compete!(cons::Cons=forest)
    while cons != nothing
        compete_individual!(cons)
        cons = cons.cdr
    end
end

"""
All saplings grow until they reach maturity, then eventually die of old age.
"""
function grow!(cons::Cons=forest)
    while cons != nothing
        tree = cons.car
        next = cons.cdr
        if !tree.mature
            tree.size += tree.species.growth_rate
            tree.size >= tree.species.max_size && (tree.mature = true)
        elseif tree.age >= tree.species.max_age
            killtree!(tree, cons, "old age")
            cons = next
            continue
        end
        tree.age += 1
        cons = next
        recordindividual(tree)
    end
end

"""
An infected tree in the given cons cell spreads its infection
"""
function spread_infection(cons::Cons)
    tree = cons.car
    pathogen = tree.infection
    maxdist = pathogen.infection_radius
    # go right until we're sure we're out of the infection range
    next = cons.cdr
    while next != nothing && abs(next.car.position.x - tree.position.x) < maxdist
        tree2 = next.car
        dist = hypot(abs(tree.position.x-tree2.position.x),
                     abs(tree.position.y-tree2.position.y))
        prob = (pathogen.infection_rate-tree2.species.pathogen_resistance)*((maxdist-dist)/maxdist)
        #A tree is infected if...
        if dist <= maxdist && #... it's within range
            tree2.infection == nothing && #... it isn't infected anyway
            tree2.species.id == tree.species.id && #... it belongs to the same species
            prob > rand(Float16) #... it passes a random draw dependent on its resistance and distance
            tree2.infection = Pathogen(pathogen.host)
            @debug "Infected tree $(tree.uid) @$(tree2.position.x)/$(tree2.position.y)"
        end
        next = next.cdr
    end
    # then go left and repeat
    next = cons.prev
    while next != nothing && abs(next.car.position.x - tree.position.x) < maxdist
        tree2 = next.car
        dist = hypot(abs(tree.position.x-tree2.position.x),
                     abs(tree.position.y-tree2.position.y))
        prob = (pathogen.infection_rate-tree2.species.pathogen_resistance)*((maxdist-dist)/maxdist)
        if dist <= maxdist &&
            tree2.infection == nothing &&
            tree2.species.id == tree.species.id &&
            prob > rand(Float16)
            tree2.infection = Pathogen(pathogen.host)
            @debug "Infected tree $(tree.uid) @$(tree2.position.x)/$(tree2.position.y)"
        end
        next = next.prev
    end
end

"""
Carry out the pathogen spread phase and check for mortality
"""
function infect!(cons::Cons=forest)
    while cons != nothing
        tree = cons.car
        next = cons.cdr
        if tree.infection != nothing
            pathogen = tree.infection
            # pathogens have a one-update incubation period
            pathogen.infectious ? spread_infection(cons) : pathogen.infectious = true
            if pathogen.lethality > rand(Float16)
                killtree!(tree, cons, "disease")
            end
        end
        cons = next
    end
end


# General case - to be used with bc::NoBC or m::PrescribedXModels
function soil_boundary_flux!(
    nf,
    bc::AbstractBoundaryConditions,
    m::AbstractSoilComponentModel,
    land::LandModel,
    state⁺::Vars,
    diff⁺::Vars,
    aux⁺::Vars,
    nM,
    state⁻::Vars,
    diff⁻::Vars,
    aux⁻::Vars,
    t,
    _...,
) end

function soil_boundary_state!(
    nf,
    bc::AbstractBoundaryConditions,
    m::AbstractSoilComponentModel,
    land::LandModel,
    state⁺::Vars,
    aux⁺::Vars,
    nM,
    state⁻::Vars,
    aux⁻::Vars,
    t,
    _...,
)

end

# Dirichlet Methods for SoilHeat and SoilWater

"""
    function soil_boundary_state!(
        nf,
        bc::Dirichlet,
        water::SoilWaterModel,
        land::LandModel,
        state⁺::Vars,
        aux⁺::Vars,
        nM,
        state⁻::Vars,
        aux⁻::Vars,
        t,
    )

The Dirichlet-type method for `soil_boundary_state!` for the
`SoilWaterModel`.
"""
function soil_boundary_state!(
    nf,
    bc::Dirichlet,
    water::SoilWaterModel,
    land::LandModel,
    state⁺::Vars,
    aux⁺::Vars,
    nM,
    state⁻::Vars,
    aux⁻::Vars,
    t,
    _...,
)
    bc_function = bc.state_bc
    state⁺.soil.water.ϑ_l = bc_function(aux⁻, t)

end

"""
    function soil_boundary_state!(
        nf,
        bc::Dirichlet,
        heat::SoilHeatModel,
        land::LandModel,
        state⁺::Vars,
        aux⁺::Vars,
        nM,
        state⁻::Vars,
        aux⁻::Vars,
        t,
    )

The Dirichlet-type method for `soil_boundary_state!` for the
`SoilHeatModel`.
"""
function soil_boundary_state!(
    nf,
    bc::Dirichlet,
    heat::SoilHeatModel,
    land::LandModel,
    state⁺::Vars,
    aux⁺::Vars,
    nM,
    state⁻::Vars,
    aux⁻::Vars,
    t,
    _...,
)
    bc_function = bc.state_bc
    param_set = parameter_set(land)
    ϑ_l, θ_i = get_water_content(land.soil.water, aux⁻, state⁻, t)
    θ_l = volumetric_liquid_fraction(ϑ_l, land.soil.param_functions.porosity)
    ρc_s = volumetric_heat_capacity(
        θ_l,
        θ_i,
        land.soil.param_functions.ρc_ds,
        param_set,
    )

    ρe_int_bc =
        volumetric_internal_energy(θ_i, ρc_s, bc_function(aux⁻, t), param_set)

    state⁺.soil.heat.ρe_int = ρe_int_bc
end

# Neumann conditions for SoilHeat and SoilWater

"""
    function soil_boundary_flux!(
        nf,
        bc::Neumann,
        water::SoilWaterModel,
        land::LandModel,
        state⁺::Vars,
        diff⁺::Vars,
        aux⁺::Vars,
        n̂,
        state⁻::Vars,
        diff⁻::Vars,
        aux⁻::Vars,
        t,
    )

The Neumann method for `soil_boundary_flux!` for the
`SoilWaterModel`.
"""
function soil_boundary_flux!(
    nf,
    bc::Neumann,
    water::SoilWaterModel,
    land::LandModel,
    state⁺::Vars,
    diff⁺::Vars,
    aux⁺::Vars,
    n̂,
    state⁻::Vars,
    diff⁻::Vars,
    aux⁻::Vars,
    t,
    _...,
)
    bc_function = bc.scalar_flux_bc
    # on faces at the top and bottom of the domain,
    # the vector n̂ can point in either ± ẑ, depending on which side of the domain
    # we are on. The user supplies a scalar flux F, such that the flux at the boundary
    # is assumed to be F⃗ = F ẑ - i.e. the user doesn't need to worry about the normal vector.
    # so, we take the absolute value of n̂ here.
    # The minus sign is because the condition is applied on minus the flux.
    # the same argument applies for the other directions.
    diff⁺.soil.water.K∇h = abs.(n̂) * (-bc_function(aux⁻, t))
end

"""
    function soil_boundary_flux!(
        nf,
        bc::Neumann,
        heat::SoilHeatModel,
        land::LandModel,
        state⁺::Vars,
        diff⁺::Vars,
        aux⁺::Vars,
        n̂,
        state⁻::Vars,
        diff⁻::Vars,
        aux⁻::Vars,
        t,
    )

The Neumann method for `soil_boundary_flux!` for the
`SoilHeatModel`.
"""
function soil_boundary_flux!(
    nf,
    bc::Neumann,
    heat::SoilHeatModel,
    land::LandModel,
    state⁺::Vars,
    diff⁺::Vars,
    aux⁺::Vars,
    n̂,
    state⁻::Vars,
    diff⁻::Vars,
    aux⁻::Vars,
    t,
    _...,
)
    bc_function = bc.scalar_flux_bc
    # on faces at the top and bottom of the domain,
    # the vector n̂ can point in either ± ẑ, depending on which side of the domain
    # we are on. The user supplies a scalar flux F, such that the flux at the boundary
    # is assumed to be F⃗ = F ẑ - i.e. the user doesn't need to worry about the normal vector.
    # so, we take the absolute value of n̂ here.
    # The minus sign is because the condition is applied on minus the flux.
    # the same argument applies for the other directions.
    diff⁺.soil.heat.κ∇T = abs.(n̂) * (-bc_function(aux⁻, t))
end

# SurfaceDriven conditions for for SoilHeat and SoilWater

"""
    function soil_boundary_flux!(
        nf,
        bc::SurfaceDrivenWaterBoundaryConditions,
        water::SoilWaterModel,
        land::LandModel,
        state⁺::Vars,
        diff⁺::Vars,
        aux⁺::Vars,
        n̂,
        state⁻::Vars,
        diff⁻::Vars,
        aux⁻::Vars,
        t,
    )

The Surface Driven BC method for `soil_boundary_flux!` for the
`SoilWaterModel`.
"""
function soil_boundary_flux!(
    nf,
    bc::SurfaceDrivenWaterBoundaryConditions,
    water::SoilWaterModel,
    land::LandModel,
    state⁺::Vars,
    diff⁺::Vars,
    aux⁺::Vars,
    n̂,
    state⁻::Vars,
    diff⁻::Vars,
    aux⁻::Vars,
    t,
    _...,
)

    diff⁺.soil.water.K∇h = compute_surface_grad_bc(
        land.soil,
        bc.runoff_model,
        bc.precip_model,
        n̂,
        state⁻,
        diff⁻,
        aux⁻,
        t,
    )

end

"""
    soil_boundary_flux!(
        nf,
        bc::SurfaceDrivenHeatBoundaryConditions,
        heat::SoilHeatModel,
        land::LandModel,
        state⁺::Vars,
        diff⁺::Vars,
        aux⁺::Vars,
        n̂,
        state⁻::Vars,
        diff⁻::Vars,
        aux⁻::Vars,
        t,
    )

The Surface Driven BC method for `soil_boundary_flux!` for the
`SoilHeatModel`.
"""
function soil_boundary_flux!(
    nf,
    bc::SurfaceDrivenHeatBoundaryConditions,
    heat::SoilHeatModel,
    land::LandModel,
    state⁺::Vars,
    diff⁺::Vars,
    aux⁺::Vars,
    n̂,
    state⁻::Vars,
    diff⁻::Vars,
    aux⁻::Vars,
    t,
    _...,
)

    net_surface_flux = compute_net_radiative_energy_flux(bc.nswf_model, t)
    diff⁺.soil.heat.κ∇T = n̂ * (-net_surface_flux)
end


struct SimulationResults
    global_index::MAPPING_DICT
    bus_lookup::Dict{Int, Int}
    system::PSY.System
    time_log::Dict{Symbol, Any}
    solution::SciMLBase.AbstractODESolution
    function SimulationResults(
        inputs::SimulationInputs,
        system::PSY.System,
        time_log,
        solution,
    )
        new(make_global_state_map(inputs), get_lookup(inputs), system, time_log, solution)
    end
end

get_global_index(res::SimulationResults) = res.global_index
get_bus_count(res::SimulationResults) = get_n_buses(res.system)
get_bus_lookup(res::SimulationResults) = res.bus_lookup
get_system(res::SimulationResults) = res.system
get_solution(res::SimulationResults) = res.solution

"""
Internal function to obtain as a Vector of Float64 of a specific state. It receives the solution and the
global index for a state.

"""
function _post_proc_state_series(solution, ix::Int, dt::Union{Nothing, Float64})
    if dt === nothing
        ix_t = unique(i -> solution.t[i], eachindex(solution.t))
        ts = solution.t[ix_t]
        state = solution[ix, ix_t]
    else
        ts = range(0, stop = solution.t[end], step = dt)
        state = solution(collect(ts); idxs = ix)
    end
    return ts, state
end

"""
Function to obtain the state time series of a specific state. It receives the simulation, and a tuple
containing the name of the Dynamic Device and the symbol of the state.
"""
function post_proc_state_series(
    res::SimulationResults,
    ref::Tuple{String, Symbol},
    dt::Union{Nothing, Float64},
)
    global_state_index = get_global_index(res)
    ix = get(global_state_index[ref[1]], ref[2], 0)
    return _post_proc_state_series(get_solution(res), ix, dt)
end

"""
Function to obtain voltage and output currents for a dynamic device. It receives the simulation, and the name
of the Dynamic Device.
"""
function post_proc_voltage_current_series(
    res::SimulationResults,
    name::String,
    dt::Union{Nothing, Float64},
)::NTuple{5, Vector{Float64}}
    #Note: Type annotation since get_dynamic_injector is type unstable and solution is Union{Nothing, DAESol}
    system = get_system(res)
    bus_lookup = get_bus_lookup(res)
    n_buses = length(bus_lookup)
    solution = res.solution
    device = PSY.get_component(PSY.StaticInjection, system, name)
    if isnothing(device)
        error("Device $(name) not found in the system")
    end
    bus_ix = get(bus_lookup, PSY.get_number(PSY.get_bus(device)), -1)
    ts, V_R, V_I = post_proc_voltage_series(solution, bus_ix, n_buses, dt)
    dyn_device = PSY.get_dynamic_injector(device)
    _, I_R, I_I = compute_output_current(res, dyn_device, V_R, V_I, dt)
    return ts, V_R, V_I, I_R, I_I
end

"""
Function to obtain voltage using the bus index (and not the bus number). It receives the solution, the bus index and
the total number of buses.

"""
function post_proc_voltage_series(
    solution,
    bus_ix::Int,
    n_buses::Int,
    dt::Union{Nothing, Float64},
)
    bus_ix < 0 && error("Bus number $(bus_number) not found.")
    ts, V_R = _post_proc_state_series(solution, bus_ix, dt)
    _, V_I = _post_proc_state_series(solution, bus_ix + n_buses, dt)
    return collect(ts), collect(V_R), collect(V_I)
end

"""
Function to compute the real current output time series of a Dynamic Injection series out of the DAE Solution. It receives the solution and the
string name of the Dynamic Injection device.

"""
function post_proc_real_current_series(
    res::SimulationResults,
    name::String,
    dt::Union{Nothing, Float64},
)
    ts, _, _, I_R, _ = post_proc_voltage_current_series(res, name, dt)
    return ts, I_R
end
"""
Function to compute the imaginary current output time series of a Dynamic Injection series out of the DAE Solution. It receives the solution and the
string name of the Dynamic Injection device.

"""
function post_proc_imaginary_current_series(
    res::SimulationResults,
    name::String,
    dt::Union{Nothing, Float64},
)
    ts, _, _, _, I_I = post_proc_voltage_current_series(res, name, dt)
    return ts, I_I
end

"""
Function to compute the active power output time series of a Dynamic Injection series out of the DAE Solution. It receives the solution and the
string name of the Dynamic Injection device.

"""
function post_proc_activepower_series(
    res::SimulationResults,
    name::String,
    dt::Union{Nothing, Float64},
)
    ts, V_R, V_I, I_R, I_I = post_proc_voltage_current_series(res, name, dt)
    return ts, V_R .* I_R + V_I .* I_I
end

"""
Function to compute the active power output time series of a Dynamic Injection series out of the DAE Solution. It receives the solution and the
string name of the Dynamic Injection device.

"""
function post_proc_reactivepower_series(
    res::SimulationResults,
    name::String,
    dt::Union{Nothing, Float64},
)
    ts, V_R, V_I, I_R, I_I = post_proc_voltage_current_series(res, name, dt)
    return ts, V_I .* I_R - V_R .* I_I
end

"""
Function to compute the field current output time series of a Dynamic Injection series out of the DAE Solution. It receives the solution and the
string name of the Dynamic Injection device.

"""
function post_proc_field_current_series(
    res::SimulationResults,
    name::String,
    dt::Union{Nothing, Float64},
)
    system = get_system(res)
    bus_lookup = get_bus_lookup(res)
    n_buses = length(bus_lookup)
    solution = res.solution
    device = PSY.get_component(PSY.StaticInjection, system, name)
    bus_ix = get(bus_lookup, PSY.get_number(PSY.get_bus(device)), -1)
    ts, V_R, V_I = post_proc_voltage_series(solution, bus_ix, n_buses, dt)
    dyn_device = PSY.get_dynamic_injector(device)
    _, I_fd = compute_field_current(res, dyn_device, V_R, V_I, dt)
    return ts, I_fd
end

"""
Function to compute the field voltage output time series of a Dynamic Injection series out of the DAE Solution. It receives the solution and the
string name of the Dynamic Injection device.

"""
function post_proc_field_voltage_series(
    res::SimulationResults,
    name::String,
    dt::Union{Nothing, Float64},
)
    system = get_system(res)
    device = PSY.get_component(PSY.StaticInjection, system, name)
    dyn_device = PSY.get_dynamic_injector(device)
    ts, Vf = compute_field_voltage(res, dyn_device, dt)
    return ts, Vf
end

"""
    get_state_series(
        res::SimulationResults,
        ref::Tuple{String, Symbol};
        dt::Union{Nothing, Float64} = nothing
    )
    end

Function to obtain series of states out of DAE Solution.

# Arguments

- `res::SimulationResults` : Simulation Results object that contains the solution
- `ref:Tuple{String, Symbol}` : Tuple used to identify the dynamic device, via its name, as a `String`, and the associated state as a `Symbol`.
"""
function get_state_series(res::SimulationResults, ref::Tuple{String, Symbol}; dt = nothing)
    return post_proc_state_series(res, ref, dt)
end

"""
    get_voltage_magnitude_series(
        res::SimulationResults,
        bus_number::Int
    )

Function to obtain the voltage magnitude series out of the DAE Solution.

# Arguments:

- `res::SimulationResults` : Simulation Results object that contains the solution
- `bus_number::Int` : Bus number identifier
"""
function get_voltage_magnitude_series(res::SimulationResults, bus_number::Int; dt = nothing)
    n_buses = get_bus_count(res)
    bus_ix = get(get_bus_lookup(res), bus_number, 0)
    ts, V_R, V_I = post_proc_voltage_series(res.solution, bus_ix, n_buses, dt)
    return ts, sqrt.(V_R .^ 2 .+ V_I .^ 2)
end

"""
    get_voltage_angle_series(
        res::SimulationResults,
        bus_number::Int
    )

Function to obtain the voltage angle series out of the DAE Solution.

# Arguments

- `res::SimulationResults` : Simulation Results object that contains the solution
- `bus_number::Int` : Bus number identifier
"""
function get_voltage_angle_series(res::SimulationResults, bus_number::Int; dt = nothing)
    n_buses = get_bus_count(res)
    bus_ix = get(get_bus_lookup(res), bus_number, 0)
    ts, V_R, V_I = post_proc_voltage_series(res.solution, bus_ix, n_buses, dt)
    return ts, atan.(V_I, V_R)
end

"""
    get_real_current_series(
            res::SimulationResults,
            name::String,
    )

Function to obtain the real current time series of a Dynamic Injection series out of the DAE Solution.

# Arguments

- `res::SimulationResults` : Simulation Results object that contains the solution
- `name::String` : Name to identify the specified device
"""
function get_real_current_series(res::SimulationResults, name::String; dt = nothing)
    return post_proc_real_current_series(res, name, dt)
end

"""
    get_imaginary_current_series(
            res::SimulationResults,
            name::String,
    )

Function to obtain the imaginary current time series of a Dynamic Injection series out of the DAE Solution.

# Arguments

- `res::SimulationResults` : Simulation Results object that contains the solution
- `name::String` : Name to identify the specified device
"""
function get_imaginary_current_series(res::SimulationResults, name::String; dt = nothing)
    return post_proc_imaginary_current_series(res, name, dt)
end

"""
    get_activepower_series(
            res::SimulationResults,
            name::String,
    )

Function to obtain the active power output time series of a Dynamic Injection series out of the DAE Solution.

# Arguments

- `res::SimulationResults` : Simulation Results object that contains the solution
- `name::String` : Name to identify the specified device
"""
function get_activepower_series(res::SimulationResults, name::String; dt = nothing)
    return post_proc_activepower_series(res, name, dt)
end

"""
    get_reactivepower_series(
            res::SimulationResults,
            name::String,
    )

Function to obtain the reactive power output time series of a Dynamic Injection series out of the DAE Solution.

# Arguments

- `res::SimulationResults` : Simulation Results object that contains the solution
- `name::String` : Name to identify the specified device
"""
function get_reactivepower_series(res::SimulationResults, name::String; dt = nothing)
    return post_proc_reactivepower_series(res, name, dt)
end

"""
    get_field_current_series(
            res::SimulationResults,
            name::String,
    )

Function to obtain the field current time series of a Dynamic Generator out of the DAE Solution.

# Arguments

- `res::SimulationResults` : Simulation Results object that contains the solution
- `name::String` : Name to identify the specified device
"""
function get_field_current_series(res::SimulationResults, name::String; dt = nothing)
    return post_proc_field_current_series(res, name, dt)
end

"""
    get_field_voltage_series(
            res::SimulationResults,
            name::String,
    )

Function to obtain the field voltage time series of a Dynamic Generator out of the DAE Solution.

# Arguments

- `res::SimulationResults` : Simulation Results object that contains the solution
- `name::String` : Name to identify the specified device
"""
function get_field_voltage_series(res::SimulationResults, name::String; dt = nothing)
    return post_proc_field_voltage_series(res, name, dt)
end

"""
    show_states_initial_value(res::SimulationResults)

Function to print initial states.

# Arguments

- `res::SimulationResults` : Simulation Results object that contains the solution
"""
function show_states_initial_value(res::SimulationResults)
    bus_size = get_bus_count(res)
    system = get_system(res)
    x0_init = res.solution.u[1]
    println("Voltage Variables")
    println("====================")
    buses_sorted =
        sort(collect(PSY.get_components(PSY.Bus, system)); by = x -> PSY.get_number(x))
    for bus in buses_sorted
        name = PSY.get_name(bus)
        println(name)
        println("====================")
        bus_n = PSY.get_number(bus)
        bus_ix = get_bus_lookup(res)[bus_n]
        V_R = x0_init[bus_ix]
        V_I = x0_init[bus_ix + bus_size]
        Vm = sqrt(V_R^2 + V_I^2)
        θ = angle(V_R + V_I * 1im)
        print("Vm ", round(Vm, digits = 4), "\n")
        print("θ ", round(θ, digits = 4), "\n")
        println("====================")
    end
    println("====================")
    for device in PSY.get_components(PSY.DynamicInjection, system)
        states = PSY.get_states(device)
        name = PSY.get_name(device)
        println("Differential States")
        println(name)
        println("====================")
        global_index = get_global_index(res)[name]
        for s in states
            print(s, " ", round(x0_init[global_index[s]], digits = 4), "\n")
        end
        println("====================")
    end
    dyn_branches = PSY.get_components(PSY.DynamicBranch, system)
    if !isempty(dyn_branches)
        println("====================")
        println("Line Current States")
        for br in dyn_branches
            states = PSY.get_states(br)
            name = PSY.get_name(br)
            printed_name = "Line " * name
            println("====================")
            println(printed_name)
            global_index = get_global_index(res)[name]
            for (i, s) in enumerate(states)
                print(s, " ", round(x0_init[global_index[s]], digits = 5), "\n")
            end
            println("====================")
        end
    end
    return
end


const DenseNativeArray = DenseArray{<:NativeTypes}

"""
`vstorent!` (non-temporal store) requires data to be aligned.
`alignstores!` will align `y` in preparation for the non-temporal maps.
"""
function alignstores!(
    f::F, y::DenseArray{T},
    args::Vararg{DenseNativeArray,A}
) where {F, T <: Base.HWReal, A}
    N = length(y)
    ptry = VectorizationBase.zero_offsets(stridedpointer(y))
    ptrargs = VectorizationBase.zero_offsets.(stridedpointer.(args))
    V = VectorizationBase.pick_vector_width_val(T)
    W = unwrap(V)
    zero_index = MM{W}(Static(0))
    uintptry = reinterpret(UInt, pointer(ptry))
    @assert iszero(uintptry & (sizeof(T) - 1)) "The destination vector (`dest`) must be aligned at least to `sizeof(eltype(dest))`."
    alignment = uintptry & (VectorizationBase.REGISTER_SIZE - 1)
    if alignment > 0
        i = reinterpret(Int, W - (alignment >>> VectorizationBase.intlog2(sizeof(T))))
        m = mask(T, i)
        if N < i
            m &= mask(T, N & (W - 1))
        end
        vnoaliasstore!(ptry, f(vload.(ptrargs, ((zero_index,),), m)...), (zero_index,), m)
        gesp(ptry, (i,)), gesp.(ptrargs, ((i,),)), N - i
    else
        ptry, ptrargs, N
    end
end

function vmap_singlethread!(
    f::F, y::DenseArray{T},
    ::Val{NonTemporal},
    args::Vararg{DenseNativeArray,A}
) where {F,T <: Base.HWReal, A, NonTemporal}
    if NonTemporal # if stores into `y` aren't aligned, we'll get a crash
        ptry, ptrargs, N = alignstores!(f, y, args...)
    else
        N = length(y)
        ptry = VectorizationBase.zero_offsets(stridedpointer(y))
        ptrargs = VectorizationBase.zero_offsets.(stridedpointer.(args))
    end
    i = 0
    V = VectorizationBase.pick_vector_width_val(T)
    W = unwrap(V)
    st = VectorizationBase.static_sizeof(T)
    zero_index = MM{W}(Static(0), st)
    while i < N - ((W << 2) - 1)

        # vstore!(stridedpointer(B), VectorizationBase.VecUnroll((v1,v2,v3)), VectorizationBase.Unroll{AU,1,3,AV,W64,zero(UInt)}((i, j, k)))
        # vload(stridedpointer(B), VectorizationBase.Unroll{1,1,4,1,W,0x0000000000000000}((i,)))

        index = VectorizationBase.Unroll{1,1,4,1,W,0x0000000000000000}((i,))
        v = f(vload.(ptrargs, index)...)
        if NonTemporal
            vstorent!(ptry, v, index)
        else
            vnoaliasstore!(ptry, v, index)
        end
        i = vadd(i, 4W)
    end
    while i < N - (W - 1) # stops at 16 when
        vᵣ = f(vload.(ptrargs, ((MM{W}(i),),))...)
        if NonTemporal
            vstorent!(ptry, vᵣ, (MM{W}(i),))
        else
            vnoaliasstore!(ptry, vᵣ, (MM{W}(i),))
        end
        i = vadd(i, W)
    end
    if i < N
        m = mask(T, N & (W - 1))
        vnoaliasstore!(ptry, f(vload.(ptrargs, ((MM{W}(i),),), m)...), (MM{W}(i,),), m)
    end
    y
end

function vmap_multithreaded!(
    f::F,
    y::DenseArray{T},
    ::Val{true},
    args::Vararg{DenseNativeArray,A}
) where {F,T,A}
    ptry, ptrargs, N = alignstores!(f, y, args...)
    N > 0 || return y
    W, Wshift = VectorizationBase.pick_vector_width_shift(T)
    V = VectorizationBase.pick_vector_width_val(T)
    Wsh = Wshift + 2
    Niter = N >>> Wsh
    Base.Threads.@threads for j ∈ 0:Niter-1
        index = VectorizationBase.Unroll{1,1,4,1,W,0x0000000000000000}((j << Wsh,))
        vstorent!(ptry, f(vload.(ptrargs, index)...), index)
    end
    ii = Niter << Wsh
    while ii < N - (W - 1) # stops at 16 when
        vstorent!(ptry, f(vload.(ptrargs, ((MM{W}(ii),),))...), (MM{W}(ii),))
        ii = vadd(ii, W)
    end
    if ii < N
        m = mask(T, N & (W - 1))
        vnoaliasstore!(ptry, f(vload.(ptrargs, ((MM{W}(ii),),), m)...), (MM{W}(ii),), m)
    end
    y
end
function vmap_multithreaded!(
    f::F,
    y::DenseArray{T},
    ::Val{false},
    args::Vararg{DenseNativeArray,A}
) where {F,T,A}
    N = length(y)
    ptry = VectorizationBase.zero_offsets(stridedpointer(y))
    ptrargs = VectorizationBase.zero_offsets.(stridedpointer.(args))
    N > 0 || return y
    W, Wshift = VectorizationBase.pick_vector_width_shift(T)
    V = VectorizationBase.pick_vector_width_val(T)
    Wsh = Wshift + 2
    Niter = N >>> Wsh
    Base.Threads.@threads for j ∈ 0:Niter-1
        index = VectorizationBase.Unroll{1,1,4,1,W,0x0000000000000000}((j << Wsh,))
        vnoaliasstore!(ptry, f(vload.(ptrargs, index)...), index)
    end
    ii = Niter << Wsh
    while ii < N - (W - 1) # stops at 16 when
        vnoaliasstore!(ptry, f(vload.(ptrargs, ((MM{W}(ii),),))...), (MM{W}(ii),))
        ii = vadd(ii, W)
    end
    if ii < N
        m = mask(T, N & (W - 1))
        vnoaliasstore!(ptry, f(vload.(ptrargs, ((MM{W}(ii),),), m)...), (MM{W}(ii),), m)
    end
    y
end

"""
    vmap!(f, destination, a::AbstractArray)
    vmap!(f, destination, a::AbstractArray, b::AbstractArray, ...)

Vectorized-`map!`, applying `f` to each element of `a` (or paired elements of `a`, `b`, ...)
and storing the result in `destination`.
"""
function vmap!(
    f::F, y::DenseArray{T}, args::Vararg{DenseNativeArray,A}
) where {F,T<:Base.HWReal,A}
    vmap_singlethread!(f, y, Val{false}(), args...)
end

"""
    vmapt!(::Function, dest, args...)

Like `vmap!` (see `vmap!`), but uses `Threads.@threads` for parallel execution.
"""
function vmapt!(
    f::F, y::DenseArray{T}, args::Vararg{DenseNativeArray,A}
) where {F,T<:Base.HWReal,A}
    vmap_multithreaded!(f, y, Val{false}(), args...)
end

"""
    vmapnt!(::Function, dest, args...)

This is a vectorized map implementation using nontemporal store operations. This means that the write operations to the destination will not go to the CPU's cache.
If you will not immediately be reading from these values, this can improve performance because the writes won't pollute your cache. This can especially be the case if your arguments are very long.

```julia
julia> using LoopVectorization, BenchmarkTools

julia> x = rand(10^8); y = rand(10^8); z = similar(x);

julia> f(x,y) = exp(-0.5abs2(x - y))
f (generic function with 1 method)

julia> @benchmark map!(f, \$z, \$x, \$y)
BenchmarkTools.Trial:
  memory estimate:  0 bytes
  allocs estimate:  0
  --------------
  minimum time:     439.613 ms (0.00% GC)
  median time:      440.729 ms (0.00% GC)
  mean time:        440.695 ms (0.00% GC)
  maximum time:     441.665 ms (0.00% GC)
  --------------
  samples:          12
  evals/sample:     1

julia> @benchmark vmap!(f, \$z, \$x, \$y)
BenchmarkTools.Trial:
  memory estimate:  0 bytes
  allocs estimate:  0
  --------------
  minimum time:     178.147 ms (0.00% GC)
  median time:      178.381 ms (0.00% GC)
  mean time:        178.430 ms (0.00% GC)
  maximum time:     179.054 ms (0.00% GC)
  --------------
  samples:          29
  evals/sample:     1

julia> @benchmark vmapnt!(f, \$z, \$x, \$y)
BenchmarkTools.Trial:
  memory estimate:  0 bytes
  allocs estimate:  0
  --------------
  minimum time:     144.183 ms (0.00% GC)
  median time:      144.338 ms (0.00% GC)
  mean time:        144.349 ms (0.00% GC)
  maximum time:     144.641 ms (0.00% GC)
  --------------
  samples:          35
  evals/sample:     1
```
"""
function vmapnt!(
    f::F, y::DenseArray{T}, args::Vararg{DenseNativeArray,A}
) where {F,T<:Base.HWReal,A}
    vmap_singlethread!(f, y, Val{true}(), args...)
end

"""
    vmapntt!(::Function, dest, args...)

Like `vmapnt!` (see `vmapnt!`), but uses `Threads.@threads` for parallel execution.
"""
function vmapntt!(
    f::F, y::DenseArray{T}, args::Vararg{DenseNativeArray,A}
) where {F,T<:Base.HWReal,A}
    vmap_multithreaded!(f, y, Val{true}(), args...)
end

# generic fallbacks
@inline vmap!(f, args...) = map!(f, args...)
@inline vmapt!(f, args...) = map!(f, args...)
@inline vmapnt!(f, args...) = map!(f, args...)
@inline vmapntt!(f, args...) = map!(f, args...)

function vmap_call(f::F, vm!::V, args::Vararg{Any,N}) where {V,F,N}
    T = Base._return_type(f, Base.Broadcast.eltypes(args))
    dest = similar(first(args), T)
    vm!(f, dest, args...)
end

"""
    vmap(f, a::AbstractArray)
    vmap(f, a::AbstractArray, b::AbstractArray, ...)

SIMD-vectorized `map`, applying `f` to each element of `a` (or paired elements of `a`, `b`, ...)
and returning a new array.
"""
vmap(f::F, args::Vararg{Any,N}) where {F,N} = vmap_call(f, vmap!, args...)

"""
    vmapt(f, a::AbstractArray)
    vmapt(f, a::AbstractArray, b::AbstractArray, ...)

A threaded variant of [`vmap`](@ref).
"""
vmapt(f::F, args::Vararg{Any,N}) where {F,N} = vmap_call(f, vmapt!, args...)

"""
    vmapnt(f, a::AbstractArray)
    vmapnt(f, a::AbstractArray, b::AbstractArray, ...)

A "non-temporal" variant of [`vmap`](@ref). This can improve performance in cases where
`destination` will not be needed soon.
"""
vmapnt(f::F, args::Vararg{Any,N}) where {F,N} = vmap_call(f, vmapnt!, args...)

"""
    vmapntt(f, a::AbstractArray)
    vmapntt(f, a::AbstractArray, b::AbstractArray, ...)

A threaded variant of [`vmapnt`](@ref).
"""
vmapntt(f::F, args::Vararg{Any,N}) where {F,N} = vmap_call(f, vmapntt!, args...)

# @inline vmap!(f, y, x...) = @avx y .= f.(x...)
# @inline vmap(f, x...) = @avx f.(x...)


using Revise
using Plots
using Pkg
# Pkg.activate(".")
using RadiativeTransfer
using RadiativeTransfer.Architectures
using RadiativeTransfer.Absorption
using RadiativeTransfer.Scattering
using RadiativeTransfer.vSmartMOM
using RadiativeTransfer.SolarModel
using InstrumentOperator
using Interpolations
using Polynomials
using ForwardDiff
using Distributions
using NCDatasets

## Atmospheric Radiative Transfer

# Load parameters from file
parameters = vSmartMOM.parameters_from_yaml("test/test_parameters/O2Parameters.yaml")
#parameters.architecture = CPU()
FT = Float64

# Load OCO Data:
# File names:
L1File = "/net/fluo/data1/group/oco2/L1bSc/oco2_L1bScND_26780a_190715_B10003r_200429212407.h5"
metFile = "/net/fluo/data1/group/oco2/L2Met/oco2_L2MetND_26780a_190715_B10003r_200429212406.h5"
dictFile = "/home/cfranken/code/gitHub/InstrumentOperator.jl/json/oco2.yaml"

# Load L1 file (could just use filenames here as well)
oco = InstrumentOperator.load_L1(dictFile,L1File, metFile);

# oco2_L1bScND_18688a_180105_B8100r_180206190633

oco_file = "/net/fluo/data1/group/oco2/L1bSc/oco2_L1bScGL_15258a_170515_B10003r_200214061601.h5"
oco_met_file = "/net/fluo/data1/group/oco2/L2Met/oco2_L2MetGL_26777a_190715_B10003r_200429213029.h5"
ils_file = "test/prototyping/ils_oco2.json"

# Pick some bands as tuple (or just one)
bands = (1,);
# Indices within that band:
indices = (92:885,);
# Geo Index (footprint,sounding):
GeoInd = [5,5000];

# Get data for that sounding:
oco_sounding = InstrumentOperator.getMeasurement(oco, bands, indices, GeoInd)

fp = 5
iOrbit = 5000
band = 1

sza_ = 32.4436
vza_ = [0.072]

function conv_spectra_local(m::VariableKernelInstrument, ν, spectrum; stride=1)
    # FT = eltype(m.ν_out)
    # Define grid where to perform convolution:

    # Padding at both sides required:
    off = ceil(Int, size(m.kernel, 1) / 2)
    ind = off:stride:(length(ν) - off)

    # knots where convolution will be applied to
    knots = view(ν, ind)
    te = LinearInterpolation(m.ν_out, Float32.(m.ind_out))
    spec_out = zeros(Real, length(knots));
    for i in eachindex(knots)
        # Simple first, nearest neighbor ILS
        ind_fraction = round(Int, te(knots[i]));
        kernel = view(m.kernel, :, ind_fraction)
        for j in eachindex(kernel)
            spec_out[i] += kernel[j] * spectrum[ind[i] + j]
        end
    end
    # Change this later to only perform conv around output grid!
    fin = LinearInterpolation(ν[ind], spec_out; extrapolation_bc=Interpolations.Flat())
    return fin(m.ν_out)
end;

# Runner is used to set AD fields as duals
function runner!(y, x, parameters=parameters, oco_file=oco_file,
                                              oco_met_file=oco_met_file,
                                              ils_file=ils_file, fp=fp, iOrbit=iOrbit,
                                              sza_=sza_, vza_=vza_)

    # Set parameters fields as the dual numbers
    parameters.brdf = [vSmartMOM.LambertianSurfaceLegendre([x[1],x[3],x[4]])]

    parameters.scattering_params.rt_aerosols[1].τ_ref = exp(x[2]);
    parameters.scattering_params.rt_aerosols[1].p₀    = 70000.0; #x[4]
    # parameters.scattering_params.rt_aerosols[1].aerosol.size_distribution = LogNormal(log(x[3]), log(x[4]), check_args=false)

    # parameters.scattering_params.rt_aerosols[1].aerosol.nᵣ = x[5];
    # parameters.scattering_params.rt_aerosols[1].aerosol.nᵢ = x[6];

    # parameters.scattering_params.rt_aerosols[1].p₀ = x[7];
    # parameters.scattering_params.rt_aerosols[1].σp = x[8];
    ocoData = Dataset(oco_file)
    SoundingGeometry = ocoData.group["SoundingGeometry"]
    ϕ = SoundingGeometry["sounding_polarization_angle"][fp,iOrbit]
    m₁ = 0.5
    m₂ = cosd(2ϕ)/2
    m₃ = sind(2ϕ)/2
    # Stokes:

    # Set profiles properly
    met = Dataset(oco_met_file);
    T_met = met.group["Meteorology"]["temperature_profile_met"][:,fp,iOrbit];
    ak = met.group["MeteorologyDiagnostics"]["ak"][:,fp,iOrbit];
    bk = met.group["MeteorologyDiagnostics"]["bk"][:,fp,iOrbit];
    ak = [0.000000e+00, 4.804826e-02, 6.593752e+00, 1.313480e+01, 1.961311e+01, 2.609201e+01,
3.257081e+01, 3.898201e+01, 4.533901e+01, 5.169611e+01, 5.805321e+01, 6.436264e+01,
7.062198e+01, 7.883422e+01, 8.909992e+01, 9.936521e+01, 1.091817e+02, 1.189586e+02,
1.286959e+02, 1.429100e+02, 1.562600e+02, 1.696090e+02, 1.816190e+02, 1.930970e+02,
2.032590e+02, 2.121500e+02, 2.187760e+02, 2.238980e+02, 2.243630e+02, 2.168650e+02,
2.011920e+02, 1.769300e+02, 1.503930e+02, 1.278370e+02, 1.086630e+02, 9.236572e+01,
7.851231e+01, 6.660341e+01, 5.638791e+01, 4.764391e+01, 4.017541e+01, 3.381001e+01,
2.836781e+01, 2.373041e+01, 1.979160e+01, 1.645710e+01, 1.364340e+01, 1.127690e+01,
9.292942e+00, 7.619842e+00, 6.216801e+00, 5.046801e+00, 4.076571e+00, 3.276431e+00,
2.620211e+00, 2.084970e+00, 1.650790e+00, 1.300510e+00, 1.019440e+00, 7.951341e-01,
6.167791e-01, 4.758061e-01, 3.650411e-01, 2.785261e-01, 2.113490e-01, 1.594950e-01,
1.197030e-01, 8.934502e-02, 6.600001e-02, 4.758501e-02, 3.270000e-02, 2.000000e-02,
1.000000e-02][end:-1:1]

bk = [1.000000e+00, 9.849520e-01, 9.634060e-01, 9.418650e-01, 9.203870e-01, 8.989080e-01,
8.774290e-01, 8.560180e-01, 8.346609e-01, 8.133039e-01, 7.919469e-01, 7.706375e-01,
7.493782e-01, 7.211660e-01, 6.858999e-01, 6.506349e-01, 6.158184e-01, 5.810415e-01,
5.463042e-01, 4.945902e-01, 4.437402e-01, 3.928911e-01, 3.433811e-01, 2.944031e-01,
2.467411e-01, 2.003501e-01, 1.562241e-01, 1.136021e-01, 6.372006e-02, 2.801004e-02,
6.960025e-03, 8.175413e-09, 0.000000e+00, 0.000000e+00, 0.000000e+00, 0.000000e+00,
0.000000e+00, 0.000000e+00, 0.000000e+00, 0.000000e+00, 0.000000e+00, 0.000000e+00,
0.000000e+00, 0.000000e+00, 0.000000e+00, 0.000000e+00, 0.000000e+00, 0.000000e+00,
0.000000e+00, 0.000000e+00, 0.000000e+00, 0.000000e+00, 0.000000e+00, 0.000000e+00,
0.000000e+00, 0.000000e+00, 0.000000e+00, 0.000000e+00, 0.000000e+00, 0.000000e+00,
0.000000e+00, 0.000000e+00, 0.000000e+00, 0.000000e+00, 0.000000e+00, 0.000000e+00,
0.000000e+00, 0.000000e+00, 0.000000e+00, 0.000000e+00, 0.000000e+00, 0.000000e+00,
0.000000e+00][end:-1:1]

    p_surf = met.group["Meteorology"]["surface_pressure_met"][fp,iOrbit];
    p_half = (ak + bk * p_surf);
    #p_half = vcat(p_half, p_surf);
    q = met.group["Meteorology"]["specific_humidity_profile_met"][:,fp,iOrbit];
    parameters.p = p_half / 100
    parameters.q = q # zeros(size(q))
    parameters.T = T_met
    #parameters.T[end-30:end] .-= 10
    parameters.sza = sza_
    parameters.vza = vza_

    @show parameters.sza, parameters.vza

    # @show parameters.sza
    # plot!(parameters.p)
    # return

    model = model_from_parameters(parameters);
    @show sum(model.τ_rayl[1] )
    # Run the model to obtain reflectance matrix
    R = vSmartMOM.rt_run(model, i_band=1);
    RR = m₁*R[1,1,:] + m₂*R[1,2,:] + m₃*R[1,3,:]
    @show size(R)
    # Produce black-body in wavenumber range
    T = 5777
    λ_grid = reverse(1e4 ./ parameters.spec_bands[1]) #collect(757:0.01:777)
    black_body = planck_spectrum_wl(T, λ_grid) * 2.1629e-05 * pi
    black_body = SolarModel.watts_to_photons(λ_grid, black_body)

    # Get solar transmittance spectrum
    #solar_transmission = solar_transmission_from_file("/home/rjeyaram/RadiativeTransfer/src/solar_merged_20160127_600_26316_100.out", parameters.spec_bands[1])
    solar_transmission = solar_transmission_from_file("/home/rjeyaram/RadiativeTransfer/src/SolarModel/solar.out", parameters.spec_bands[1])
    # Get outgoing solar radiation
    sun_out = reverse(solar_transmission) .* black_body

    # Apply Earth reflectance matrix
    earth_out = sun_out .* reverse(RR[:])

    # Set up InstrumentOperator
    ils_Δ, ils_in, dispersion = InstrumentOperator.read_ils_table(oco_file, ils_file);

    # Define model grid:
    res = 0.001

    # Just consider the ILS within ± 0.35nm
    grid_x = -0.35e-3:res*1e-3:0.35e-3

    extended_dims = [fp,band] # Footprint, band

    # Re-interpolate I from ν_grid to new grid/resolution
    interp_I = LinearInterpolation(λ_grid, earth_out);
    wl = 757.5:res:771.0
    I_wl = interp_I(wl/1000)

    # Pixels to be used
    ind_out = collect(0:1015);

    # Eventual grid of OCO-2 for Band 1, FP 5:
    dispPoly = Polynomial(view(dispersion, :, extended_dims...))
    ν = Float32.(dispPoly.(1:1016))

    # Prepare ILS table:
    ils_pixel   = InstrumentOperator.prepare_ils_table(grid_x, ils_in, ils_Δ,extended_dims)
    oco2_kernel = VariableKernelInstrument(ils_pixel, ν, ind_out)

    # Convolve input spectrum with variable kernel
    I_conv = conv_spectra_local(oco2_kernel, wl./1e3, I_wl)

    y[:] = I_conv[:]

end

directory = "/net/fluo/data1/group/oco2/L1bSc/"
files_list = filter(x->endswith(x, ".h5") && startswith(x, "oco2_L1bScND_26"), readdir(directory))

met_files_list = filter(x->endswith(x, ".h5"), readdir("/net/fluo/data1/group/oco2/L2Met/"))

I_convs_all = zeros(1016, length(files_list))
oco2_Abands_all = zeros(1016, length(files_list))

albedos = zeros(length(files_list))
cloud_levels = zeros(length(files_list))

optimal_x = zeros(8, length(files_list))

#for i in 1:length(files_list)
i = 1
ind = 92:885
@show i

oco_file = files_list[i]

    # try
ocoData = Dataset(directory * oco_file)
oco_met_file = "/net/fluo/data1/group/oco2/L2Met/" * filter(x->contains(x, oco_file[14:19]), met_files_list)[1]
oco_file = directory * oco_file
met = Dataset(oco_met_file)
cloud_levels[i] = met.group["Meteorology"]["cloud_liquid_water_path_met"][fp,iOrbit]

## Getting an OCO spectrum for fun to fit:
o2AbandSpectra = ocoData.group["SoundingMeasurements"]["radiance_o2"]
SoundingGeometry = ocoData.group["SoundingGeometry"]
ϕ = SoundingGeometry["sounding_polarization_angle"][fp,iOrbit]
m₁ = 0.5
m₂ = cosd(2ϕ)/2
m₃ = sind(2ϕ)/2
vza = SoundingGeometry["sounding_zenith"]
sza = SoundingGeometry["sounding_solar_zenith"]
lat = SoundingGeometry["sounding_latitude"]
lon = SoundingGeometry["sounding_longitude"]
o2AbandSpectra = ocoData.group["SoundingMeasurements"]["radiance_o2"]
oco2_Aband = o2AbandSpectra[:,fp,iOrbit]

println("$(round(Float64(lat[fp,iOrbit]), digits=2)) $(round(Float64(lon[fp,iOrbit]), digits=4))")
# continue
sza_ = sza[fp,iOrbit]
vza_ = [vza[fp,iOrbit]]

# State vector
x = FT[0.2377,
        -1, -0.00244, 0]#,

# Run FW model:
# @time runner(x);
I_conv = zeros(1016)
y = oco2_Aband[ind];
for i=1:3
    dfdx = ForwardDiff.jacobian(runner!, I_conv, x);
    Fx = I_conv[ind];
    K = dfdx[ind,:];
    dx = K \ (y-Fx)
    x += dx
    @show x
end
runner!(I_conv,x)
Fx = I_conv[ind];

# plot fits
ils_Δ, ils_in, dispersion = InstrumentOperator.read_ils_table(oco_file, ils_file);
extended_dims = [fp,band]; # Footprint, band
# Eventual grid of OCO-2 for Band 1, FP 5:
dispPoly = Polynomial(view(dispersion, :, extended_dims...));
ν = Float32.(dispPoly.(1:1016));
plot(ν[ind]*1e3,  y/1e20, label="Meas")
plot!(ν[ind]*1e3, Fx/1e20, label="Mod")
plot!(ν[ind]*1e3, (y-Fx)/1e20, label="Meas-mod")

function sampleOCO_Orbit(fp,iOrbit,l1,met)

end


function FieldVector3D(arg0::Field, arg1::Vector3D)
    return FieldVector3D((Field, Vector3D), arg0, arg1)
end

function FieldVector3D(arg0::RealFieldElement, arg1::FieldVector3D)
    return FieldVector3D((RealFieldElement, FieldVector3D), arg0, arg1)
end

function FieldVector3D(arg0::RealFieldElement, arg1::FieldVector3D, arg2::RealFieldElement, arg3::FieldVector3D)
    return FieldVector3D((RealFieldElement, FieldVector3D, RealFieldElement, FieldVector3D), arg0, arg1, arg2, arg3)
end

function FieldVector3D(arg0::RealFieldElement, arg1::FieldVector3D, arg2::RealFieldElement, arg3::FieldVector3D, arg4::RealFieldElement, arg5::FieldVector3D)
    return FieldVector3D((RealFieldElement, FieldVector3D, RealFieldElement, FieldVector3D, RealFieldElement, FieldVector3D), arg0, arg1, arg2, arg3, arg4, arg5)
end

function FieldVector3D(arg0::RealFieldElement, arg1::FieldVector3D, arg2::RealFieldElement, arg3::FieldVector3D, arg4::RealFieldElement, arg5::FieldVector3D, arg6::RealFieldElement, arg7::FieldVector3D)
    return FieldVector3D((RealFieldElement, FieldVector3D, RealFieldElement, FieldVector3D, RealFieldElement, FieldVector3D, RealFieldElement, FieldVector3D), arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7)
end

function FieldVector3D(arg0::RealFieldElement, arg1::RealFieldElement)
    return FieldVector3D((RealFieldElement, RealFieldElement), arg0, arg1)
end

function FieldVector3D(arg0::RealFieldElement, arg1::RealFieldElement, arg2::RealFieldElement)
    return FieldVector3D((RealFieldElement, RealFieldElement, RealFieldElement), arg0, arg1, arg2)
end

function FieldVector3D(arg0::RealFieldElement, arg1::Vector3D)
    return FieldVector3D((RealFieldElement, Vector3D), arg0, arg1)
end

function FieldVector3D(arg0::RealFieldElement, arg1::Vector3D, arg2::RealFieldElement, arg3::Vector3D)
    return FieldVector3D((RealFieldElement, Vector3D, RealFieldElement, Vector3D), arg0, arg1, arg2, arg3)
end

function FieldVector3D(arg0::RealFieldElement, arg1::Vector3D, arg2::RealFieldElement, arg3::Vector3D, arg4::RealFieldElement, arg5::Vector3D)
    return FieldVector3D((RealFieldElement, Vector3D, RealFieldElement, Vector3D, RealFieldElement, Vector3D), arg0, arg1, arg2, arg3, arg4, arg5)
end

function FieldVector3D(arg0::RealFieldElement, arg1::Vector3D, arg2::RealFieldElement, arg3::Vector3D, arg4::RealFieldElement, arg5::Vector3D, arg6::RealFieldElement, arg7::Vector3D)
    return FieldVector3D((RealFieldElement, Vector3D, RealFieldElement, Vector3D, RealFieldElement, Vector3D, RealFieldElement, Vector3D), arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7)
end

function FieldVector3D(arg0::Vector{RealFieldElement})
    return FieldVector3D((Vector{RealFieldElement},), arg0)
end

function FieldVector3D(arg0::jdouble, arg1::FieldVector3D)
    return FieldVector3D((jdouble, FieldVector3D), arg0, arg1)
end

function FieldVector3D(arg0::jdouble, arg1::FieldVector3D, arg2::jdouble, arg3::FieldVector3D)
    return FieldVector3D((jdouble, FieldVector3D, jdouble, FieldVector3D), arg0, arg1, arg2, arg3)
end

function FieldVector3D(arg0::jdouble, arg1::FieldVector3D, arg2::jdouble, arg3::FieldVector3D, arg4::jdouble, arg5::FieldVector3D)
    return FieldVector3D((jdouble, FieldVector3D, jdouble, FieldVector3D, jdouble, FieldVector3D), arg0, arg1, arg2, arg3, arg4, arg5)
end

function FieldVector3D(arg0::jdouble, arg1::FieldVector3D, arg2::jdouble, arg3::FieldVector3D, arg4::jdouble, arg5::FieldVector3D, arg6::jdouble, arg7::FieldVector3D)
    return FieldVector3D((jdouble, FieldVector3D, jdouble, FieldVector3D, jdouble, FieldVector3D, jdouble, FieldVector3D), arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7)
end

function add(obj::FieldVector3D, arg0::FieldVector3D)
    return jcall(obj, "add", FieldVector3D, (FieldVector3D,), arg0)
end

function add(obj::FieldVector3D, arg0::RealFieldElement, arg1::FieldVector3D)
    return jcall(obj, "add", FieldVector3D, (RealFieldElement, FieldVector3D), arg0, arg1)
end

function add(obj::FieldVector3D, arg0::RealFieldElement, arg1::Vector3D)
    return jcall(obj, "add", FieldVector3D, (RealFieldElement, Vector3D), arg0, arg1)
end

function add(obj::FieldVector3D, arg0::Vector3D)
    return jcall(obj, "add", FieldVector3D, (Vector3D,), arg0)
end

function add(obj::FieldVector3D, arg0::jdouble, arg1::FieldVector3D)
    return jcall(obj, "add", FieldVector3D, (jdouble, FieldVector3D), arg0, arg1)
end

function add(obj::FieldVector3D, arg0::jdouble, arg1::Vector3D)
    return jcall(obj, "add", FieldVector3D, (jdouble, Vector3D), arg0, arg1)
end

function angle(::Type{FieldVector3D}, arg0::FieldVector3D, arg1::FieldVector3D)
    return jcall(FieldVector3D, "angle", RealFieldElement, (FieldVector3D, FieldVector3D), arg0, arg1)
end

function angle(::Type{FieldVector3D}, arg0::FieldVector3D, arg1::Vector3D)
    return jcall(FieldVector3D, "angle", RealFieldElement, (FieldVector3D, Vector3D), arg0, arg1)
end

function angle(::Type{FieldVector3D}, arg0::Vector3D, arg1::FieldVector3D)
    return jcall(FieldVector3D, "angle", RealFieldElement, (Vector3D, FieldVector3D), arg0, arg1)
end

function cross_product(::Type{FieldVector3D}, arg0::FieldVector3D, arg1::FieldVector3D)
    return jcall(FieldVector3D, "crossProduct", FieldVector3D, (FieldVector3D, FieldVector3D), arg0, arg1)
end

function cross_product(::Type{FieldVector3D}, arg0::FieldVector3D, arg1::Vector3D)
    return jcall(FieldVector3D, "crossProduct", FieldVector3D, (FieldVector3D, Vector3D), arg0, arg1)
end

function cross_product(::Type{FieldVector3D}, arg0::Vector3D, arg1::FieldVector3D)
    return jcall(FieldVector3D, "crossProduct", FieldVector3D, (Vector3D, FieldVector3D), arg0, arg1)
end

function cross_product(obj::FieldVector3D, arg0::FieldVector3D)
    return jcall(obj, "crossProduct", FieldVector3D, (FieldVector3D,), arg0)
end

function cross_product(obj::FieldVector3D, arg0::Vector3D)
    return jcall(obj, "crossProduct", FieldVector3D, (Vector3D,), arg0)
end

function distance(::Type{FieldVector3D}, arg0::FieldVector3D, arg1::FieldVector3D)
    return jcall(FieldVector3D, "distance", RealFieldElement, (FieldVector3D, FieldVector3D), arg0, arg1)
end

function distance(::Type{FieldVector3D}, arg0::FieldVector3D, arg1::Vector3D)
    return jcall(FieldVector3D, "distance", RealFieldElement, (FieldVector3D, Vector3D), arg0, arg1)
end

function distance(::Type{FieldVector3D}, arg0::Vector3D, arg1::FieldVector3D)
    return jcall(FieldVector3D, "distance", RealFieldElement, (Vector3D, FieldVector3D), arg0, arg1)
end

function distance(obj::FieldVector3D, arg0::FieldVector3D)
    return jcall(obj, "distance", RealFieldElement, (FieldVector3D,), arg0)
end

function distance(obj::FieldVector3D, arg0::Vector3D)
    return jcall(obj, "distance", RealFieldElement, (Vector3D,), arg0)
end

function distance1(::Type{FieldVector3D}, arg0::FieldVector3D, arg1::FieldVector3D)
    return jcall(FieldVector3D, "distance1", RealFieldElement, (FieldVector3D, FieldVector3D), arg0, arg1)
end

function distance1(::Type{FieldVector3D}, arg0::FieldVector3D, arg1::Vector3D)
    return jcall(FieldVector3D, "distance1", RealFieldElement, (FieldVector3D, Vector3D), arg0, arg1)
end

function distance1(::Type{FieldVector3D}, arg0::Vector3D, arg1::FieldVector3D)
    return jcall(FieldVector3D, "distance1", RealFieldElement, (Vector3D, FieldVector3D), arg0, arg1)
end

function distance1(obj::FieldVector3D, arg0::FieldVector3D)
    return jcall(obj, "distance1", RealFieldElement, (FieldVector3D,), arg0)
end

function distance1(obj::FieldVector3D, arg0::Vector3D)
    return jcall(obj, "distance1", RealFieldElement, (Vector3D,), arg0)
end

function distance_inf(::Type{FieldVector3D}, arg0::FieldVector3D, arg1::FieldVector3D)
    return jcall(FieldVector3D, "distanceInf", RealFieldElement, (FieldVector3D, FieldVector3D), arg0, arg1)
end

function distance_inf(::Type{FieldVector3D}, arg0::FieldVector3D, arg1::Vector3D)
    return jcall(FieldVector3D, "distanceInf", RealFieldElement, (FieldVector3D, Vector3D), arg0, arg1)
end

function distance_inf(::Type{FieldVector3D}, arg0::Vector3D, arg1::FieldVector3D)
    return jcall(FieldVector3D, "distanceInf", RealFieldElement, (Vector3D, FieldVector3D), arg0, arg1)
end

function distance_inf(obj::FieldVector3D, arg0::FieldVector3D)
    return jcall(obj, "distanceInf", RealFieldElement, (FieldVector3D,), arg0)
end

function distance_inf(obj::FieldVector3D, arg0::Vector3D)
    return jcall(obj, "distanceInf", RealFieldElement, (Vector3D,), arg0)
end

function distance_sq(::Type{FieldVector3D}, arg0::FieldVector3D, arg1::FieldVector3D)
    return jcall(FieldVector3D, "distanceSq", RealFieldElement, (FieldVector3D, FieldVector3D), arg0, arg1)
end

function distance_sq(::Type{FieldVector3D}, arg0::FieldVector3D, arg1::Vector3D)
    return jcall(FieldVector3D, "distanceSq", RealFieldElement, (FieldVector3D, Vector3D), arg0, arg1)
end

function distance_sq(::Type{FieldVector3D}, arg0::Vector3D, arg1::FieldVector3D)
    return jcall(FieldVector3D, "distanceSq", RealFieldElement, (Vector3D, FieldVector3D), arg0, arg1)
end

function distance_sq(obj::FieldVector3D, arg0::FieldVector3D)
    return jcall(obj, "distanceSq", RealFieldElement, (FieldVector3D,), arg0)
end

function distance_sq(obj::FieldVector3D, arg0::Vector3D)
    return jcall(obj, "distanceSq", RealFieldElement, (Vector3D,), arg0)
end

function dot_product(::Type{FieldVector3D}, arg0::FieldVector3D, arg1::FieldVector3D)
    return jcall(FieldVector3D, "dotProduct", RealFieldElement, (FieldVector3D, FieldVector3D), arg0, arg1)
end

function dot_product(::Type{FieldVector3D}, arg0::FieldVector3D, arg1::Vector3D)
    return jcall(FieldVector3D, "dotProduct", RealFieldElement, (FieldVector3D, Vector3D), arg0, arg1)
end

function dot_product(::Type{FieldVector3D}, arg0::Vector3D, arg1::FieldVector3D)
    return jcall(FieldVector3D, "dotProduct", RealFieldElement, (Vector3D, FieldVector3D), arg0, arg1)
end

function dot_product(obj::FieldVector3D, arg0::FieldVector3D)
    return jcall(obj, "dotProduct", RealFieldElement, (FieldVector3D,), arg0)
end

function dot_product(obj::FieldVector3D, arg0::Vector3D)
    return jcall(obj, "dotProduct", RealFieldElement, (Vector3D,), arg0)
end

function equals(obj::FieldVector3D, arg0::Object)
    return jcall(obj, "equals", jboolean, (Object,), arg0)
end

function get_alpha(obj::FieldVector3D)
    return jcall(obj, "getAlpha", RealFieldElement, ())
end

function get_delta(obj::FieldVector3D)
    return jcall(obj, "getDelta", RealFieldElement, ())
end

function get_minus_i(::Type{FieldVector3D}, arg0::Field)
    return jcall(FieldVector3D, "getMinusI", FieldVector3D, (Field,), arg0)
end

function get_minus_j(::Type{FieldVector3D}, arg0::Field)
    return jcall(FieldVector3D, "getMinusJ", FieldVector3D, (Field,), arg0)
end

function get_minus_k(::Type{FieldVector3D}, arg0::Field)
    return jcall(FieldVector3D, "getMinusK", FieldVector3D, (Field,), arg0)
end

function get_na_n(::Type{FieldVector3D}, arg0::Field)
    return jcall(FieldVector3D, "getNaN", FieldVector3D, (Field,), arg0)
end

function get_negative_infinity(::Type{FieldVector3D}, arg0::Field)
    return jcall(FieldVector3D, "getNegativeInfinity", FieldVector3D, (Field,), arg0)
end

function get_norm(obj::FieldVector3D)
    return jcall(obj, "getNorm", RealFieldElement, ())
end

function get_norm1(obj::FieldVector3D)
    return jcall(obj, "getNorm1", RealFieldElement, ())
end

function get_norm_inf(obj::FieldVector3D)
    return jcall(obj, "getNormInf", RealFieldElement, ())
end

function get_norm_sq(obj::FieldVector3D)
    return jcall(obj, "getNormSq", RealFieldElement, ())
end

function get_plus_i(::Type{FieldVector3D}, arg0::Field)
    return jcall(FieldVector3D, "getPlusI", FieldVector3D, (Field,), arg0)
end

function get_plus_j(::Type{FieldVector3D}, arg0::Field)
    return jcall(FieldVector3D, "getPlusJ", FieldVector3D, (Field,), arg0)
end

function get_plus_k(::Type{FieldVector3D}, arg0::Field)
    return jcall(FieldVector3D, "getPlusK", FieldVector3D, (Field,), arg0)
end

function get_positive_infinity(::Type{FieldVector3D}, arg0::Field)
    return jcall(FieldVector3D, "getPositiveInfinity", FieldVector3D, (Field,), arg0)
end

function get_x(obj::FieldVector3D)
    return jcall(obj, "getX", RealFieldElement, ())
end

function get_y(obj::FieldVector3D)
    return jcall(obj, "getY", RealFieldElement, ())
end

function get_z(obj::FieldVector3D)
    return jcall(obj, "getZ", RealFieldElement, ())
end

function get_zero(::Type{FieldVector3D}, arg0::Field)
    return jcall(FieldVector3D, "getZero", FieldVector3D, (Field,), arg0)
end

function hash_code(obj::FieldVector3D)
    return jcall(obj, "hashCode", jint, ())
end

function is_infinite(obj::FieldVector3D)
    return jcall(obj, "isInfinite", jboolean, ())
end

function is_na_n(obj::FieldVector3D)
    return jcall(obj, "isNaN", jboolean, ())
end

function negate(obj::FieldVector3D)
    return jcall(obj, "negate", FieldVector3D, ())
end

function normalize(obj::FieldVector3D)
    return jcall(obj, "normalize", FieldVector3D, ())
end

function orthogonal(obj::FieldVector3D)
    return jcall(obj, "orthogonal", FieldVector3D, ())
end

function scalar_multiply(obj::FieldVector3D, arg0::RealFieldElement)
    return jcall(obj, "scalarMultiply", FieldVector3D, (RealFieldElement,), arg0)
end

function scalar_multiply(obj::FieldVector3D, arg0::jdouble)
    return jcall(obj, "scalarMultiply", FieldVector3D, (jdouble,), arg0)
end

function subtract(obj::FieldVector3D, arg0::FieldVector3D)
    return jcall(obj, "subtract", FieldVector3D, (FieldVector3D,), arg0)
end

function subtract(obj::FieldVector3D, arg0::RealFieldElement, arg1::FieldVector3D)
    return jcall(obj, "subtract", FieldVector3D, (RealFieldElement, FieldVector3D), arg0, arg1)
end

function subtract(obj::FieldVector3D, arg0::RealFieldElement, arg1::Vector3D)
    return jcall(obj, "subtract", FieldVector3D, (RealFieldElement, Vector3D), arg0, arg1)
end

function subtract(obj::FieldVector3D, arg0::Vector3D)
    return jcall(obj, "subtract", FieldVector3D, (Vector3D,), arg0)
end

function subtract(obj::FieldVector3D, arg0::jdouble, arg1::FieldVector3D)
    return jcall(obj, "subtract", FieldVector3D, (jdouble, FieldVector3D), arg0, arg1)
end

function subtract(obj::FieldVector3D, arg0::jdouble, arg1::Vector3D)
    return jcall(obj, "subtract", FieldVector3D, (jdouble, Vector3D), arg0, arg1)
end

function to_array(obj::FieldVector3D)
    return jcall(obj, "toArray", Vector{RealFieldElement}, ())
end

function to_string(obj::FieldVector3D)
    return jcall(obj, "toString", JString, ())
end

function to_string(obj::FieldVector3D, arg0::NumberFormat)
    return jcall(obj, "toString", JString, (NumberFormat,), arg0)
end

function to_vector3d(obj::FieldVector3D)
    return jcall(obj, "toVector3D", Vector3D, ())
end


include("boundary_conditions_adapted.jl")
"""
    calc_boundary_state!(::NumericalFluxSecondOrder, ::Impenetrable{FreeSlip}, ::ModelSetup)
apply free slip boundary condition for velocity
apply zero numerical flux in the normal direction
"""
function calc_boundary_state!(
    ::NumericalFluxSecondOrder,
    ::Impenetrable{FreeSlip},
    ::Union{ModelSetup},
    ::Nothing,
    state⁺,
    gradflux⁺,
    hyperflux⁺,
    aux⁺,
    n⁻,
    state⁻,
    gradflux⁻,
    hyperflux⁻,
    aux⁻,
    t,
    _...,
)
    state⁺.ρu = state⁻.ρu

    return nothing
end

function calc_boundary_state!(
    ::NumericalFluxSecondOrder,
    ::Impenetrable{FreeSlip},
    ::Union{ModelSetup},
    ::ConstantViscosity,
    state⁺,
    gradflux⁺,
    hyperflux⁺,
    aux⁺,
    n⁻,
    state⁻,
    gradflux⁻,
    hyperflux⁻,
    aux⁻,
    t,
    _...,
)
    state⁺.ρu = state⁻.ρu
    gradflux⁺.ν∇u = n⁻ * (@SVector [-0, -0, -0])'

    return nothing
end

"""
    calc_boundary_state!(::NumericalFluxSecondOrder, ::Impenetrable{NoSlip}, ::ModelSetup)
apply no slip boundary condition for velocity
sets ghost point to have no numerical flux on the boundary for U
"""
@inline function calc_boundary_state!(
    ::NumericalFluxSecondOrder,
    ::Impenetrable{NoSlip},
    ::Union{ModelSetup},
    ::Nothing,
    state⁺,
    gradflux⁺,
    hyperflux⁺,
    aux⁺,
    n⁻,
    state⁻,
    gradflux⁻,
    hyperflux⁻,
    aux⁻,
    t,
    _...,
)
    state⁺.ρu = -state⁻.ρu

    return nothing
end

@inline function calc_boundary_state!(
    ::NumericalFluxSecondOrder,
    ::Impenetrable{NoSlip},
    ::Union{ModelSetup},
    ::ConstantViscosity,
    state⁺,
    gradflux⁺,
    hyperflux⁺,
    aux⁺,
    n⁻,
    state⁻,
    gradflux⁻,
    hyperflux⁻,
    aux⁻,
    t,
    _...,
)
    state⁺.ρu = -state⁻.ρu
    gradflux⁺.ν∇u = gradflux⁻.ν∇u

    return nothing
end

"""
    calc_boundary_state!(::NumericalFluxSecondOrder, ::Insulating, ::HBModel)

apply insulating boundary condition for velocity
sets ghost point to have no numerical flux on the boundary for κ∇θ
"""
@inline function calc_boundary_state!(
    ::NumericalFluxSecondOrder,
    ::Union{Insulating, CoupledSecondaryBoundary, CoupledPrimaryBoundary},
    ::Union{ModelSetup},
    ::Nothing,
    state⁺,
    gradflux⁺,
    hyperflux⁺,
    aux⁺,
    n⁻,
    state⁻,
    gradflux⁻,
    hyperflux⁻,
    aux⁻,
    t,
    _...,
)
    state⁺.ρθ = state⁻.ρθ

    return nothing
end

@inline function calc_boundary_state!(
    ::NumericalFluxSecondOrder,
    ::Union{Insulating, CoupledSecondaryBoundary, CoupledPrimaryBoundary},
    ::Union{ModelSetup},
    ::ConstantViscosity,
    state⁺,
    gradflux⁺,
    hyperflux⁺,
    aux⁺,
    n⁻,
    state⁻,
    gradflux⁻,
    hyperflux⁻,
    aux⁻,
    t,
    _...,
)
    state⁺.ρθ = state⁻.ρθ
    gradflux⁺.κ∇θ = n⁻ * -0

    return nothing
end


for (syevd, elty, relty) in    ((:ssyevd, :Float32, :Float32),
                            (:dsyevd, :Float64, :Float64)),
                            interface in (:Matrix, :CuMatrix)
    @eval begin
        function magma_syevd!(jobz::AbstractChar, uplo::AbstractChar, A::$interface{$elty})
            A = Matrix{$elty}(A)

            n = checksquare(A)
            lda     = max(1, stride(A, 2))
            W = similar(A, $relty, n)
            work = Vector{$elty}(undef, 1)
            lwork = -1
            iwork = Vector{Int32}(undef, 1)
            liwork = -1
            info    = Ref{Int}()

            jobz_magma      = char_to_magmaInt(jobz)
            uplo_magma     = char_to_magmaIntUplo(uplo)

            for i in 1:2
                func = eval(@magmafunc($syevd))
                func(
                    jobz_magma,
                    uplo_magma,
                    n,
                    A,
                    lda,
                    W,
                    work,
                    lwork,
                    iwork,
                    liwork,
                    info
                )

                if i == 1
                    lwork = ceil(Int, nextfloat(real(work[1])))
                    resize!(work, lwork)
                    liwork = Int(iwork[1])
                    resize!(iwork, liwork)
                end
            end

            jobz == 'V' ? (W, A) : W
        end

        function magma_syevd_m!(ngpu::Int, jobz::AbstractChar, uplo::AbstractChar, A::$interface{$elty})
            A = Matrix{$elty}(A)

            n = checksquare(A)
            lda     = max(1, stride(A, 2))
            W = similar(A, $relty, n)
            work = Vector{$elty}(undef, 1)
            lwork = -1
            iwork = Vector{Int32}(undef, 1)
            liwork = -1
            info    = Ref{Int}()

            jobz_magma      = char_to_magmaInt(jobz)
            uplo_magma     = char_to_magmaIntUplo(uplo)

            for i in 1:2
                func = eval(@magmafunc_m($syevd))
                func(
                    ngpu,
                    jobz_magma,
                    uplo_magma,
                    n,
                    A,
                    lda,
                    W,
                    work,
                    lwork,
                    iwork,
                    liwork,
                    info
                )

                if i == 1
                    lwork = ceil(Int, nextfloat(real(work[1])))
                    resize!(work, lwork)
                    liwork = Int(iwork[1])
                    resize!(iwork, liwork)
                end
            end

            jobz == 'V' ? (W, A) : W
        end
    end
end

for (syevd, elty, relty) in    ((:cheevd, :ComplexF32, :Float32),
                            (:zheevd, :ComplexF64, :Float64)),
                            interface in (:Matrix, :CuMatrix)
    @eval begin
        function magma_syevd!(jobz::AbstractChar, uplo::AbstractChar, A::$interface{$elty})
            A = Matrix{$elty}(A)

            n = checksquare(A)
            lda     = max(1, stride(A, 2))
            W = similar(A, $relty, n)
            work = Vector{$elty}(undef, 1)
            lwork = -1
            rwork = Vector{$relty}(undef, 1)
            lrwork = -1
            iwork = Vector{Int32}(undef, 1)
            liwork = -1
            info    = Ref{Int}()

            jobz_magma      = char_to_magmaInt(jobz)
            uplo_magma     = char_to_magmaIntUplo(uplo)

            for i in 1:2
                func = eval(@magmafunc($syevd))
                func(
                    jobz_magma,
                    uplo_magma,
                    n,
                    A,
                    lda,
                    W,
                    work,
                    lwork,
                    rwork,
                    lrwork,
                    iwork,
                    liwork,
                    info
                )

                if i == 1
                    lwork = ceil(Int, nextfloat(real(work[1])))
                    resize!(work, lwork)
                    lrwork = ceil(Int, nextfloat(real(rwork[1])))
                    resize!(rwork, lrwork)
                    liwork = Int(iwork[1])
                    resize!(iwork, liwork)
                end
            end

            jobz == 'V' ? (W, A) : W
        end

        function magma_syevd_m!(ngpu::Int, jobz::AbstractChar, uplo::AbstractChar, A::$interface{$elty})
            A = Matrix{$elty}(A)

            n = checksquare(A)
            lda     = max(1, stride(A, 2))
            W = similar(A, $relty, n)
            work = Vector{$elty}(undef, 1)
            lwork = -1
            rwork = Vector{$relty}(undef, 1)
            lrwork = -1
            iwork = Vector{Int32}(undef, 1)
            liwork = -1
            info    = Ref{Int}()

            jobz_magma      = char_to_magmaInt(jobz)
            uplo_magma     = char_to_magmaIntUplo(uplo)

            for i in 1:2
                func = eval(@magmafunc_m($syevd))
                func(
                    ngpu,
                    jobz_magma,
                    uplo_magma,
                    n,
                    A,
                    lda,
                    W,
                    work,
                    lwork,
                    rwork,
                    lrwork,
                    iwork,
                    liwork,
                    info
                )

                if i == 1
                    lwork = ceil(Int, nextfloat(real(work[1])))
                    resize!(work, lwork)
                    lrwork = ceil(Int, nextfloat(real(rwork[1])))
                    resize!(rwork, lrwork)
                    liwork = Int(iwork[1])
                    resize!(iwork, liwork)
                end
            end

            jobz == 'V' ? (W, A) : W
        end
    end
end


struct Replicate_Report
    ids::Vector{Chain_ID} #replicates
    state_vecs::Dict{Chain_ID,Matrix{Float64}} #vectors of autotransition probabilities evolving by iterate, concatanated to matrices, indexed by Chain_ID
    emission_arrays::Dict{Chain_ID,Array{Float64}} #iterate*symbol*state_vecs
    sorted_id1_states::Vector{Integer}
    sort_dicts::Dict{Chain_ID,Dict{Integer,Integer}}#emission channels sorted on the basis of euclidean closeness to ids[1]
    sorted_symbols::Dict{Integer,Vector{Integer}}

    Replicate_Report(ids,state_vecs,emission_arrays, sorted_id1_states,sort_dicts,sorted_symbols) = assert_repreport(ids) && new(ids,state_vecs,emission_arrays,sorted_id1_states,sort_dicts,sorted_symbols)
end

function assert_repreport(ids::Vector{Chain_ID})
    Ks=Vector{Int64}()
    orders=Vector{Int64}()
    replicates=Vector{Int64}()
    obsids=Vector{String}()

    for id in ids
        push!(Ks,id.K)
        push!(orders,id.order)
        push!(obsids,id.obs_id)
        push!(replicates, id.replicate)
    end

    length(unique(Ks)) > 1 && throw(ArgumentError("Replicate set includes chains with different K state numbers! All chains must have HMMs with the same number of states."))
    length(unique(orders)) > 1 && throw(ArgumentError("Replicate set includes chains with different order coding numbers! All chains must have HMMs with the same order coding."))
    !allunique(replicates) && throw(ArgumentError("Replicate set includes chains with the same replicate #. Replicates should be unique!"))

    return true
end

function report_replicates(repset::Vector{Chain_ID},chains::Dict{Chain_ID,Vector{EM_step}})
    assert_repreport(repset) #check that the repset will pass its constructor before doing the work
    emission_arrays=Dict{Chain_ID,AbstractArray{AbstractFloat}}()
    state_arrays=Dict{Chain_ID,AbstractArray{AbstractFloat}}()

    for id in repset
        emission_array=zeros(length(chains[id]),4^(id.order+1),id.K)
        state_array=zeros(length(chains[id]),id.K)
        for (it, step) in enumerate(chains[id])
            for k in 1:id.K
                emission_array[it,:,k]=step.hmm.B[k].p
                state_array[it,k]=step.hmm.A[k,k]
            end
        end
        emission_arrays[id]=emission_array
        state_arrays[id]=state_array
    end

    println(repset)
    println(emission_arrays)
    println(state_arrays)
    sort_dicts,sorted_id1_states,sorted_symbols=sort_emitters_by_distance!(repset,emission_arrays)

    return Replicate_Report(repset,state_arrays,emission_arrays,sorted_id1_states, sort_dicts,sorted_symbols)
end

function sort_emitters_by_distance!(repset,emission_arrays)
    id1=repset[1]
    length(repset)==1 && (return Dict{Chain_ID,Dict{Integer,Integer}}(),[1:id1.K...],[1:4^id1.order+1])

    final_emissions = zeros(length(repset),size(emission_arrays[id1])[2:3]...)
    for (n,id) in enumerate(repset)
        final_emissions[n,:,:] = emission_arrays[id][end,:,:]
    end

    distances=zeros(length(repset)-1,id1.K,id1.K)
    for rep in 1:length(repset)-1
        for k in 1:id1.K, j in 1:id1.K
            distances[rep,k,j]=euclidean(final_emissions[1,:,k],final_emissions[1+rep,:,j])
        end
    end

    sortdicts=Dict{Chain_ID,Dict{Integer,Integer}}()
    sorted_id1_states=Vector{Integer}()
    sorted_symbols=Dict{Integer,Vector{Integer}}()
    for rep in 1:length(repset)-1
        sortdicts[repset[rep+1]]=Dict{Integer,Integer}()
    end

    for i in 1:id1.K
        id1_mindist_K=findmin([sum([minimum(distances[rep,k,:]) for rep in 1:length(repset)-1]) for k in 1:id1.K])[2]#find the state from the first replicate that has minimum cumulative distance to the closest state in the other replicates, excluding already-chosen states by spiking their values
        push!(sorted_id1_states,id1_mindist_K)
        for rep in 1:length(repset)-1
            rep_k=findmin(distances[rep,id1_mindist_K,:])[2]
            sortdicts[repset[rep+1]][id1_mindist_K]=rep_k
            distances[rep,:,rep_k].=1.0 #mask
            distances[1,id1_mindist_K,:].=1.0 #mask
        end

        symbol_distances=sum(hcat([euclidean.(final_emissions[1,:,id1_mindist_K],final_emissions[n+1,:,sortdicts[id][id1_mindist_K]]) for (n,id) in enumerate(repset[2:end])]...),dims=2)
        sorted_symbols[id1_mindist_K]=sortperm(symbol_distances[:,1])
    end

    return sortdicts,sorted_id1_states,sorted_symbols
end

function Base.show(io::IO, report::Replicate_Report; top_states::Integer=report.ids[1].K, top_symbols::Integer=2)
    printstyled(io, "REPLICATE CONVERGENCE REPORT\n", color=:green)
    println(" ̶̶ ̶ ̶ ̶ ̶̶ ̶ ̶ ̶ ̶̶ ̶ ̶ ̶ ̶̶ ̶ ̶ ̶ ̶̶ ̶ ̶ ̶ ̶̶ ̶ ̶ ̶ ̶̶ ̶ ̶ ̶ ̶̶ ̶ ̶ ̶ ̶̶ ̶ ̶ ̶ ̶̶ ̶ ̶ ̶ ̶̶ ̶ ̶ ̶ ̶̶ ̶ ̶ ̶ ̶̶ ̶ ̶ ̶ ̶̶ ̶ ̶ ̶ ̶̶ ̶ ̶ ̶")
    for i in 1:top_states
        id=report.ids[1]
        id1_state=report.sorted_id1_states[i]
        itmax=maximum([size(report.state_vecs[id],1) for id in report.ids])

        autoplt=lineplot(report.state_vecs[id][:,id1_state],
                        title="Autotransition prob. convergence",
                        name="$(report.ids[1]) K$id1_state",
                        xlim=(0,ceil(itmax, sigdigits=2)),
                        ylim=(0,1),
                        xlabel="iterate",
                        ylabel="p")

        symplot=lineplot(report.emission_arrays[id][:,report.sorted_symbols[id1_state][1],id1_state],
                        report.emission_arrays[id][:,report.sorted_symbols[id1_state][2],id1_state],
                        title="Symbol prob. convergence",
                        name="$(report.ids[1]) K$id1_state",
                        xlim=(0,1),
                        ylim=(0,1),
                        xlabel="Symbol 1 p",
                        ylabel="S2 p")

        for (n,id) in enumerate(report.ids)
            if n > 1
                lineplot!(autoplt, report.state_vecs[id][:,report.sort_dicts[id][id1_state]],
                            name="$(report.ids[n]) K$(report.sort_dicts[id][id1_state])")
                lineplot!(symplot,
                            report.emission_arrays[id][:,report.sorted_symbols[id1_state][1],report.sort_dicts[id][id1_state]],
                            report.emission_arrays[id][:,report.sorted_symbols[id1_state][2],report.sort_dicts[id][id1_state]],
                            name="$(report.ids[n]) K$(report.sort_dicts[id][id1_state])")
            end
        end

        show(autoplt)
        println("\n")
        show(symplot)
        println("\n")
    end
end