function substitution(code::Union{Expr,Symbol},p::Program_;op=nothing,arg=false,pre=nothing,post=nothing,sym=nothing, #Type of symbol between operator, pre/post script and arguments
    nargs = nothing, addArgs = nothing,                       #Args for the functions that depend on arguments
    only=["Local","Global","Identity","GlobalArray","Medium"],           #Subset of parameters to modify
    addName=nothing, expl=nothing,                            #Add something to the vectorized name of fully substitute by something
    ic=[:ic1_,:ic2_,:ic3_], index=nothing, factor=1,          #Change loop symbols and additional indexes
    dims=nothing,                                             #Operators modifying the behaviour of cells
    opF = nothing                                             #Form of the new index
    )

    if dims === nothing
        dims = p.agent.dims
    end

    if op !== nothing

        if nargs === nothing
            code = postwalk(x->@capture(x, opAux_(argAux_)) && opAux == op ?
                substitution(argAux, p;arg = true, nargs=nargs, only=only, addName=addName, expl=expl, ic=ic, index=index, factor=factor, dims=dims, opF=opF)
            : x, code)
        else
            code = postwalk(x->@capture(x, opAux_(argAux__)) && opAux == op ?
            if length(argAux) != nargs
                error("Operator ", op, " should be declared with just ", nargs, " arguments. ", length(argAux), " where passed.")
            else
                if opF !== nothing
                    opAux = opF
                end
                if addArgs !== nothing
                    :($opAux($(argAux...),$(addArgs...)))
                else
                    :($opAux($(argAux...)))
                end
            end
            : x, code)
        end

    elseif arg

        add = substitution(code, p, only=only, addName=addName, expl=expl, ic=ic, index=index[1], dims=dims)
        if factor[1] != 1
            add = :($(factor[i])*$add)
        end
        for i in 2:length(index)
            add2 = substitution(code, p, only=only, addName=addName, expl=expl, ic=ic, index=index[i], dims=dims)
            if factor[i] != 1
                add = :($add+$(factor[i])*$add2)
            else
                add = :($add+$add2)
            end
        end

        if opF !== nothing
            code = postwalk(x->@capture(x, vAux_) && vAux == :X ? add : x, opF)
        else
            code = add
        end

    else

        if "Local" in only
            for (i,v) in enumerate(p.agent.declaredSymbols["Local"])

                if pre !== nothing
                    v = Meta.parse(string(pre,v))
                elseif post !== nothing
                    v = Meta.parse(string(v,post))
                end

                if expl !== nothing
                    name = expl
                elseif addName !== nothing
                    name = Meta.parse(string(:localV,addName))
                else
                    name = :localV
                end

                if index !== nothing
                    if index[1] != 0
                        ic_ = :($(ic[1])+$(index[1]))
                    else
                        ic_ = :($(ic[1]))
                    end
                else
                    ic_ = ic[1]
                end

                if opF !== nothing
                    subs = postwalk(x->@capture(x, vAux_) && vAux == X ? :($name[$ic_,$i]) : x, opF)
                else
                    subs = :($name[$ic_,$i])
                end

                code = postwalk(x->@capture(x, vAux_) && vAux == v ? subs : x, code)

            end
        end

        if "Identity" in only
            for (i,v) in enumerate(p.agent.declaredSymbols["Identity"])

                if pre !== nothing
                    v = Meta.parse(string(pre,v))
                elseif post !== nothing
                    v = Meta.parse(string(v,post))
                end

                if expl !== nothing
                    name = expl
                elseif addName !== nothing
                    name = Meta.parse(string(:identityV,addName))
                else
                    name = :identityV
                end

                if index !== nothing
                    if index[1] != 0
                        ic_ = :($(ic[1])+$(index[1]))
                    else
                        ic_ = :($(ic[1]))
                    end
                else
                    ic_ = ic[1]
                end

                if opF !== nothing
                    subs = postwalk(x->@capture(x, vAux_) && vAux == X ? :($name[$ic_,$i]) : x, opF)
                else
                    subs = :($name[$ic_,$i])
                end

                code = postwalk(x->@capture(x, vAux_) && vAux == v ? subs : x, code)
            end
        end

        if "Global" in only
            for (i,v) in enumerate(p.agent.declaredSymbols["Global"])

                if pre !== nothing
                    v = Meta.parse(string(pre,v))
                elseif post !== nothing
                    v = Meta.parse(string(v,post))
                end

                if expl !== nothing
                    name = expl
                elseif addName !== nothing
                    name = Meta.parse(string(:globalV,addName))
                else
                    name = :globalV
                end

                if opF !== nothing
                    subs = postwalk(x->@capture(x, vAux_) && vAux == X ? :($name[$i]) : x, opF)
                else
                    subs = :($name[$i])
                end

                code = postwalk(x->@capture(x, vAux_) && vAux == v ? subs : x, code)
            end
        end

        if "GlobalArray" in only
            for (i,v) in enumerate(p.agent.declaredSymbols["GlobalArray"])

                if pre !== nothing
                    v2 = Meta.parse(string(pre,v))
                elseif post !== nothing
                    v2 = Meta.parse(string(v,post))
                else
                    v2 = v
                end

                if expl !== nothing
                    name = expl
                elseif addName !== nothing
                    name = Meta.parse(string(v,addName))
                else
                    name = v
                end

                code = postwalk(x->@capture(x, vAux_) && vAux == v2 ? name : x, code)
            end
        end

        if "Medium" in only
            for (i,v) in enumerate(p.agent.declaredSymbols["Medium"])

                if pre !== nothing
                    v = Meta.parse(string(pre,v))
                elseif post !== nothing
                    v = Meta.parse(string(v,post))
                end

                if expl !== nothing
                    name = expl
                elseif addName !== nothing
                    name = Meta.parse(string(:mediumV,addName))
                else
                    name = :mediumV
                end

                if index !== nothing
                    if index[1] != 0
                        ic1_ = :($(ic[1])+$(index[1]))
                    else
                        ic1_ = :($(ic[1]))
                    end
                    if index[2] != 0
                        ic2_ = :($(ic[2])+$(index[2]))
                    else
                        ic2_ = :($(ic[2]))
                    end
                    if index[3] != 0
                        ic3_ = :($(ic[3])+$(index[3]))
                    else
                        ic3_ = :($(ic[3]))
                    end
                else
                    ic1_ = ic[1]
                    ic2_ = ic[2]
                    ic3_ = ic[3]
                end

                if opF !== nothing
                    if dims == 1
                        subs = postwalk(x->@capture(x, vAux_) && vAux == X ? :($name[$ic1_,$i]) : x, opF)
                    elseif dims == 2
                        subs = postwalk(x->@capture(x, vAux_) && vAux == X ? :($name[$ic1_,$ic2_,$i]) : x, opF)
                    elseif dims == 3
                        subs = postwalk(x->@capture(x, vAux_) && vAux == X ? :($name[$ic1_,$ic2_,$ic3_,$i]) : x, opF)
                    end
                else
                    if dims == 1
                        subs = :($name[$ic1_,$i])
                    elseif dims == 2
                        subs = :($name[$ic1_,$ic2_,$i])
                    elseif dims == 3
                        subs = :($name[$ic1_,$ic2_,$ic3_,$i])
                    end
                end

                code = postwalk(x->@capture(x, vAux_)  && vAux == v  ? subs : x, code)

            end
        end

    end

    return code
end


#!/usr/bin/env julia
using ObjectDetectionStats
using Test

@testset "Basic Data Ops" begin
    z = DataFrame( (:a => [1,2], :B => [120,13], :c => [111,111]))
    @test highest_vote!(z, [:a,:B,:c])[:Highest_Vote] .== [ :B, :c ]

    barycenter = DataFrame( Dict( [   :notneeded    => ["a", "b"],
                                      :center_x     => [15, 15],
                                      :center_y     => [10, 10],
                                      :width        => [5, 10],
                                      :height       => [5, 10]
                                  ]) )

    @test has_required_columns(barycenter, [ :center_x, :center_y, :width, :height ]  )
    @test !has_required_columns(barycenter, [ :center_x, :center_y, :width, :height, :NOPE ]  )
    #Test DF for comparing all others with
    KeyDF = DataFrame( Dict( [    :notneeded        => ["a", "b"],
                                  :upper_left_x     => [12.5, 10.0],
                                  :lower_right_x    => [17.5, 20.0],
                                  :upper_left_y     => [7.50, 5.00],
                                  :lower_right_y    => [12.5, 15.0]
                             ] ) )
    #Convert barycenter boxes to ULLR
    barycenter_boxes!( barycenter )
    @test all( Matrix(KeyDF) .== Matrix(barycenter[!, names(KeyDF)]) )

    area!(barycenter)
    @test all( barycenter[ :Area ] .== [ 25, 100 ] )

end

@testset "Box Translate and Clamp Tests" begin
    a            = Box(5,5,15,15)
    trans_result = translate( a, -5, -5 )
    @test trans_result                          == Box(0,0,10,10)
    @test ObjectDetectionStats.clamp(trans_result, Box(5,5,15,15))   == Box(5,5,10,10)
    @test ObjectDetectionStats.clamp(trans_result, Box(0,0, 5, 5))   == Box(0,0, 5, 5)
end

@testset "Box Area Tests" begin
    @test area( Box(5,5,15,15) ) == 100
    @test area( Box(5,5,11,11) ) == 36
end

@testset "Box Intersection Tests" begin
    a = Box(5,5,15,15)
    b = Box(5,5,15,15)
    @test intersection_area( a, b ) == 100
    @test intersection_over_union( a, b ) == 1.0

    a = Box(10,10,15,15)
    b = Box(5,5,15,15)
    @test intersection_area( a, b ) == 25
    @test intersection_over_union( a, b ) == 0.25

    b = Box(10,10,15,15)
    a = Box(5,5,15,15)
    @test intersection_area( a, b ) == 25
    @test intersection_over_union( a, b ) == 0.25

    a = Box(1,1,15,15)
    b = Box(25,25,35,35)
    @test intersection_area( a, b ) == 0
    @test intersection_over_union( a, b ) == 0.0
end

@testset "3 TP test" begin
    ods_machine     = ObjectDetectionScore( 3 )
    pred_scores     = [ 0.2 0.9 0.5 ; #3
                        0.2 0.9 0.5;  #2
                        0.5 0.9 0.2]  #1
    pred_locations  = [ Box( 1,     1,  10,     10 ),
                        Box( 15,    1,  25,     20 ),
                        Box( 1,    15,  10,     25 ) ]
    GT_cold_encodings = [ 2, 2, 2 ]# all correct
    #prepare inputs for evaluation...
    hcl = HotClassLocalization( pred_scores, pred_locations )
    ccl = ColdClassLocalization( GT_cold_encodings, pred_locations )
    ods_machine( hcl, ccl )
    @test all(ods_machine.TP .== [0,3,0])
    @test all(ods_machine.FP .== [0,0,0])
    @test all(ods_machine.FN .== [0,0,0])
end

@testset "3 wrong class test" begin
    ods_machine     = ObjectDetectionScore( 3 )
    pred_scores     = [ 0.2 0.9 0.5 ; #3
                        0.2 0.9 0.5; #2
                        0.5 0.9 0.2]  #1
    pred_locations  = [ Box( 1,     1,  10,     10 ),
                        Box( 15,    1,  25,     20 ),
                        Box( 1,    15,  10,     25 ) ]
    GT_cold_encodings = [ 1, 1, 1 ]# all incorrect class but perfect overlap
    #prepare inputs for evaluation...
    hcl = HotClassLocalization( pred_scores, pred_locations )
    ccl = ColdClassLocalization( GT_cold_encodings, pred_locations )

    ods_machine( hcl, ccl )
    @test all(ods_machine.TP .== [0,0,0])
    @test all(ods_machine.FP .== [0,3,0])
    @test all(ods_machine.FN .== [3,0,0])
end

@testset "3 wrong class locations with correct predictions" begin
    ods_machine     = ObjectDetectionScore( 3 )
    pred_scores     = [ 0.2 0.9 0.5 ; #2
                        0.2 0.9 0.5;  #2
                        0.5 0.9 0.2 ] #2
    pred_locations  = [ Box( 1,     1,  10,     10 ),
                        Box( 15,    1,  25,     20 ),
                        Box( 1,    15,  10,     25 ) ]
    GT_cold_encodings = [ 2, 2, 2 ]# all correct class
    GT_locations  = [   Box( 100,    100,  100,     100 ),
                        Box( 150,    100,  250,     200 ),
                        Box( 100,    150,  100,     250 ) ]
    #prepare inputs for evaluation...
    hcl = HotClassLocalization( pred_scores, pred_locations )
    ccl = ColdClassLocalization( GT_cold_encodings, GT_locations )

    ods_machine( hcl, ccl )

    @test all(ods_machine.TP .== [0,0,0])
    @test all(ods_machine.FP .== [0,3,0])
    @test all(ods_machine.FN .== [0,3,0])
end

@testset "3 wrong class locations with wrong predictions" begin
    ods_machine     = ObjectDetectionScore( 3 )
    pred_scores     = [ 0.2 0.5 0.9; #3
                        0.2 0.9 0.5; #2
                        0.9 0.5 0.2] #1
    pred_locations  = [ Box( 1,     1,  10,     10 ),
                        Box( 15,    1,  25,     20 ),
                        Box( 1,    15,  10,     25 ) ]
    GT_cold_encodings     = [   3, #correct
                                1, #incorrect
                                1 ]#correct
    GT_locations  = [   Box( 1,     1,  10,     10 ), #TP class 3
                        Box( 15,    1,  25,     20 ), #FP class 1
                        Box( 30,    30, 42,     42 )] #FN class 1
    #prepare inputs for evaluation...
    hcl = HotClassLocalization( pred_scores, pred_locations )
    ccl = ColdClassLocalization( GT_cold_encodings, GT_locations )
    ods_machine( hcl, ccl )
    @test all(ods_machine.TP .== [0,0,1])
    @test all(ods_machine.FP .== [1,1,0])
    @test all(ods_machine.FN .== [2,0,0])
end

@testset "2 correct overlapping a prediction and 1 nonprediction" begin
    ods_machine     = ObjectDetectionScore( 3 )
    pred_scores     = [ 0.2 0.9 0.5 ; #2
                        0.2 0.9 0.5;  #2
                        0.0 0.0 0.0;] #0 not predicted
    pred_locations  = [ Box( 1,     1,  10,     10 ),
                        Box( 1,     1,  10,     10 ),
                        Box( 15,   15,  20,     20 )  ]
    GT_cold_encodings = [ 2 ]# all correct class
    GT_locations  = [   Box( 1,     1,  10,     10 ) ]
    #prepare inputs for evaluation...
    hcl = HotClassLocalization( pred_scores, pred_locations )
    ccl = ColdClassLocalization( GT_cold_encodings, GT_locations )

    ods_machine( hcl, ccl )
    @test all(ods_machine.TP .== [0,1,0])
    @test all(ods_machine.FP .== [0,1,0])
    @test all(ods_machine.FN .== [0,0,0])
end


import Base: iterate

"""
    iterate(trigger, dt[, n=number_of_times])

Iterate from instant `dt` using trigger with a given iteration number `n`
if `n < 0` (`-1` by default), it iterates indefinitely.

# Usage
```
julia> trigger = Trigger(Dates.Time(20, 30))

julia> for dt in iterate(trigger, DateTime(2020, 1, 1), n=3)
         @show dt
       end
dt = 2020-01-01T20:30:00
dt = 2020-01-02T20:30:00
dt = 2020-01-03T20:30:00

julia> collect(iterate(trigger, DateTime(2020, 1, 1), n=3))
3-element Array{Any,1}:
 2020-01-01T20:30:00
 2020-01-02T20:30:00
 2020-01-03T20:30:00
```
"""
function iterate(trigger::AbstractTrigger, dt; n=-1)
    TriggerIterator(trigger, dt, n)
end

abstract type AbstractTriggerIterator end

struct FiniteTriggerIterator <: AbstractTriggerIterator
    trigger::AbstractTrigger
    dt
    n::Int
end

struct InfiniteTriggerIterator <: AbstractTriggerIterator
    trigger::AbstractTrigger
    dt
end

function TriggerIterator(trigger::AbstractTrigger, dt, n)
    if n < 0
        if IteratorSize(trigger) == IsInfinite()
            InfiniteTriggerIterator(trigger, dt)
        else
            FiniteTriggerIterator(trigger, dt, trigger.n)
        end
    else
        if IteratorSize(trigger) == IsInfinite()
            FiniteTriggerIterator(trigger, dt, n)
        else
            FiniteTriggerIterator(trigger, dt, min(n, trigger.n))
        end
    end
end

function iterate(itr::AbstractTriggerIterator, state=(itr.dt, 0))
    dt_now, i = state
    i >= itr.n && return nothing
    dt_next = get_next_dt_fire(itr.trigger, DateTime(0), dt_now)
    i += 1
    state = dt_next, i
    return (dt_next, state)
end

function iterate(itr::InfiniteTriggerIterator, state=(itr.dt, 0))
    dt_now, i = state
    dt_next = get_next_dt_fire(itr.trigger, DateTime(0), dt_now)
    i += 1
    state = dt_next, i
    return (dt_next, state)
end

IteratorSize(itr::InfiniteTriggerIterator) = IsInfinite()
IteratorSize(itr::FiniteTriggerIterator) = HasLength()

length(itr::FiniteTriggerIterator) = itr.n


using NESSie.Format
using LightXML: parse_string, root, name, child_elements, attribute, content

@testset "writevtk (surface model)" begin
    for T in testtypes
        # empty system
        model = Model{T, Triangle{T}}()
        (fname, fh) = mktemp()
        try
            xroot = root(parse_string(readback(fh -> writevtk(fh, model))))
            @test name(xroot) == "VTKFile"
            @test attribute(xroot, "type") == "PolyData"
            xgrid = collect(child_elements(xroot))
            @test length(xgrid) == 1
            @test name(xgrid[1]) == "PolyData"
            xgrid = collect(child_elements(xgrid[1]))
            @test length(xgrid) == 1
            @test name(xgrid[1]) == "Piece"
            @test attribute(xgrid[1], "NumberOfPoints") == "0"
            @test attribute(xgrid[1], "NumberOfVerts") == "0"
            @test attribute(xgrid[1], "NumberOfLines") == "0"
            @test attribute(xgrid[1], "NumberOfStrips") == "0"
            @test attribute(xgrid[1], "NumberOfPolys") == "0"
            xpiece = collect(child_elements(xgrid[1]))
            @test length(xpiece) == 2
            @test name(xpiece[1]) == "Points"
            xdata = collect(child_elements(xpiece[1]))
            @test length(xdata) == 1
            @test name(xdata[1]) == "DataArray"
            @test attribute(xdata[1], "type") == "$T"
            @test attribute(xdata[1], "NumberOfComponents") == "3"
            @test attribute(xdata[1], "format") == "ascii"
            @test strip(content(xdata[1])) == ""
            @test name(xpiece[2]) == "Polys"
            xdata = collect(child_elements(xpiece[2]))
            @test length(xdata) == 2
            @test name(xdata[1]) == "DataArray"
            @test attribute(xdata[1], "type") == "Int32"
            @test attribute(xdata[1], "Name") == "connectivity"
            @test attribute(xdata[1], "format") == "ascii"
            @test strip(content(xdata[1])) == ""
            @test name(xdata[2]) == "DataArray"
            @test attribute(xdata[2], "type") == "Int32"
            @test attribute(xdata[2], "Name") == "offsets"
            @test attribute(xdata[2], "format") == "ascii"
            @test strip(content(xdata[2])) == ""
        finally
            close(fh)
            rm(fname)
        end

        # small system
        nodes = Vector{T}[[0, 0, 0], [0, 0, 3], [0, 3, 0], [1, -3, 3]]
        model = Model(
            nodes,
            Triangle{T}[
                Triangle(nodes[1], nodes[2], nodes[3]),
                Triangle(nodes[1], nodes[4], nodes[2])
            ]
        )
        (fname, fh) = mktemp()
        try
            xroot = root(parse_string(readback(fh -> writevtk(fh, model))))
            @test name(xroot) == "VTKFile"
            @test attribute(xroot, "type") == "PolyData"
            xgrid = collect(child_elements(xroot))
            @test length(xgrid) == 1
            @test name(xgrid[1]) == "PolyData"
            xgrid = collect(child_elements(xgrid[1]))
            @test length(xgrid) == 1
            @test name(xgrid[1]) == "Piece"
            @test attribute(xgrid[1], "NumberOfPoints") == "4"
            @test attribute(xgrid[1], "NumberOfVerts") == "0"
            @test attribute(xgrid[1], "NumberOfLines") == "0"
            @test attribute(xgrid[1], "NumberOfStrips") == "0"
            @test attribute(xgrid[1], "NumberOfPolys") == "2"
            xpiece = collect(child_elements(xgrid[1]))
            @test length(xpiece) == 2
            @test name(xpiece[1]) == "Points"
            xdata = collect(child_elements(xpiece[1]))
            @test length(xdata) == 1
            @test name(xdata[1]) == "DataArray"
            @test attribute(xdata[1], "type") == "$T"
            @test attribute(xdata[1], "NumberOfComponents") == "3"
            @test attribute(xdata[1], "format") == "ascii"
            @test [parse(T, e) for e in split(strip(content(xdata[1])))] == T[0, 0, 0, 0, 0, 3, 0, 3, 0, 1, -3, 3]
            @test name(xpiece[2]) == "Polys"
            xdata = collect(child_elements(xpiece[2]))
            @test length(xdata) == 2
            @test name(xdata[1]) == "DataArray"
            @test attribute(xdata[1], "type") == "Int32"
            @test attribute(xdata[1], "Name") == "connectivity"
            @test attribute(xdata[1], "format") == "ascii"
            @test [parse(Int, e) for e in split(strip(content(xdata[1])))] == [0, 1, 2, 0, 3, 1]
            @test name(xdata[2]) == "DataArray"
            @test attribute(xdata[2], "type") == "Int32"
            @test attribute(xdata[2], "Name") == "offsets"
            @test attribute(xdata[2], "format") == "ascii"
            @test [parse(Int, e) for e in split(strip(content(xdata[2])))] == [3, 6]
        finally
            close(fh)
            rm(fname)
        end
    end
end

@testset "writevtk (volume model)" begin
    for T in testtypes
        # empty system
        model = Model{T, Tetrahedron{T}}()
        (fname, fh) = mktemp()
        try
            xroot = root(parse_string(readback(fh -> writevtk(fh, model))))
            @test name(xroot) == "VTKFile"
            @test attribute(xroot, "type") == "UnstructuredGrid"
            xgrid = collect(child_elements(xroot))
            @test length(xgrid) == 1
            @test name(xgrid[1]) == "UnstructuredGrid"
            xgrid = collect(child_elements(xgrid[1]))
            @test length(xgrid) == 1
            @test name(xgrid[1]) == "Piece"
            @test attribute(xgrid[1], "NumberOfPoints") == "0"
            @test attribute(xgrid[1], "NumberOfCells") == "0"
            xpiece = collect(child_elements(xgrid[1]))
            @test length(xpiece) == 2
            @test name(xpiece[1]) == "Points"
            xdata = collect(child_elements(xpiece[1]))
            @test length(xdata) == 1
            @test name(xdata[1]) == "DataArray"
            @test attribute(xdata[1], "type") == "$T"
            @test attribute(xdata[1], "NumberOfComponents") == "3"
            @test attribute(xdata[1], "format") == "ascii"
            @test strip(content(xdata[1])) == ""
            @test name(xpiece[2]) == "Cells"
            xdata = collect(child_elements(xpiece[2]))
            @test length(xdata) == 3
            @test name(xdata[1]) == "DataArray"
            @test attribute(xdata[1], "type") == "Int32"
            @test attribute(xdata[1], "Name") == "connectivity"
            @test attribute(xdata[1], "format") == "ascii"
            @test strip(content(xdata[1])) == ""
            @test name(xdata[2]) == "DataArray"
            @test attribute(xdata[2], "type") == "Int32"
            @test attribute(xdata[2], "Name") == "offsets"
            @test attribute(xdata[2], "format") == "ascii"
            @test strip(content(xdata[2])) == ""
            @test name(xdata[3]) == "DataArray"
            @test attribute(xdata[3], "type") == "UInt8"
            @test attribute(xdata[3], "Name") == "types"
            @test attribute(xdata[3], "format") == "ascii"
            @test strip(content(xdata[3])) == ""
        finally
            close(fh)
            rm(fname)
        end

        # small system
        nodes = Vector{T}[[0, 0, 0], [1, 0, 0], [0, 1, 0], [0, 0, 1], [-2, 0, 0]]
        model = Model(
            nodes,
            Tetrahedron{T}[
                Tetrahedron(nodes[1], nodes[2], nodes[3], nodes[4]),
                Tetrahedron(nodes[1], nodes[2], nodes[3], nodes[5])
            ]
        )
        (fname, fh) = mktemp()
        try
            xroot = root(parse_string(readback(fh -> writevtk(fh, model))))
            @test name(xroot) == "VTKFile"
            @test attribute(xroot, "type") == "UnstructuredGrid"
            xgrid = collect(child_elements(xroot))
            @test length(xgrid) == 1
            @test name(xgrid[1]) == "UnstructuredGrid"
            xgrid = collect(child_elements(xgrid[1]))
            @test length(xgrid) == 1
            @test name(xgrid[1]) == "Piece"
            @test attribute(xgrid[1], "NumberOfPoints") == "5"
            @test attribute(xgrid[1], "NumberOfCells") == "2"
            xpiece = collect(child_elements(xgrid[1]))
            @test length(xpiece) == 2
            @test name(xpiece[1]) == "Points"
            xdata = collect(child_elements(xpiece[1]))
            @test length(xdata) == 1
            @test name(xdata[1]) == "DataArray"
            @test attribute(xdata[1], "type") == "$T"
            @test attribute(xdata[1], "NumberOfComponents") == "3"
            @test attribute(xdata[1], "format") == "ascii"
            @test [parse(T, e) for e in split(strip(content(xdata[1])))] == T[0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, -2, 0, 0]
            @test name(xpiece[2]) == "Cells"
            xdata = collect(child_elements(xpiece[2]))
            @test length(xdata) == 3
            @test name(xdata[1]) == "DataArray"
            @test attribute(xdata[1], "type") == "Int32"
            @test attribute(xdata[1], "Name") == "connectivity"
            @test attribute(xdata[1], "format") == "ascii"
            @test [parse(Int, e) for e in split(strip(content(xdata[1])))] == [0, 1, 2, 3, 0, 1, 2, 4]
            @test name(xdata[2]) == "DataArray"
            @test attribute(xdata[2], "type") == "Int32"
            @test attribute(xdata[2], "Name") == "offsets"
            @test attribute(xdata[2], "format") == "ascii"
            @test [parse(Int, e) for e in split(strip(content(xdata[2])))] == [4, 8]
            @test name(xdata[3]) == "DataArray"
            @test attribute(xdata[3], "type") == "UInt8"
            @test attribute(xdata[3], "Name") == "types"
            @test attribute(xdata[3], "format") == "ascii"
            @test [parse(Int, e) for e in split(strip(content(xdata[3])))] == [10, 10]
        finally
            close(fh)
            rm(fname)
        end
    end
end


include("./deeper-cut/deeper-cut.config.jl")

function trainresults(
    save_tag,
    model,
    data_trn,
    data_tst,
    learning_rate_per_epoch,
    optimizer = momentum,
    should_save = false;
    accuracy_func = accuracy,
    error_func,
    secondary_accuracy_func,
    data_items_trn,
    data_items_val,
    check_loss = true
)
    GC.gc(true)
    epoch_count = 0
    # TODO: we can make better use of it no need to run forward pass multiple times
    function snapshot(i)

        trn_loss = check_loss ? model(data_trn) : 0
        tst_loss = check_loss ? model(data_trn) : 0

        naive_trn_accuracy = compute_accuracy_in_training(model, data_trn, accuracy_func)
        naive_tst_accuracy = compute_accuracy_in_training(model, data_tst, accuracy_func)

        pck_trn_accuracy, pck_trn_distribution =
            data_items_trn == nothing ? (0, 0) :
            compute_secondary_accuracy_in_training(
                model,
                data_trn,
                data_items_trn,
                secondary_accuracy_func,
            )

        pck_val_accuracy, pck_val_distribution = data_items_val == nothing ?  (0, 0) : compute_secondary_accuracy_in_training(
            model,
            data_tst,
            data_items_val,
            secondary_accuracy_func,
        )

        if pck_val_distribution != 0
                pck_val_distribution = read_accuracy_results(pck_val_distribution)
        end

        open("$(pre_full_path)$(save_tag)-training_snapshots.txt", "a") do io
            write(io, "***** epoch: $(epoch_count) ***** \n")
            write(io, "trn_loss: $(trn_loss) \n")
            write(io, "tst_loss: $(tst_loss) \n")
            write(io, "naive_trn_accuracy: $(naive_trn_accuracy) \n")
            write(io, "naive_tst_accuracy: $(naive_tst_accuracy)  \n")
            write(io, "pck_val_accuracy: $(pck_val_accuracy)  \n")
            write(io, "$(pck_val_distribution)")
            write(io, "\n")
        end

        epoch_count += 1

        Knet.save("$(pre_full_path)$(save_tag)-training_model.jld2", "$(save_tag)-training_model", model)

        snap_res = (
            0,
            trn_loss,
            tst_loss,
            0,
            0,
            naive_trn_accuracy,
            naive_tst_accuracy,
            pck_trn_accuracy,
            pck_val_accuracy,
        )
        println(snap_res)
        return snap_res
    end
    # compute_error_in_training(model, data_trn, error_func),
    # compute_error_in_training(model, data_tst, error_func),

    results = []
    for (lr, for_epoch) in learning_rate_per_epoch

        training = optimizer(model, ncycle(data_trn, for_epoch), lr = lr,
            gamma = momentum_gamma)

        #   (snapshot(x) for x in takenth(progress(training), length(data_trn))) |> collect

        snapshots = (snapshot(x) for x in takenth(progress(training), length(data_trn)))
        intermediate_res = reshape(collect(Iterators.flatten(snapshots)), (9, :))
        if isempty(results)
            results = intermediate_res
        else
            results = hcat(results, intermediate_res)
        end

    end

    if (should_save)
        Knet.save("$(save_tag)_results.jld2", "$(save_tag)_results", results)
    end
    return results

end

# TODO: modify this for our purposes
function alternative_train!(model, trn, dev, tst...; iteration_count = 1000)
    bestmodel, bestloss = deepcopy(model), loss(model, dev)
    progress!(adam(model, trn), steps = iteration_count) do y
        losses = [loss(model, d) for d in (dev, tst...)]
        if losses[1] < bestloss
            bestmodel, bestloss = deepcopy(model), losses[1]
        end
        return (losses...,)
    end
    return bestmodel
end

function compute_accuracy_in_training(model, data, accuracy_func)
    if accuracy_func !== nothing
        return accuracy_func(model, data)
    else
        return 0
        train_accuracy = 0
    end
end

function compute_secondary_accuracy_in_training(model, data, data_items, accuracy_func)
    if accuracy_func !== nothing
        return accuracy_func(model, data, data_items)
    else
        return (0, 0)
    end
end

function compute_error_in_training(model, data, error_func)
    if error_func !== nothing
        return error_func(model, data)
    else
        return 0
    end
end


# This file is a part of Julia. License is MIT: https://julialang.org/license

# The epochs used for date rounding are based ISO 8601's "year zero" notation
const DATEEPOCH = value(Date(0))
const DATETIMEEPOCH = value(DateTime(0))

# According to ISO 8601, the first day of the first week of year 0000 is 0000-01-03
const WEEKEPOCH = value(Date(0, 1, 3))

"""
    epochdays2date(days) -> Date

Takes the number of days since the rounding epoch (`0000-01-01T00:00:00`) and returns the
corresponding `Date`.
"""
epochdays2date(i) = Date(UTD(DATEEPOCH + Int64(i)))

"""
    epochms2datetime(milliseconds) -> DateTime

Takes the number of milliseconds since the rounding epoch (`0000-01-01T00:00:00`) and
returns the corresponding `DateTime`.
"""
epochms2datetime(i) = DateTime(UTM(DATETIMEEPOCH + Int64(i)))

"""
    date2epochdays(dt::Date) -> Int64

Takes the given `Date` and returns the number of days since the rounding epoch
(`0000-01-01T00:00:00`) as an `Int64`.
"""
date2epochdays(dt::Date) = value(dt) - DATEEPOCH

"""
    datetime2epochms(dt::DateTime) -> Int64

Takes the given `DateTime` and returns the number of milliseconds since the rounding epoch
(`0000-01-01T00:00:00`) as an `Int64`.
"""
datetime2epochms(dt::DateTime) = value(dt) - DATETIMEEPOCH

function Base.floor(dt::Date, p::Year)
    value(p) < 1 && throw(DomainError())
    years = year(dt)
    return Date(years - mod(years, value(p)))
end

function Base.floor(dt::Date, p::Month)
    value(p) < 1 && throw(DomainError())
    y, m = yearmonth(dt)
    months_since_epoch = y * 12 + m - 1
    month_offset = months_since_epoch - mod(months_since_epoch, value(p))
    target_month = mod(month_offset, 12) + 1
    target_year = div(month_offset, 12) - (month_offset < 0 && target_month != 1)
    return Date(target_year, target_month)
end

function Base.floor(dt::Date, p::Week)
    value(p) < 1 && throw(DomainError())
    days = value(dt) - WEEKEPOCH
    days = days - mod(days, value(Day(p)))
    return Date(UTD(WEEKEPOCH + Int64(days)))
end

function Base.floor(dt::Date, p::Day)
    value(p) < 1 && throw(DomainError())
    days = date2epochdays(dt)
    return epochdays2date(days - mod(days, value(p)))
end

Base.floor(dt::DateTime, p::DatePeriod) = DateTime(Base.floor(Date(dt), p))

function Base.floor(dt::DateTime, p::TimePeriod)
    value(p) < 1 && throw(DomainError())
    milliseconds = datetime2epochms(dt)
    return epochms2datetime(milliseconds - mod(milliseconds, value(Millisecond(p))))
end

"""
    floor(dt::TimeType, p::Period) -> TimeType

Returns the nearest `Date` or `DateTime` less than or equal to `dt` at resolution `p`.

For convenience, `p` may be a type instead of a value: `floor(dt, Dates.Hour)` is a shortcut
for `floor(dt, Dates.Hour(1))`.

```jldoctest
julia> floor(Date(1985, 8, 16), Dates.Month)
1985-08-01

julia> floor(DateTime(2013, 2, 13, 0, 31, 20), Dates.Minute(15))
2013-02-13T00:30:00

julia> floor(DateTime(2016, 8, 6, 12, 0, 0), Dates.Day)
2016-08-06T00:00:00
```
"""
Base.floor(::Dates.TimeType, ::Dates.Period)

"""
    ceil(dt::TimeType, p::Period) -> TimeType

Returns the nearest `Date` or `DateTime` greater than or equal to `dt` at resolution `p`.

For convenience, `p` may be a type instead of a value: `ceil(dt, Dates.Hour)` is a shortcut
for `ceil(dt, Dates.Hour(1))`.

```jldoctest
julia> ceil(Date(1985, 8, 16), Dates.Month)
1985-09-01

julia> ceil(DateTime(2013, 2, 13, 0, 31, 20), Dates.Minute(15))
2013-02-13T00:45:00

julia> ceil(DateTime(2016, 8, 6, 12, 0, 0), Dates.Day)
2016-08-07T00:00:00
```
"""
function Base.ceil(dt::TimeType, p::Period)
    f = floor(dt, p)
    return (dt == f) ? f : f + p
end

"""
    floorceil(dt::TimeType, p::Period) -> (TimeType, TimeType)

Simultaneously return the `floor` and `ceil` of a `Date` or `DateTime` at resolution `p`.
More efficient than calling both `floor` and `ceil` individually.
"""
function floorceil(dt::TimeType, p::Period)
    f = floor(dt, p)
    return f, (dt == f) ? f : f + p
end

"""
    round(dt::TimeType, p::Period, [r::RoundingMode]) -> TimeType

Returns the `Date` or `DateTime` nearest to `dt` at resolution `p`. By default
(`RoundNearestTiesUp`), ties (e.g., rounding 9:30 to the nearest hour) will be rounded up.

For convenience, `p` may be a type instead of a value: `round(dt, Dates.Hour)` is a shortcut
for `round(dt, Dates.Hour(1))`.

```jldoctest
julia> round(Date(1985, 8, 16), Dates.Month)
1985-08-01

julia> round(DateTime(2013, 2, 13, 0, 31, 20), Dates.Minute(15))
2013-02-13T00:30:00

julia> round(DateTime(2016, 8, 6, 12, 0, 0), Dates.Day)
2016-08-07T00:00:00
```

Valid rounding modes for `round(::TimeType, ::Period, ::RoundingMode)` are
`RoundNearestTiesUp` (default), `RoundDown` (`floor`), and `RoundUp` (`ceil`).
"""
function Base.round(dt::TimeType, p::Period, r::RoundingMode{:NearestTiesUp})
    f, c = floorceil(dt, p)
    return (dt - f) < (c - dt) ? f : c
end

Base.round(dt::TimeType, p::Period, r::RoundingMode{:Down}) = Base.floor(dt, p)
Base.round(dt::TimeType, p::Period, r::RoundingMode{:Up}) = Base.ceil(dt, p)

# No implementation of other `RoundingMode`s: rounding to nearest "even" is skipped because
# "even" is not defined for Period; rounding toward/away from zero is skipped because ISO
# 8601's year 0000 is not really "zero".
Base.round(::TimeType, ::Period, ::RoundingMode) = throw(DomainError())

# Default to RoundNearestTiesUp.
Base.round(dt::TimeType, p::Period) = Base.round(dt, p, RoundNearestTiesUp)

# Make rounding functions callable using Period types in addition to values.
Base.floor(dt::TimeType, p::Type{<:Period}) = Base.floor(dt, p(1))
Base.ceil(dt::TimeType, p::Type{<:Period}) = Base.ceil(dt, p(1))

function Base.round(dt::TimeType, p::Type{<:Period}, r::RoundingMode=RoundNearestTiesUp)
    return Base.round(dt, p(1), r)
end


using RecSys

import RecSys: train, recommend, rmse

if isless(Base.VERSION, v"0.5.0-")
    using SparseVectors
end

type MusicRec
    trainingset::FileSpec
    artist_names::FileSpec
    artist_map::FileSpec
    als::ALSWR
    artist_mat::Nullable{Dict{Int64,AbstractString}}

    function MusicRec(trainingset::FileSpec, artist_names::FileSpec, artist_map::FileSpec)
        T, N = map_artists(trainingset, artist_names, artist_map)
        new(trainingset, artist_names, artist_map, ALSWR(SparseMat(T), ParShmem()), Nullable(N))
    end
    function MusicRec(user_item_ratings::FileSpec, item_user_ratings::FileSpec, artist_names::FileSpec, artist_map::FileSpec)
        new(user_item_ratings, artist_names, artist_map, ALSWR(user_item_ratings, item_user_ratings, ParBlob()), nothing)
    end
end

function read_artist_map(artist_map::FileSpec)
    t1 = time()
    RecSys.@logmsg("reading artist map")
    A = read_input(artist_map)
    valid = map(x->isa(x, Integer), A)
    valid = valid[:,1] & valid[:,2]
    Avalid = convert(Matrix{Int64}, A[valid, :])

    amap = Dict{Int64,Int64}()
    for idx in 1:size(Avalid,1)
        bad_id = Avalid[idx, 1]
        good_id = Avalid[idx, 2]
        amap[bad_id] = good_id
    end
    RecSys.@logmsg("read artist map in $(time()-t1) secs")
    amap
end

function read_trainingset(trainingset::FileSpec, amap::Dict{Int64,Int64})
    t1 = time()
    RecSys.@logmsg("reading trainingset")
    T = read_input(trainingset)
    for idx in 1:size(T,1)
        artist_id = T[idx,2]
        if artist_id in keys(amap)
            T[idx,2] = amap[artist_id]
        end
    end
    users   = convert(Vector{Int64},   T[:,1])
    artists = convert(Vector{Int64},   T[:,2])
    ratings = convert(Vector{Float64}, T[:,3])
    S = sparse(users, artists, ratings)
    RecSys.@logmsg("read trainingset in $(time()-t1) secs")
    S
end

function read_artist_names(artist_names::FileSpec, amap::Dict{Int64,Int64})
    t1 = time()
    RecSys.@logmsg("reading artist names")
    A = read_input(artist_names)
    name_map = Dict{Int64,AbstractString}()
    for idx in 1:size(A,1)
        artist_id = A[idx, 1]
        artist_name = A[idx, 2]
        (isa(artist_id, Integer) && isa(artist_name, AbstractString)) || continue

        if artist_id in keys(amap)
            artist_id = amap[artist_id]
            (artist_id in keys(name_map)) || (name_map[artist_id] = artist_name)
        else
            name_map[artist_id] = artist_name
        end
    end
    RecSys.@logmsg("read artist names in $(time()-t1) secs")
    name_map
end

function map_artists(trainingset::FileSpec, artist_names::FileSpec, artist_map::FileSpec)
    amap = read_artist_map(artist_map)
    T = read_trainingset(trainingset, amap)
    N = read_artist_names(artist_names, amap)
    T, N
end

function artist_names(rec::MusicRec)
    if isnull(rec.artist_mat)
        T, N = map_artists(rec.trainingset, rec.artist_names, rec.artist_map)
        rec.artist_mat = Nullable(N)
    end

    get(rec.artist_mat)
end

train(musicrec::MusicRec, args...) = train(musicrec.als, args...)
rmse(musicrec::MusicRec) = rmse(musicrec.als)
recommend(musicrec::MusicRec, args...; kwargs...) = recommend(musicrec.als, args...; kwargs...)

function print_list(mat::Dict, idxs::Vector{Int}, header::AbstractString)
    if !isempty(idxs)
        println(header)
        for idx in idxs
            println("[$idx] $(mat[idx])")
        end
    end
end

function print_recommendations(rec::MusicRec, recommended::Vector{Int}, listened::Vector{Int}, nexcl::Int)
    anames = artist_names(rec)

    print_list(anames, listened, "Already listened:")
    (nexcl == 0) || println("Excluded $(nexcl) artists already listened")
    print_list(anames, recommended, "Recommended:")
    nothing
end

function print_recommendations(recommended::Vector{Int}, listened::Vector{Int}, nexcl::Int)
    println("Already listened: $listened")
    (nexcl == 0) || println("Excluded $(nexcl) artists already listened")
    println("Recommended: $recommended")
    nothing
end

function test(dataset_path)
    ratings_file = DlmFile(joinpath(dataset_path, "user_artist_data.txt"))
    artist_names = DlmFile(joinpath(dataset_path, "artist_data.txt"); dlm='\t', quotes=false)
    artist_map = DlmFile(joinpath(dataset_path, "artist_alias.txt"))

    rec = MusicRec(ratings_file, artist_names, artist_map)
    train(rec, 20, 20)

    err = rmse(rec)
    println("rmse of the model: $err")

    println("recommending existing user:")
    print_recommendations(rec, recommend(rec, 9875)...)

    println("recommending anonymous user:")
    u_idmap = RecSys.user_idmap(rec.als.inp)
    i_idmap = RecSys.item_idmap(rec.als.inp)
    # take user 9875
    actual_user = isempty(u_idmap) ? 9875 : findfirst(u_idmap, 9875)
    rated_anon, ratings_anon = RecSys.items_and_ratings(rec.als.inp, actual_user)
    actual_music_ids = isempty(i_idmap) ? rated_anon : i_idmap[rated_anon]
    nmusic = isempty(i_idmap) ? RecSys.nitems(rec.als.inp) : maximum(i_idmap)
    sp_ratings_anon = SparseVector(nmusic, actual_music_ids, ratings_anon)
    print_recommendations(rec, recommend(rec, sp_ratings_anon)...)

    println("saving model to model.sav")
    clear(rec.als)
    localize!(rec.als)
    save(rec, "model.sav")

    nothing
end

function test_chunks(dataset_path, splits_dir, model_path)
    mem = Base.Sys.free_memory()
    mem_model = mem_inputs = round(Int, mem/3)
    user_item_ratings = SparseBlobs(joinpath(dataset_path, splits_dir, "R_itemwise"); maxcache=mem_inputs)
    item_user_ratings = SparseBlobs(joinpath(dataset_path, splits_dir, "RT_userwise"); maxcache=mem_inputs)
    artist_names = DlmFile(joinpath(dataset_path, "artist_data.txt"); dlm='\t', quotes=false)
    artist_map = DlmFile(joinpath(dataset_path, "artist_alias.txt"))

    rec = MusicRec(user_item_ratings, item_user_ratings, artist_names, artist_map)
    train(rec, 20, 20, model_path, mem_model)

    err = rmse(rec)
    println("rmse of the model: $err")
    println("recommending existing user:")
    print_recommendations(recommend(rec, 9875)...)

    nothing
end


struct Hydro end

const curtailconstraints = PowerSimulations.powerconstraints

function constructdevice!(category::Type{Hydro}, transmission::Type{CopperPlatePowerModel}, m::JuMP.AbstractModel, devices_netinjection::T, sys::PowerSystems.PowerSystem, constraints::Array{<:Function}=[powerconstraints]) where T <: JumpExpressionMatrix

    devices = [d for d in sys.generators.hydro if (d.available == true && !isa(d, HydroFix))]

    if !isempty(devices)

        pre, devices_netinjection = activepowervariables(m, devices_netinjection, devices, sys.time_periods)

        for c in constraints

            m = c(m, devices, sys.time_periods)

        end

    else

        @warn "Hydro dispatch variables created without constraints, the problem might be unbounded"

    end

    return m, devices_netinjection

end


# MIT License
#
# Copyright (c) 2018 Martin Biel
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

# SingleDecision #
# ========================== #
mutable struct SingleDecisionConstraintBridge{T, S <: MOI.AbstractScalarSet} <: MOIB.Constraint.AbstractBridge
    constraint::CI{MOI.SingleVariable, S}
    decision::SingleDecision
end

function MOIB.Constraint.bridge_constraint(::Type{SingleDecisionConstraintBridge{T,S}},
                                           model,
                                           f::SingleDecision,
                                           set::S) where {T, S <: MOI.AbstractScalarSet}
    # Perform the bridge mapping manually
    g = MOIB.bridged_variable_function(model, f.decision)
    mapped_variable = MOI.SingleVariable(only(g.terms).variable_index)
    # Add the bridged constraint
    constraint = MOI.add_constraint(model,
                                    mapped_variable,
                                    set)
    # Save the constraint index and the decision to allow modifications
    return SingleDecisionConstraintBridge{T,S}(constraint, f)
end

function MOI.supports_constraint(::Type{<:SingleDecisionConstraintBridge{T}},
                                 ::Type{SingleDecision},
                                 ::Type{<:MOI.AbstractScalarSet}) where T
    return true
end
function MOIB.added_constrained_variable_types(::Type{<:SingleDecisionConstraintBridge})
    return Tuple{DataType}[]
end
function MOIB.added_constraint_types(::Type{<:SingleDecisionConstraintBridge{T,S}}) where {T,S}
    return [(MOI.SingleVariable, S), (MOI.ScalarAffineFunction{T}, MOI.EqualTo{T})]
end
function MOIB.Constraint.concrete_bridge_type(::Type{<:SingleDecisionConstraintBridge{T}},
                              ::Type{SingleDecision},
                              S::Type{<:MOI.AbstractScalarSet}) where T
    return SingleDecisionConstraintBridge{T,S}
end

MOI.get(b::SingleDecisionConstraintBridge{T,S}, ::MOI.NumberOfConstraints{MOI.SingleVariable, S}) where {T,S} = 1
MOI.get(b::SingleDecisionConstraintBridge{T}, ::MOI.ListOfConstraintIndices{MOI.SingleVariable, S}) where {T,S} = [b.constraint]

function MOI.get(model::MOI.ModelLike, attr::MOI.AbstractConstraintAttribute,
                 bridge::SingleDecisionConstraintBridge)
    return MOI.get(model, attr, bridge.constraint)
end

function MOI.get(model::MOI.ModelLike, ::DecisionIndex,
                 bridge::SingleDecisionConstraintBridge{T}) where T
    return bridge.constraint
end

function MOI.delete(model::MOI.ModelLike, bridge::SingleDecisionConstraintBridge)
    if bridge.constraint.value != 0
        MOI.delete(model, bridge.constraint)
    end
    return nothing
end

function MOI.set(model::MOI.ModelLike, ::MOI.ConstraintSet,
                 bridge::SingleDecisionConstraintBridge{T,S}, change::S) where {T, S <: MOI.AbstractScalarSet}
    MOI.set(model, MOI.ConstraintSet(), bridge.constraint, change)
    return nothing
end


"""
spin30.jl - unscented transform robustness for the δfq problem
"""

WDIR = joinpath(@__DIR__, "../../")
include(joinpath(WDIR, "src", "spin", "spin.jl"))

using Altro
using Distributions
using HDF5
using LinearAlgebra
using Random
using RobotDynamics
using StaticArrays
using TrajectoryOptimization
const RD = RobotDynamics
const TO = TrajectoryOptimization

# paths
const EXPERIMENT_META = "spin"
const EXPERIMENT_NAME = "spin30"
const SAVE_PATH = joinpath(WDIR, "out", EXPERIMENT_META, EXPERIMENT_NAME)

# problem
const CONTROL_COUNT = 1
const ACONTROL_SIZE = CONTROL_COUNT
const STATE_COUNT = 3
# const STATE_COUNT = 4
const ASTATE_SIZE_BASE = STATE_COUNT * HDIM_ISO + 3 * CONTROL_COUNT
const SAMPLES_PER_STATE = 10
const PENALTY_SIZE = 1
const CHUNK_SIZE = SAMPLES_PER_STATE * HDIM_ISO + PENALTY_SIZE
# state indices
const STATE1_IDX = SVector{HDIM_ISO}(1:HDIM_ISO)
const STATE2_IDX = SVector{HDIM_ISO}(STATE1_IDX[end] + 1:STATE1_IDX[end] + HDIM_ISO)
const STATE3_IDX = SVector{HDIM_ISO}(STATE2_IDX[end] + 1:STATE2_IDX[end] + HDIM_ISO)
# const STATE4_IDX = SVector{HDIM_ISO}(STATE3_IDX[end] + 1:STATE3_IDX[end] + HDIM_ISO)
# const INTCONTROLS_IDX = SVector{CONTROL_COUNT}(STATE4_IDX[end] + 1:STATE4_IDX[end] + CONTROL_COUNT)
const INTCONTROLS_IDX = SVector{CONTROL_COUNT}(STATE3_IDX[end] + 1:STATE3_IDX[end] + CONTROL_COUNT)
const CONTROLS_IDX = SVector{CONTROL_COUNT}(INTCONTROLS_IDX[end] + 1:
                                            INTCONTROLS_IDX[end] + CONTROL_COUNT)
const DCONTROLS_IDX = SVector{CONTROL_COUNT}(CONTROLS_IDX[end] + 1:
                                             CONTROLS_IDX[end] + CONTROL_COUNT)
# control indices
const D2CONTROLS_IDX = SVector{CONTROL_COUNT}(1:CONTROL_COUNT)
# sample indices
const S1_IDX = SVector{HDIM_ISO}(HDIM_ISO * 0 + 1:HDIM_ISO * 1)
const S2_IDX = SVector{HDIM_ISO}(HDIM_ISO * 1 + 1:HDIM_ISO * 2)
const S3_IDX = SVector{HDIM_ISO}(HDIM_ISO * 2 + 1:HDIM_ISO * 3)
const S4_IDX = SVector{HDIM_ISO}(HDIM_ISO * 3 + 1:HDIM_ISO * 4)
const S5_IDX = SVector{HDIM_ISO}(HDIM_ISO * 4 + 1:HDIM_ISO * 5)
const S6_IDX = SVector{HDIM_ISO}(HDIM_ISO * 5 + 1:HDIM_ISO * 6)
const S7_IDX = SVector{HDIM_ISO}(HDIM_ISO * 6 + 1:HDIM_ISO * 7)
const S8_IDX = SVector{HDIM_ISO}(HDIM_ISO * 7 + 1:HDIM_ISO * 8)
const S9_IDX = SVector{HDIM_ISO}(HDIM_ISO * 8 + 1:HDIM_ISO * 9)
const S10_IDX = SVector{HDIM_ISO}(HDIM_ISO * 9 + 1:HDIM_ISO * 10)
const STATE_IDX = SVector{HDIM_ISO}(1:HDIM_ISO)

# model
struct Model <: RD.AbstractModel
    S::Diagonal{Float64,SVector{HDIM_ISO,Float64}}
    nominal_idxs::Array{SVector{HDIM_ISO,Int},1}
    fq_cov::Float64
    alpha::Float64
    sample_state_count::Int
end
@inline RD.state_dim(model::Model) = (
    ASTATE_SIZE_BASE + model.sample_state_count * CHUNK_SIZE
)
@inline RD.control_dim(model::Model) = ACONTROL_SIZE
@inline astate_sample_inds(sample_state_index::Int, sample_index::Int) = (
    SVector{HDIM_ISO}((
        ASTATE_SIZE_BASE + (sample_state_index - 1) * CHUNK_SIZE
        + (sample_index - 1) * HDIM_ISO + 1
    ):(
        ASTATE_SIZE_BASE + (sample_state_index - 1) * CHUNK_SIZE
        + sample_index * HDIM_ISO
    ))
)
@inline sample_idxs(model::Model) = [
    astate_sample_inds(i, j)
    for i = 1:model.sample_state_count
    for j = 1:SAMPLES_PER_STATE
]

function unscented_transform(model::Model, astate::AbstractVector,
                             negi_hc::AbstractMatrix, h_prop::AbstractMatrix,
                             dt::Real, i::Int)
    # get states
    offset = ASTATE_SIZE_BASE + (i - 1) * CHUNK_SIZE
    s1 = astate[offset + S1_IDX]
    s2 = astate[offset + S2_IDX]
    s3 = astate[offset + S3_IDX]
    s4 = astate[offset + S4_IDX]
    s5 = astate[offset + S5_IDX]
    s6 = astate[offset + S6_IDX]
    s7 = astate[offset + S7_IDX]
    s8 = astate[offset + S8_IDX]
    s9 = astate[offset + S9_IDX]
    s10 = astate[offset + S10_IDX]
    # grab chol info
    fq_chol5 = sqrt(model.fq_cov)
    # propagate states
    s1 = h_prop * s1
    s2 = h_prop * s2
    s3 = h_prop * s3
    s4 = h_prop * s4
    s5 = exp(dt * ((FQ + fq_chol5) * NEGI_H0_ISO + negi_hc)) * s5
    s6 = h_prop * s6
    s7 = h_prop * s7
    s8 = h_prop * s8
    s9 = h_prop * s9
    s10 = exp(dt * ((FQ - fq_chol5) * NEGI_H0_ISO + negi_hc)) * s10
    # compute state mean
    sm = 1//SAMPLES_PER_STATE .* (
        s1 + s2 + s3 + s4 + s5
        + s6 + s7 + s8 + s9 + s10
    )
    # compute state covariance
    d1 = s1 - sm
    d2 = s2 - sm
    d3 = s3 - sm
    d4 = s4 - sm
    d5 = s5 - sm
    d6 = s6 - sm
    d7 = s7 - sm
    d8 = s8 - sm
    d9 = s9 - sm
    d10 = s10 - sm
    s_cov = 1 / (2 * model.alpha^2) .* (
        d1 * d1' + d2 * d2' + d3 * d3' + d4 * d4'
        + d5 * d5' + d6 * d6' + d7 * d7' + d8 * d8'
        + d9 * d9' + d10 * d10'
    )
    # perform cholesky decomposition on joint covariance
    cov = zeros(eltype(s_cov), HDIM_ISO + 1, HDIM_ISO + 1)
    cov[1:HDIM_ISO, 1:HDIM_ISO] .= s_cov
    cov[HDIM_ISO + 1, HDIM_ISO + 1] = model.fq_cov
    # TOOD: cholesky! requires writing zeros in upper triangle
    cov_chol = model.alpha * cholesky(Symmetric(cov)).L
    # resample states
    s_chol1 = cov_chol[STATE_IDX, 1]
    s_chol2 = cov_chol[STATE_IDX, 2]
    s_chol3 = cov_chol[STATE_IDX, 3]
    s_chol4 = cov_chol[STATE_IDX, 4]
    s1 = sm + s_chol1
    s2 = sm + s_chol2
    s3 = sm + s_chol3
    s4 = sm + s_chol4
    s5 = sm
    s6 = sm - s_chol1
    s7 = sm - s_chol2
    s8 = sm - s_chol3
    s9 = sm - s_chol4
    s10 = sm
    # normalize
    s1 = s1 ./sqrt(s1's1)
    s2 = s2 ./sqrt(s2's2)
    s3 = s3 ./sqrt(s3's3)
    s4 = s4 ./sqrt(s4's4)
    s5 = s5 ./sqrt(s5's5)
    s6 = s6 ./sqrt(s6's6)
    s7 = s7 ./sqrt(s7's7)
    s8 = s8 ./sqrt(s8's8)
    s9 = s9 ./sqrt(s9's9)
    s10 = s10 ./sqrt(s10's10)
    # compute penalty
    dnom = sm - astate[model.nominal_idxs[i]]
    penalty = SVector{1}(tr(s_cov * model.S) + dnom' * model.S * dnom)

    samples = [s1; s2; s3; s4; s5; s6; s7; s8; s9; s10; penalty]

    return samples
end

# dynamics
function RD.discrete_dynamics(::Type{RK3}, model::Model, astate::StaticVector,
                              acontrol::StaticVector, time::Real, dt::Real) where {n}
    # base dynamics
    negi_hc = astate[CONTROLS_IDX[1]] * NEGI_H1_ISO
    h_prop = exp(dt * (FQ_NEGI_H0_ISO + negi_hc))
    state1 = h_prop * astate[STATE1_IDX]
    state2 = h_prop * astate[STATE2_IDX]
    state3 = h_prop * astate[STATE3_IDX]
    # state4 = h_prop * astate[STATE4_IDX]
    intcontrols = astate[INTCONTROLS_IDX] + dt * astate[CONTROLS_IDX]
    controls = astate[CONTROLS_IDX] + dt * astate[DCONTROLS_IDX]
    dcontrols = astate[DCONTROLS_IDX] + dt * acontrol[D2CONTROLS_IDX]

    astate_ = [
        # state1; state2; state3; state4; intcontrols; controls; dcontrols;
        state1; state2; state3; intcontrols; controls; dcontrols;
    ]

    # unscented transform
    for i = 1:model.sample_state_count
        sample_states = unscented_transform(model, astate, negi_hc, h_prop, dt, i)
        astate_ = [astate_; sample_states]
    end

    return astate_
end

# main
function run_traj(;gate_type=xpiby2, evolution_time=60., solver_type=altro,
                  sqrtbp=false, integrator_type=rk3, qs=[1e0, 1e0, 1e0, 1e-1, 1e0, 1e-1],
                  dt_inv=Int64(1e1), smoke_test=false, constraint_tol=1e-8, al_tol=1e-4,
                  pn_steps=2, max_penalty=1e11, verbose=true, save=true,
                  fq_cov=FQ * 1e-2, max_iterations=Int64(2e5), gradient_tol_int=1,
                  dJ_counter_limit=Int(1e2), state_cov=1e-2, seed=0, alpha=1.,
                  sample_states=[IS1_ISO_], nominal_idxs=[STATE1_IDX], static=true,
                  benchmark=false)
    Random.seed!(seed)
    (sample_state_count,) = size(sample_states)
    S = Diagonal(SVector{HDIM_ISO}(fill(qs[5], HDIM_ISO)))
    model = Model(S, nominal_idxs, fq_cov, alpha, sample_state_count)
    n = RD.state_dim(model)
    m = RD.control_dim(model)
    t0 = 0.

    # initial state, target state
    x0 = zeros(n)
    xf = zeros(n)
    gate = GT_GATE_ISO[gate_type]
    x0[STATE1_IDX] = IS1_ISO_
    x0[STATE2_IDX] = IS2_ISO_
    x0[STATE3_IDX] = IS3_ISO_
    # x0[STATE4_IDX] = IS4_ISO_
    xf[STATE1_IDX] = gate * IS1_ISO_
    xf[STATE2_IDX] = gate * IS2_ISO_
    state_dist = Distributions.Normal(0., state_cov)
    for i = 1:sample_state_count
        sample_state = sample_states[i]
        for j = 1:SAMPLES_PER_STATE
            sample_idx = astate_sample_inds(i, j)
            sample = sample_state .+ rand(state_dist, HDIM_ISO)
            x0[sample_idx] = sample ./ sqrt(sample'sample)
        end
    end
    if static
        x0 = SVector{n}(x0)
        xf = SVector{n}(xf)
    end

    # control amplitude constraint
    x_max = fill(Inf, n)
    x_max[CONTROLS_IDX] .= MAX_CONTROL_NORM_0
    x_min = fill(-Inf, n)
    x_min[CONTROLS_IDX] .= -MAX_CONTROL_NORM_0
    if static
        x_max = SVector{n}(x_max)
        x_min = SVector{n}(x_min)
    end

    # control amplitude constraint at boundary
    x_max_boundary = fill(Inf, n)
    x_max_boundary[CONTROLS_IDX] .= 0
    x_min_boundary = fill(-Inf, n)
    x_min_boundary[CONTROLS_IDX] .= 0
    if static
        x_max_boundary = SVector{n}(x_max_boundary)
        x_min_boundary = SVector{n}(x_min_boundary)
    end

    # initial trajectory
    dt = dt_inv^(-1)
    N = Int(floor(evolution_time * dt_inv)) + 1
    if static
        U0 = [SVector{m}(fill(1e-4, CONTROL_COUNT)) for k = 1:N-1]
        X0 = [SVector{n}(fill(NaN, n)) for k = 1:N]
    else
        U0 = [fill(1e-4, CONTROL_COUNT) for k = 1:N-1]
        X0 = [fill(NaN, n) for k = 1:N]
    end
    Z = Traj(X0, U0, dt * ones(N))

    # cost function
    Q = zeros(n)
    Q[STATE1_IDX] = Q[STATE2_IDX] = fill(qs[1], HDIM_ISO)
    Q[INTCONTROLS_IDX] = fill(qs[2], CONTROL_COUNT)
    Q[CONTROLS_IDX] = fill(qs[3], CONTROL_COUNT)
    Q[DCONTROLS_IDX] = fill(qs[4], CONTROL_COUNT)
    # penalty has unit value, its cost can be increased through qs[5],
    # which modifies the model.S matrix
    for i = 1:sample_state_count
        Q[ASTATE_SIZE_BASE + i * CHUNK_SIZE] = 1
    end
    R = zeros(m)
    R[D2CONTROLS_IDX] = fill(qs[6], CONTROL_COUNT)
    if static
        Q = SVector{n}(Q)
        R = SVector{m}(R)
    end
    Q = Diagonal(Q)
    Qf = Q * N
    R = Diagonal(R)
    objective = LQRObjective(Q, R, Qf, xf, N)

    # constraints
    # must satisfy control amplitude bound
    control_bnd = BoundConstraint(n, m, x_max=x_max, x_min=x_min)
    # must statisfy conrols start and end at 0
    control_bnd_boundary = BoundConstraint(n, m, x_max=x_max_boundary, x_min=x_min_boundary)
    # must reach target state, must have zero net flux
    target_astate_constraint = GoalConstraint(xf, [STATE1_IDX; STATE2_IDX; INTCONTROLS_IDX])
    # must obey unit norm
    norm_idxs = sample_idxs(model)
    push!(norm_idxs, STATE1_IDX)
    push!(norm_idxs, STATE2_IDX)
    push!(norm_idxs, STATE3_IDX)
    # push!(norm_idxs, STATE4_IDX)
    norm_constraints = [NormConstraint(n, m, 1, TO.Equality(), idx) for idx in norm_idxs]
    constraints = ConstraintList(n, m, N)
    add_constraint!(constraints, control_bnd, 2:N-2)
    add_constraint!(constraints, control_bnd_boundary, N-1:N-1)
    add_constraint!(constraints, target_astate_constraint, N:N)
    for norm_constraint in norm_constraints
        add_constraint!(constraints, norm_constraint, 2:N-1)
    end

    # solve problem
    prob = Problem{IT_RDI[integrator_type]}(model, objective, constraints,
                                            x0, xf, Z, N, t0, evolution_time)
    solver = ALTROSolver(prob)
    verbose_pn = verbose ? true : false
    verbose_ = verbose ? 2 : 0
    projected_newton = solver_type == altro ? true : false
    constraint_tolerance = solver_type == altro ? constraint_tol : al_tol
    iterations_inner = smoke_test ? 1 : 300
    iterations_outer = smoke_test ? 1 : 30
    n_steps = smoke_test ? 1 : pn_steps
    static_bp = static ? true : false
    set_options!(
        solver, square_root=sqrtbp, constraint_tolerance=constraint_tolerance,
        projected_newton_tolerance=al_tol, n_steps=n_steps,
        penalty_max=max_penalty, verbose_pn=verbose_pn, verbose=verbose_,
        projected_newton=projected_newton, iterations_inner=iterations_inner,
        iterations_outer=iterations_outer, iterations=max_iterations,
        gradient_tolerance_intermediate=gradient_tol_int,
        dJ_counter_limit=dJ_counter_limit, static_bp=static_bp
    )
    if benchmark
        benchmark_result = Altro.benchmark_solve!(solver)
    else
        benchmark_result = nothing
        Altro.solve!(solver)
    end

    # Post-process.
    acontrols_raw = TO.controls(solver)
    acontrols_arr = permutedims(reduce(hcat, map(Array, acontrols_raw)), [2, 1])
    astates_raw = TO.states(solver)
    astates_arr = permutedims(reduce(hcat, map(Array, astates_raw)), [2, 1])
    cidx_arr = Array(CONTROLS_IDX)
    d2cidx_arr = Array(D2CONTROLS_IDX)
    cmax = TO.max_violation(solver)
    cmax_info = TO.findmax_violation(TO.get_constraints(solver))
    iterations_ = iterations(solver)

    result = Dict(
        "acontrols" => acontrols_arr,
        "controls_idx" => cidx_arr,
        "d2controls_dt2_idx" => d2cidx_arr,
        "evolution_time" => evolution_time,
        "astates" => astates_arr,
        "qs" => qs,
        "cmax" => cmax,
        "cmax_info" => cmax_info,
        "dt" => dt,
        "solver_type" => Integer(solver_type),
        "sqrtbp" => Integer(sqrtbp),
        "max_penalty" => max_penalty,
        "constraint_tol" => constraint_tol,
        "al_tol" => al_tol,
        "gradient_tolerance_intermediate" => gradient_tol_int,
        "dJ_counter_limit" => dJ_counter_limit,
        "integrator_type" => Integer(integrator_type),
        "gate_type" => Integer(gate_type),
        "save_type" => Integer(jl),
        "iterations" => iterations_,
        "seed" => seed,
        "fq_cov" => fq_cov,
        "max_iterations" => max_iterations,
    )

    # save
    if save
        save_file_path = generate_file_path("h5", EXPERIMENT_NAME, SAVE_PATH)
        println("Saving this optimization to $(save_file_path)")
        h5open(save_file_path, "cw") do save_file
            for key in keys(result)
                write(save_file, key, result[key])
            end
        end
        result["save_file_path"] = save_file_path
    end

    result = benchmark ? benchmark_result : result

    return result
end


#############################
# Directed acyclic graph IR #
#############################

include("dag.jl")

##############################
# generative functions types #
##############################

struct StaticIRGenerativeFunctionOptions
    track_diffs::Bool
    cache_julia_nodes::Bool
end

# trace code generation
include("trace.jl")

"""
    StaticIRGenerativeFunction{T,U} <: GenerativeFunction{T,U}

Abstact type for a static IR generative function with return type T and trace type U.

Contains an intermediate representation based on a directed acyclic graph.
Most generative function interface methods are generated from the intermediate representation.
"""
abstract type StaticIRGenerativeFunction{T,U} <: GenerativeFunction{T,U} end

function get_ir end
function get_gen_fn_type end
function get_options end

function generate_generative_function(ir::StaticIR, name::Symbol; track_diffs=false, cache_julia_nodes=true)
    options = StaticIRGenerativeFunctionOptions(track_diffs, cache_julia_nodes)
    generate_generative_function(ir, name, options)
end

function generate_generative_function(ir::StaticIR, name::Symbol, options::StaticIRGenerativeFunctionOptions)

    (trace_defns, trace_struct_name) = generate_trace_type_and_methods(ir, name, options)

    gen_fn_type_name = gensym("StaticGenFunction_$name")
    return_type = ir.return_node.typ
    trace_type = trace_struct_name
    has_argument_grads = tuple(map((node) -> node.compute_grad, ir.arg_nodes)...)
    accepts_output_grad = ir.accepts_output_grad

    gen_fn_defn = quote
        struct $gen_fn_type_name <: $(QuoteNode(StaticIRGenerativeFunction)){$return_type,$trace_type}
            params_grad::Dict{Symbol,Any}
            params::Dict{Symbol,Any}
        end
        (gen_fn::$gen_fn_type_name)(args...) = $(GlobalRef(Gen, :propose))(gen_fn, args)[3]
        $(GlobalRef(Gen, :get_ir))(::Type{$gen_fn_type_name}) = $(QuoteNode(ir))
        $(GlobalRef(Gen, :get_trace_type))(::Type{$gen_fn_type_name}) = $trace_struct_name
        $(GlobalRef(Gen, :has_argument_grads))(::$gen_fn_type_name) = $(QuoteNode(has_argument_grads))
        $(GlobalRef(Gen, :accepts_output_grad))(::$gen_fn_type_name) = $(QuoteNode(accepts_output_grad))
        $(GlobalRef(Gen, :get_gen_fn))(trace::$trace_struct_name) = $(Expr(:(.), :trace, QuoteNode(static_ir_gen_fn_ref)))
        $(GlobalRef(Gen, :get_gen_fn_type))(::Type{$trace_struct_name}) = $gen_fn_type_name
        $(GlobalRef(Gen, :get_options))(::Type{$gen_fn_type_name}) = $(QuoteNode(options))
    end
    Expr(:block, trace_defns, gen_fn_defn, Expr(:call, gen_fn_type_name, :(Dict{Symbol,Any}()), :(Dict{Symbol,Any}())))
end

include("render_ir.jl")

###########################
# generative function API #
###########################

# variable names used in generated code
const trace = gensym("trace")
const weight = gensym("weight")
const subtrace = gensym("subtrace")

include("simulate.jl")
include("generate.jl")
include("project.jl")
include("update.jl")
include("backprop.jl")


"""
    DQMC(m::M; kwargs...) where M<:Model

Create a determinant quantum Monte Carlo simulation for model `m` with
keyword parameters `kwargs`.

### Keyword Arguments:
- `seed`: The random seed used by the simulation.
- `checkerboard=false`: If true, the simulation uses a generic checkerboard
decomposition.
- `thermalization_measurements::Dict{Symbol, AbstractMeasurement}`: A collection
of measurements run during the thermalization stage. By default, none are used.
- `measurements::Dict{Symbol, AbstractMeasurement}`: A collection of measurements
run during the measurement stage. Calls `default_measurements` if not specified.
- `recorder = ConfigRecorder`: Type of recorder used for saving configurations
generated during the simulation. Used (by default) when `replay!`ing simulations.
 (`Discarder` or `ConfigRecorder`)
- `recording_rate = measure_rate`: Rate at which configurations are recorded.
- `thermalization = 100`: Number of thermalization sweeps
- `sweeps`: Number of measurement sweeps
- `all_checks = true`: Check for Propagation instabilities and sign problems.
- `safe_mult = 10`: Number of "safe" matrix multiplications. Every `safe_mult`
multiplications, a UDT decomposition is used to stabilize the product.
- `delta_tau = 0.1`: Time discretization of the path integral
- `beta::Float64`: Inverse temperature used in the simulation
- `slices::Int = beta / delta_tau`: Number of imaginary time slice in the
simulation
- `measure_rate = 10`: Number of sweeps discarded between every measurement.
- `last_sweep = 0`: Sets the index of the last finished sweep. The simulation
will start with sweep `last_sweep + 1`.
"""
function DQMC(model::M;
        seed::Int=-1,
        checkerboard::Bool=false,
        thermalization_measurements = Dict{Symbol, AbstractMeasurement}(),
        measurements = Dict{Symbol, AbstractMeasurement}(),
        last_sweep = 0,
        measure_rate = 10,
        recording_rate = measure_rate,
        recorder = ConfigRecorder(DQMC, M, recording_rate),
        scheduler = SimpleScheduler(LocalSweep()),
        kwargs...
    ) where M<:Model
    # default params
    # paramskwargs = filter(kw->kw[1] in fieldnames(DQMCParameters), kwargs)
    parameters = DQMCParameters(measure_rate = measure_rate; kwargs...)

    HET = hoppingeltype(DQMC, model)
    GET = greenseltype(DQMC, model)
    HMT = hopping_matrix_type(DQMC, model)
    GMT = greens_matrix_type(DQMC, model)
    IMT = interaction_matrix_type(DQMC, model)
    stack = DQMCStack{GET, HET, GMT, HMT, IMT}()
    ut_stack = UnequalTimeStack{GET, GMT}()

    seed == -1 || Random.seed!(seed)
    conf = rand(DQMC, model, parameters.slices)
    analysis = DQMCAnalysis()
    CB = checkerboard ? CheckerboardTrue : CheckerboardFalse

    mc = DQMC(
        CB,
        model, conf, deepcopy(conf), last_sweep,
        stack, ut_stack, scheduler,
        parameters, analysis,
        recorder, thermalization_measurements, measurements
    )

    init!(mc)
    return mc
end

"""
    DQMC(m::M, params::Dict)
    DQMC(m::M, params::NamedTuple)

Create a determinant quantum Monte Carlo simulation for model `m` with
(keyword) parameters as specified in the dictionary/named tuple `params`.
"""
DQMC(m::Model, params::Dict{Symbol}) = DQMC(m; params...)
DQMC(m::Model, params::NamedTuple) = DQMC(m; params...)

# convenience
@inline beta(mc::DQMC) = mc.parameters.beta
@inline nslices(mc::DQMC) = mc.parameters.slices
@inline model(mc::DQMC) = mc.model
@inline conf(mc::DQMC) = mc.conf
@inline current_slice(mc::DQMC) = mc.stack.current_slice
@inline last_sweep(mc::DQMC) = mc.last_sweep
@inline configurations(mc::DQMC) = mc.recorder
@inline lattice(mc::DQMC) = lattice(mc.model)
@inline parameters(mc::DQMC) = merge(parameters(mc.parameters), parameters(mc.model))
@inline parameters(p::DQMCParameters) = (
    beta = p.beta, delta_tau = p.delta_tau, thermalization = p.thermalization, sweeps = p.sweeps
)

# cosmetics
import Base.summary
import Base.show
Base.summary(mc::DQMC) = "DQMC simulation of $(summary(mc.model))"
function Base.show(io::IO, mc::DQMC)
    print(io, "Determinant quantum Monte Carlo simulation\n")
    print(io, "Model: ", mc.model, "\n")
    print(io, "Beta: ", mc.parameters.beta, " (T ≈ $(round(1/mc.parameters.beta, sigdigits=3)))\n")
    N_th_meas = length(mc.thermalization_measurements)
    N_me_meas = length(mc.measurements)
    print(io, "Measurements: ", N_th_meas + N_me_meas, " ($N_th_meas + $N_me_meas)")
end
Base.show(io::IO, m::MIME"text/plain", mc::DQMC) = print(io, mc)

function init!(mc::DQMC)
    init_hopping_matrices(mc, mc.model)
    initialize_stack(mc, mc.stack)
    init_scheduler!(mc, mc.scheduler)
    # generate_communication_functions(mc.conf)
    nothing
end
@deprecate resume_init!(mc::DQMC) init!(mc) false

"""
    run!(mc::DQMC[; kwargs...])

Runs the given Monte Carlo simulation `mc`. Returns true if the run finished and
false if it cancelled early to generate a resumable save-file.

### Keyword Arguments:
- `verbose = true`: If true, print progress messaged to stdout.
- `thermalization`: Number of thermalization sweeps. Uses the value passed to
`DQMC` by default.
- `sweeps`: Number of measurement sweeps. Uses the value passed to `DQMC` by
default.
- `safe_every::TimePeriod`: Set the interval for regularly scheduled saves.
- `safe_before::Date`: If this date is passed, `run!` will generate a resumable
save file and exit
- `grace_period = Minute(5)`: Buffer between the current time and `safe_before`.
The time required to generate a save file should be included here.
- `resumable_filename`: Name of the resumable save file. The default is based on
`safe_before`.
- `overwrite = false`: If set to true a file with the same name as
`resumable_filename` will be overwritten. (This will create a temporary backup)

See also: [`resume!`](@ref)
"""
@bm function run!(
        mc::DQMC;
        verbose::Bool = true,
        ignore = tuple(),
        sweeps::Int = mc.parameters.sweeps,
        thermalization = mc.parameters.thermalization,
        safe_before::TimeType = now() + Year(100),
        safe_every::TimePeriod = Hour(10000),
        grace_period::TimePeriod = Minute(5),
        resumable_filename::String = "resumable_$(Dates.format(safe_before, "d_u_yyyy-HH_MM")).jld2",
        overwrite = false
    )

    # Update number of sweeps
    if (mc.parameters.thermalization != thermalization) || (mc.parameters.sweeps != sweeps)
        verbose && println("Rebuilding DQMCParameters with new number of sweeps.")
        mc.parameters = DQMCParameters(mc.parameters, thermalization = thermalization, sweeps = sweeps)
    end
    total_sweeps = sweeps + thermalization

    # Generate measurement groups
    th_groups = generate_groups(
        mc, mc.model,
        [mc.measurements[k] for k in keys(mc.thermalization_measurements) if !(k in ignore)]
    )
    groups = generate_groups(
        mc, mc.model,
        [mc.measurements[k] for k in keys(mc.measurements) if !(k in ignore)]
    )

    start_time = now()
    last_checkpoint = now()
    max_sweep_duration = 0.0
    verbose && println("Started: ", Dates.format(start_time, "d.u yyyy HH:MM"))

    # fresh stack
    verbose && println("Preparing Green's function stack")

    init!(mc)
    reverse_build_stack(mc, mc.stack)
    propagate(mc)

    _time = time() # for step estimations
    t0 = time() # for analysis.runtime, may need to reset
    verbose && println("\n\nThermalization stage - ", thermalization)

    next_print = (div(mc.last_sweep, mc.parameters.print_rate) + 1) * mc.parameters.print_rate
    while mc.last_sweep < total_sweeps
        verbose && (mc.last_sweep == thermalization + 1) && println("\n\nMeasurement stage - ", sweeps)

        # Perform whatever update is scheduled next
        update(mc.scheduler, mc, mc.model)

        # Trigger measurements
        if mc.last_sweep ≤ thermalization
            if iszero(mc.last_sweep % mc.parameters.measure_rate)
                for (requirement, group) in th_groups
                    apply!(requirement, group, mc, mc.model, mc.last_sweep)
                end
            end
            if mc.last_sweep == thermalization
                mc.analysis.th_runtime += time() - t0
                t0 = time()
            end
        else
            push!(mc.recorder, mc, mc.model, mc.last_sweep)
            if iszero(mc.last_sweep % mc.parameters.measure_rate)
                for (requirement, group) in groups
                    apply!(requirement, group, mc, mc.model, mc.last_sweep)
                end
            end
        end

        # Show sweep statistics - i.e. time/sweep, acceptance rates
        if mc.last_sweep >= next_print
            next_print += mc.parameters.print_rate
            sweep_dur = (time() - _time)/mc.parameters.print_rate
            max_sweep_duration = max(max_sweep_duration, sweep_dur)
            if verbose
                println("\t", mc.last_sweep)
                @printf("\t\tsweep dur: %.3fs\n", sweep_dur)
                show_statistics(stdout, mc.scheduler, "\t\t")
            end

            flush(stdout)
            _time = time()
        end

        # Trigger checkpoint safe/early exit save
        if safe_before - now() < Millisecond(grace_period) +
                Millisecond(round(Int, 2e3max_sweep_duration))

            println("Early save initiated for sweep #$(mc.last_sweep).\n")
            verbose && println("Current time: ", Dates.format(now(), "d.u yyyy HH:MM"))
            verbose && println("Target time:  ", Dates.format(safe_before, "d.u yyyy HH:MM"))
            save(resumable_filename, mc, overwrite = overwrite, rename = false)
            verbose && println("\nEarly save finished")
            disconnect(connected_ids)
            return false
        elseif (now() - last_checkpoint) > safe_every
            verbose && println("Performing scheduled save.")
            last_checkpoint = now()
            save(resumable_filename, mc, overwrite = overwrite, rename = false)
        end
    end

    if mc.last_sweep ≤ thermalization
        mc.analysis.th_runtime += time() - t0
    else
        mc.analysis.me_runtime += time() - t0
    end

    disconnect(connected_ids)

    # Print (numerical) error information
    if verbose
        if length(mc.analysis.imaginary_probability) > 0
            s = mc.analysis.imaginary_probability
            println("\nImaginary Probability Errors: ($(s.count))")
            @printf("\tmax  = %0.3e\n", max(s))
            @printf("\tmean = %0.3e\n", mean(s))
            @printf("\tmin  = %0.3e\n\n", min(s))
        end
        if length(mc.analysis.negative_probability) > 0
            s = mc.analysis.negative_probability
            println("\nNegative Probability Errors: ($(s.count))")
            @printf("\tmax  = %0.3e\n", max(s))
            @printf("\tmean = %0.3e\n", mean(s))
            @printf("\tmin  = %0.3e\n\n", min(s))
        end
        if length(mc.analysis.propagation_error) > 0
            s = mc.analysis.propagation_error
            println("\nPropagation Errors: ($(s.count))")
            @printf("\tmax  = %0.3e\n", max(s))
            @printf("\tmean = %0.3e\n", mean(s))
            @printf("\tmin  = %0.3e\n\n", min(s))
        end
    end

    # Total timings
    end_time = now()
    if verbose
        println("\nEnded: ", Dates.format(end_time, "d.u yyyy HH:MM"))
        @printf("Duration: %.2f minutes", (end_time - start_time).value/1000. /60.)
        println()
    end

    return true
end

"""
    replay(mc::DQMC[; configurations::Iterable = mc.recorder; kwargs...])

Replays previously generated configurations and measures observables along the
way.

### Keyword Arguments (both):
- `verbose = true`: If true, print progress messaged to stdout.
- `safe_every::TimePeriod`: Set the interval for regularly scheduled saves.
- `safe_before::Date`: If this date is passed, `replay!` will generate a
resumable save file and exit
- `grace_period = Minute(5)`: Buffer between the current time and `safe_before`.
The time required to generate a save file should be included here.
- `filename`: Name of the save file. The default is based on `safe_before`.
- `start=1`: The first sweep in the simulation. This will be changed when using
`resume!(save_file)`.
- `ignore`: A collection of measurement keys to ignore. Defaults to the key of
the configuration measurement.
- `measure_rate = 1`: Rate at which measurements are taken. Note that this is
based on the recorded configurations, not actual sweeps.
"""
function replay!(
        mc::DQMC, configurations = mc.recorder;
        ignore = tuple(),
        verbose::Bool = true,
        safe_before::TimeType = now() + Year(100),
        safe_every::TimePeriod = Hour(10000),
        grace_period::TimePeriod = Minute(5),
        resumable_filename::String = "resumable_$(Dates.format(safe_before, "d_u_yyyy-HH_MM")).jld",
        overwrite = false,
        measure_rate = 1
    )
    start_time = now()
    last_checkpoint = now()
    max_sweep_duration = 0.0
    verbose && println("Started: ", Dates.format(start_time, "d.u yyyy HH:MM"))

    # Check for measurements
    isempty(mc.thermalization_measurements) && @debug(
        "There is no thermalization process in a replayed simulation."
    )
    isempty(mc.measurements) && @warn(
        "There are no measurements set up for this simulation!"
    )
    # Generate measurement groups
    groups = generate_groups(
        mc, mc.model,
        [mc.measurements[k] for k in keys(mc.measurements) if !(k in ignore)]
    )

    if measure_rate != mc.parameters.measure_rate
        mc.parameters = DQMCParameters(mc.parameters, measure_rate = measure_rate)
    end

    verbose && println("Preparing Green's function stack")
    init!(mc)
    build_stack(mc, mc.stack)
    propagate(mc)
    mc.stack.current_slice = 1
    mc.conf = rand(DQMC, mc.model, nslices(mc))

    _time = time()
    verbose && println("\n\nReplaying measurement stage - ", length(configurations))
    prepare!(mc.measurements, mc, mc.model)
    for i in mc.last_sweep+1:mc.parameters.measure_rate:length(configurations)
        copyto!(mc.conf, decompress(mc, mc.model, configurations[i]))
        calculate_greens(mc, 0) # outputs to mc.stack.greens
        for (requirement, group) in groups
            apply!(requirement, group, mc, mc.model, i)
        end
        mc.last_sweep = i

        if mod(i, mc.parameters.print_rate) == 0
            sweep_dur = (time() - _time)/mc.parameters.print_rate
            max_sweep_duration = max(max_sweep_duration, sweep_dur)
            if verbose
                println("\t", i)
                @printf("\t\tsweep dur: %.3fs\n", sweep_dur)
            end
            flush(stdout)
            _time = time()
        end

        if safe_before - now() < Millisecond(grace_period) +
                Millisecond(round(Int, 2e3max_sweep_duration))

            println("Early save initiated for sweep #$i.\n")
            verbose && println("Current time: ", Dates.format(now(), "d.u yyyy HH:MM"))
            verbose && println("Target time:  ", Dates.format(safe_before, "d.u yyyy HH:MM"))
            save(resumable_filename, mc, overwrite = overwrite, rename = false)
            verbose && println("\nEarly save finished")

            return false
        elseif (now() - last_checkpoint) > safe_every
            verbose && println("Performing scheduled save.")
            last_checkpoint = now()
            save(resumable_filename, mc, overwrite = overwrite, rename = false)
        end
    end
    finish!(mc.measurements, mc, mc.model)

    end_time = now()
    verbose && println("Ended: ", Dates.format(end_time, "d.u yyyy HH:MM"))
    verbose && @printf("Duration: %.2f minutes", (end_time - start_time).value/1000. /60.)

    return true
end


abstract type AbstractAbundanceTable{T <: Real,
                                     F <: AbstractFeature,
                                     S <: AbstractSample} <: EcoBase.AbstractAssemblage{T, F, S}
end

"""
    CommunityProfile{T, F, S} <: AbstractAbundanceTable{T, F, S}

An `AbstractAssemblage` from [EcoBase.jl](https://github.com/EcoJulia/EcoBase.jl)
that uses an `AxisArray` of a `SparseMatrixCSC` under the hood.

`CommunityProfile`s are tables with `AbstractFeature`-indexed rows and
`AbstractSample`-indexed columns.
Note - we can use the `name` of samples and features to index.

```jldoctest community
julia> txs = [Taxon("taxon\$i") for i in 1:10];

julia> mss = [MicrobiomeSample("sample\$i") for i in 1:5];

julia> mat = spzeros(10,5);

julia> for i in 1:5; mat[i,i] = 1.; end

julia> comm = CommunityProfile(mat, txs, mss)
CommunityProfile{Float64, Taxon, MicrobiomeSample} with 10 things in 5 places

Thing names:
taxon1, taxon2, taxon3...taxon9, taxon10

Place names:
sample1, sample2, sample3, sample4, sample5

julia> comm["taxon1", "sample1"]
1.0

julia> comm[:,["sample1", "sample5"]]
CommunityProfile{Float64, Taxon, MicrobiomeSample} with 10 things in 2 places

Thing names:
taxon1, taxon2, taxon3...taxon9, taxon10

Place names:
sample1, sample5

julia> comm[Taxon("taxon3", :kingdom), "sample1"]
0.0
```
"""
mutable struct CommunityProfile{T, F, S} <: AbstractAbundanceTable{T, F, S}
    aa::NamedAxisArray

    function CommunityProfile(aa::NamedAxisArray)
        @assert dimnames(aa) == (:features, :samples)
        T = eltype(parent(aa))
        F = eltype(keys(axes(aa, 1)))
        S = eltype(keys(axes(aa, 2)))
        return new{T, F, S}(aa)
    end
end

function CommunityProfile(tab::SparseMatrixCSC{<:Real},
                          features::AbstractVector{<:AbstractFeature},
                          samples::AbstractVector{<:AbstractSample})
    return CommunityProfile(NamedAxisArray(tab, features=features, samples=samples))
end

function CommunityProfile{T, F, S}(tab::SparseMatrixCSC{<:T},
                                   features::AbstractVector{F},
                                   samples::AbstractVector{S}) where {T, F, S}
    return CommunityProfile(tab, features, samples)
end
## -- Convienience functions -- ##

"""
    features(at::AbstractAbundanceTable)

Returns features in `at`. To get featurenames instead, use [`featurenames`](@ref).
"""
features(at::AbstractAbundanceTable) = axes(at.aa, 1) |> keys

"""
    samples(at::AbstractAbundanceTable)

Returns samples in `at`. To get samplenames instead, use [`samplenames`](@ref).
"""
samples(at::AbstractAbundanceTable) = axes(at.aa, 2) |> keys

profiletype(at::AbstractAbundanceTable) = eltype(features(at))
clades(at::AbstractAbundanceTable) = clade.(features(at))

Base.size(at::AbstractAbundanceTable, dims...) = size(at.aa, dims...)

# -- Indexing -- #

function Base.getindex(at::CommunityProfile, inds...)
    idx = at.aa[inds...]

    # single value - return that value
    ndims(idx) == 0 && return idx
    # another table - return a new CommunityProfile with that table
    ndims(idx) == 2 && return CommunityProfile(idx)
    # a row or a column, figure out which, and make it 2D
    if ndims(idx) == 1
        dn = dimnames(idx)[1]
        # if it's a row...
        if dn == :samples
            return at[[inds[1]], inds[2]]
        # if it's a column
        elseif dn == :features
            return at[inds[1], [inds[2]]]
        end
    end
end

## -- EcoBase Translations -- ##
# see src/ecobase.jl for Microbiome function names
# thing => feature
# place => sample
# occurrences => abundances (or totals)

EcoBase.thingnames(at::AbstractAbundanceTable) = name.(features(at))
EcoBase.placenames(at::AbstractAbundanceTable) = name.(samples(at))
EcoBase.occurrences(at::AbstractAbundanceTable) = parent(parent(at.aa)) # first parent is the unnamed AxisArray
EcoBase.nthings(at::AbstractAbundanceTable) = size(at, 1)
EcoBase.nplaces(at::AbstractAbundanceTable) = size(at, 2)
# # todo
# EcoBase.thingoccurrences(at::AbstractAbundanceTable, things) = nothing
# EcoBase.placeoccurrences(at::AbstractAbundanceTable, places) = nothing

"""
    featuretotals(at::AbstractAbundanceTable)

Returns sum of each row (feature) in `at`.
Note, return value is a nfeatures x 1 `Matrix`, not a `Vector`.
If you need 1D `Vector`, use `vec(featuretotals(at))`.
"""
featuretotals(at::AbstractAbundanceTable) = sum(abundances(at), dims=2)

"""
    sampletotals(at::AbstractAbundanceTable)

Returns sum of each row (feature) in `at`.
Note, return value is a 1 x nsamples `Matrix`, not a `Vector`.
If you need 1D `Vector`, use `vec(sampletotals(at))`.
"""
sampletotals(at::AbstractAbundanceTable) = sum(abundances(at), dims=1)

## -- Tables Interface -- ##

Tables.istable(::AbstractAbundanceTable) = true
Tables.columnaccess(::AbstractAbundanceTable) = true
Tables.rowaccess(::AbstractAbundanceTable) = true

Tables.getcolumn(at::AbstractAbundanceTable, i::Int) = i == 1 ? featurenames(at) : abundances(at[:, i-1])
Tables.getcolumn(at::AbstractAbundanceTable, i::AbstractString) = i == "features" ? featurenames(at) : abundances(at[:, i])
Tables.getcolumn(at::AbstractAbundanceTable, i::Symbol) = Tables.getcolumn(at, string(i))

Tables.columnnames(at::AbstractAbundanceTable) = [:features, Symbol.(samplenames(at))...]

function Tables.schema(at::AbstractAbundanceTable)
    elt = eltype(abundances(at))
    coltypes = [eltype(features(at)), (elt for _ in 1:nsamples(at))...]
    return Tables.Schema(Tables.columnnames(at), coltypes)
end

Tables.columns(at::AbstractAbundanceTable) = (; (col => Tables.getcolumn(at, col) for col in Tables.columnnames(at))...)

function _makerow(row::AbstractAbundanceTable)
    size(row, 1) == 1 || error("Can't make row from table of size $(size(row))")
    NamedTuple{(:features, Symbol.(samplenames(row))...)}((name(features(row)[1]), abundances(row)...))
end

Tables.rows(at::AbstractAbundanceTable) = (_makerow(at[i, :]) for i in 1:nfeatures(at))

## -- Methods for absolute and relative abundances -- ##

# """
#     filterabund(abun::AbstractComMatrix, n::Int=minimum(10, nfeatures(abun)))

# Filter an abundance table to the top `n` features accross all samples

# This function also adds a row for "other", which sums the abundances of the
# remaining features.
# """
# function filterabund(abun::AbstractAbundanceTable, n::Int=minimum(10, nfeatures(abun)))
#     # TODO: add prevalence filter

#     totals = featuretotals(abun)

#     srt = sortperm(totals, rev=true)

#     newabun = getfeature(abun, srt[1:n])

#     remainder = [sum(occurrences(abun)[srt[n+1:end], i]) for i in 1:size(abun, 2)]'
#     newabun = vcat(newabun, remainder)
#     newrows = cat(featurenames(abun)[srt[1:n]], ["other"], dims=1)

#     return abundancetable(newabun, samplenames(abun), newrows)
# end

"""
    relativeabundance!(a::AbstractAbundanceTable; kind::Symbol=:fraction)

Normalize each sample in AbstractAbundanceTable to the sum of the sample.

By default, columns sum to 1.0.
Use `kind=:percent` for columns to sum to 100.
"""
function relativeabundance!(at::AbstractAbundanceTable; kind::Symbol=:fraction)
    in(kind, [:percent, :fraction]) || throw(ArgumentError("Invalid kind: $kind"))
    eltype(abundances(at)) <: AbstractFloat || throw(ArgumentError("relativeabundance! requires profile to have AbstractFloat eltype. Try relativeabundance instead"))
    abund = abundances(at)
    abund ./= sampletotals(at)
    kind == :percent && (abund .*= 100)
    dropzeros!(abund) # shouldn't need dropzeros - https://github.com/JuliaLang/julia/issues/39018
    return at
end

"""
    relativeabundance(at::AbstractAbundanceTable, kind::Symbol=:fraction)

Like [`relativeabundance!`](@ref), but does not mutate original.
"""
function relativeabundance(at::T, kind::Symbol=:fraction) where T <: AbstractAbundanceTable
    comm = T(float.(abundances(at)), deepcopy(features(at)), deepcopy(samples(at)))
    relativeabundance!(comm)
end

"""
    present(t::Union{Real, Missing}, minabundance::Real=0.0)
    present(at::AbstractAbundanceTable, minabundance::Real=0.0)

Check if a given (non-zero) value is greater than or equal to a minimum value.
If the minimum abundance is 0, just checks if value is non-zero.

If used on an `AbstractAbundanceTable`, returns a sparse boolean matrix of the same size.
"""
function present(t::Real, minabundance::Real=0.0)
    (minabundance >= 0 && t >= 0) || throw(DomainError("Only defined for positive values"))
    t == 0 ? false : t >= minabundance
end

present(::Missing, m) = missing

function present(at::AbstractAbundanceTable, minabundance::Real=0.0)
    mat = spzeros(Bool, Ssize(at)...)
    for i in eachindex(mat)
        mat[i] = present(at[i], minabundance)
    end
    return mat
end

"""
    prevalence(a::AbstractArray{<:Real}, minabundance::Real=0.0)
    prevalence(at::AbstractAbundanceTable, minabundance::Real=0.0)

Return the fraction of values that are greater than or equal to a minimum.
If the minimum abundance is 0, returns the fraction of non-zero values.

If used on an `AbstractAbundanceTable`,
returns a prevalence value for each `feature` accross the `sample`s.
"""
prevalence(a::AbstractArray{<:Real}, minabundance::Real=0.0) = mean(x-> present(x, minabundance), a)

# makes it work for any iterable
prevalence(a, minabundance::Real=0.0) = mean(x-> present(x, minabundance), (y for y in a))

function prevalence(at::AbstractAbundanceTable, minabundance::Real=0.0)
    mean(x-> present(x, minabundance), abundances(at), dims=2)
end


module EMLvalve
	using DanaTypes
	using DotPlusInheritance
	using Reexport
	@reexport using ...streams.EMLstreams
	import EMLtypes.length
	include("valve/valve_simplified.jl")
	include("valve/valve_flow.jl")
	include("valve/valve.jl")
end