# state_updates.jl
# David Merrell
#
# A collection of functions for updating variables in a trace,
# or storing updates from MCMC.

import Base: lock, ReentrantLock
import Base.Threads: @threads

function old_update_loop(tr, proposal_param_vec::Vector{Float64})

    lambda_prop_std = proposal_param_vec[end]

    V = length(proposal_param_vec) - 1
    lambda_acc = false
    tr, lambda_acc = Gen.mh(tr, lambda_proposal, (lambda_prop_std,))

    acc_vec = zeros(V)
    for i=1:V
        tr, acc_vec[i] = Gen.mh(tr, smart_proposal,
                                (i, proposal_param_vec[i], V),
                                smart_involution)
    end

    return tr, (lambda_acc, acc_vec)
end

"""
Loop through the model variables and perform
Metropolis-Hastings updates on them.
It uses the parent set proposal distribution.
"""
function vertex_lambda_update_loop(tr, proposal_param_vec::Vector{Float64})

    lambda_prop_std = proposal_param_vec[end]

    V = length(proposal_param_vec) - 1

    acc_vec = zeros(V)
    lambda_acc = false
    for i=1:V
        tr, lambda_acc = Gen.mh(tr, lambda_vec_proposal, (i, lambda_prop_std))
        tr, acc_vec[i] = Gen.mh(tr, smart_proposal,
				(i, proposal_param_vec[i], V),
				smart_involution)
    end

    return tr, (lambda_acc, acc_vec)
end

"""
Equivalent to `vertex_lambda_update_loop`, except that it uses
multithread parallelism to update the parent sets.
"""
function multithread_update_loop(tr, proposal_param_vec::Vector{Float64})

    lambda_prop_std = proposal_param_vec[end]
    cmap = Gen.choicemap()
    r = ReentrantLock()

    V = length(proposal_param_vec) - 1

    acc_vec = zeros(Bool, V)
    lambda_acc = zeros(Bool, V)
    # Parallelize the loop over vertices
    @threads for i=1:V

        # Update the inverse temperature for this vertex
        new_tr, lambda_acc[i] = Gen.mh(tr, lambda_vec_proposal, (i, lambda_prop_std))
        if lambda_acc[i]
            lock(r) do
                cmap[:lambda_vec => i => :lambda] = new_tr[:lambda_vec => i => :lambda]
            end
        end

        # Update the parent set for this vertex
        new_tr, acc_vec[i] = Gen.mh(new_tr, smart_proposal,
				(i, proposal_param_vec[i], V),
				smart_involution)
        if acc_vec[i]
            # Update the choice map in a thread-safe fashion
            lock(r) do
                cmap[:parent_sets => i => :parents] = new_tr[:parent_sets => i => :parents]
            end
        end
    end

    tr, _, _, _ = Gen.update(tr, Gen.get_args(tr), (), cmap)
    return tr, (true, acc_vec)
end

function uniform_update_loop(tr, proposal_param_vec::Vector{Float64})

    lambda_prop_std = proposal_param_vec[end]

    V = length(proposal_param_vec) - 1

    acc_vec = zeros(V)
    lambda_acc = false
    for i=1:V
        tr, lambda_acc = Gen.mh(tr, lambda_vec_proposal, (i, lambda_prop_std))
        tr, acc_vec[i] = Gen.mh(tr, uniform_proposal, (V,), uniform_involution)
    end

    return tr, (lambda_acc, acc_vec)
end

import DataStructures: DefaultDict

"""
Aggregate summary statistics for the quantities of interest
while the markov chain runs, in an online fashion.
"""
function update_results_summary(results, tr, args)

    V = args[1]
    if results == nothing
        results = Dict("parent_sets" => [DefaultDict{Int64,Int64}(0) for i=1:V],
                       "lambda_mean" => 0.0,
                       "lambda_var" => 0.0,
                       "n" => 0
                       )
    end

    increment_counts!(results["parent_sets"], tr)

    results["lambda_mean"],
    results["lambda_var"],
    results["n"] = update_moments(results["lambda_mean"],
                                  results["lambda_var"],
                                  tr[:lambda],results["n"])

    return results
end

function update_moments(cur_mean, cur_var, new_x, cur_N)
    new_N = cur_N + 1
    diff = new_x - cur_mean
    new_mean = cur_mean + diff/new_N
    diff2 = new_x - new_mean
    if cur_N == 0
        new_var = 0.0
    else
        new_var = ((cur_N-1)*cur_var + diff*diff2)/cur_N
    end
    return new_mean, new_var, new_N
end

"""
Store a compact representation of all samples by
tracking *changes* in the model variables.
"""
function update_results_storediff(results, tr, args)

    V = args[1]
    vector_lambda = Bool(args[2])
    # Initialize the state
    if results == nothing
        results = Dict("parent_sets" => [DefaultDict{Int64,Vector{Tuple{Int64,Int64}}}([]) for i=1:V],
                       "n" => 0,
                       "prev_parents" => [Vector{Int64}() for i=1:V]
                       )
        if vector_lambda
            results["lambda"] = [Vector{Tuple{Int64,Float64}}() for i=1:V]
            for i=1:V
                push!(results["lambda"][i], (0, tr[:lambda_vec => i => :lambda]))
            end
        else
            results["lambda"] = Vector{Tuple{Int64,Float64}}()
            push!(results["lambda"], (0, tr[:lambda]))
        end

        for i=1:V
            for parent in tr[:parent_sets => i => :parents]
                push!(results["parent_sets"][i][parent], (0,1))
            end
            results["prev_parents"][i] = tr[:parent_sets => i => :parents]
        end

    else # Update the state

        n = results["n"] + 1
        # Lambda diffs
        if vector_lambda
            for i=1:V
                if results["lambda"][i][end][2] != tr[:lambda_vec => i => :lambda]
                    push!(results["lambda"][i], (n, tr[:lambda_vec => i => :lambda]))
                end
            end
        else
            if results["lambda"][end][2] != tr[:lambda]
                push!(results["lambda"], (n, tr[:lambda]))
            end
        end
        # Parent set diffs
        for i=1:V
            prev_ps = results["prev_parents"][i]
            cur_ps = tr[:parent_sets => i => :parents]
            # Has a new parent been added?
            new_parents = setdiff(cur_ps, prev_ps)
            for new_parent in new_parents
                push!(results["parent_sets"][i][new_parent], (n, 1))
            end

            # Has a parent been removed?
            removed_parents = setdiff(prev_ps, cur_ps)
            for rem_parent in removed_parents
                push!(results["parent_sets"][i][rem_parent], (n, 0))
            end

            results["prev_parents"][i] = copy(cur_ps)
        end
        # Finished. update n.
        results["n"] = n

    end

    return results
end

"""
Helper function for updating edge counts during inference.

The entry
tr[:parent_sets => i => :parents]
indicates existence of edge j --> i
(kind of backward from what you might expect.)

So this matrix should be read "row = child; column = parent"
"""
function increment_counts!(edge_counts, tr)

    for i=1:length(edge_counts) # for each vertex
        # increment the parents
        ps = tr[:parent_sets => i => :parents]
        for p in ps
            edge_counts[i][p] += 1
	end
    end
end


# Dependencies
push!(LOAD_PATH,"./")
using CpelNano

# IO
dataDir = dirname(dirname(pathof(CpelNano)))
nano = "$(dataDir)/examples/full_example/nanopolish/full_example_noise2.0_methylation_calls.sorted.tsv"
fasta = "$(dataDir)/examples/full_example/reference/hg38_chr17_43023997_43145780.fa"
outdir = "$(dataDir)/examples/full_example/cpelnano/"

# Output files
mml_path = "$(outdir)/full_example_mml.cpelnano"
nme_path = "$(outdir)/full_example_nme.cpelnano"
theta_path = "$(outdir)/full_example_theta.cpelnano"

# Remove output in outdir
isfile(mml_path) && rm(mml_path)
isfile(nme_path) && rm(nme_path)
isfile(theta_path) && rm(theta_path)

# Configuration
min_cov = 2.5; max_size_subreg = 500; size_est_reg = 3000; max_em_init = 1; max_em_iters = 20;
config = CpelNano.CpelNanoConfig(min_cov, max_size_subreg, size_est_reg, max_em_init, max_em_iters);
config.out_dir = outdir; config.out_prefix = "full_example"; config.verbose = false;

# Analyze each region in chr
CpelNano.analyze_nano(nano,fasta,config)

# Print files
println("*******************************MML*******************************")
println(String(read(mml_path)))
println("*****************************************************************")
readline()
println("*******************************NME*******************************")
println(String(read(nme_path)))
println("*****************************************************************")
readline()
println("*******************************THETA******************************")
println(String(read(theta_path)))
println("*****************************************************************")
readline()


mutable struct CProps
  ai::Vector{Int}
  bi::Vector{Int}
  ci::Vector{Int}
  nactiveA::Int
  nactiveB::Int
  nactiveC::Int
  AtoB::Vector{Int}
  AtoC::Vector{Int}
  BtoC::Vector{Int}
  permuteA::Bool
  permuteB::Bool
  permuteC::Bool
  dleft::Int
  dmid::Int
  dright::Int
  ncont::Int
  Acstart::Int
  Bcstart::Int
  Austart::Int
  Bustart::Int
  PA::Vector{Int}
  PB::Vector{Int}
  PC::Vector{Int}
  ctrans::Bool
  newArange::Vector{Int}
  newBrange::Vector{Int}
  newCrange::Vector{Int}
  function CProps(ai::Vector{Int},
                  bi::Vector{Int},
                  ci::Vector{Int})
    new(ai,bi,ci,0,0,0,Vector{Int}(),Vector{Int}(),Vector{Int}(),false,false,false,1,1,1,0,
        length(ai),length(bi),length(ai),length(bi),Vector{Int}(),Vector{Int}(),Vector{Int}(),
        false,Vector{Int}(),Vector{Int}(),Vector{Int}())
  end
end

function compute_perms!(props::CProps)::Nothing
  #Use !AtoB.empty() as a check to see if we've already run this
  length(props.AtoB)!=0 && return

  na = length(props.ai)
  nb = length(props.bi)
  nc = length(props.ci)

  props.AtoB = fill(0,na)
  props.AtoC = fill(0,na)
  props.BtoC = fill(0,nb)
  for i = 1:na
    for j = 1:nb
      if props.ai[i]==props.bi[j]
        props.ncont += 1
        #TODO: check this if this should be i,j or i-1,j-1 (0-index or 1-index)
        i<=props.Acstart && (props.Acstart = i)
        j<=props.Bcstart && (props.Bcstart = j)
        props.AtoB[i] = j
        break
      end
    end
  end

  for i = 1:na
    for k = 1:nc
      if props.ai[i]==props.ci[k]
        #TODO: check this if this should be i,j or i-1,j-1 (0-index or 1-index)
        i<=props.Austart && (props.Austart = i)
        props.AtoC[i] = k
        break
      end
    end
  end

  for j = 1:nb
    for k = 1:nc
      if props.bi[j]==props.ci[k]
        #TODO: check this if this should be i,j or i-1,j-1 (0-index or 1-index)
        j<=props.Bustart && (props.Bustart = j)
        props.BtoC[j] = k
        break
      end
    end
  end

end

function is_trivial_permutation(P::Vector{Int})::Bool
  for n = 1:length(P)
    P[n]!=n && return false
  end
  return true
end

function checkACsameord(props::CProps)::Bool
  props.Austart>=length(props.ai) && return true
  aCind = props.AtoC[props.Austart]
  for i = 1:length(props.ai)
    if !contractedA(props,i)
      props.AtoC[i]!=aCind && return false
      aCind += 1
    end
  end
  return true
end

function checkBCsameord(props::CProps)::Bool
  props.Bustart>=length(props.bi) && return true
  bCind = props.BtoC[props.Bustart]
  for i = 1:length(props.bi)
    if !contractedB(props,i)
      props.BtoC[i]!=bCind && return false
      bCind += 1
    end
  end
  return true
end

contractedA(props::CProps,i::Int) = (props.AtoC[i]<1)
contractedB(props::CProps,i::Int) = (props.BtoC[i]<1)
Atrans(props::CProps) = contractedA(props,1)
Btrans(props::CProps) = !contractedB(props,1)
Ctrans(props::CProps) = props.ctrans

function find_index(v::Vector{Int},t)::Int
  for i = 1:length(v)
    v[i]==t && return i
  end
  return -1
end

function permute_extents(R::Vector{Int},P::Vector{Int})::Vector{Int}
  Rb = fill(0,length(R))
  n = 1
  for pn in P
    Rb[pn] = R[n]
    n += 1
  end
  return Rb
end

function compute!(props::CProps,
                  A::Array,
                  B::Array,
                  C::Array)
  compute_perms!(props)

  #Use props.PC.size() as a check to see if we've already run this
  length(props.PC)!=0 && return

  ra = length(props.ai)
  rb = length(props.bi)
  rc = length(props.ci)

  props.PC = fill(0,rc)

  props.dleft = 1
  props.dmid = 1
  props.dright = 1
  c = 1
  for i = 1:ra
    if !contractedA(props,i)
      props.dleft *= size(A,i)
      props.PC[props.AtoC[i]] = c
      c += 1
    else
      props.dmid *= size(A,i)
    end
  end
  for j = 1:rb
    if !contractedB(props,j)
      props.dright *= size(B,j)
      props.PC[props.BtoC[j]] = c
      c += 1
    end
  end

  if !is_trivial_permutation(props.PC)
    props.permuteC = true
    if checkBCsameord(props) && checkACsameord(props)
      #Can avoid permuting C by
      #computing Bt*At = Ct
      props.ctrans = true
      props.permuteC = false
    end
  end

  #Check if A can be treated as a matrix without permuting
  props.permuteA = false
  if !(contractedA(props,1) || contractedA(props,ra))
    #If contracted indices are not all at front or back,
    #will have to permute A
    props.permuteA = true
  else
    #Contracted ind start at front or back, check if contiguous
    #TODO: check that the limits are correct (1-indexed vs. 0-indexed)
    for i = 1:props.ncont
      if !contractedA(props,props.Acstart+i-1)
        #Contracted indices not contiguous, must permute
        props.permuteA = true
        break
      end
    end
  end

  #Check if B is matrix-like
  props.permuteB = false
  if !(contractedB(props,1) || contractedB(props,rb))
    #If contracted indices are not all at front or back,
    #will have to permute B
    props.permuteB = true
  else
    #TODO: check that the limits are correct (1-indexed vs. 0-indexed)
    for i = 1:props.ncont
      if !contractedB(props,props.Bcstart+i-1)
        #Contracted inds not contiguous, permute
        props.permuteB = true
        break
      end
    end
  end

  if !props.permuteA && !props.permuteB
    #Check if contracted inds. in same order
    #TODO: check these limits are correct
    for i = 1:props.ncont
      if props.AtoB[props.Acstart+i-1]!=(props.Bcstart+i-1)
        #If not in same order,
        #must permute one of A or B
        #so permute the smaller one
        props.dleft<props.dright ? (props.permuteA = true) : (props.permuteB = true)
        break
      end
    end
  end

  if props.permuteC && !(props.permuteA && props.permuteB)
    PCost(d::Real) = d*d
    #Could avoid permuting C if
    #permute both A and B, worth it?
    pCcost = PCost(props.dleft*props.dright)
    extra_pABcost = 0
    !props.permuteA && (extra_pABcost += PCost(props.dleft*props.dmid))
    !props.permuteB && (extra_pABcost += PCost(props.dmid*props.dright))
    if extra_pABcost<pCcost
      props.permuteA = true
      props.permuteB = true
      props.permuteC = false
    end
  end

  if props.permuteA
    props.PA = fill(0,ra)
    #Permute contracted indices to the front,
    #in the same order as on B
    newi = 0
    #TODO: check this is correct for 1-indexing
    bind = props.Bcstart
    for i = 1:props.ncont
      while !contractedB(props,bind) bind += 1 end
      j = find_index(props.ai,props.bi[bind])
      newi += 1
      props.PA[newi] = j
      bind += 1
    end
    #Reset p.AtoC:
    fill!(props.AtoC,0)
    #Permute uncontracted indices to
    #appear in same order as on C
    #TODO: check this is correct for 1-indexing
    for k = 1:rc
      j = find_index(props.ai,props.ci[k])
      if j!=-1
        props.AtoC[newi] = k
        props.PA[newi+1] = j
        newi += 1
      end
      newi==ra && break
    end
  end

  ##Also update props.Austart,props.Acstart
  props.Acstart = ra+1
  props.Austart = ra+1
  #TODO: check this is correct for 1-indexing
  for i = 1:ra
    if contractedA(props,i)
      props.Acstart = min(i,props.Acstart)
    else
      props.Austart = min(i,props.Austart)
    end
    props.newArange = permute_extents([size(A)...],props.PA)
  end

  if(props.permuteB)
    props.PB = fill(0,rb)
    #TODO: check this is correct for 1-indexing
    newi = 0 #1
    if(props.permuteA)
      #A's contracted indices already set to
      #be in same order as B above, so just
      #permute contracted indices to the front
      #keeping relative order
      #TODO: how to translate this for loop?
      #for(int i = props.Bcstart; newi < props.ncont; ++newi)
      i = props.Bcstart
      #TODO: check this is correct for 1-indexing
      while newi < props.ncont
        while !contractedB(props,i) i += 1 end
        props.PB[newi+1] = i
        i += 1
        newi += 1
      end
    else
      #Permute contracted indices to the
      #front and in same order as on A
      aind = props.Acstart
      for i = 0:(props.ncont-1)
        while !contractedA(props,aind) aind += 1 end
        j = find_index(props.bi,props.ai[aind])
        newi += 1
        props.PB[newi] = j
        aind += 1
      end
    end
    #Reset p.BtoC:
    fill!(props.BtoC,0)
    #Permute uncontracted indices to
    #appear in same order as on C
    for k = 1:rc
      j = find_index(props.bi,props.ci[k])
      if j!=-1
        props.BtoC[newi] = k
        props.PB[newi+1] = j
        newi += 1
      end
      newi==rb && break
    end
    props.Bcstart = rb
    props.Bustart = rb
    for i = 1:rb
      if(contractedB(props,i))
          props.Bcstart = min(i,props.Bcstart)
      else
          props.Bustart = min(i,props.Bustart)
      end
    end
    props.newBrange = permute_extents([size(B)...],props.PB)
  end

  if props.permuteA || props.permuteB
    #Recompute props.PC
    c = 1
    #TODO: check this is correct for 1-indexing
    for i = 1:ra
      if !contractedA(props,i)
        props.PC[props.AtoC[i]] = c
        c += 1
      end
    end
    #TODO: check this is correct for 1-indexing
    for j = 1:rb
      if !contractedB(props,j)
        props.PC[props.BtoC[j]] = c
        c += 1
      end
    end
    props.ctrans = false
    if(is_trivial_permutation(props.PC))
      props.permuteC = false
    else
      props.permuteC = true
      #Here we already know since pc_triv = false that
      #at best indices from B precede those from A (on result C)
      #so if both sets remain in same order on C
      #just need to transpose C, not permute it
      if  checkBCsameord(props) && checkACsameord(props)
        props.ctrans = true
        props.permuteC = false
      end
    end
  end

  if props.permuteC
    Rb = Int[]
    if !props.permuteA
      #TODO: check this is correct for 1-indexing
      for i = 1:ra
        if !contractedA(props,i)
          push!(Rb,size(A,i))
        end
      end
    else
      #TODO: check this is correct for 1-indexing
      for i = 1:ra
        if !contractedA(props,i)
          push!(Rb,size(props.newArange,i))
        end
      end
    end
    if !props.permuteB
      #TODO: check this is correct for 1-indexing
      for j = 1:rb
        if !contractedB(props,j)
          push!(Rb,size(B,j))
        end
      end
    else
      #TODO: check this is correct for 1-indexing
      for j = 1:rb
        if !contractedB(props,j)
          push!(Rb,size(props.newBrange,j))
        end
      end
    end
    props.newCrange = Rb
  end

end

function contract!(C::Array{T},
                   p::CProps,
                   A::Array{T},
                   B::Array{T},
                   α::T=one(T),
                   β::T=zero(T)) where {T}

  tA = 'N'
  if p.permuteA
    aref = reshape(permutedims(A,p.PA),p.dmid,p.dleft)
    tA = 'T'
  else
    #A doesn't have to be permuted
    if Atrans(p)
      aref = reshape(A,p.dmid,p.dleft)
      tA = 'T'
    else
      aref = reshape(A,p.dleft,p.dmid)
    end
  end

  tB = 'N'
  if p.permuteB
    bref = reshape(permutedims(B,p.PB),p.dmid,p.dright)
  else
    if Btrans(p)
      bref = reshape(B,p.dright,p.dmid)
      tB = 'T'
    else
      bref = reshape(B,p.dmid,p.dright)
    end
  end

  if p.permuteC
    cref = reshape(copy(C),p.dleft,p.dright)
  else
    if Ctrans(p)
      cref = reshape(C,p.dleft,p.dright)
      if tA=='N' && tB=='N'
        (aref,bref) = (bref,aref)
        tA = tB = 'T'
      elseif tA=='T' && tB=='T'
        (aref,bref) = (bref,aref)
        tA = tB = 'N'
      end
    else
      cref = reshape(C,p.dleft,p.dright)
    end
  end

  #BLAS.gemm!(tA,tB,promote_type(T,Tα)(α),aref,bref,promote_type(T,Tβ)(β),cref)
  BLAS.gemm!(tA,tB,α,aref,bref,β,cref)

  if p.permuteC
    permutedims!(C,reshape(cref,p.newCrange...),p.PC)
  end
  return
end

#TODO: this should be optimized
function contract_scalar!(Cdata::Array,Clabels::Vector{Int},
                          Bdata::Array,Blabels::Vector{Int},α,β)
  p = calculate_permutation(Blabels,Clabels)
  if β==0
    if is_trivial_permutation(p)
      Cdata .= α.*Bdata
    else
      #TODO: make an optimized permutedims!() that also scales the data
      permutedims!(Cdata,α*Bdata)
    end
  else
    if is_trivial_permutation(p)
      Cdata .= α.*Bdata .+ β.*Cdata
    else
      #TODO: make an optimized permutedims!() that also adds and scales the data
      permBdata = permutedims(Bdata,p)
      Cdata .= α.*permBdata .+ β.*Cdata
    end
  end
  return
end

function contract!(Cdata::Array{T},Clabels::Vector{Int},
                   Adata::Array{T},Alabels::Vector{Int},
                   Bdata::Array{T},Blabels::Vector{Int},
                   α::T=one(T),β::T=zero(T)) where {T}
  if(length(Alabels)==0)
    contract_scalar!(Cdata,Clabels,Bdata,Blabels,α*Adata[1],β)
  elseif(length(Blabels)==0)
    contract_scalar!(Cdata,Clabels,Adata,Alabels,α*Bdata[1],β)
  else
    props = CProps(Alabels,Blabels,Clabels)
    compute!(props,Adata,Bdata,Cdata)
    contract!(Cdata,props,Adata,Bdata,α,β)
  end
  return
end

function contract(Cinds::IndexSet,
                  Clabels::Vector{Int},
                  Astore::Dense{SA},
                  Ainds::IndexSet,
                  Alabels::Vector{Int},
                  Bstore::Dense{SB},
                  Binds::IndexSet,
                  Blabels::Vector{Int}) where {SA<:Number,SB<:Number}
  SC = promote_type(SA,SB)

  # Convert the arrays to a common type
  # since we will call BLAS
  Astore = convert(Dense{SC},Astore)
  Bstore = convert(Dense{SC},Bstore)

  Adims = dims(Ainds)
  Bdims = dims(Binds)
  Cdims = dims(Cinds)

  # Create storage for output tensor
  Cstore = Dense{SC}(prod(Cdims))

  Adata = reshape(data(Astore),Adims)
  Bdata = reshape(data(Bstore),Bdims)
  Cdata = reshape(data(Cstore),Cdims)

  contract!(Cdata,Clabels,Adata,Alabels,Bdata,Blabels)
  return Cstore
end


import DCELs.squeeze_edge

@testset "Squeezing - empty" begin
    u, v = Vertex(0, 0), Vertex(1, 0)
    a1, a2 = Edge(), Edge()
    a1.twin, a2.twin = a2, a1
    a1.dest, a2.dest = u, v

    @test squeeze_edge(u, a1) == a1
end

@testset "Squeezing - line" begin
    u, v = Vertex(0, 0), Vertex(1, 0)
    a1, a2 = Edge(), Edge()

    a1.twin, a2.twin = a2, a1
    a1.prev, a2.prev = a2, a1
    a1.next, a2.next = a2, a1
    a1.dest, a2.dest = u, v

    u.edge, v.edge = a1, a2

    z = Vertex(2, 0)
    h1, h2 = Edge(), Edge()
    h1.twin, h2.twin = h2, h1
    h1.dest, h2.dest = v, z
    @test squeeze_edge(v, h1) == a2
end

@testset "Squeezing - cross" begin
    u, v, w, x, c = Vertex(1, 0), Vertex(0, 1), Vertex(-1, 0), Vertex(0, -1), Vertex(0, 0)
    a1, a2, b1, b2, c1, c2, d1, d2 = [Edge() for _ in 1:8]

    a1.twin, a2.twin = a2, a1
    b1.twin, b2.twin = b2, b1
    c1.twin, c2.twin = c2, c1
    d1.twin, d2.twin = d2, d1

    a1.dest, a2.dest = u, c
    b1.dest, b2.dest = v, c
    c1.dest, c2.dest = w, c
    d1.dest, d2.dest = x, c

    a1.prev, a2.prev = b2, a1
    b1.prev, b2.prev = c2, b1
    c1.prev, c2.prev = d2, c1
    d1.prev, d2.prev = a2, d1

    a1.next, a2.next = a2, d1
    b1.next, b2.next = b2, a1
    c1.next, c2.next = c2, b1
    d1.next, d2.next = d2, c1

    u.edge, v.edge, w.edge, x.edge = a1, b1, c1, d1
    c.edge = a2

    z = Vertex(1, -1)
    h1, h2 = Edge(), Edge()
    h1.twin, h2.twin = h2, h1
    h1.dest, h2.dest = c, z
    @test squeeze_edge(c, h1) == a2
end

@testset "Squeezing - triangle" begin
    u, v, w = Vertex(1, 0), Vertex(0, 1), Vertex(-1, 0)
    a1, a2, b1, b2, c1, c2 = [Edge() for _ in 1:6]

    u.edge, v.edge, w.edge = a1, b1, c1

    a1.twin, a2.twin = a2, a1
    b1.twin, b2.twin = b2, b1
    c1.twin, c2.twin = c2, c1

    a1.dest, b1.dest, c1.dest = u, v, w
    a2.dest, b2.dest, c2.dest = w, u, v

    a1.next, b1.next, c1.next = b1, c1, a1
    a1.prev, b1.prev, c1.prev = c1, a1, b1

    a2.next, b2.next, c2.next = c2, a2, b2
    a2.prev, b2.prev, c2.prev = b2, c2, a2

    x = Vertex(2, 0)
    h1, h2 = Edge(), Edge()
    h1.twin, h2.twin = h2, h1
    h1.dest, h2.dest = u, x
    @test squeeze_edge(u, h1) == b2

    x = Vertex(0, 0.5)
    h1, h2 = Edge(), Edge()
    h1.twin, h2.twin = h2, h1
    h1.dest, h2.dest = u, x
    @test squeeze_edge(u, h1) == a1
end


#### test set 2 begin

function inc(x)
    x+1
end

@testset "Scheduler" begin
    @testset "order" begin
        @par begin
            a = 1
            b = inc(a)
            c = inc(b)
        end

        deps = dependents(c)
        @test deps == Dict(a => Set([b]), b => Set([c]), c=>Set())
        @test noffspring(deps) == Dict(a=>2, b=>1, c=>0)
        @test order([c], noffspring(deps)) == Dict(a => 3, b => 2, c => 1)
    end
    @testset begin
        @par begin
            a = 1
            b = 2
            c = inc(a)
            d = b+c
        end
        deps = dependents(d)
        @test noffspring(deps) == Dict(a => 2, b => 1, c => 1, d => 0)
        @test order([d], noffspring(deps)) == Dict(d=>1, c=>3, b=>2, a=>4)
    end
    @testset "simple compute" begin
        @par begin
            a = 1
            b = 2
            c = inc(a)
            d = b+c
        end

        deps = dependents(d)
        @test noffspring(deps) == Dict(a => 2, b => 1, c => 1, d => 0)
        @test order([d], noffspring(deps)) == Dict(d=>1, c=>3, b=>2, a=>4)

        @test compute(Context(), d) == 4
    end
end


"""
    load_OPFproblems(input_type, instance_path::String)

Read provided files in `instance_path` function of `input_type` and create a structure ds of type DataSource for each scenario. Also create empty structures gs and mp for each scenario
For each scenario, treat data in ds to complete gs : from bus and links in ds, generate node_linksin and node_linksout
For each scenario, complete mp structure
Return a dictonary Scenario => Scenario structure

# Arguments
- `input_type::T` : type of entry format for example MatpowerInput, GOCInput
- `instance_path::String` : path to the instance to treat, provided the instance path must be associated to an instance of format input_type

# Output
- `OPFpbs`

# Examples
```
julia > OPFpbs = load_OPFproblems(MatpowerInput, "instances\\matpower\\WB2.m")
julia > OPFpbs
Dict{String,Scenario} with 1 entry:
  "BaseCase" => Scenario
```
"""

function load_OPFproblems(input_type, instance_path::String)
  OPFpbs = read_input(input_type, instance_path)
  # baseMVA, bus, link, node_elems, link_elems, elem_to_typekey = read_input(input_type, filenames)
  # ds = DataSource(baseMVA, bus, link)

  for scenario in keys(OPFpbs)
    node_linksin, node_linksout = Dict{String, Set{Link}}(), Dict{String, Set{Link}}()

    for (linkname, _) in OPFpbs[scenario].ds.link
      if !haskey(node_linksin, linkname.dest)
        node_linksin[linkname.dest] = Set{Link}()
      end
      union!(node_linksin[linkname.dest], [linkname])

      if !haskey(node_linksout, linkname.orig)
        node_linksout[linkname.orig] = Set{Link}()
      end
      union!(node_linksout[linkname.orig], [linkname])
    end

    OPFpbs[scenario].gs.node_linksin = node_linksin
    OPFpbs[scenario].gs.node_linksout = node_linksout

    node_vars=Dict{String, Dict{String, Variable}}(busname => Dict{String, Variable}() for busname in keys(OPFpbs[scenario].ds.bus))
    link_vars=Dict{Link, Dict{String, Variable}}(link => Dict{String, Variable}() for link in keys(OPFpbs[scenario].ds.link))

    node_formulations = Dict{String, Dict{String, Symbol}}()
    link_formulations = Dict{Link, Dict{String, Symbol}}()
    for bus in keys(OPFpbs[scenario].ds.bus)
      node_formulations[bus] = Dict{String, Symbol}(elem => :NbMinVar for elem in keys(OPFpbs[scenario].ds.bus[bus]))
    end
    for link in keys(OPFpbs[scenario].ds.link)
      link_formulations[link] = Dict{String, Symbol}(elem => :NbMinVar for elem in keys(OPFpbs[scenario].ds.link[link]))
    end

    OPFpbs[scenario].mp = MathematicalProgramming(node_formulations, link_formulations, node_vars, link_vars)
  end

  return OPFpbs
end


### ARSState

# ARSState holds the internal state ("local variables") of the acceptance-rejection sampler

mutable struct ARSState{S<:ValueSupport, F<:VariateForm} <: MCSamplerState{F}
  pstate::ParameterState{S, F} # Parameter state used internally by ARS
  tune::MCTunerState
  logproposal::Real
  weight::Real
  diagnosticindices::Dict{Symbol, Integer}
end

ARSState(
  pstate::ParameterState{S, F},
  tune::MCTunerState=BasicMCTune()
) where {S<:ValueSupport, F<:VariateForm} =
  ARSState(pstate, tune, NaN, NaN, diagnosticindices)

### Acceptance-rejection sampler (ARS)

struct ARS <: MCSampler
  logproposal::Function # Possibly unnormalized log-proposal (envelope)
  proposalscale::Real # Scale factor for logproposal (envelope) to cover target
  jumpscale::Real # Scale factor for adapting jump size

  function ARS(logproposal::Function, proposalscale::Real, jumpscale::Real)
    @assert jumpscale > 0 "Scale factor for adapting jump size is not positive"
    new(logproposal, proposalscale, jumpscale)
  end
end

ARS(logproposal::Function; proposalscale::Real=1., jumpscale::Real=1.) = ARS(logproposal, proposalscale, jumpscale)

### Initialize ARS sampler

## Initialize parameter state

function initialize!(
  pstate::ParameterState{Continuous, F},
  parameter::Parameter{Continuous, F},
  sampler::ARS,
  outopts::Dict
) where F<:VariateForm
  parameter.logtarget!(pstate)
  @assert isfinite(pstate.logtarget) "Log-target not finite: initial value out of support"

  if !isempty(outopts[:diagnostics])
    pstate.diagnostickeys = copy(outopts[:diagnostics])
    pstate.diagnosticvalues = Array(Any, length(pstate.diagnostickeys))
  end
end

## Initialize ARSState

function sampler_state(
  parameter::Parameter{S, F},
  sampler::ARS,
  tuner::MCTuner,
  pstate::ParameterState{S, F},
  vstate::VariableStateVector,
  diagnostickeys::Vector{Symbol}
) where {S<:ValueSupport, F<:VariateForm}
  sstate = ARSState(generate_empty(pstate), tuner_state(parameter, sampler, tuner))
  set_diagnosticindices!(sstate, [:accept], diagnostickeys)
  sstate
end

## Reset parameter state

function reset!(
  pstate::ParameterState{Continuous, Univariate},
  x::Real,
  parameter::Parameter{Continuous, Univariate},
  sampler::ARS
)
  pstate.value = x
  parameter.logtarget!(pstate)
end

function reset!(
  pstate::ParameterState{Continuous, Multivariate},
  x::RealVector,
  parameter::Parameter{Continuous, Multivariate},
  sampler::ARS
)
  pstate.value = copy(x)
  parameter.logtarget!(pstate)
end

reset!(
  sstate::ARSState{S, F},
  pstate::ParameterState{S, F},
  parameter::Parameter{S, F},
  sampler::MCSampler,
  tuner::MCTuner
) where {S<:ValueSupport, F<:VariateForm} =
  reset!(sstate.tune, sampler, tuner)

show(io::IO, sampler::ARS) =
  print(io, "ARS sampler: proposal scale = $(sampler.proposalscale), jump scale = $(sampler.jumpscale)")


#
# Copyright (c) 2021 Tobias Thummerer, Lars Mikelsons, Josef Kircher
# Licensed under the MIT license. See LICENSE file in the project root for details.
#

using FMI

# our simulation setup
t_start = 0.0
t_stop = 8.0

# this FMU runs under Windows/Linux
pathToFMU = joinpath(dirname(@__FILE__), "../model/OpenModelica/v1.17.0/SpringFrictionPendulum1D.fmu")

# this FMU runs only under Windows
if Sys.iswindows()
    pathToFMU = joinpath(dirname(@__FILE__), "../model/Dymola/2020x/SpringFrictionPendulum1D.fmu")
end

# load the FMU container
myFMU = fmiLoad(pathToFMU)

# print some useful FMU-information into the REPL
fmiInfo(myFMU)

# make an instance from the FMU
fmiInstantiate!(myFMU; loggingOn=true)

# setup the experiment, start time = 0.0 (optional for setup=true)
#fmiSetupExperiment(myFMU, t_start)

# enter and exit initialization (optional for setup=true)
#fmiEnterInitializationMode(myFMU)
#fmiExitInitializationMode(myFMU)

# run the FMU in mode Model-Exchange (ME) with adaptive step sizes, result values are stored in `solution`
solution, _ = fmiSimulateME(myFMU, t_start, t_stop; setup=true)

# plot the results
fmiPlot(myFMU, solution)

# unload the FMU, remove unpacked data on disc ("clean up")
fmiUnload(myFMU)


using GPUifyLoops

function kernel!(A, B)
    @inbounds @loop for i in (1:size(A,1);
                              (blockIdx().x-1)*blockDim().x + threadIdx().x)
        A[i] = B[i]
    end
    nothing
end

a = rand(Float32, 10^9)
b = similar(a)
kernel!(b, a)
@show Base.summarysize(a)/10^9

@assert b == a

@static if Base.find_package("CuArrays") !== nothing

    using CuArrays, CUDAnative

    @eval function kernel!(A::CuArray, B::CuArray)
        threads = 1024
        blocks = ceil(Int, size(A,1)/threads)
        @launch(CUDA(), threads=threads, blocks=blocks, kernel!(A, B))
    end

    ca = CuArray(a)
    cb = similar(ca)

    kernel!(cb, ca)

    @assert Array(cb) == Array(ca)

end


function update_constant!(s::MOI.LessThan{T}, val) where {T}
    return MOI.LessThan{T}(s.upper - val)
end

function update_constant!(s::MOI.GreaterThan{T}, val) where {T}
    return MOI.GreaterThan{T}(s.lower - val)
end

function update_constant!(s::MOI.EqualTo{T}, val) where {T}
    return MOI.EqualTo{T}(s.value - val)
end

# Affine
function update_parameter_in_affine_constraints!(model::Optimizer)
    for S in SUPPORTED_SETS
        affine_constraint_cache_inner =
            model.affine_constraint_cache[MOI.ScalarAffineFunction{Float64}, S]
        if !isempty(affine_constraint_cache_inner)
            update_parameter_in_affine_constraints!(
                model.optimizer,
                model.parameters,
                model.updated_parameters,
                affine_constraint_cache_inner,
            )
        end
    end
    return model
end

function update_parameter_in_affine_constraints!(
    optimizer::OT,
    parameters::Dict{MOI.VariableIndex,T},
    updated_parameters::Dict{MOI.VariableIndex,T},
    affine_constraint_cache_inner::MOI.Utilities.DoubleDicts.WithType{F,S},
) where {OT,T,F,S}
    for (ci, param_array) in affine_constraint_cache_inner
        update_parameter_in_affine_constraints!(
            optimizer,
            ci,
            param_array,
            parameters,
            updated_parameters,
        )
    end
    return optimizer
end

function update_parameter_in_affine_constraints!(
    optimizer::OT,
    ci::CI,
    param_array::Vector{MOI.ScalarAffineTerm{T}},
    parameters::Dict{MOI.VariableIndex,T},
    updated_parameters::Dict{MOI.VariableIndex,T},
) where {OT,T,CI}
    param_constant = zero(T)
    for term in param_array
        if haskey(updated_parameters, term.variable_index) # TODO This haskey can be slow
            param_constant +=
                term.coefficient * (
                    updated_parameters[term.variable_index] -
                    parameters[term.variable_index]
                )
        end
    end
    if param_constant != 0
        set = MOI.get(optimizer, MOI.ConstraintSet(), ci)
        set = update_constant!(set, param_constant)
        MOI.set(optimizer, MOI.ConstraintSet(), ci, set)
    end
    return ci
end

function update_parameters_in_affine_objective!(model::Optimizer)
    if !isempty(model.affine_objective_cache)
        objective_constant = 0
        for j in model.affine_objective_cache
            if haskey(model.updated_parameters, j.variable_index)
                param_old = model.parameters[j.variable_index]
                param_new = model.updated_parameters[j.variable_index]
                aux = param_new - param_old
                objective_constant += j.coefficient * aux
            end
        end
        if objective_constant != 0
            F = MOI.get(model.optimizer, MOI.ObjectiveFunctionType())
            f = MOI.get(model.optimizer, MOI.ObjectiveFunction{F}())
            fvar = MOI.ScalarAffineFunction(
                f.terms,
                f.constant + objective_constant,
            )
            MOI.set(model.optimizer, MOI.ObjectiveFunction{F}(), fvar)
        end
    end
    return model
end

function update_parameter_in_quadratic_constraints_pc!(model::Optimizer)
    for (ci, fparam) in model.quadratic_constraint_cache_pc
        param_constant = 0
        for j in fparam
            if haskey(model.updated_parameters, j.variable_index)
                param_old = model.parameters[j.variable_index]
                param_new = model.updated_parameters[j.variable_index]
                aux = param_new - param_old
                param_constant += j.coefficient * aux
            end
        end
        if param_constant != 0
            set = MOI.get(
                model.optimizer,
                MOI.ConstraintSet(),
                model.quadratic_added_cache[ci],
            )
            set = update_constant!(set, param_constant)
            MOI.set(
                model.optimizer,
                MOI.ConstraintSet(),
                model.quadratic_added_cache[ci],
                set,
            )
        end
    end
end

function update_parameter_in_quadratic_objective_pc!(model::Optimizer)
    if !isempty(model.quadratic_objective_cache_pc)
        objective_constant = 0
        for j in model.quadratic_objective_cache_pc
            if haskey(model.updated_parameters, j.variable_index)
                param_old = model.parameters[j.variable_index]
                param_new = model.updated_parameters[j.variable_index]
                aux = param_new - param_old
                objective_constant += j.coefficient * aux
            end
        end
        if objective_constant != 0
            F = MOI.get(model.optimizer, MOI.ObjectiveFunctionType())
            f = MOI.get(model.optimizer, MOI.ObjectiveFunction{F}())

            # TODO
            # Is there another way to verify the Type of F without expliciting {Float64}?
            # Something like isa(F, MOI.ScalarAffineFunction)
            fvar = if F == MathOptInterface.ScalarAffineFunction{Float64}
                MOI.ScalarAffineFunction(f.terms, f.constant + objective_constant)
            else
                MOI.ScalarQuadraticFunction(
                    f.affine_terms,
                    f.quadratic_terms,
                    f.constant + objective_constant,
                )
            end
            MOI.set(model.optimizer, MOI.ObjectiveFunction{F}(), fvar)
        end
    end
    return model
end

function update_parameter_in_quadratic_constraints_pp!(model::Optimizer)
    for (ci, fparam) in model.quadratic_constraint_cache_pp
        param_constant = 0
        for j in fparam
            if haskey(model.updated_parameters, j.variable_index_1) &&
               haskey(model.updated_parameters, j.variable_index_2)
                param_new_1 = model.updated_parameters[j.variable_index_1]
                param_new_2 = model.updated_parameters[j.variable_index_2]
                param_old_1 = model.parameters[j.variable_index_1]
                param_old_2 = model.parameters[j.variable_index_2]
                param_constant +=
                    j.coefficient *
                    ((param_new_1 * param_new_2) - (param_old_1 * param_old_2))
            elseif haskey(model.updated_parameters, j.variable_index_1)
                param_new_1 = model.updated_parameters[j.variable_index_1]
                param_old_1 = model.parameters[j.variable_index_1]
                param_old_2 = model.parameters[j.variable_index_2]
                param_constant +=
                    j.coefficient * param_old_2 * (param_new_1 - param_old_1)
            elseif haskey(model.updated_parameters, j.variable_index_2)
                param_new_2 = model.updated_parameters[j.variable_index_2]
                param_old_1 = model.parameters[j.variable_index_1]
                param_old_2 = model.parameters[j.variable_index_2]
                param_constant +=
                    j.coefficient * param_old_1 * (param_new_2 - param_old_2)
            end
        end
        if param_constant != 0
            set = MOI.get(
                model.optimizer,
                MOI.ConstraintSet(),
                model.quadratic_added_cache[ci],
            )
            set = update_constant!(set, param_constant)
            MOI.set(
                model.optimizer,
                MOI.ConstraintSet(),
                model.quadratic_added_cache[ci],
                set,
            )
        end
    end
end

function update_parameter_in_quadratic_objective_pp!(model::Optimizer)
    if !isempty(model.quadratic_objective_cache_pp)
        objective_constant = 0
        for j in model.quadratic_objective_cache_pp
            if haskey(model.updated_parameters, j.variable_index_1) &&
               haskey(model.updated_parameters, j.variable_index_2)
                param_new_1 = model.updated_parameters[j.variable_index_1]
                param_new_2 = model.updated_parameters[j.variable_index_2]
                param_old_1 = model.parameters[j.variable_index_1]
                param_old_2 = model.parameters[j.variable_index_2]
                objective_constant +=
                    j.coefficient *
                    ((param_new_1 * param_new_2) - (param_old_1 * param_old_2))
            elseif haskey(model.updated_parameters, j.variable_index_1)
                param_new_1 = model.updated_parameters[j.variable_index_1]
                param_old_1 = model.parameters[j.variable_index_1]
                param_old_2 = model.parameters[j.variable_index_2]
                objective_constant +=
                    j.coefficient * param_old_2 * (param_new_1 - param_old_1)
            elseif haskey(model.updated_parameters, j.variable_index_2)
                param_new_2 = model.updated_parameters[j.variable_index_2]
                param_old_1 = model.parameters[j.variable_index_1]
                param_old_2 = model.parameters[j.variable_index_2]
                objective_constant +=
                    j.coefficient * param_old_1 * (param_new_2 - param_old_2)
            end
        end
        if objective_constant != 0
            F = MOI.get(model.optimizer, MOI.ObjectiveFunctionType())
            f = MOI.get(model.optimizer, MOI.ObjectiveFunction{F}())

            # TODO
            # Is there another way to verify the Type of F without expliciting {Float64}?
            # Something like isa(F, MOI.ScalarAffineFunction)
            fvar = if F == MathOptInterface.ScalarAffineFunction{Float64}
                MOI.ScalarAffineFunction(f.terms, f.constant + objective_constant)
            else
                MOI.ScalarQuadraticFunction(
                    f.affine_terms,
                    f.quadratic_terms,
                    f.constant + objective_constant,
                )
            end
            MOI.set(model.optimizer, MOI.ObjectiveFunction{F}(), fvar)
        end
    end
end

# Vector Affine
function update_parameter_in_vector_affine_constraints!(model::Optimizer)
    for S in SUPPORTED_VECTOR_SETS
        vector_constraint_cache_inner =
            model.vector_constraint_cache[MOI.VectorAffineFunction{Float64}, S]
        if !isempty(vector_constraint_cache_inner)
            update_parameter_in_vector_affine_constraints!(
                model.optimizer,
                model.parameters,
                model.updated_parameters,
                vector_constraint_cache_inner,
            )
        end
    end
    return model
end

function update_parameter_in_vector_affine_constraints!(
    optimizer::OT,
    parameters::Dict{MOI.VariableIndex,T},
    updated_parameters::Dict{MOI.VariableIndex,T},
    vector_constraint_cache_inner::MOI.Utilities.DoubleDicts.WithType{F,S},
) where {OT,T,F,S}
    for (ci, param_array) in vector_constraint_cache_inner
        update_parameter_in_vector_affine_constraints!(
            optimizer,
            ci,
            param_array,
            parameters,
            updated_parameters,
        )
    end

    return optimizer
end

function update_parameter_in_vector_affine_constraints!(
    optimizer::OT,
    ci::CI,
    param_array::Vector{MOI.VectorAffineTerm{T}},
    parameters::Dict{MOI.VariableIndex,T},
    updated_parameters::Dict{MOI.VariableIndex,T},
) where {OT,T,CI}
    cf = MOI.get(optimizer, MOI.ConstraintFunction(), ci)

    n_dims = length(cf.constants)
    param_constants = zeros(T, n_dims)

    for term in param_array
        vi = term.scalar_term.variable_index

        if haskey(updated_parameters, vi) # TODO This haskey can be slow
            param_constants[term.output_index] =
                term.scalar_term.coefficient *
                (updated_parameters[vi] - parameters[vi])
        end
    end

    if param_constants != zeros(T, n_dims)
        MOI.modify(
            optimizer,
            ci,
            MOI.VectorConstantChange(cf.constants + param_constants),
        )
    end

    return ci
end

function update_parameters!(model::Optimizer)
    update_parameter_in_affine_constraints!(model)
    update_parameters_in_affine_objective!(model)
    update_parameter_in_quadratic_constraints_pc!(model)
    update_parameter_in_quadratic_objective_pc!(model)
    update_parameter_in_quadratic_constraints_pp!(model)
    update_parameter_in_quadratic_objective_pp!(model)
    update_parameter_in_vector_affine_constraints!(model)

    # TODO make this part better
    constraint_aux_dict = Dict{Any,Any}()

    for (ci, fparam) in model.quadratic_constraint_cache_pv
        for j in fparam
            if haskey(model.updated_parameters, j.variable_index_1)
                coef = j.coefficient
                param_new = model.updated_parameters[j.variable_index_1]
                if haskey(constraint_aux_dict, (ci, j.variable_index_2))
                    constraint_aux_dict[(ci, j.variable_index_2)] +=
                        param_new * coef
                else
                    constraint_aux_dict[(ci, j.variable_index_2)] =
                        param_new * coef
                end
            end
        end
    end

    for (ci, fparam) in
        model.quadratic_constraint_variables_associated_to_parameters_cache
        for j in fparam
            coef = j.coefficient
            if haskey(constraint_aux_dict, (ci, j.variable_index))#
                constraint_aux_dict[(ci, j.variable_index)] += coef
            else
                constraint_aux_dict[(ci, j.variable_index)] = coef
            end
        end
    end

    for ((ci, vi), value) in constraint_aux_dict
        old_ci = model.quadratic_added_cache[ci]
        MOI.modify(
            model.optimizer,
            old_ci,
            MOI.ScalarCoefficientChange(vi, value),
        )
    end

    objective_aux_dict = Dict{Any,Any}()

    if !isempty(model.quadratic_objective_cache_pv)
        for j in model.quadratic_objective_cache_pv
            if haskey(model.updated_parameters, j.variable_index_1)
                coef = j.coefficient
                param_new = model.updated_parameters[j.variable_index_1]
                if haskey(objective_aux_dict, (j.variable_index_2))
                    objective_aux_dict[(j.variable_index_2)] += param_new * coef
                else
                    objective_aux_dict[(j.variable_index_2)] = param_new * coef
                end
            end
        end
    end

    for j in model.quadratic_objective_variables_associated_to_parameters_cache
        coef = j.coefficient
        if haskey(objective_aux_dict, j.variable_index)
            objective_aux_dict[j.variable_index] += coef
        else
            objective_aux_dict[j.variable_index] = coef
        end
    end

    F_pv = MOI.get(model.optimizer, MOI.ObjectiveFunctionType())

    for (key, value) in objective_aux_dict
        MOI.modify(
            model.optimizer,
            MOI.ObjectiveFunction{F_pv}(),
            MOI.ScalarCoefficientChange(key, value),
        )
    end

    for (i, val) in model.updated_parameters
        model.parameters[i] = val
    end

    empty!(model.updated_parameters)

    return model
end


@with_kw mutable struct Feedback
    t0::Float64=time()                      # start time
    t1::Float64=time()                      # time for duration estimation
    nans_detected::Bool=false               # did NaNs occur in the simulation?
    progress_txt::Union{IOStream,Nothing}   # txt is a Nothing in case of no output
    output::Bool                            # output to netCDF?
    i::Int=0                                # time step increment
    nt::Int                                 # number of time steps
    nout::Int                               # number of time steps with output
    run_id::Int                             # run identification number
    runpath::String                         # output path plus run????/
end

"""Returns a human readable string representing seconds in terms of days, hours, minutes or seconds."""
function readable_secs(secs::Real)
    days = Int(floor(secs/3600/24))
    hours = Int(floor((secs/3600) % 24))
    minutes = Int(floor((secs/60) % 60))
    seconds = Int(floor(secs%3600%60))
    secs1f = @sprintf "%.1fs" secs%3600%60
    secs2f = @sprintf "%.2fs" secs%3600%60

    if days > 0
        return "$(days)d, $(hours)h"
    elseif hours > 0
        return "$(hours)h, $(minutes)min"
    elseif minutes > 0
        return "$(minutes)min, $(seconds)s"
    elseif seconds > 10
        return secs1f
    else
        return secs2f
    end
end

"""Estimates the total time the model integration will take."""
function duration_estimate(feedback::Feedback,S::ModelSetup)

    @unpack t1,i,nt,output,progress_txt = feedback
    @unpack nadvstep = S.grid

    time_per_step = (time()-t1) / (i-nadvstep)
    time_total = Int(round(time_per_step*nt))
    time_to_go = Int(round(time_per_step*(nt-i)))

    s1 = "Model integration will take approximately "*readable_secs(time_total)*","
    s2 = "and is hopefully done on "*Dates.format(now() + Dates.Second(time_to_go),Dates.RFC1123Format)

    print("\r\u1b[K")
    println(s1)     # print inline
    println(s2)
    if output == 1  # print in txt
        write(progress_txt,"\n"*s1*"\n")
        write(progress_txt,s2*"\n")
        flush(progress_txt)
    end
end

"""Returns a boolean whether the prognostic variables contains a NaN."""
function nan_detection!(Prog::PrognosticVars,feedback::Feedback)

    #TODO include a check for Posits
    #TODO include check for sst

    @unpack u,v,η,sst = Prog

    n_nan = sum(isnan.(u)) + sum(isnan.(v)) + sum(isnan.(η)) + sum(isnan.(sst))
    if n_nan > 0
        feedback.nans_detected = true
    end
end

"""Initialises the progress txt file."""
function feedback_init(S::ModelSetup)
    @unpack output,init_starti = S.parameters
    @unpack nt,nout = S.grid

    if output

        @unpack Ndays,initial_cond,init_run_id,T,bc,α = S.parameters
        @unpack nx,ny,Δ = S.grid

        run_id,runpath = get_run_id_path(S)

        txt = open(joinpath(runpath,"progress.txt"),"w")
        s = "Starting ShallowWaters run $run_id on "*Dates.format(now(),Dates.RFC1123Format)
        println(s)
        write(txt,s*"\n")
        write(txt,"Model will integrate $(Ndays)days at a resolution of $(nx)x$(ny) with Δ=$(Δ/1e3)km\n")
        write(txt,"Initial conditions are ")
        if initial_cond == "rest"
            write(txt,"rest.\n")
        else
            if init_starti > 0
                write(txt,"time step $init_starti of run $init_run_id.\n")
            else
                write(txt,"last time step of run $init_run_id.\n")
            end
        end
        write(txt,"Boundary conditions are $bc with lbc=$α.\n")
        write(txt,"Number format is "*string(T)*".\n")
        write(txt,"\nAll data will be stored in $runpath\n")

        # Parameter.txt
        ptxt = open(joinpath(runpath,"parameter.txt"),"w")
        print(ptxt,S.parameters)
        close(ptxt)
    else
        println("Starting ShallowWaters on "*Dates.format(now(),Dates.RFC1123Format)*" without output.")
        txt = nothing
        run_id = -1
        runpath = ""
    end

    return Feedback(progress_txt=txt,output=output,nt=nt,nout=nout,run_id=run_id,runpath=runpath)
end

"""Feedback function that calls duration estimate, nan_detection and progress."""
function feedback!(Prog::PrognosticVars,feedback::Feedback,S::ModelSetup)

    @unpack nadvstep = S.grid
    @unpack i = feedback

    if feedback.output
        if i == nadvstep # measure time after tracer advection executed once
            feedback.t1 = time()
        elseif i == 2*nadvstep
            # after the tracer advection executed twice or at least 50 steps
            duration_estimate(feedback,S)
        end
    end

    @unpack i, nans_detected, nout, output = feedback

    if !nans_detected
        if i % nout == 0    # only check for nans when output is produced
            nan_detection!(Prog,feedback)
            if feedback.nans_detected
                println(" NaNs detected at time step $i")
                if output
                    @unpack progress_txt = feedback
                    write(progress_txt," NaNs detected at time step $i")
                    flush(progress_txt)
                end
            end
        end
    end

    if feedback.i > 100      # show percentage only after duration is estimated
        progress!(feedback)
    end
end

"""Finalises the progress txt file."""
function feedback_end!(feedback::Feedback)
    @unpack output,t0,progress_txt = feedback

    s = " Integration done in "*readable_secs(time()-t0)*"."
    println(s)
    if output
        write(progress_txt,"\n"*s[2:end]*"\n")  # close txt file with last output
        flush(progress_txt)
    end
end

"""Converts time step into percent for feedback."""
function progress!(feedback::Feedback)

    @unpack i,nt,progress_txt,output = feedback

    if ((i+1)/nt*100 % 1) < (i/nt*100 % 1)  # update every 1 percent steps.
        percent = Int(round((i+1)/nt*100))
        print("\r\u1b[K")
        print("$percent%")
        if output && (percent % 5 == 0) # write out only every 5 percent step.
            write(progress_txt,"\n$percent%")
            flush(progress_txt)
        end
    end
end


"""
$(DocStringExtensions.README)
"""
module Catalyst

using DocStringExtensions
using Reexport, ModelingToolkit
using ModelingToolkit: Symbolic, value, istree
@reexport using ModelingToolkit
import MacroTools
import Base: (==), merge!, merge
using Latexify, Requires

# as used in Catlab
const USE_GV_JLL = Ref(false)
function __init__()
    @require Graphviz_jll="3c863552-8265-54e4-a6dc-903eb78fde85" begin
      USE_GV_JLL[] = true
      let cfg = joinpath(Graphviz_jll.artifact_dir, "lib", "graphviz", "config6")
        if !isfile(cfg)
          Graphviz_jll.dot(path -> run(`$path -c`))
        end
      end
    end
  end

const ExprValues = Union{Expr,Symbol,Float64,Int}

include("expression_utils.jl")
include("reaction_network.jl")

# reaction network macro
export @reaction_network, @add_reactions

include("spatial_network.jl")
export @spatial_reaction_network
export graph_iterator

# functions to query network properties
include("networkapi.jl")
export species, params, reactions, speciesmap, paramsmap, numspecies, numreactions, numparams
export make_empty_network, addspecies!, addparam!, addreaction!
export dependants, dependents, substoichmat, prodstoichmat

# for Latex printing of ReactionSystems
include("latexify_recipes.jl")

# for making and saving graphs
import Base.Iterators: flatten
import DataStructures: OrderedDict
import Parameters: @with_kw_noshow
include("graphs.jl")
export Graph, savegraph

end # module


using Flux

"""
Get `m` random samples from a dataset.
"""
function Base.rand(data::Flux.Data.DataLoader, m::Int=1)
    idx = rand(data.indices, m)

    x = selectrand(data.data[1], idx)
    y = selectrand(data.data[2], idx)
    return (x,y)
end

selectrand(data::Union{Matrix, Flux.OneHotMatrix}, idx) = data[:, idx]
selectrand(data::Union{BitArray, Vector}, idx) = data[idx]

function getmapping(data)
    mapping = Dict()
    for (i, (x,y)) in enumerate(data)
        mapping[i] = (x, y)
    end
    return mapping
end


using Dates
using Test

using MarketData

using TimeSeries

struct _TestType
end

@testset "combine" begin

@testset "collapse operations" begin
    @testset "collapse squishes correctly" begin
        @test values(collapse(cl, week, first))[2]    == 97.75
        @test timestamp(collapse(cl, week, first))[2] == Date(2000,1,10)

        @test values(collapse(cl, week, first, last))[2]    == 100.44
        @test timestamp(collapse(cl, week, first, last))[2] == Date(2000,1,10)

        @test values(collapse(cl, month, first))[2]    == 100.25
        @test timestamp(collapse(cl, month, first))[2] == Date(2000,2,1)

        @test values(collapse(ohlc, week, first))[2, :] == [102.0, 102.25, 94.75, 97.75]
        @test timestamp(collapse(ohlc, week, first))[2] == Date(2000,1,10)

        @test values(collapse(ohlc, week, first, last))[2, :] == [100.0, 102.25, 99.38, 100.44]
        @test timestamp(collapse(ohlc, week, first, last))[2] == Date(2000,1,10)

        @test values(collapse(ohlc, month, first))[2, :] == [104.0, 105.0, 100.0, 100.25]
        @test timestamp(collapse(ohlc, month, first))[2] == Date(2000,2,1)
    end

    # https://github.com/JuliaStats/TimeSeries.jl/pull/397
    @testset "Array of String" begin
        A = string.(Char.(rand(97:97+25, size(cl))))
        ts = timestamp(cl)
        ta = TimeArray(ts, A)

        let
            ta = collapse(ta, week, first)

            @test values(ta)[2]    == A[6]
            @test timestamp(ta)[2] == ts[6]
        end
    end
end

@testset "merge works correctly" begin
    cl1  = cl[1:3]
    op1  = cl[2:4]
    aapl = tail(AAPL)
    ba   = tail(BA)

    @testset "takes colnames kwarg correctly" begin
        @test colnames(merge(cl, ohlc[:High, :Low], colnames = [:a, :b, :c])) == [:a, :b, :c]
        @test colnames(merge(cl, op, colnames = [:a, :b]))                    == [:a, :b]
        @test_throws ArgumentError merge(cl, op, colnames = [:a])
        @test_throws ArgumentError merge(cl, op, colnames = [:a, :b, :c])

        for mode ∈ [:inner, :left, :right, :outer]
            @test colnames(merge(cl, ohlc[:High, :Low], method = mode, colnames = [:a, :b, :c])) == [:a, :b, :c]
            @test colnames(merge(cl, op, method = mode, colnames = [:a, :b])) == [:a, :b]
            @test_throws ArgumentError merge(cl, op, method = mode, colnames = [:a])
            @test_throws ArgumentError merge(cl, op, method = mode, colnames = [:a, :b, :c])
        end

        # issue #475
        @test colnames(merge(cl, cl, cl, colnames = [:a, :b, :c])) == [:a, :b, :c]
    end

    @testset "returns correct alignment with Dates and values" begin
        @test values(merge(cl, op))     == values(merge(cl, op, method = :inner))
        @test values(merge(cl,op))[2,1] == values(cl)[2,1]
        @test values(merge(cl,op))[2,2] == values(op)[2,1]
    end

    @testset "aligns with disparate sized objects" begin
        @test values(merge(cl, op[2:5]))[1,1]  == values(cl)[2,1]
        @test values(merge(cl, op[2:5]))[1,2]  == values(op)[2,1]
        @test timestamp(merge(cl, op[2:5]))[1] == Date(2000,1,4)
        @test length(merge(cl, op[2:5]))       == 4

        @test length(merge(cl1, op1, method = :inner))      == 2
        @test values(merge(cl1, op1, method = :inner))[2,1] == values(cl1)[3,1]
        @test values(merge(cl1, op1, method = :inner))[2,2] == values(op1)[2,1]

        @test length(merge(cl1, op1, method = :left))      == 3
        @test values(merge(cl1, op1, method = :left))[2,1] == values(cl1)[2,1]
        @test values(merge(cl1, op1, method = :left))[2,2] == values(op1)[1,1]
        @test isnan(values(merge(cl1, op1, method = :left))[1,2])

        @test length(merge(cl1, op1, method = :right))      == 3
        @test values(merge(cl1, op1, method = :right))[2,1] == values(cl1)[3,1]
        @test values(merge(cl1, op1, method = :right))[2,2] == values(op1)[2,1]
        @test isnan(values(merge(cl1, op1, method = :right))[3,1])

        @test length(merge(cl1, op1, method = :outer))      == 4
        @test values(merge(cl1, op1, method = :outer))[2,1] == values(cl1)[2,1]
        @test values(merge(cl1, op1, method = :outer))[2,2] == values(op1)[1,1]
        @test isnan(values(merge(cl1, op1, method = :outer))[1,2])
        @test isnan(values(merge(cl1, op1, method = :outer))[4,1])
    end

    @testset "column names match the correct values" begin
        @test colnames(merge(cl, op[2:5])) == [:Close, :Open]
        @test colnames(merge(op[2:5], cl)) == [:Open, :Close]

        @test colnames(merge(cl, op[2:5], method = :inner)) == [:Close, :Open]
        @test colnames(merge(op[2:5], cl, method = :inner)) == [:Open, :Close]

        @test colnames(merge(cl, op[2:5], method = :left))  == [:Close, :Open]
        @test colnames(merge(op[2:5], cl, method = :left))  == [:Open, :Close]

        @test colnames(merge(cl, op[2:5], method = :right)) == [:Close, :Open]
        @test colnames(merge(op[2:5], cl, method = :right)) == [:Open, :Close]

        @test colnames(merge(cl, op[2:5], method = :outer)) == [:Close, :Open]
        @test colnames(merge(op[2:5], cl, method = :outer)) == [:Open, :Close]
    end

    @testset "unknown method" begin
        @test_throws ArgumentError merge(cl, op, method = :unknown)
    end

    @testset "custom missing values" begin
        ts1 = TimeArray([Date(2018, 1, 1), Date(2018, 1, 2)], [1, 2])
        ts2 = TimeArray([Date(2018, 1, 2), Date(2018, 1, 3)], [3, 4])

        m1 = merge(ts1, ts2, method =  :left, padvalue = 0)
        @test timestamp(m1) == [Date(2018, 1, 1), Date(2018, 1, 2)]
        @test values(m1)    == [1 0; 2 3]

        m2 = merge(ts1, ts2, method = :right, padvalue = 0)
        @test timestamp(m2) == [Date(2018, 1, 2), Date(2018, 1, 3)]
        @test values(m2)    == [2 3; 0 4]

        m3 = merge(ts1, ts2, method = :outer, padvalue = 0)
        @test timestamp(m3) == [Date(2018, 1, 1), Date(2018, 1, 2), Date(2018, 1, 3)]
        @test values(m3)    == [1 0; 2 3; 0 4]
    end

    @testset "vararg input" begin
        ta1 = merge(op, ohlc, cl)
        ta2 = merge(merge(op, ohlc), cl)

        @test timestamp(ta1) == timestamp(ta2)
        @test values(ta1)    == values(ta2)

        # test keyword arguments passing
        ta1 = merge(op, ohlc, cl, method = :outer)
        ta2 = merge(merge(op, ohlc, method = :outer), cl, method = :outer)

        @test timestamp(ta1) == timestamp(ta2)
        @test values(ta1)    == values(ta2)
    end
end

@testset "hcat" begin
    let ta = [cl op cl]
        @test meta(ta)             == meta(cl)
        @test length(colnames(ta)) == 3
        @test colnames(ta)[1]      != colnames(ta)[3]
        @test colnames(ta)[1]      == :Close
        @test colnames(ta)[2]      == :Open
        @test values(ta)           == [values(cl) values(op) values(cl)]
    end

    let ta = [cl ohlc]
        @test meta(ta)             == meta(ta)
        @test length(colnames(ta)) == 5
        @test colnames(ta)[1]      == :Close
        @test colnames(ta)[1]      != colnames(ta)[end]
        @test values(ta)           == [values(cl) values(ohlc)]
    end

    @test_throws DimensionMismatch [cl[1:3] cl]
end

@testset "vcat works correctly" begin
    @testset "concatenates time series correctly in 1D" begin
        a = TimeArray([Date(2015, 10, 01), Date(2015, 11, 01)], [15, 16], [:Number])
        b = TimeArray([Date(2015, 12, 01)], [17], [:Number])
        c = vcat(a, b)

        @test length(c)   == (length(a) + length(b))
        @test colnames(c) == colnames(a)
        @test colnames(c) == colnames(b)
        @test values(c)   == [15, 16, 17]
    end

    @testset "concatenates time series correctly in 2D" begin
        a = TimeArray([Date(2015, 09, 01), Date(2015, 10, 01), Date(2015, 11, 01)], [[15 16]; [17 18]; [19 20]], [:Number1, :Number2])
        b = TimeArray([Date(2015, 12, 01)], [18 18], [:Number1, :Number2])
        c = vcat(a, b)

        @test length(c)   == length(a) + length(b)
        @test colnames(c) == colnames(a)
        @test colnames(c) == colnames(b)
        @test values(c)   == [[15 16]; [17 18]; [19 20]; [18 18]]
    end

    @testset "rejects when column names do not match" begin
        a = TimeArray([Date(2015, 10, 01), Date(2015, 11, 01)], [15, 16], [:foo])
        b = TimeArray([Date(2015, 12, 01)], [17], [:bar])

        @test_throws ArgumentError vcat(a, b)
    end

    @testset "rejects when metas do not match" begin
        a = TimeArray([Date(2015, 10, 01), Date(2015, 11, 01)], [15, 16], [:A], :FirstMeta)
        b = TimeArray([Date(2015, 12, 01)], [17], [:A], :SecondMeta)

        @test_throws ArgumentError vcat(a, b)
    end

    @testset "duplicated timestamps order" begin
        a = TimeArray([Date(2015, 10, 1), Date(2015, 10, 2), Date(2015, 11, 1)], [15, 16, 17])
        b = TimeArray([Date(2015, 10, 2), Date(2015, 11, 1)], [18, 19])

        ts = [Date(2015, 10, 1),
              Date(2015, 10, 2), Date(2015, 10, 2),
              Date(2015, 11, 1), Date(2015, 11, 1)]
        ta = vcat(a, b)
        @test timestamp(ta) == ts
        @test values(ta)    == [15, 16, 18, 17, 19]
    end

    @testset "still works when dates are mixed" begin
        a = TimeArray([Date(2015, 10, 01), Date(2015, 12, 01)], [15, 17])
        b = TimeArray([Date(2015, 11, 01)], [16])
        c = vcat(a, b)

        @test length(c)   == length(a) + length(b)
        @test colnames(c) == colnames(a)
        @test colnames(c) == colnames(b)
        @test values(c)   == [15, 16, 17]
        @test issorted(timestamp(c))
    end
end

@testset "map works correctly" begin
    @testset "works on both time stamps and 1D values" begin
        a = TimeArray([Date(2015, 10, 01), Date(2015, 11, 01)], [15, 16], [:A], :Something)
        b = map((timestamp, values) -> (timestamp + Dates.Year(1), values .- 1), a)

        @test length(b)                   == length(a)
        @test colnames(b)                 == colnames(a)
        @test Dates.year(timestamp(b)[1]) == Dates.year(timestamp(a)[1]) + 1
        @test values(b)[1]                == values(a)[1] - 1
        @test meta(b)                     == meta(a)
    end

    @testset "works on both time stamps and 2D values" begin
        a = TimeArray([Date(2015, 09, 01), Date(2015, 10, 01), Date(2015, 11, 01)], [[15 16]; [17 18]; [19 20]], [:A, :B])
        b = map((timestamp, values) -> (timestamp + Dates.Year(1), [values[1] + 2, values[2] - 1]), a)

        @test length(b)                   == length(a)
        @test colnames(b)                 == colnames(a)
        @test Dates.year(timestamp(b)[1]) == Dates.year(timestamp(a)[1]) + 1
        @test values(b)[1, 1]             == values(a)[1, 1] + 2
        @test values(b)[1, 2]             == values(a)[1, 2] - 1
    end

    @testset "works with order of elements that varies after modifications" begin
        a = TimeArray([Date(2015, 10, 01), Date(2015, 12, 01)], [15, 16], [:A])
        b = map((timestamp, values) -> (timestamp + Dates.Year((timestamp >= Date(2015, 11, 01)) ? -1 : 1), values), a)

        @test length(b) == length(a)
        @test issorted(timestamp(b))
    end

    @testset "map callable object" begin
        (::_TestType)(ts, x) = (ts, x + 42)

        ta = map(_TestType(), cl)
        @test timestamp(ta) == timestamp(cl)
        @test values(ta)    == values(cl) .+ 42
        @test meta(ta)      == meta(cl)
    end
end

end  # @testset "combine"


macro def(name, definition)
  esc(quote
    macro $name()
      esc($(Expr(:quote, definition)))
    end
  end)
end

# TODO decide if this is wanted and/or necessary
@def add_generic_fields begin
    method_string::String
    n::Int64
    x::Array{T}
    f_x::T
    f_calls::Int64
    g_calls::Int64
    h_calls::Int64
end

@def add_linesearch_fields begin
    x_ls::Array{T}
    g_ls::Array{T}
    alpha::T
    mayterminate::Bool
    lsr::LineSearches.LineSearchResults
end

@def initial_linesearch begin
    (similar(initial_x), # Buffer of x for line search in state.x_ls
    similar(initial_x), # Buffer of g for line search in state.g_ls
    LineSearches.alphainit(one(T), initial_x, g, f_x), # Keep track of step size in state.alpha
    false, # state.mayterminate
    LineSearches.LineSearchResults(T))
end


using ConservationLawsParticles
using RecursiveArrayTools
using Test
using TestSetExtensions

@testset ExtendedTestSet "ConservationLawsParticles.jl" begin
    @testset "densities" begin
        include("test_densities.jl")
    end

    @testset "velocities" begin
        include("test_velocities.jl")
    end

    @testset "utils" begin
        @testset "empty_like" begin
            @test length(@test_deprecated empty_like([1,2,3])) == 3
            @test typeof(@test_deprecated empty_like([1,2,3])) == Vector{Int}
        end
        @testset "@time_independent" begin
            @time_independent V_time_indep(x, y) = x + y
            @test V_time_indep(2, 3) == 5
            @test V_time_indep(0, 2, 3) == 5
            @test V_time_indep(:time, 2, 3) == 5
            @test V_time_indep(nothing, 2, 3) == 5
        end
    end
end


# Holger Teichgraeber, 2017

 ######################
 # ClustForOpt
 # Analyzing clustering techniques as input for energy systems optimization
 #
 #####################
using StatsKit
using JLD2
using FileIO
using JuMP #QUESTION should this be part of ClustForOpt?

include(joinpath("utils","datastructs.jl"))
include(joinpath("utils","utils.jl"))
include(joinpath("utils","load_data.jl"))
include(joinpath("optim_problems","run_opt.jl"))
include(joinpath("clustering","run_clust.jl"))
include(joinpath("clustering","exact_kmedoids.jl"))
include(joinpath("clustering","extreme_vals.jl"))
include(joinpath("clustering","attribute_weighting.jl"))
include(joinpath("clustering","intraperiod_segmentation.jl"))
include(joinpath("clustering","other_clust.jl"))


#!/usr/bin/env julia

using PyCall
@pyimport matplotlib as mpl
mpl.rc("text", usetex=true)
mpl.use("Agg")
using PyPlot
using JLD2
using FileIO
using ArgParse

function singleplot(filename::String, name::String, compare::String = "")
    d = load(filename*".jld2")
  if compare == ""
    comptimes = d[name]
    ylab = "computional time [s]"
  else
    comptimes = d[name]./d[compare]
    ylab = "speedup"
  end
  x = d["x"]
  t = d["t"]
  m = d["m"]
  mpl.rc("font", family="serif", size = 7)
  fig, ax = subplots(figsize = (2.5, 2.))
  col = ["red", "blue", "black", "green", "yellow", "orange"]
  marker = [":s", ":o", ":v", ":<", ":>", ":d"]
  for i in 1:size(comptimes, 2)
    tt = t[i]
    ax[:plot](d[x], comptimes[:,i], marker[i], label= "t = $tt", color = col[i], markersize=2.5, linewidth = 1)
  end
  PyPlot.ylabel(ylab, labelpad = -1.8)
  PyPlot.xlabel(x, labelpad = -2)
  if maximum(comptimes) > 10
    f = matplotlib[:ticker][:ScalarFormatter]()
    f[:set_powerlimits]((-3, 2))
  else
    f = matplotlib[:ticker][:FormatStrFormatter]("%.1f")
  end
  ax[:yaxis][:set_major_formatter](f)
  ax[:legend](fontsize = 6, loc = 2, ncol = 1)
  subplots_adjust(left = 0.15, bottom = 0.12,top=0.92)
  fig[:savefig](name*filename*".pdf")
end

"""
  pltspeedup(comptimes::Array{Float}, m::Int, n::Vector{Int}, T::Vector{Int}, label::String)

Returns a figure in .pdf format of the computional speedup of cumulants function

"""

function pltspeedup(filename::String)
  d = load(filename)
  filename = replace(filename, ".jld2"=>"")
  for f in d["functions"]
    singleplot(filename::String, f...)
  end
end

function main(args)
  s = ArgParseSettings("description")
  @add_arg_table s begin
    "file"
    help = "the file name"
    arg_type = String
  end
  parsed_args = parse_args(s)
  pltspeedup(parsed_args["file"])
end

main(ARGS)


# file contains default parameters used throughout Iris

# PML/cPML parameters
const EXTINCTION = 1e-10 # extinction in PML layer
const SCALING_ANGLE = .15 # phase in conductivity to accelerate evanscent decay
const BL_DEPTH = .1 # default depth of cPML or PML layer

const DEFAULT_BC = :noBC # default boundary condition (be specified as symbol or string)
const DEFAULT_BL = :noBL # default boundary layer (specified as symbol or string)
const LUPACK = :USolver # default LU package, must be one of USolver, MSolver, PSolver (specified as symbol or string)
const INDEX_OFFSET = 7 # position index offset from midpoint where field is real (SPECTRAL), or real and non-vanishing (LASING/SCPA)
const EQUIVALENT_SOURCE_RELATIVE_CUTOFF = 1e-8 # relative tolerance condition for dropping components of equivalent current
const DEFAULT_CFL_NUMBER = 0.9
const DEFAULT_LINEAR_EIGENSOLVER = :Arpack
const DEFAULT_NONLINEAR_EIGENSOLVER = :NonlinearEigenproblems
const NUM_SUBPIXELS = 5
const ORTHOGONALIZE_OVERLAP_THRESHOLD = .1
const SPA_ACCEPTANCE_PHASE = 5*π/180 # largest phase deviation from real axis considered as "real" for single pole approximation

# 2D unsymmetric shape defaults
const NL_NORMAL_ALGORITHM = :LD_MMA # NLopt algorithm used to find the normal, tangent, and distance to shape perimeter
const NL_NORMAL_XTOL = 1e-5 # NLopt's xtol_rel
const NL_NORMAL_FTOL = 1e-5 # NLopt's ftol_rel
const NL_NORMAL_MAXEVAL = 400 # NLopt's maxeval

# for plotting
const SHAPE_FILL_ALPHA = .2
const SHAPE_COLOR = :red
const BOUNDARY_COLOR = :gray
const BOUNDARY_PML_COLOR = :blue
const BOUNDARY_CPML_COLOR = :red
const BOUNDARY_DIRICHLET_LINETYPE = :solid
const BOUNDARY_NEUMANN_LINETYPE = :dash
const BOUNDARY_FLOQUET_MATCHED_LINETYPE = :dashdot
const BOUNDARY_BC_COLOR = :black
const MARKERSHAPE = :rect
const LINK_SCALE_REDUCTION = 1/2
const MARKERSIZE_SCALE = 135
const PLOT_QUANTILE = .95 # optimize the dynamic range of plotting wavefunctions to cover this percentile
const PLOT_SCALE_FUDGE = 1.3 # multiply plot_quantile result by this factor to get colorbar scale

# Pretty Printing Colors
const PRINTED_COLOR_NUMBER = :light_cyan # numbers
const PRINTED_COLOR_VARIABLE = :cyan # varialbes and functions
const PRINTED_COLOR_WARN = :light_yellow # to grab attention but not necessarily b/c of an error
const PRINTED_COLOR_GOOD = :green # successful solution
const PRINTED_COLOR_DARK = 63 # names of structures that are part of simulation, but do not themselves contain calculations
const PRINTED_COLOR_LIGHT = 171 # names of structures used to calculate and/or containing solutions
const PRINTED_COLOR_BAD = :light_red # indicates error
const PRINTED_COLOR_INSTRUCTION = 244 # parenthetical instructions

"""
PML extinction factor: `$EXTINCTION`
"""
EXTINCTION

"""
PML scaling angle: `$SCALING_ANGLE`
"""
SCALING_ANGLE

"""
default PML depth: `$BL_DEPTH`
"""
BL_DEPTH

"""
default boundary condition: `$DEFAULT_BC`
"""
DEFAULT_BC

"""
default boundary layer: `$DEFAULT_BL`
"""
DEFAULT_BL

"""
default LU package (one of `USolver`, `PSolver`, `MSolver` for UMFPACK, Pardiso, MUMPS): `$LUPACK`
"""
LUPACK

"""
index offset from midpoint where field is real (SPECTRAL), or real and nonzero (LASING/SaturableCPA): `$INDEX_OFFSET`
"""
INDEX_OFFSET

"""
relative tolerance condition for dropping components of equivalent current: `$EQUIVALENT_SOURCE_RELATIVE_CUTOFF`
"""
EQUIVALENT_SOURCE_RELATIVE_CUTOFF

"""
Courant-Friedrichs-Lewy number (N*space-to-time step ratio in time domain simulations in `N` dimensions): `$DEFAULT_CFL_NUMBER`
"""
DEFAULT_CFL_NUMBER

"""
`$DEFAULT_LINEAR_EIGENSOLVER`
"""
DEFAULT_LINEAR_EIGENSOLVER

"""
`$DEFAULT_NONLINEAR_EIGENSOLVER`
"""
DEFAULT_NONLINEAR_EIGENSOLVER

"""
subsampling rate for simulation smoothing: `$NUM_SUBPIXELS`
"""
NUM_SUBPIXELS

#
# # Tessellation defaults
# const TESSELLATION_EXPANSION_FACTOR = 1.025 # to make accessing the tessellation a little more stable
#
# # for interpolating in Simulations
# const N_CUBIC = 10
# const N_QUADRATIC = 6
# const N_BILINEAR = 4
# const N_LINEAR = 3
#
# const NNN_BULK = N_CUBIC # number of nearest neighbors used in bulk
# const NNN_SURFACE = N_LINEAR # number of nearest neighbors for surface
# const NNN_CORNER = N_LINEAR # number of nearest neighbors at corners


using TextParse
using TextParse: Record, Field, Numeric, tryparsenext

# Helper type to memory map a file as a string
struct MMapString{T} <: AbstractArray{T, 1}
    # using Type parameter, too lazy to write out the result type of mmap,
    # but also don't want to loose performance
    x::T
end
MMapString(path::String) = MMapString(Mmap.mmap(open(path), Vector{UInt8}))
# we only need to overload the iteration protocol for TextParse to work!
Base.getindex(x::MMapString, i) = Char(x.x[i])
Base.next(x::MMapString, i) = Char(x.x[i]), i + 1
Base.done(x::MMapString, i) = i > length(x.x)
Base.start(x::MMapString) = 1
Base.size(x::MMapString) = size(x.x)

# Base.write creates a mutable container (Ref) for its argument to savely pass it to C.
# Even Base.unsafe_write that directly takes a pointer still has a check that allocates 128 bytes.
# Be ready for unsafe_unsafe_write, short uuwrite.
# This saves us 40 seconds && 3.5 gb less memory allocation!
# This is actually a great example of how Julia currently fails, and how one can claw back the performance.
# We just call the C library directly and use the almost deprecated syntax (`&`) to directly take a pointer from the tuple.
# in Julia 0.7 this is likely no issue and we should be able to just use `write`,
# since the compiler got a lot better at stack allocating and eliminating mutable containers.
uuwrite(io, ptr::T) where T = Int(ccall(:ios_write, Csize_t, (Ptr{Cvoid}, Ref{T}, Csize_t), io.ios, ptr, sizeof(T)))

function save2bin(path, n = typemax(Int))
    str = MMapString(path)
    # Parser descriptor of 'num,num\n' which is the format in the csv
    rec = Record((
        Field(Numeric(Float32), delim = ','),
        Field(Numeric(Float32), eoldelim = true)
    ))
    # skip the header...
    pos = findfirst(str, '\n') + 1 # Nice thing is Julia's findfirst works with any iterator
    io = open(homedir()*"/gpspoints.bin", "w")
    i = 0
    while !done(str, pos) && i <= n
        p_or_null, pos = tryparsenext(
            rec, str, pos, length(str)
        )
        isnull(p_or_null) && continue
        p = get(p_or_null)
        uuwrite(io, p)
        i += 1
    end
    close(io)
    i
end

tic()
save2bin(homedir() * "/Downloads/gps-points.csv");
toc()

"""
Transforms from longitude/latitude to pixel on screen, with `dims` refering to
the dimensions of the screen in pixel
"""
@inline function gps2pixel(point, dims)
    lon, lat = point[1], point[2]
    x = ((dims[1] / 180.0) * (90.0 + (lon / 10^7)))
    y = ((dims[2] / 360.0) * (180.0 - (lat / 10^7)))
    (x, y)
end

function to_image_inner!(img, points, start, stop)
    dims = size(img)
    for i = start:stop
        @inbounds begin
            p0 = gps2pixel(points[i], dims)
            idx = Int.(round.(p0))
            xidx, yidx = dims[1] - idx[1], dims[2] - idx[2]
            if checkbounds(Bool, img, xidx, yidx)
                # we should give each point a radius and then add the coverage to each pixel
                # for a smoother image
                # this does well enough for this short example:
                img[xidx, yidx] += 0.001f0
            end
        end
    end
end
function to_image!(img, points, range)
    N = length(range)
    NT = Threads.nthreads()
    slices = floor(Int, N / NT)
    offset = minimum(range)
    Threads.@threads for i = 1:NT
        # @threads creates a closure, which sometimes introduces type stabilities.
        # this is why it's a good practise to move the loop body behind a function barrier
        # (https://docs.julialang.org/en/latest/manual/performance-tips/#kernal-functions-1)
        to_image_inner!(img, points, offset + ((i-1) * slices + 1), offset + (i * slices))
    end
    return img
end

# Simply saving the image
using FileIO, Colors
img = zeros(Float32, 600, 960)
io = open(homedir() * "/gpspoints.bin")
points = Mmap.mmap(io, Vector{NTuple{2, Float32}})
tic()
to_image!(img, points, 1:length(points))
toc()
FileIO.save("gps.png", Gray.(clamp.(1f0 .- img, 0, 1)))
close(io)

# Or an interactive version
using Makie, Images
io = open(homedir() * "/gpspoints.bin")
# Now that we have the data as a binary blob, we can just memory map
# it as a Vector of points (NTuple{2, Float32})
points = Mmap.mmap(io, Vector{NTuple{2, Float32}})
resolution = (600, 960)
scene = Scene(resolution = reverse(resolution))
img = zeros(Float32, resolution)
imviz = heatmap(img, colornorm = (0, 1))
center!(scene)
fill!(img, 0f0)
slice = 10^7
range = slice:slice:length(points)
stop = 1
vio = VideoStream(scene, homedir()*"/Desktop/", "gps")
while true
    start = stop
    stop = min(stop + slice, length(points))
    to_image!(img, points, start:stop)
    imviz[:heatmap] = img # update image in place
    recordframe!(vio)
    stop == length(points) && break
end
finish(vio, "gif") # finish streaming and export as gif!

using Matcha

function test(str, pattern)
    res = matchat(str, 1, pattern)
    for i = 2:length(str)
        match, res = matchat(str, i, pattern)
        match && return res
    end
    res
end

mf(x) = x in ('h', 'y', 'e')

str = MMapString(homedir() * "/gpspoints.bin")

pattern = (Greed(x-> x in ('h', 'y', 'e'), 3:3),)
@time matchone(str, pattern)


#Test code
#-------------------------------------------------------------------------------

using InspectDR
using Base.Test

#Simply test for exceptions on sample plots for now:
include("../sample/runsamples.jl")

:Test_Complete


using Test
import ClimaCore.DataLayouts: VIFH, slab, column

@testset "VIFH" begin
    TestFloatTypes = (Float32, Float64)
    for FT in TestFloatTypes
        S = Tuple{Complex{FT}, FT}
        Nv = 10 # number of vertical levels
        Ni = 4  # number of nodal points
        Nh = 10 # number of elements

        # construct a data object with 10 cells in vertical and
        # 10 elements in horizontal with 4 nodal points per element in horizontal
        array = rand(FT, Nv, Ni, 3, Nh)

        data = VIFH{S, Ni}(array)
        sum(x -> x[2], data)

        @test getfield(data.:1, :array) == @view(array[:, :, 1:2, :])
        @test getfield(data.:2, :array) == @view(array[:, :, 3:3, :])

        @test size(data) == (Ni, 1, 1, Nv, Nh)

        # test tuple assignment on columns
        val = (Complex{FT}(-1.0, -2.0), FT(-3.0))
        column(data, 1, 1)[1] = val
        @test array[1, 1, 1, 1] == -1.0
        @test array[1, 1, 2, 1] == -2.0
        @test array[1, 1, 3, 1] == -3.0

        # test value of assing tuple on slab
        sdata = slab(data, 1, 1)
        @test sdata[1] == val

        # sum of all the first field elements
        @test sum(data.:1) ≈
              Complex{FT}(sum(array[:, :, 1, :]), sum(array[:, :, 2, :]))
        @test sum(x -> x[2], data) ≈ sum(array[:, :, 3, :])
    end
end

@testset "VIFH type safety" begin
    Nv = 1 # number of vertical levels
    Ni = 1  # number of nodal points per element
    Nh = 1 # number of elements

    # check that types of the same bitstype throw a conversion error
    SA = (a = 1.0, b = 2.0)
    SB = (c = 1.0, d = 2.0)

    array = zeros(Float64, Nv, Ni, 2, Nh)
    data = VIFH{typeof(SA), Ni}(array)

    cdata = column(data, 1, 1)
    cdata[1] = SA
    @test cdata[1] isa typeof(SA)
    @test_throws MethodError cdata[1] = SB

    sdata = slab(data, 1, 1)
    @test sdata[1] isa typeof(SA)
    @test_throws MethodError sdata[1] = SB
end

@testset "broadcasting between VIFH data object + scalars" begin
    FT = Float64
    data1 = ones(FT, 2, 2, 2, 2)
    S = Complex{Float64}
    data1 = VIFH{S, 2}(data1)
    res = data1 .+ 1
    @test res isa VIFH{S}
    @test parent(res) ==
          FT[f == 1 ? 2 : 1 for i in 1:2, j in 1:2, f in 1:2, h in 1:2]
    @test sum(res) == Complex(16.0, 8.0)
    @test sum(Base.Broadcast.broadcasted(+, data1, 1)) == Complex(16.0, 8.0)
end

@testset "broadcasting between VF + IFH data object => VIFH" begin
    FT = Float64
    S = Complex{FT}
    data_vf = VF{S}(ones(FT, 3, 2))
    data_ifh = IFH{FT, 2}(ones(FT, 2, 1, 2))
    data_vifh = data_vf .+ data_ifh
    @test data_vifh isa VIFH{S}
    @test size(data_vifh) == (2, 1, 1, 3, 2)
    @test parent(data_vifh) ==
          FT[f == 1 ? 2 : 1 for v in 1:3, i in 1:2, f in 1:2, h in 1:2]

    @test parent(data_vifh .+ data_vf) ==
          FT[f == 1 ? 3 : 2 for v in 1:3, i in 1:2, f in 1:2, h in 1:2]
    @test parent(data_vifh .+ data_ifh) ==
          FT[f == 1 ? 3 : 1 for v in 1:3, i in 1:2, f in 1:2, h in 1:2]

end


abstract type AbstractUniqueIndex <: AbstractIndex; end

struct UniqueIndex <: AbstractUniqueIndex; end

accelerate(a::AbstractArray, ::Type{UniqueIndex}) = AcceleratedArray(a, UniqueIndex())

Base.unique(a::AcceleratedArray{<:Any, <:Any, <:Any, <:AbstractUniqueIndex}) = a


if !@isdefined(TestINet)
    struct TestINet <: ANetwork; end
    struct TestIDiNet <: ADiNetwork; end
    struct TestIIdxEdge <: AIndexedEdge; end
end

@testset "$TEST $G" begin

g = TestINet()
h = TestIDiNet()
e = TestIIdxEdge()

@test_throws ErrorException idx(e)

if G<: ANetwork

g = G(10,20)
m = ones(10,10)
em = add_edge_property!(g, "rand", m)
@test typeof(em) <: EdgeMap
@test em.data === m
@test eprop(g, "rand")[1,1] == 1

m = fill(2, 10)
vm = add_vertex_property!(g, "rand", m)
@test typeof(vm) <: VertexMap
@test vm.data === m
@test vprop(g, "rand")[1] == 2

end #if

end # testset


import Base: copy, show, print
using Base.Enums

@enum Player begin
    void = 0
    black = 1
    white = -1
end
other(player::Player)::Player = if (player == black) white elseif (player == white) black else void end
tosymbol(player::Player) = if (player == black) "x" elseif (player==white) "o" else "." end

# ----------------------
Point = NamedTuple{(:row, :col), Tuple{Int, Int}}

up(p::Point)::Point = (row = p.row - 1, col = p.col)
down(p::Point)::Point = (row = p.row + 1, col = p.col)
left(p::Point)::Point = (row = p.row, col = p.col - 1)
right(p::Point)::Point = (row = p.row, col = p.col + 1)
nbrs(p::Point)::Vector{Point} = [up(p), down(p), left(p), right(p)]

# ----------------------
struct Move
    point::Point
    is_play::Bool
    is_pass::Bool
    is_resign::Bool
end

play(point::Point)::Move = Move(point, true, false, false)
pass_turn()::Move = Move((row=0, col=0), false, true, false)
resign()::Move = Move((row=0, col=0), false, false, true)

# ----------------------
IdType = UInt16

struct GoString
    color::Player
    stones::Set{Point}
    liberties::Set{Point}
    id::IdType
end

remove_liberty!(s::GoString, p::Point)::Nothing = (delete!(s.liberties, p); nothing)
add_liberty!(s::GoString, p::Point)::Nothing = (push!(s.liberties, p); nothing)
num_liberties(s::GoString)::Int = length(s.liberties)

function merge!(receiver::GoString, sender::GoString)::Nothing
    @assert receiver.color === sender.color
    union!(receiver.stones, sender.stones)
    union!(receiver.liberties, sender.liberties)
    setdiff!(receiver.liberties, receiver.stones)
    nothing
end

const EMPTY_STRING = GoString(void, Set{Point}(), Set{Point}(), IdType(0))

isvoid(s::GoString) = (s.color == void)
copy(s::GoString) = isvoid(s) ? EMPTY_STRING : GoString(s.color, copy(s.stones), copy(s.liberties), s.id)
show(io::IO, s::GoString) = print(io, tosymbol(s.color))
print(io::IO, s::GoString) = show(io, s)

mutable struct GoStringBuffer
    dict::Dict{IdType, GoString}
    lastid::IdType
end


function test_gray_over_gray(bm)
    @test all(c->gray(bm(c[1], c[2])) === red(bm(RGB(c[1]), RGB(c[2]))),
              ((c1, c2) for c1 in gray_f64, c2 in gray_f64))
    @test all(c->gray(bm(c[1], c[2])) === red(bm(RGB(c[1]), RGB(c[2]))),
              ((c1, c2) for c1 in gray_f32, c2 in gray_f32))
    @test all(c->gray(bm(c[1], c[2])) === red(bm(RGB(c[1]), RGB(c[2]))),
              ((c1, c2) for c1 in gray_n0f8, c2 in gray_n0f8))
    @test all(c->gray(bm(c[1], c[2])) === red(bm(RGB(c[1]), RGB(c[2]))),
              ((c1, c2) for c1 in gray_n0f16, c2 in gray_n0f16))
end

function test_gray_over_rgb(bm)
    @test all(c->bm(c[1], c[2]) === bm(c[1], RGB(c[2])),
              ((c1, c2) for c1 in rgb2_f64, c2 in gray_f64))
    @test all(c->bm(c[1], c[2]) === bm(c[1], RGB(c[2])),
              ((c1, c2) for c1 in rgb2_f32, c2 in gray_f32))
    @test all(c->bm(c[1], c[2]) === bm(c[1], RGB(c[2])),
              ((c1, c2) for c1 in rgb2_n0f8, c2 in gray_n0f8))
    @test all(c->bm(c[1], c[2]) === bm(c[1], RGB(c[2])),
              ((c1, c2) for c1 in rgb2_n0f16, c2 in gray_n0f16))
end

function test_rgb_over_gray(bm)
    @test all(c->bm(c[1], c[2]) === bm(c[1], Gray(c[2])),
              ((c1, c2) for c1 in gray_f64, c2 in rgb2_f64))
    @test all(c->bm(c[1], c[2]) === bm(c[1], Gray(c[2])),
              ((c1, c2) for c1 in gray_f32, c2 in rgb2_f32))
    @test all(c->bm(c[1], c[2]) === bm(c[1], Gray(c[2])),
              ((c1, c2) for c1 in gray_n0f8, c2 in rgb2_n0f8))
    @test all(c->bm(c[1], c[2]) === bm(c[1], Gray(c[2])),
              ((c1, c2) for c1 in gray_n0f16, c2 in rgb2_n0f16))
end

@testset "Gray" begin
    @testset "$(keyword(m))" for m in separable_modes
        @testset "Gray over Gray" begin
            test_gray_over_gray(m)
        end

        @testset "GrayA over Gray: $T" for T in Ts
            rgb = m(RGB{T}(1, 0.75, 0), RGBA{T}(0, 0.5, 1, 0.6))
            actual = (m(Gray{T}(1.0),  GrayA{T}(0.0, 0.6)),
                      m(Gray{T}(0.75), GrayA{T}(0.5, 0.6)),
                      m(Gray{T}(0.0),  GrayA{T}(1.0, 0.6)))
            expected = Gray{T}.((red(rgb), green(rgb), blue(rgb)))
            @test all(t -> t[1] ≈ t[2], zip(actual, expected))
        end

        @testset "Gray over GrayA: $T" for T in Ts
            rgb = m(RGBA{T}(1, 0.75, 0, 0.6), RGB{T}(0, 0.5, 1))
            actual = (m(GrayA{T}(1.0,  0.6), Gray{T}(0.0)),
                      m(GrayA{T}(0.75, 0.6), Gray{T}(0.5)),
                      m(GrayA{T}(0.0,  0.6), Gray{T}(1.0)))
            expected = GrayA{T}.((red(rgb), green(rgb), blue(rgb)), alpha(rgb))
            @test all(t -> t[1] ≈ t[2], zip(actual, expected))
        end

        @testset "GrayA over GrayA: $T" for T in Ts
            rgb = m(RGBA{T}(1, 0.75, 0, 0.6), RGBA{T}(0, 0.5, 1, 0.6))
            actual = (m(GrayA{T}(1.0,  0.6), GrayA{T}(0.0, 0.6)),
                      m(GrayA{T}(0.75, 0.6), GrayA{T}(0.5, 0.6)),
                      m(GrayA{T}(0.0,  0.6), GrayA{T}(1.0, 0.6)))
            expected = GrayA{T}.((red(rgb), green(rgb), blue(rgb)), alpha(rgb))
            @test all(t -> t[1] ≈ t[2], zip(actual, expected))
        end
    end
end

@testset "Gray over RGB" begin
    @testset "$(keyword(m))" for m in separable_modes
        @testset "Gray over RGB" begin
            test_gray_over_rgb(m)
        end

        @testset "GrayA over RGB: $T" for T in Ts
            grays = (GrayA{T}(0.0, 0.6), GrayA{T}(0.5, 0.6), GrayA{T}(1.0, 0.6))
            actual = m.(RGB{T}(1, 0.75, 0), grays)
            expected = m.(RGB{T}(1, 0.75, 0), RGBA{T}.(grays))
            @test all(t -> t[1] ≈ t[2], zip(actual, expected))
        end

        @testset "Gray over RGBA: $T" for T in Ts
            grays = (Gray{T}(0.0), Gray{T}(0.5), Gray{T}(1.0))
            actual = m.(RGBA{T}(1, 0.75, 0, 0.6), grays)
            expected = m.(RGBA{T}(1, 0.75, 0, 0.6), RGB{T}.(grays))
            @test all(t -> t[1] ≈ t[2], zip(actual, expected))
        end

        @testset "GrayA over RGBA: $T" for T in Ts
            grays = (GrayA{T}(0.0, 0.6), GrayA{T}(0.5, 0.6), GrayA{T}(1.0, 0.6))
            actual = m.(RGBA{T}(1, 0.75, 0, 0.6), grays)
            expected = m.(RGBA{T}(1, 0.75, 0, 0.6), RGBA{T}.(grays))
            @test all(t -> t[1] ≈ t[2], zip(actual, expected))
        end
    end
end

@testset "RGB over Gray" begin
    @testset "$(keyword(m))" for m in separable_modes
        @testset "RGB over Gray" begin
            test_rgb_over_gray(m)
        end

        @testset "RGBA over Gray: $T" for T in Ts
            grays = (Gray{T}(1.0), Gray{T}(0.75), Gray{T}(0.0))
            actual = m.(grays, RGBA{T}(0.0, 0.6, 1.0, 0.6))
            expected = m.(grays, GrayA{T}(0.464, 0.6))
            @test all(t -> t[1] ≈ t[2], zip(actual, expected))
        end

        @testset "RGB over GrayA: $T" for T in Ts
            grays = (GrayA{T}(1.0, 0.6), GrayA{T}(0.75, 0.6), GrayA{T}(0.0, 0.6))
            actual = m.(grays, RGB{T}(0, 0.6, 1))
            expected = m.(grays, Gray{T}(0.464))
            @test all(t -> t[1] ≈ t[2], zip(actual, expected))
        end

        @testset "RGBA over GrayA: $T" for T in Ts
            grays = (GrayA{T}(1.0, 0.6), GrayA{T}(0.75, 0.6), GrayA{T}(0.0, 0.6))
            actual = m.(grays, RGBA{T}(0.0, 0.6, 1.0, 0.6))
            expected = m.(grays, GrayA{T}(0.464, 0.6))
            @test all(t -> t[1] ≈ t[2], zip(actual, expected))
        end
    end
end

@testset "RGB24/ARGB32/Gray24/AGray32" begin
    @test BlendHue(       RGB24(1, 0.8, 0), RGB24(0, 0.4, 1))       === RGB24(0.651, 0.792, 1.0)
    @test BlendSaturation(RGB24(1, 0.8, 0), ARGB32(0, 0.4, 1, 0.6)) === RGB24(1, 0.8, 0)
    @test BlendColor(     RGB24(1, 0.8, 0), Gray24(0.4))            === RGB24(0.773, 0.773, 0.773)
    @test BlendLuminosity(RGB24(1, 0.8, 0), AGray32(0.4, 0.6))      === RGB24(0.710, 0.569, 0.000)
    @test BlendNormal(  ARGB32(1, 0.8, 0, 0.6), RGB24(0, 0.4, 1))       === ARGB32(0, 0.4, 1, 1)
    @test BlendMultiply(ARGB32(1, 0.8, 0, 0.6), ARGB32(0, 0.4, 1, 0.6)) === ARGB32(0.286, 0.482, 0.286, 0.84)
    @test BlendScreen(  ARGB32(1, 0.8, 0, 0.6), Gray24(0.4))            === ARGB32(0.761, 0.686, 0.400, 1)
    @test BlendOverlay( ARGB32(1, 0.8, 0, 0.6), AGray32(0.4, 0.6))      === ARGB32(0.827, 0.667, 0.114, 0.84)
    @test BlendDarken(    Gray24(0.8), RGB24(0, 0.4, 1))       === Gray24(0.345)
    @test BlendLighten(   Gray24(0.8), ARGB32(0, 0.4, 1, 0.6)) === Gray24(0.8)
    @test BlendColorDodge(Gray24(0.8), Gray24(0.4))            === Gray24(1.0)
    @test BlendColorBurn( Gray24(0.8), AGray32(0.4, 0.6))      === Gray24(0.62)
    @test BlendHardLight( AGray32(0.8, 0.6), RGB24(0, 0.4, 1))       === AGray32(0.471, 1)
    @test BlendSoftLight( AGray32(0.8, 0.6), ARGB32(0, 0.4, 1, 0.6)) === AGray32(0.647, 0.84)
    @test BlendDifference(AGray32(0.8, 0.6), Gray24(0.4))            === AGray32(0.4, 1)
    @test BlendExclusion( AGray32(0.8, 0.6), AGray32(0.4, 0.6))      === AGray32(0.584, 0.84)
end


# This file contans the common interface for LightGraphs.

_NI(m) = error("Not implemented: $m")

"""
    AbstractEdge

An absract type representing a single edge between two vertices of a graph.
"""
abstract type AbstractEdge end

"""
    AbstractEdgeIter

An abstract type representing an edge iterator.
"""
abstract type AbstractEdgeIter end

"""
    AbstractGraph

An abstract type representing a graph.
"""
abstract type AbstractGraph end

@traitdef IsDirected{G<:AbstractGraph}
@traitimpl IsDirected{G} <- is_directed(G)

#
# Interface for AbstractEdges
#

"""
    src(e)

Return the source vertex of edge `e`.
"""
src(e::AbstractEdge) = _NI("src")

"""
    dst(e)

Return the destination vertex of edge `e`.
"""
dst(e::AbstractEdge) = _NI("dst")

Pair(e::AbstractEdge) = _NI("Pair")
Tuple(e::AbstractEdge) = _NI("Tuple")

"""
    reverse(e)

Create a new edge from `e` with source and destination vertices reversed.
"""
reverse(e::AbstractEdge) = _NI("reverse")

==(e1::AbstractEdge, e2::AbstractEdge) = _NI("==")

#
# Interface for AbstractGraphs
#
"""
    edgetype(g)

Return the type of graph `g`'s edge
"""
edgetype(g::AbstractGraph) = _NI("edgetype")

"""
    eltype(g)

Return the type of the graph's vertices (must be <: Integer)
"""
eltype(g::AbstractGraph) = _NI("eltype")

"""
    nv(g)

Return the number of vertices in `g`.
"""
nv(g::AbstractGraph) = _NI("nv")

"""
    ne(g)

Return the number of edges in `g`.
"""
ne(g::AbstractGraph) = _NI("ne")

"""
    vertices(g)

Return (an iterator to or collection of) the vertices of a graph.

### Implementation Notes
A returned iterator is valid for one pass over the edges, and
is invalidated by changes to `g`.

"""
vertices(g::AbstractGraph) = _NI("vertices")

"""
    edges(g)
Return (an iterator to or collection of) the edges of a graph.

### Implementation Notes
A returned iterator is valid for one pass over the edges, and
is invalidated by changes to `g`.
"""
edges(x...) = _NI("edges")

is_directed(x...) = _NI("is_directed")
is_directed{T}(::Type{T}) = _NI("is_directed")
"""
    add_vertex!(g)

Add a new vertex to the graph `g`.
Return true if the vertex was added successfully, false otherwise.
"""
add_vertex!(x...) = _NI("add_vertex!")

"""
    add_edge!(g, e)

Add a new edge `e` to `g`. Return false if add fails
(e.g., if vertices are not in the graph, or edge already exists), true otherwise.
"""
add_edge!(x...) = _NI("add_edge!")

"""
    rem_vertex!(g)

Remove the vertex `v` from graph `g`. Return false if removal fails
(e.g., if vertex is not in the graph), true otherwise.
"""
rem_vertex!(x...) = _NI("rem_vertex!")

"""
    rem_edge!(g, e)

Remove the edge `e` from `g`. Return false if edge removal fails
(e.g., if edge does not exist), true otherwise.
"""
rem_edge!(x...) = _NI("rem_edge!")

"""
    has_vertex(g, v)

Return true if `v` is a vertex of `g`.
"""
has_vertex(x...) = _NI("has_vertex")

"""
    has_edge(g, e)

Return true if the graph `g` has an edge `e`.
"""
has_edge(x...) = _NI("has_edge")

"""
    in_neighbors(g, v)

Return a list of all neighbors connected to vertex `v` by an incoming edge.

### Implementation Notes
Returns a reference, not a copy. Do not modify result.
"""
in_neighbors(x...) = _NI("in_neighbors")

"""
    out_neighbors(g, v)

Return a list of all neighbors connected to vertex `v` by an outgoing edge.

# Implementation Notes
Returns a reference, not a copy. Do not modify result.
"""
out_neighbors(x...) = _NI("out_neighbors")

"""
    zero(g)

Return a zero-vertex, zero-edge version of the same type of graph as `g`.
"""
zero(g::AbstractGraph) = _NI("zero")